diff --git a/.dockerignore b/.dockerignore index c7ffe132..05c2a8e3 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,40 +1,15 @@ -# Local build artifacts -target +// Ignore everything +* -# Data folder -data - -# Misc -.env -.env.template -.gitattributes -.gitignore -rustfmt.toml - -# IDE files -.vscode -.idea -.editorconfig -*.iml - -# Documentation -.github -*.md -*.txt -*.yml -*.yaml - -# Docker -hooks -tools -Dockerfile -.dockerignore -docker/** +// Allow what is needed +!.git !docker/healthcheck.sh !docker/start.sh - -# Web vault -web-vault - -# Vaultwarden Resources -resources +!migrations +!src + +!build.rs +!Cargo.lock +!Cargo.toml +!rustfmt.toml +!rust-toolchain.toml diff --git a/.env.template b/.env.template index 07d7dbc0..fb624703 100644 --- a/.env.template +++ b/.env.template @@ -92,15 +92,20 @@ ########################## ## Enables push notifications (requires key and id from https://bitwarden.com/host) -## If you choose "European Union" Data Region, uncomment PUSH_RELAY_URI and PUSH_IDENTITY_URI then replace .com by .eu ## Details about mobile client push notification: ## - https://github.com/dani-garcia/vaultwarden/wiki/Enabling-Mobile-Client-push-notification # PUSH_ENABLED=false # PUSH_INSTALLATION_ID=CHANGEME # PUSH_INSTALLATION_KEY=CHANGEME -## Don't change this unless you know what you're doing. + +# WARNING: Do not modify the following settings unless you fully understand their implications! +# Default Push Relay and Identity URIs # PUSH_RELAY_URI=https://push.bitwarden.com # PUSH_IDENTITY_URI=https://identity.bitwarden.com +# European Union Data Region Settings +# If you have selected "European Union" as your data region, use the following URIs instead. +# PUSH_RELAY_URI=https://api.bitwarden.eu +# PUSH_IDENTITY_URI=https://identity.bitwarden.eu ##################### ### Schedule jobs ### @@ -152,6 +157,10 @@ ## Cron schedule of the job that cleans old auth requests from the auth request. ## Defaults to every minute. Set blank to disable this job. # AUTH_REQUEST_PURGE_SCHEDULE="30 * * * * *" +## +## Cron schedule of the job that cleans expired Duo contexts from the database. Does nothing if Duo MFA is disabled or set to use the legacy iframe prompt. +## Defaults to every minute. Set blank to disable this job. +# DUO_CONTEXT_PURGE_SCHEDULE="30 * * * * *" ######################## ### General settings ### @@ -320,15 +329,15 @@ ## The default is 10 seconds, but this could be to low on slower network connections # ICON_DOWNLOAD_TIMEOUT=10 -## Icon blacklist Regex -## Any domains or IPs that match this regex won't be fetched by the icon service. +## Block HTTP domains/IPs by Regex +## Any domains or IPs that match this regex won't be fetched by the internal HTTP client. ## Useful to hide other servers in the local network. Check the WIKI for more details ## NOTE: Always enclose this regex withing single quotes! -# ICON_BLACKLIST_REGEX='^(192\.168\.0\.[0-9]+|192\.168\.1\.[0-9]+)$' +# HTTP_REQUEST_BLOCK_REGEX='^(192\.168\.0\.[0-9]+|192\.168\.1\.[0-9]+)$' -## Any IP which is not defined as a global IP will be blacklisted. +## Enabling this will cause the internal HTTP client to refuse to connect to any non global IP address. ## Useful to secure your internal environment: See https://en.wikipedia.org/wiki/Reserved_IP_addresses for a list of IPs which it will block -# ICON_BLACKLIST_NON_GLOBAL_IPS=true +# HTTP_REQUEST_BLOCK_NON_GLOBAL_IPS=true ## Client Settings ## Enable experimental feature flags for clients. @@ -362,8 +371,9 @@ ## Log level ## Change the verbosity of the log output ## Valid values are "trace", "debug", "info", "warn", "error" and "off" -## Setting it to "trace" or "debug" would also show logs for mounted -## routes and static file, websocket and alive requests +## Setting it to "trace" or "debug" would also show logs for mounted routes and static file, websocket and alive requests +## For a specific module append a comma separated `path::to::module=log_level` +## For example, to only see debug logs for icons use: LOG_LEVEL="info,vaultwarden::api::icons=debug" # LOG_LEVEL=info ## Token for the admin interface, preferably an Argon2 PCH string @@ -409,6 +419,18 @@ ## KNOW WHAT YOU ARE DOING! # ORG_GROUPS_ENABLED=false +## Increase secure note size limit (Know the risks!) +## Sets the secure note size limit to 100_000 instead of the default 10_000. +## WARNING: This could cause issues with clients. Also exports will not work on Bitwarden servers! +## KNOW WHAT YOU ARE DOING! +# INCREASE_NOTE_SIZE_LIMIT=false + +## Enforce Single Org with Reset Password Policy +## Enforce that the Single Org policy is enabled before setting the Reset Password policy +## Bitwarden enforces this by default. In Vaultwarden we encouraged to use multiple organizations because groups were not available. +## Setting this to true will enforce the Single Org Policy to be enabled before you can enable the Reset Password policy. +# ENFORCE_SINGLE_ORG_WITH_RESET_PW_POLICY=false + ######################## ### MFA/2FA settings ### ######################## @@ -422,15 +444,21 @@ # YUBICO_SERVER=http://yourdomain.com/wsapi/2.0/verify ## Duo Settings -## You need to configure all options to enable global Duo support, otherwise users would need to configure it themselves +## You need to configure the DUO_IKEY, DUO_SKEY, and DUO_HOST options to enable global Duo support. +## Otherwise users will need to configure it themselves. ## Create an account and protect an application as mentioned in this link (only the first step, not the rest): ## https://help.bitwarden.com/article/setup-two-step-login-duo/#create-a-duo-security-account ## Then set the following options, based on the values obtained from the last step: -# DUO_IKEY= -# DUO_SKEY= +# DUO_IKEY= +# DUO_SKEY= # DUO_HOST= ## After that, you should be able to follow the rest of the guide linked above, ## ignoring the fields that ask for the values that you already configured beforehand. +## +## If you want to attempt to use Duo's 'Traditional Prompt' (deprecated, iframe based) set DUO_USE_IFRAME to 'true'. +## Duo no longer supports this, but it still works for some integrations. +## If you aren't sure, leave this alone. +# DUO_USE_IFRAME=false ## Email 2FA settings ## Email token size diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md deleted file mode 100644 index 128c5f58..00000000 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ /dev/null @@ -1,66 +0,0 @@ ---- -name: Bug report -about: Use this ONLY for bugs in vaultwarden itself. Use the Discourse forum (link below) to request features or get help with usage/configuration. If in doubt, use the forum. -title: '' -labels: '' -assignees: '' - ---- - - - - -### Subject of the issue - - -### Deployment environment - - - - - - -* vaultwarden version: - - -* Install method: - -* Clients used: - -* Reverse proxy and version: - -* MySQL/MariaDB or PostgreSQL version: - -* Other relevant details: - -### Steps to reproduce - - -### Expected behaviour - - -### Actual behaviour - - -### Troubleshooting data - diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml new file mode 100644 index 00000000..7168e8ea --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -0,0 +1,167 @@ +name: Bug Report +description: File a bug report +labels: ["bug"] +body: + # + - type: markdown + attributes: + value: | + Thanks for taking the time to fill out this bug report! + + Please *do not* submit feature requests or ask for help on how to configure Vaultwarden here. + + The [GitHub Discussions](https://github.com/dani-garcia/vaultwarden/discussions/) has sections for Questions and Ideas. + + Also, make sure you are running [![GitHub Release](https://img.shields.io/github/release/dani-garcia/vaultwarden.svg)](https://github.com/dani-garcia/vaultwarden/releases/latest) of Vaultwarden! + And search for existing open or closed issues or discussions regarding your topic before posting. + + Be sure to check and validate the Vaultwarden Admin Diagnostics (`/admin/diagnostics`) page for any errors! + See here [how to enable the admin page](https://github.com/dani-garcia/vaultwarden/wiki/Enabling-admin-page). + # + - id: support-string + type: textarea + attributes: + label: Vaultwarden Support String + description: Output of the **Generate Support String** from the `/admin/diagnostics` page. + placeholder: | + 1. Go to the Vaultwarden Admin of your instance https://example.domain.tld/admin/diagnostics + 2. Click on `Generate Support String` + 3. Click on `Copy To Clipboard` + 4. Replace this text by pasting it into this textarea without any modifications + validations: + required: true + # + - id: version + type: input + attributes: + label: Vaultwarden Build Version + description: What version of Vaultwarden are you running? + placeholder: ex. v1.31.0 or v1.32.0-3466a804 + validations: + required: true + # + - id: deployment + type: dropdown + attributes: + label: Deployment method + description: How did you deploy Vaultwarden? + multiple: false + options: + - Official Container Image + - Build from source + - OS Package (apt, yum/dnf, pacman, apk, nix, ...) + - Manually Extracted from Container Image + - Downloaded from GitHub Actions Release Workflow + - Other method + validations: + required: true + # + - id: deployment-other + type: textarea + attributes: + label: Custom deployment method + description: If you deployed Vaultwarden via any other method, please describe how. + # + - id: reverse-proxy + type: input + attributes: + label: Reverse Proxy + description: Are you using a reverse proxy, if so which and what version? + placeholder: ex. nginx 1.26.2, caddy 2.8.4, traefik 3.1.2, haproxy 3.0 + validations: + required: true + # + - id: os + type: dropdown + attributes: + label: Host/Server Operating System + description: On what operating system are you running the Vaultwarden server? + multiple: false + options: + - Linux + - NAS/SAN + - Cloud + - Windows + - macOS + - Other + validations: + required: true + # + - id: os-version + type: input + attributes: + label: Operating System Version + description: What version of the operating system(s) are you seeing the problem on? + placeholder: ex. Arch Linux, Ubuntu 24.04, Kubernetes, Synology DSM 7.x, Windows 11 + # + - id: clients + type: dropdown + attributes: + label: Clients + description: What client(s) are you seeing the problem on? + multiple: true + options: + - Web Vault + - Browser Extension + - CLI + - Desktop + - Android + - iOS + validations: + required: true + # + - id: client-version + type: input + attributes: + label: Client Version + description: What version(s) of the client(s) are you seeing the problem on? + placeholder: ex. CLI v2024.7.2, Firefox 130 - v2024.7.0 + # + - id: reproduce + type: textarea + attributes: + label: Steps To Reproduce + description: How can we reproduce the behavior. + value: | + 1. Go to '...' + 2. Click on '....' + 3. Scroll down to '....' + 4. Click on '...' + 5. Etc '...' + validations: + required: true + # + - id: expected + type: textarea + attributes: + label: Expected Result + description: A clear and concise description of what you expected to happen. + validations: + required: true + # + - id: actual + type: textarea + attributes: + label: Actual Result + description: A clear and concise description of what is happening. + validations: + required: true + # + - id: logs + type: textarea + attributes: + label: Logs + description: Provide the logs generated by Vaultwarden during the time this issue occurs. + render: text + # + - id: screenshots + type: textarea + attributes: + label: Screenshots or Videos + description: If applicable, add screenshots and/or a short video to help explain your problem. + # + - id: additional-context + type: textarea + attributes: + label: Additional Context + description: Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index 51a76d39..5796d8e6 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -1,8 +1,8 @@ blank_issues_enabled: false contact_links: - - name: Discourse forum for vaultwarden - url: https://vaultwarden.discourse.group/ - about: Use this forum to request features or get help with usage/configuration. - - name: GitHub Discussions for vaultwarden + - name: GitHub Discussions for Vaultwarden url: https://github.com/dani-garcia/vaultwarden/discussions - about: An alternative to the Discourse forum, if this is easier for you. + about: Use the discussions to request features or get help with usage/configuration. + - name: Discourse forum for Vaultwarden + url: https://vaultwarden.discourse.group/ + about: An alternative to the GitHub Discussions, if this is easier for you. diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 7956c382..a025041f 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -28,6 +28,7 @@ on: jobs: build: + # We use Ubuntu 22.04 here because this matches the library versions used within the Debian docker containers runs-on: ubuntu-22.04 timeout-minutes: 120 # Make warnings errors, this is to prevent warnings slipping through. @@ -46,7 +47,7 @@ jobs: steps: # Checkout the repo - name: "Checkout" - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 #v4.1.7 + uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 #v4.2.1 # End Checkout the repo @@ -74,7 +75,7 @@ jobs: # Only install the clippy and rustfmt components on the default rust-toolchain - name: "Install rust-toolchain version" - uses: dtolnay/rust-toolchain@21dc36fb71dd22e3317045c0c31a3f4249868b17 # master @ Jun 13, 2024, 6:20 PM GMT+2 + uses: dtolnay/rust-toolchain@7b1c307e0dcbda6122208f10795a713336a9b35a # master @ Aug 8, 2024, 7:36 PM GMT+2 if: ${{ matrix.channel == 'rust-toolchain' }} with: toolchain: "${{steps.toolchain.outputs.RUST_TOOLCHAIN}}" @@ -84,7 +85,7 @@ jobs: # Install the any other channel to be used for which we do not execute clippy and rustfmt - name: "Install MSRV version" - uses: dtolnay/rust-toolchain@21dc36fb71dd22e3317045c0c31a3f4249868b17 # master @ Jun 13, 2024, 6:20 PM GMT+2 + uses: dtolnay/rust-toolchain@7b1c307e0dcbda6122208f10795a713336a9b35a # master @ Aug 8, 2024, 7:36 PM GMT+2 if: ${{ matrix.channel != 'rust-toolchain' }} with: toolchain: "${{steps.toolchain.outputs.RUST_TOOLCHAIN}}" diff --git a/.github/workflows/hadolint.yml b/.github/workflows/hadolint.yml index bd890580..a671f936 100644 --- a/.github/workflows/hadolint.yml +++ b/.github/workflows/hadolint.yml @@ -8,14 +8,26 @@ on: [ jobs: hadolint: name: Validate Dockerfile syntax - runs-on: ubuntu-22.04 + runs-on: ubuntu-24.04 timeout-minutes: 30 steps: # Checkout the repo - name: Checkout - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 #v4.2.1 # End Checkout the repo + # Start Docker Buildx + - name: Setup Docker Buildx + uses: docker/setup-buildx-action@c47758b77c9736f4b2ef4073d4d51994fabfe349 # v3.7.1 + # https://github.com/moby/buildkit/issues/3969 + # Also set max parallelism to 2, the default of 4 breaks GitHub Actions and causes OOMKills + with: + buildkitd-config-inline: | + [worker.oci] + max-parallelism = 2 + driver-opts: | + network=host + # Download hadolint - https://github.com/hadolint/hadolint/releases - name: Download hadolint shell: bash @@ -26,8 +38,18 @@ jobs: HADOLINT_VERSION: 2.12.0 # End Download hadolint - # Test Dockerfiles + # Test Dockerfiles with hadolint - name: Run hadolint shell: bash run: hadolint docker/Dockerfile.{debian,alpine} - # End Test Dockerfiles + # End Test Dockerfiles with hadolint + + # Test Dockerfiles with docker build checks + - name: Run docker build check + shell: bash + run: | + echo "Checking docker/Dockerfile.debian" + docker build --check . -f docker/Dockerfile.debian + echo "Checking docker/Dockerfile.alpine" + docker build --check . -f docker/Dockerfile.alpine + # End Test Dockerfiles with docker build checks diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index b1fb85d4..22fc4e28 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -13,7 +13,7 @@ jobs: # Some checks to determine if we need to continue with building a new docker. # We will skip this check if we are creating a tag, because that has the same hash as a previous run already. skip_check: - runs-on: ubuntu-22.04 + runs-on: ubuntu-24.04 if: ${{ github.repository == 'dani-garcia/vaultwarden' }} outputs: should_skip: ${{ steps.skip_check.outputs.should_skip }} @@ -27,7 +27,7 @@ jobs: if: ${{ github.ref_type == 'branch' }} docker-build: - runs-on: ubuntu-22.04 + runs-on: ubuntu-24.04 timeout-minutes: 120 needs: skip_check if: ${{ needs.skip_check.outputs.should_skip != 'true' && github.repository == 'dani-garcia/vaultwarden' }} @@ -58,24 +58,24 @@ jobs: steps: # Checkout the repo - name: Checkout - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 #v4.2.1 with: fetch-depth: 0 - name: Initialize QEMU binfmt support - uses: docker/setup-qemu-action@68827325e0b33c7199eb31dd4e31fbe9023e06e3 # v3.0.0 + uses: docker/setup-qemu-action@49b3bc8e6bdd4a60e6116a5414239cba5943d3cf # v3.2.0 with: platforms: "arm64,arm" # Start Docker Buildx - name: Setup Docker Buildx - uses: docker/setup-buildx-action@d70bba72b1f3fd22344832f00baa16ece964efeb # v3.3.0 + uses: docker/setup-buildx-action@c47758b77c9736f4b2ef4073d4d51994fabfe349 # v3.7.1 # https://github.com/moby/buildkit/issues/3969 - # Also set max parallelism to 3, the default of 4 breaks GitHub Actions and causes OOMKills + # Also set max parallelism to 2, the default of 4 breaks GitHub Actions and causes OOMKills with: buildkitd-config-inline: | [worker.oci] - max-parallelism = 3 + max-parallelism = 2 driver-opts: | network=host @@ -102,7 +102,7 @@ jobs: # Login to Docker Hub - name: Login to Docker Hub - uses: docker/login-action@0d4c9c5ea7693da7b068278f7b52bda2a190a446 # v3.2.0 + uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 # v3.3.0 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} @@ -116,7 +116,7 @@ jobs: # Login to GitHub Container Registry - name: Login to GitHub Container Registry - uses: docker/login-action@0d4c9c5ea7693da7b068278f7b52bda2a190a446 # v3.2.0 + uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 # v3.3.0 with: registry: ghcr.io username: ${{ github.repository_owner }} @@ -131,7 +131,7 @@ jobs: # Login to Quay.io - name: Login to Quay.io - uses: docker/login-action@0d4c9c5ea7693da7b068278f7b52bda2a190a446 # v3.2.0 + uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 # v3.3.0 with: registry: quay.io username: ${{ secrets.QUAY_USERNAME }} @@ -165,7 +165,7 @@ jobs: echo "CONTAINER_REGISTRIES=${CONTAINER_REGISTRIES:+${CONTAINER_REGISTRIES},}localhost:5000/vaultwarden/server" | tee -a "${GITHUB_ENV}" - name: Bake ${{ matrix.base_image }} containers - uses: docker/bake-action@1c5f18a523c4c68524cfbc5161494d8bb5b29d20 # v5.0.1 + uses: docker/bake-action@2e3d19baedb14545e5d41222653874f25d5b4dfb # v5.10.0 env: BASE_TAGS: "${{ env.BASE_TAGS }}" SOURCE_COMMIT: "${{ env.SOURCE_COMMIT }}" @@ -223,28 +223,28 @@ jobs: # Upload artifacts to Github Actions - name: "Upload amd64 artifact" - uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4.3.3 + uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3 if: ${{ matrix.base_image == 'alpine' }} with: name: vaultwarden-${{ env.SOURCE_VERSION }}-linux-amd64 path: vaultwarden-amd64 - name: "Upload arm64 artifact" - uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4.3.3 + uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3 if: ${{ matrix.base_image == 'alpine' }} with: name: vaultwarden-${{ env.SOURCE_VERSION }}-linux-arm64 path: vaultwarden-arm64 - name: "Upload armv7 artifact" - uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4.3.3 + uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3 if: ${{ matrix.base_image == 'alpine' }} with: name: vaultwarden-${{ env.SOURCE_VERSION }}-linux-armv7 path: vaultwarden-armv7 - name: "Upload armv6 artifact" - uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4.3.3 + uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3 if: ${{ matrix.base_image == 'alpine' }} with: name: vaultwarden-${{ env.SOURCE_VERSION }}-linux-armv6 diff --git a/.github/workflows/releasecache-cleanup.yml b/.github/workflows/releasecache-cleanup.yml index 572330c4..6fd880bb 100644 --- a/.github/workflows/releasecache-cleanup.yml +++ b/.github/workflows/releasecache-cleanup.yml @@ -13,7 +13,7 @@ name: Cleanup jobs: releasecache-cleanup: name: Releasecache Cleanup - runs-on: ubuntu-22.04 + runs-on: ubuntu-24.04 continue-on-error: true timeout-minutes: 30 steps: diff --git a/.github/workflows/trivy.yml b/.github/workflows/trivy.yml index 2c957994..48a8bc1e 100644 --- a/.github/workflows/trivy.yml +++ b/.github/workflows/trivy.yml @@ -9,15 +9,18 @@ on: pull_request: branches: [ "main" ] schedule: - - cron: '00 12 * * *' + - cron: '08 11 * * *' permissions: contents: read jobs: trivy-scan: + # Only run this in the master repo and not on forks + # When all forks run this at the same time, it is causing `Too Many Requests` issues + if: ${{ github.repository == 'dani-garcia/vaultwarden' }} name: Check - runs-on: ubuntu-22.04 + runs-on: ubuntu-24.04 timeout-minutes: 30 permissions: contents: read @@ -25,10 +28,10 @@ jobs: actions: read steps: - name: Checkout code - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 #v4.1.7 + uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 #v4.2.1 - name: Run Trivy vulnerability scanner - uses: aquasecurity/trivy-action@7c2007bcb556501da015201bcba5aa14069b74e2 # v0.23.0 + uses: aquasecurity/trivy-action@5681af892cd0f4997658e2bacc62bd0a894cf564 # v0.27.0 with: scan-type: repo ignore-unfixed: true @@ -37,6 +40,6 @@ jobs: severity: CRITICAL,HIGH - name: Upload Trivy scan results to GitHub Security tab - uses: github/codeql-action/upload-sarif@2bbafcdd7fbf96243689e764c2f15d9735164f33 # v3.25.10 + uses: github/codeql-action/upload-sarif@2bbafcdd7fbf96243689e764c2f15d9735164f33 # v3.26.6 with: sarif_file: 'trivy-results.sarif' diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 39ce1cb5..1061e8d4 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,7 +1,7 @@ --- repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.5.0 + rev: v4.6.0 hooks: - id: check-yaml - id: check-json diff --git a/Cargo.lock b/Cargo.lock index 2834a9f1..c61253c1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4,18 +4,18 @@ version = 3 [[package]] name = "addr2line" -version = "0.22.0" +version = "0.24.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e4503c46a5c0c7844e948c9a4d6acd9f50cccb4de1c48eb9e291ea17470c678" +checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" dependencies = [ "gimli", ] [[package]] -name = "adler" -version = "1.0.2" +name = "adler2" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" +checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" [[package]] name = "ahash" @@ -111,9 +111,9 @@ dependencies = [ [[package]] name = "async-compression" -version = "0.4.11" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd066d0b4ef8ecb03a55319dc13aa6910616d0f44008a045bb1835af830abff5" +checksum = "998282f8f49ccd6116b0ed8a4de0fbd3151697920e7c7533416d6e25e76434a7" dependencies = [ "brotli", "flate2", @@ -125,14 +125,14 @@ dependencies = [ [[package]] name = "async-executor" -version = "1.12.0" +version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8828ec6e544c02b0d6691d21ed9f9218d0384a82542855073c2a3f58304aaf0" +checksum = "30ca9a001c1e8ba5149f91a74362376cc6bc5b919d92d988668657bd570bdcec" dependencies = [ "async-task", "concurrent-queue", - "fastrand 2.1.0", - "futures-lite 2.3.0", + "fastrand", + "futures-lite", "slab", ] @@ -144,59 +144,30 @@ checksum = "05b1b633a2115cd122d73b955eadd9916c18c8f510ec9cd1686404c60ad1c29c" dependencies = [ "async-channel 2.3.1", "async-executor", - "async-io 2.3.3", - "async-lock 3.4.0", + "async-io", + "async-lock", "blocking", - "futures-lite 2.3.0", + "futures-lite", "once_cell", ] [[package]] name = "async-io" -version = "1.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fc5b45d93ef0529756f812ca52e44c221b35341892d3dcc34132ac02f3dd2af" -dependencies = [ - "async-lock 2.8.0", - "autocfg", - "cfg-if", - "concurrent-queue", - "futures-lite 1.13.0", - "log", - "parking", - "polling 2.8.0", - "rustix 0.37.27", - "slab", - "socket2 0.4.10", - "waker-fn", -] - -[[package]] -name = "async-io" -version = "2.3.3" +version = "2.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d6baa8f0178795da0e71bc42c9e5d13261aac7ee549853162e66a241ba17964" +checksum = "444b0228950ee6501b3568d3c93bf1176a1fdbc3b758dcd9475046d30f4dc7e8" dependencies = [ - "async-lock 3.4.0", + "async-lock", "cfg-if", "concurrent-queue", "futures-io", - "futures-lite 2.3.0", + "futures-lite", "parking", - "polling 3.7.2", - "rustix 0.38.34", + "polling", + "rustix", "slab", "tracing", - "windows-sys 0.52.0", -] - -[[package]] -name = "async-lock" -version = "2.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "287272293e9d8c41773cec55e365490fe034813a2f172f502d6ddcf75b2f582b" -dependencies = [ - "event-listener 2.5.3", + "windows-sys 0.59.0", ] [[package]] @@ -212,55 +183,57 @@ dependencies = [ [[package]] name = "async-process" -version = "1.8.1" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea6438ba0a08d81529c69b36700fa2f95837bfe3e776ab39cde9c14d9149da88" +checksum = "63255f1dc2381611000436537bbedfe83183faa303a5a0edaf191edef06526bb" dependencies = [ - "async-io 1.13.0", - "async-lock 2.8.0", + "async-channel 2.3.1", + "async-io", + "async-lock", "async-signal", + "async-task", "blocking", "cfg-if", - "event-listener 3.1.0", - "futures-lite 1.13.0", - "rustix 0.38.34", - "windows-sys 0.48.0", + "event-listener 5.3.1", + "futures-lite", + "rustix", + "tracing", ] [[package]] name = "async-signal" -version = "0.2.8" +version = "0.2.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "794f185324c2f00e771cd9f1ae8b5ac68be2ca7abb129a87afd6e86d228bc54d" +checksum = "637e00349800c0bdf8bfc21ebbc0b6524abea702b0da4168ac00d070d0c0b9f3" dependencies = [ - "async-io 2.3.3", - "async-lock 3.4.0", + "async-io", + "async-lock", "atomic-waker", "cfg-if", "futures-core", "futures-io", - "rustix 0.38.34", + "rustix", "signal-hook-registry", "slab", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] name = "async-std" -version = "1.12.0" +version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62565bb4402e926b29953c785397c6dc0391b7b446e45008b0049eb43cec6f5d" +checksum = "c634475f29802fde2b8f0b505b1bd00dfe4df7d4a000f0b36f7671197d5c3615" dependencies = [ "async-channel 1.9.0", "async-global-executor", - "async-io 1.13.0", - "async-lock 2.8.0", + "async-io", + "async-lock", "async-process", "crossbeam-utils", "futures-channel", "futures-core", "futures-io", - "futures-lite 1.13.0", + "futures-lite", "gloo-timers", "kv-log-macro", "log", @@ -274,9 +247,9 @@ dependencies = [ [[package]] name = "async-stream" -version = "0.3.5" +version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd56dd203fef61ac097dd65721a419ddccb106b2d2b70ba60a6b529f03961a51" +checksum = "0b5a71a6f37880a80d1d7f19efd781e4b5de42c88f0722cc13bcb6cc2cfe8476" dependencies = [ "async-stream-impl", "futures-core", @@ -285,9 +258,9 @@ dependencies = [ [[package]] name = "async-stream-impl" -version = "0.3.5" +version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16e62a023e7c117e27523144c5d2459f4397fcc3cab0085af8e2224f643a0193" +checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" dependencies = [ "proc-macro2", "quote", @@ -302,9 +275,9 @@ checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" [[package]] name = "async-trait" -version = "0.1.81" +version = "0.1.83" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e0c28dcc82d7c8ead5cb13beb15405b57b8546e93215673ff8ca0349a028107" +checksum = "721cae7de5c34fbb2acd27e21e6d2cf7b886dce0c27388d46c4e6c47ea4318dd" dependencies = [ "proc-macro2", "quote", @@ -334,23 +307,23 @@ checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" [[package]] name = "autocfg" -version = "1.3.0" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" +checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" [[package]] name = "backtrace" -version = "0.3.73" +version = "0.3.74" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5cc23269a4f8976d0a4d2e7109211a419fe30e8d88d677cd60b6bc79c5732e0a" +checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a" dependencies = [ "addr2line", - "cc", "cfg-if", "libc", "miniz_oxide", "object", "rustc-demangle", + "windows-targets 0.52.6", ] [[package]] @@ -435,15 +408,15 @@ dependencies = [ "async-channel 2.3.1", "async-task", "futures-io", - "futures-lite 2.3.0", + "futures-lite", "piper", ] [[package]] name = "brotli" -version = "6.0.0" +version = "7.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74f7971dbd9326d58187408ab83117d8ac1bb9c17b085fdacd1cf2f598719b6b" +checksum = "cc97b8f16f944bba54f0433f07e30be199b6dc2bd25937444bbad560bcea29bd" dependencies = [ "alloc-no-stdlib", "alloc-stdlib", @@ -468,9 +441,9 @@ checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" [[package]] name = "bytemuck" -version = "1.16.1" +version = "1.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b236fc92302c97ed75b38da1f4917b5cdda4984745740f153a5d3059e48d725e" +checksum = "94bbb0ad554ad961ddc5da507a12a29b14e4ae5bda06b19f575a3e6079d2e2ae" [[package]] name = "byteorder" @@ -480,33 +453,33 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.6.0" +version = "1.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "514de17de45fdb8dc022b1a7975556c53c86f9f0aa5f534b98977b171857c2c9" +checksum = "428d9aa8fbc0670b7b8d6030a7fadd0f86151cae55e4dbbece15f3780a3dfaf3" [[package]] name = "cached" -version = "0.52.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8466736fe5dbcaf8b8ee24f9bbefe43c884dc3e9ff7178da70f55bffca1133c" +checksum = "b4d73155ae6b28cf5de4cfc29aeb02b8a1c6dab883cb015d15cd514e42766846" dependencies = [ "ahash", "async-trait", "cached_proc_macro", "cached_proc_macro_types", "futures", - "hashbrown", - "instant", + "hashbrown 0.14.5", "once_cell", "thiserror", "tokio", + "web-time", ] [[package]] name = "cached_proc_macro" -version = "0.22.0" +version = "0.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "575f32e012222055211b70f5b0601f951f84523410a0e65c81f2744a6042450d" +checksum = "2f42a145ed2d10dce2191e1dcf30cfccfea9026660e143662ba5eec4017d5daa" dependencies = [ "darling", "proc-macro2", @@ -522,9 +495,12 @@ checksum = "ade8366b8bd5ba243f0a58f036cc0ca8a2f069cff1a2351ef1cac6b083e16fc0" [[package]] name = "cc" -version = "1.0.106" +version = "1.1.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "066fce287b1d4eafef758e89e09d724a24808a9196fe9756b8ca90e86d0719a2" +checksum = "58e804ac3194a48bb129643eb1d62fcc20d18c6b8c181704489353d13120bcd1" +dependencies = [ + "shlex", +] [[package]] name = "cfg-if" @@ -547,9 +523,9 @@ dependencies = [ [[package]] name = "chrono-tz" -version = "0.9.0" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93698b29de5e97ad0ae26447b344c482a7284c737d9ddc5f9e52b74a336671bb" +checksum = "cd6dd8046d00723a59a2f8c5f295c515b9bb9a331ee4f8f3d4dd49e428acd3b6" dependencies = [ "chrono", "chrono-tz-build", @@ -558,12 +534,11 @@ dependencies = [ [[package]] name = "chrono-tz-build" -version = "0.3.0" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c088aee841df9c3041febbb73934cfc39708749bf96dc827e3359cd39ef11b1" +checksum = "e94fea34d77a245229e7746bd2beb786cd2a896f306ff491fb8cecb3074b10a7" dependencies = [ "parse-zoneinfo", - "phf", "phf_codegen", ] @@ -573,7 +548,7 @@ version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8eebd66744a15ded14960ab4ccdbfb51ad3b81f51f3f04a80adac98c985396c9" dependencies = [ - "hashbrown", + "hashbrown 0.14.5", "stacker", ] @@ -626,15 +601,15 @@ dependencies = [ [[package]] name = "core-foundation-sys" -version = "0.8.6" +version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" [[package]] name = "cpufeatures" -version = "0.2.12" +version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53fe5e26ff1b7aef8bca9c6080520cfb8d9333c7568e1829cef191a9723e5504" +checksum = "608697df725056feaccfa42cffdaeeec3fccc4ffc38358ecd19b243e716a78e0" dependencies = [ "libc", ] @@ -677,9 +652,9 @@ dependencies = [ [[package]] name = "darling" -version = "0.20.9" +version = "0.20.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83b2eb4d90d12bdda5ed17de686c2acb4c57914f8f921b8da7e112b5a36f3fe1" +checksum = "6f63b86c8a8826a49b8c21f08a2d07338eec8d900540f8630dc76284be802989" dependencies = [ "darling_core", "darling_macro", @@ -687,9 +662,9 @@ dependencies = [ [[package]] name = "darling_core" -version = "0.20.9" +version = "0.20.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "622687fe0bac72a04e5599029151f5796111b90f1baaa9b544d807a5e31cd120" +checksum = "95133861a8032aaea082871032f5815eb9e98cef03fa916ab4500513994df9e5" dependencies = [ "fnv", "ident_case", @@ -701,9 +676,9 @@ dependencies = [ [[package]] name = "darling_macro" -version = "0.20.9" +version = "0.20.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "733cabb43482b1a1b53eee8583c2b9e8684d592215ea83efd305dd31bc2f0178" +checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806" dependencies = [ "darling_core", "quote", @@ -717,7 +692,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "978747c1d849a7d2ee5e8adc0159961c48fb7e5db2f06af6723b80123bb53856" dependencies = [ "cfg-if", - "hashbrown", + "hashbrown 0.14.5", "lock_api", "once_cell", "parking_lot_core", @@ -725,13 +700,13 @@ dependencies = [ [[package]] name = "dashmap" -version = "6.0.1" +version = "6.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "804c8821570c3f8b70230c2ba75ffa5c0f9a4189b9a432b6656c536712acae28" +checksum = "5041cc499144891f3790297212f32a74fb938e5136a14943f338ef9e0ae276cf" dependencies = [ "cfg-if", "crossbeam-utils", - "hashbrown", + "hashbrown 0.14.5", "lock_api", "once_cell", "parking_lot_core", @@ -760,9 +735,9 @@ dependencies = [ [[package]] name = "devise" -version = "0.4.1" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6eacefd3f541c66fc61433d65e54e0e46e0a029a819a7dbbc7a7b489e8a85f8" +checksum = "f1d90b0c4c777a2cad215e3c7be59ac7c15adf45cf76317009b7d096d46f651d" dependencies = [ "devise_codegen", "devise_core", @@ -770,9 +745,9 @@ dependencies = [ [[package]] name = "devise_codegen" -version = "0.4.1" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c8cf4b8dd484ede80fd5c547592c46c3745a617c8af278e2b72bea86b2dfed6" +checksum = "71b28680d8be17a570a2334922518be6adc3f58ecc880cbb404eaeb8624fd867" dependencies = [ "devise_core", "quote", @@ -780,9 +755,9 @@ dependencies = [ [[package]] name = "devise_core" -version = "0.4.1" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35b50dba0afdca80b187392b24f2499a88c336d5a8493e4b4ccfb608708be56a" +checksum = "b035a542cf7abf01f2e3c4d5a7acbaebfefe120ae4efc7bde3df98186e4b8af7" dependencies = [ "bitflags 2.6.0", "proc-macro2", @@ -793,9 +768,9 @@ dependencies = [ [[package]] name = "diesel" -version = "2.2.1" +version = "2.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62d6dcd069e7b5fe49a302411f759d4cf1cf2c27fe798ef46fb8baefc053dd2b" +checksum = "158fe8e2e68695bd615d7e4f3227c0727b151330d3e253b525086c348d055d5e" dependencies = [ "bigdecimal", "bitflags 2.6.0", @@ -817,9 +792,9 @@ dependencies = [ [[package]] name = "diesel_derives" -version = "2.2.1" +version = "2.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59de76a222c2b8059f789cbe07afbfd8deb8c31dd0bc2a21f85e256c1def8259" +checksum = "e7f2c3de51e2ba6bf2a648285696137aaf0f5f487bcbea93972fe8a364e131a4" dependencies = [ "diesel_table_macro_syntax", "dsl_auto_type", @@ -869,6 +844,17 @@ dependencies = [ "subtle", ] +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "dotenvy" version = "0.15.7" @@ -877,13 +863,13 @@ checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" [[package]] name = "dsl_auto_type" -version = "0.1.1" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0892a17df262a24294c382f0d5997571006e7a4348b4327557c4ff1cd4a8bccc" +checksum = "c5d9abe6314103864cc2d8901b7ae224e0ab1a103a0a416661b4097b0779b607" dependencies = [ "darling", "either", - "heck 0.5.0", + "heck", "proc-macro2", "quote", "syn", @@ -907,9 +893,9 @@ dependencies = [ [[package]] name = "email_address" -version = "0.2.5" +version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1019fa28f600f5b581b7a603d515c3f1635da041ca211b5055804788673abfe" +checksum = "e079f19b08ca6239f47f8ba8509c11cf3ea30095831f7fed61441475edd8c449" dependencies = [ "serde", ] @@ -925,11 +911,11 @@ dependencies = [ [[package]] name = "enum-as-inner" -version = "0.6.0" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ffccbb6966c05b32ef8fbac435df276c4ae4d3dc55a8cd0eb9745e6c12f546a" +checksum = "a1e6a265c649f3f5979b601d26f1d05ada116434c87741c9493cb56218f76cbc" dependencies = [ - "heck 0.4.1", + "heck", "proc-macro2", "quote", "syn", @@ -966,17 +952,6 @@ version = "2.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" -[[package]] -name = "event-listener" -version = "3.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d93877bcde0eb80ca09131a08d23f0a5c18a620b01db137dba666d18cd9b30c2" -dependencies = [ - "concurrent-queue", - "parking", - "pin-project-lite", -] - [[package]] name = "event-listener" version = "5.3.1" @@ -1000,18 +975,9 @@ dependencies = [ [[package]] name = "fastrand" -version = "1.9.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e51093e27b0797c359783294ca4f0a911c270184cb10f85783b118614a1501be" -dependencies = [ - "instant", -] - -[[package]] -name = "fastrand" -version = "2.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fc0510504f03c51ada170672ac806f1f105a88aa97a5281117e1ddc3368e51a" +checksum = "e8c02a5121d4ea3eb16a80748c74f5549a5665e4c21333c6098f283870fbdea6" [[package]] name = "fern" @@ -1041,9 +1007,9 @@ dependencies = [ [[package]] name = "flate2" -version = "1.0.30" +version = "1.0.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f54427cfd1c7829e2a139fcefea601bf088ebca651d2bf53ebc600eac295dae" +checksum = "a1b589b4dc103969ad3cf85c950899926ec64300a1a46d76c03a6072957036f0" dependencies = [ "crc32fast", "miniz_oxide", @@ -1081,9 +1047,9 @@ dependencies = [ [[package]] name = "futures" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "645c6916888f6cb6350d2550b80fb63e734897a8498abe35cfb732b6487804b0" +checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" dependencies = [ "futures-channel", "futures-core", @@ -1096,9 +1062,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" dependencies = [ "futures-core", "futures-sink", @@ -1106,15 +1072,15 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" [[package]] name = "futures-executor" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a576fc72ae164fca6b9db127eaa9a9dda0d61316034f33a0a0d4eda41f02b01d" +checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" dependencies = [ "futures-core", "futures-task", @@ -1123,24 +1089,9 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1" - -[[package]] -name = "futures-lite" -version = "1.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49a9d51ce47660b1e808d3c990b4709f2f415d928835a17dfd16991515c46bce" -dependencies = [ - "fastrand 1.9.0", - "futures-core", - "futures-io", - "memchr", - "parking", - "pin-project-lite", - "waker-fn", -] +checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" [[package]] name = "futures-lite" @@ -1148,7 +1099,7 @@ version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "52527eb5074e35e9339c6b4e8d12600c7128b68fb25dcb9fa9dec18f7c25f3a5" dependencies = [ - "fastrand 2.1.0", + "fastrand", "futures-core", "futures-io", "parking", @@ -1157,9 +1108,9 @@ dependencies = [ [[package]] name = "futures-macro" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" +checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ "proc-macro2", "quote", @@ -1168,15 +1119,15 @@ dependencies = [ [[package]] name = "futures-sink" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" [[package]] name = "futures-task" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" [[package]] name = "futures-timer" @@ -1186,9 +1137,9 @@ checksum = "f288b0a4f20f9a56b5d1da57e2227c661b7b16168e2f72365f57b63326e29b24" [[package]] name = "futures-util" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" dependencies = [ "futures-channel", "futures-core", @@ -1240,9 +1191,9 @@ dependencies = [ [[package]] name = "gimli" -version = "0.29.0" +version = "0.31.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40ecd4077b5ae9fd2e9e169b102c6c330d0605168eb0e8bf79952b256dbefffd" +checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" [[package]] name = "glob" @@ -1252,9 +1203,9 @@ checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" [[package]] name = "gloo-timers" -version = "0.2.6" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b995a66bb87bebce9a0f4a95aed01daca4872c050bfcb21653361c03bc35e5c" +checksum = "bbb143cf96099802033e0d4f4963b19fd2e0b728bcf076cd9cf7f6634f092994" dependencies = [ "futures-channel", "futures-core", @@ -1303,9 +1254,9 @@ dependencies = [ [[package]] name = "h2" -version = "0.4.5" +version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa82e28a107a8cc405f0839610bdc9b15f1e25ec7d696aa5cf173edbcb1486ab" +checksum = "524e8ac6999421f49a846c2d4411f337e53497d8ec55d67753beffa43c5d9205" dependencies = [ "atomic-waker", "bytes", @@ -1328,9 +1279,9 @@ checksum = "1b43ede17f21864e81be2fa654110bf1e793774238d86ef8555c37e6519c0403" [[package]] name = "handlebars" -version = "5.1.2" +version = "6.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d08485b96a0e6393e9e4d1b8d48cf74ad6c063cd905eb33f42c1ce3f0377539b" +checksum = "ce25b617d1375ef96eeb920ae717e3da34a02fc979fe632c75128350f9e1f74a" dependencies = [ "log", "pest", @@ -1352,10 +1303,10 @@ dependencies = [ ] [[package]] -name = "heck" -version = "0.4.1" +name = "hashbrown" +version = "0.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" +checksum = "1e087f84d4f86bf4b218b927129862374b72199ae7d8657835f1e89000eea4fb" [[package]] name = "heck" @@ -1504,9 +1455,9 @@ dependencies = [ [[package]] name = "http-body" -version = "1.0.0" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1cac85db508abc24a2e48553ba12a996e87244a0395ce011e62b37158745d643" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" dependencies = [ "bytes", "http 1.1.0", @@ -1521,15 +1472,15 @@ dependencies = [ "bytes", "futures-util", "http 1.1.0", - "http-body 1.0.0", + "http-body 1.0.1", "pin-project-lite", ] [[package]] name = "httparse" -version = "1.9.4" +version = "1.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fcc0b4a115bf80b728eb8ea024ad5bd707b615bfed49e0665b6e0f86fd082d9" +checksum = "7d71d3574edd2771538b901e6549113b4006ece66150fb69c0fb6d9a2adae946" [[package]] name = "httpdate" @@ -1539,9 +1490,9 @@ checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" [[package]] name = "hyper" -version = "0.14.29" +version = "0.14.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f361cde2f109281a220d4307746cdfd5ee3f410da58a70377762396775634b33" +checksum = "a152ddd61dfaec7273fe8419ab357f33aee0d914c5f4efbf0d96fa749eea5ec9" dependencies = [ "bytes", "futures-channel", @@ -1554,7 +1505,7 @@ dependencies = [ "httpdate", "itoa", "pin-project-lite", - "socket2 0.5.7", + "socket2", "tokio", "tower-service", "tracing", @@ -1563,16 +1514,16 @@ dependencies = [ [[package]] name = "hyper" -version = "1.4.0" +version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4fe55fb7a772d59a5ff1dfbff4fe0258d19b89fec4b233e75d35d5d2316badc" +checksum = "50dfd22e0e76d0f662d429a5f80fcaf3855009297eab6a0a9f8543834744ba05" dependencies = [ "bytes", "futures-channel", "futures-util", - "h2 0.4.5", + "h2 0.4.6", "http 1.1.0", - "http-body 1.0.0", + "http-body 1.0.1", "httparse", "itoa", "pin-project-lite", @@ -1583,15 +1534,15 @@ dependencies = [ [[package]] name = "hyper-rustls" -version = "0.27.2" +version = "0.27.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ee4be2c948921a1a5320b629c4193916ed787a7f7f293fd3f7f5a6c9de74155" +checksum = "08afdbb5c31130e3034af566421053ab03787c640246a446327f550d11bcb333" dependencies = [ "futures-util", "http 1.1.0", - "hyper 1.4.0", + "hyper 1.4.1", "hyper-util", - "rustls 0.23.11", + "rustls 0.23.14", "rustls-pki-types", "tokio", "tokio-rustls 0.26.0", @@ -1605,7 +1556,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905" dependencies = [ "bytes", - "hyper 0.14.29", + "hyper 0.14.30", "native-tls", "tokio", "tokio-native-tls", @@ -1619,7 +1570,7 @@ checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" dependencies = [ "bytes", "http-body-util", - "hyper 1.4.0", + "hyper 1.4.1", "hyper-util", "native-tls", "tokio", @@ -1629,29 +1580,28 @@ dependencies = [ [[package]] name = "hyper-util" -version = "0.1.6" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ab92f4f49ee4fb4f997c784b7a2e0fa70050211e0b6a287f898c3c9785ca956" +checksum = "41296eb09f183ac68eec06e03cdbea2e759633d4067b2f6552fc2e009bcad08b" dependencies = [ "bytes", "futures-channel", "futures-util", "http 1.1.0", - "http-body 1.0.0", - "hyper 1.4.0", + "http-body 1.0.1", + "hyper 1.4.1", "pin-project-lite", - "socket2 0.5.7", + "socket2", "tokio", - "tower", "tower-service", "tracing", ] [[package]] name = "iana-time-zone" -version = "0.1.60" +version = "0.1.61" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7ffbb5a1b541ea2561f8c41c087286cc091e21e556a4f09a8f6cbf17b69b141" +checksum = "235e081f3925a06703c2d0117ea8b91f042756fd6e7a6e5d901e8ca1a996b220" dependencies = [ "android_system_properties", "core-foundation-sys", @@ -1670,6 +1620,124 @@ dependencies = [ "cc", ] +[[package]] +name = "icu_collections" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db2fa452206ebee18c4b5c2274dbf1de17008e874b4dc4f0aea9d01ca79e4526" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locid" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13acbb8371917fc971be86fc8057c41a64b521c184808a698c02acc242dbf637" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_locid_transform" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01d11ac35de8e40fdeda00d9e1e9d92525f3f9d887cdd7aa81d727596788b54e" +dependencies = [ + "displaydoc", + "icu_locid", + "icu_locid_transform_data", + "icu_provider", + "tinystr", + "zerovec", +] + +[[package]] +name = "icu_locid_transform_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdc8ff3388f852bede6b579ad4e978ab004f139284d7b28715f773507b946f6e" + +[[package]] +name = "icu_normalizer" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19ce3e0da2ec68599d193c93d088142efd7f9c5d6fc9b803774855747dc6a84f" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "utf16_iter", + "utf8_iter", + "write16", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8cafbf7aa791e9b22bec55a167906f9e1215fd475cd22adfcf660e03e989516" + +[[package]] +name = "icu_properties" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93d6020766cfc6302c15dbbc9c8778c37e62c14427cb7f6e601d849e092aeef5" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_locid_transform", + "icu_properties_data", + "icu_provider", + "tinystr", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67a8effbc3dd3e4ba1afa8ad918d5684b8868b3b26500753effea8d2eed19569" + +[[package]] +name = "icu_provider" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ed421c8a8ef78d3e2dbc98a973be2f3770cb42b606e3ab18d6237c4dfde68d9" +dependencies = [ + "displaydoc", + "icu_locid", + "icu_provider_macros", + "stable_deref_trait", + "tinystr", + "writeable", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_provider_macros" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "ident_case" version = "1.0.1" @@ -1706,14 +1774,26 @@ dependencies = [ "unicode-normalization", ] +[[package]] +name = "idna" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd69211b9b519e98303c015e21a007e293db403b6c85b9b124e133d25e242cdd" +dependencies = [ + "icu_normalizer", + "icu_properties", + "smallvec", + "utf8_iter", +] + [[package]] name = "indexmap" -version = "2.2.6" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "168fb715dda47215e360912c096649d23d58bf392ac62f73919e831745e40f26" +checksum = "707907fe3c25f5424cce2cb7e1cbcafee6bdbe735ca90ef77c29e84591e5b9da" dependencies = [ "equivalent", - "hashbrown", + "hashbrown 0.15.0", "serde", ] @@ -1723,51 +1803,31 @@ version = "0.1.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c8fae54786f62fb2918dcfae3d568594e50eb9b5c25bf04371af6fe7516452fb" -[[package]] -name = "instant" -version = "0.1.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "io-lifetimes" -version = "1.0.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eae7b9aee968036d54dce06cebaefd919e4472e753296daccd6d344e3e2df0c2" -dependencies = [ - "hermit-abi 0.3.9", - "libc", - "windows-sys 0.48.0", -] - [[package]] name = "ipconfig" version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b58db92f96b720de98181bbbe63c831e87005ab460c1bf306eb2622b4707997f" dependencies = [ - "socket2 0.5.7", + "socket2", "widestring", "windows-sys 0.48.0", - "winreg 0.50.0", + "winreg", ] [[package]] name = "ipnet" -version = "2.9.0" +version = "2.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3" +checksum = "ddc24109865250148c2e0f3d25d4f0f479571723792d3802153c60922a4fb708" [[package]] name = "is-terminal" -version = "0.4.12" +version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f23ff5ef2b80d608d61efee834934d862cd92461afc0560dedf493e4c033738b" +checksum = "261f68e344040fbd0edea105bef17c66edf46f984ddb1115b775ce31be948f4b" dependencies = [ - "hermit-abi 0.3.9", + "hermit-abi 0.4.0", "libc", "windows-sys 0.52.0", ] @@ -1797,9 +1857,9 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.69" +version = "0.3.72" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29c15563dc2726973df627357ce0c9ddddbea194836909d655df6a75d2cf296d" +checksum = "6a88f1bda2bd75b0452a14784937d796722fdebfe50df998aeb3f0b7603019a9" dependencies = [ "wasm-bindgen", ] @@ -1836,9 +1896,9 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] name = "lettre" -version = "0.11.7" +version = "0.11.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a62049a808f1c4e2356a2a380bd5f2aca3b011b0b482cf3b914ba1731426969" +checksum = "69f204773bab09b150320ea1c83db41dc6ee606a4bc36dc1f43005fe7b58ce06" dependencies = [ "async-std", "async-trait", @@ -1846,19 +1906,19 @@ dependencies = [ "chumsky", "email-encoding", "email_address", - "fastrand 2.1.0", + "fastrand", "futures-io", "futures-util", "hostname 0.4.0", "httpdate", - "idna 0.5.0", + "idna 1.0.2", "mime", "native-tls", "nom", "percent-encoding", "quoted_printable", "serde", - "socket2 0.5.7", + "socket2", "tokio", "tokio-native-tls", "tracing", @@ -1867,9 +1927,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.155" +version = "0.2.159" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" +checksum = "561d97a539a36e26a9a5fad1ea11a3039a67714694aaa379433e580854bc3dc5" [[package]] name = "libm" @@ -1889,9 +1949,9 @@ dependencies = [ [[package]] name = "libsqlite3-sys" -version = "0.28.0" +version = "0.30.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c10584274047cb335c23d3e61bcef8e323adae7c5c8c760540f73610177fc3f" +checksum = "2e99fb7a497b1e3339bc746195567ed8d3e24945ecd636e3619d20b9de9e9149" dependencies = [ "cc", "pkg-config", @@ -1906,15 +1966,15 @@ checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" [[package]] name = "linux-raw-sys" -version = "0.3.8" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef53942eb7bf7ff43a617b3e2c1c4a5ecf5944a7c1bc12d7ee39bbb15e5c1519" +checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" [[package]] -name = "linux-raw-sys" -version = "0.4.14" +name = "litemap" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" +checksum = "643cb0b8d4fcc284004d5fd0d67ccf61dfffadb7f75e1e71bc420f4688a3a704" [[package]] name = "lock_api" @@ -2024,22 +2084,23 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] name = "miniz_oxide" -version = "0.7.4" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8a240ddb74feaf34a79a7add65a741f3167852fba007066dcac1ca548d89c08" +checksum = "e2d80299ef12ff69b16a84bb182e3b9df68b5a91574d3d4fa6e41b65deec4df1" dependencies = [ - "adler", + "adler2", ] [[package]] name = "mio" -version = "0.8.11" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" +checksum = "80e04d1dcff3aae0704555fe5fee3bcfaf3d1fdf8a7e521d5b9d2b42acb52cec" dependencies = [ + "hermit-abi 0.3.9", "libc", "wasi", - "windows-sys 0.48.0", + "windows-sys 0.52.0", ] [[package]] @@ -2063,9 +2124,9 @@ dependencies = [ [[package]] name = "mysqlclient-sys" -version = "0.4.0" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2aa3a303b6e9878b34811838301b00a56878693c47f9ac0ba397f91adc7bf12" +checksum = "478e2040dbc35c73927b77a2be91a496de19deab376a6982ed61e89592434619" dependencies = [ "pkg-config", "vcpkg", @@ -2186,24 +2247,24 @@ dependencies = [ [[package]] name = "object" -version = "0.36.1" +version = "0.36.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "081b846d1d56ddfc18fdf1a922e4f6e07a11768ea1b92dec44e42b72712ccfce" +checksum = "aedf0a2d09c573ed1d8d85b30c119153926a2b36dce0ab28322c09a117a4683e" dependencies = [ "memchr", ] [[package]] name = "once_cell" -version = "1.19.0" +version = "1.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" +checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" [[package]] name = "openssl" -version = "0.10.64" +version = "0.10.66" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95a0481286a310808298130d22dd1fef0fa571e05a8f44ec801801e84b216b1f" +checksum = "9529f4786b70a3e8c61e11179af17ab6188ad8d0ded78c5529441ed39d4bd9c1" dependencies = [ "bitflags 2.6.0", "cfg-if", @@ -2233,18 +2294,18 @@ checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" [[package]] name = "openssl-src" -version = "300.3.1+3.3.1" +version = "300.3.2+3.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7259953d42a81bf137fbbd73bd30a8e1914d6dce43c2b90ed575783a22608b91" +checksum = "a211a18d945ef7e648cc6e0058f4c548ee46aab922ea203e0d30e966ea23647b" dependencies = [ "cc", ] [[package]] name = "openssl-sys" -version = "0.9.102" +version = "0.9.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c597637d56fbc83893a35eb0dd04b2b8e7a50c91e64e9493e398b5df4fb45fa2" +checksum = "7f9e8deee91df40a943c71b917e5874b951d32a802526c85721ce3b776c929d6" dependencies = [ "cc", "libc", @@ -2261,9 +2322,9 @@ checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" [[package]] name = "parking" -version = "2.2.0" +version = "2.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb813b8af86854136c6922af0598d719255ecb2179515e6e7730d468f05c9cae" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" [[package]] name = "parking_lot" @@ -2355,9 +2416,9 @@ checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" [[package]] name = "pest" -version = "2.7.11" +version = "2.7.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd53dff83f26735fdc1ca837098ccf133605d794cdae66acfc2bfac3ec809d95" +checksum = "fdbef9d1d47087a895abd220ed25eb4ad973a5e26f6a4367b038c25e28dfc2d9" dependencies = [ "memchr", "thiserror", @@ -2366,9 +2427,9 @@ dependencies = [ [[package]] name = "pest_derive" -version = "2.7.11" +version = "2.7.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a548d2beca6773b1c244554d36fcf8548a8a58e74156968211567250e48e49a" +checksum = "4d3a6e3394ec80feb3b6393c725571754c6188490265c61aaf260810d6b95aa0" dependencies = [ "pest", "pest_generator", @@ -2376,9 +2437,9 @@ dependencies = [ [[package]] name = "pest_generator" -version = "2.7.11" +version = "2.7.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c93a82e8d145725dcbaf44e5ea887c8a869efdcc28706df2d08c69e17077183" +checksum = "94429506bde1ca69d1b5601962c73f4172ab4726571a59ea95931218cb0e930e" dependencies = [ "pest", "pest_meta", @@ -2389,9 +2450,9 @@ dependencies = [ [[package]] name = "pest_meta" -version = "2.7.11" +version = "2.7.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a941429fea7e08bedec25e4f6785b6ffaacc6b755da98df5ef3e7dcf4a124c4f" +checksum = "ac8a071862e93690b6e34e9a5fb8e33ff3734473ac0245b27232222c4906a33f" dependencies = [ "once_cell", "pest", @@ -2442,26 +2503,6 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5be167a7af36ee22fe3115051bc51f6e6c7054c9348e28deb4f49bd6f705a315" -[[package]] -name = "pin-project" -version = "1.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6bf43b791c5b9e34c3d182969b4abb522f9343702850a2e57f460d00d09b4b3" -dependencies = [ - "pin-project-internal", -] - -[[package]] -name = "pin-project-internal" -version = "1.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "pin-project-lite" version = "0.2.14" @@ -2476,57 +2517,41 @@ checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" [[package]] name = "piper" -version = "0.2.3" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae1d5c74c9876f070d3e8fd503d748c7d974c3e48da8f41350fa5222ef9b4391" +checksum = "96c8c490f422ef9a4efd2cb5b42b76c8613d7e7dfc1caf667b8a3350a5acc066" dependencies = [ "atomic-waker", - "fastrand 2.1.0", + "fastrand", "futures-io", ] [[package]] name = "pkg-config" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" +checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2" [[package]] name = "polling" -version = "2.8.0" +version = "3.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b2d323e8ca7996b3e23126511a523f7e62924d93ecd5ae73b333815b0eb3dce" -dependencies = [ - "autocfg", - "bitflags 1.3.2", - "cfg-if", - "concurrent-queue", - "libc", - "log", - "pin-project-lite", - "windows-sys 0.48.0", -] - -[[package]] -name = "polling" -version = "3.7.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3ed00ed3fbf728b5816498ecd316d1716eecaced9c0c8d2c5a6740ca214985b" +checksum = "cc2790cd301dec6cd3b7a025e4815cf825724a51c98dccfe6a3e55f05ffb6511" dependencies = [ "cfg-if", "concurrent-queue", "hermit-abi 0.4.0", "pin-project-lite", - "rustix 0.38.34", + "rustix", "tracing", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] name = "portable-atomic" -version = "1.6.0" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7170ef9988bc169ba16dd36a7fa041e5c4cbeb6a35b76d4c03daded371eae7c0" +checksum = "cc9c68a3f6da06753e9335d63e27f6b9754dd1920d941135b7ea8224f141adb2" [[package]] name = "powerfmt" @@ -2536,24 +2561,27 @@ checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" [[package]] name = "ppv-lite86" -version = "0.2.17" +version = "0.2.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" +checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" +dependencies = [ + "zerocopy", +] [[package]] name = "pq-sys" -version = "0.6.1" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a24ff9e4cf6945c988f0db7005d87747bf72864965c3529d259ad155ac41d584" +checksum = "f6cc05d7ea95200187117196eee9edd0644424911821aeb28a18ce60ea0b8793" dependencies = [ "vcpkg", ] [[package]] name = "proc-macro2" -version = "1.0.86" +version = "1.0.87" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" +checksum = "b3e4daa0dcf6feba26f985457cdf104d4b4256fc5a09547140f3631bb076b19a" dependencies = [ "unicode-ident", ] @@ -2579,9 +2607,9 @@ checksum = "33cb294fe86a74cbcf50d4445b37da762029549ebeea341421c7c70370f86cac" [[package]] name = "psm" -version = "0.1.21" +version = "0.1.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5787f7cda34e3033a72192c018bc5883100330f362ef279a8cbccfce8bb4e874" +checksum = "aa37f80ca58604976033fae9515a8a2989fc13797d953f7c04fb8fa36a11f205" dependencies = [ "cc", ] @@ -2619,18 +2647,18 @@ checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" [[package]] name = "quote" -version = "1.0.36" +version = "1.0.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" +checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" dependencies = [ "proc-macro2", ] [[package]] name = "quoted_printable" -version = "0.5.0" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79ec282e887b434b68c18fe5c121d38e72a5cf35119b59e54ec5b992ea9c8eb0" +checksum = "640c9bd8497b02465aeef5375144c26062e0dcd5939dfcbb0f5db76cb8c17c73" [[package]] name = "r2d2" @@ -2675,18 +2703,18 @@ dependencies = [ [[package]] name = "raw-cpuid" -version = "11.0.2" +version = "11.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e29830cbb1290e404f24c73af91c5d8d631ce7e128691e9477556b540cd01ecd" +checksum = "1ab240315c661615f2ee9f0f2cd32d5a7343a84d5ebcccb99d46e6637565e7b0" dependencies = [ "bitflags 2.6.0", ] [[package]] name = "redox_syscall" -version = "0.5.2" +version = "0.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c82cf8cff14456045f55ec4241383baeff27af886adb72ffb2162f99911de0fd" +checksum = "9b6dfecf2c74bce2466cabf93f6664d6998a69eb21e39f4207930065b27b771f" dependencies = [ "bitflags 2.6.0", ] @@ -2713,14 +2741,14 @@ dependencies = [ [[package]] name = "regex" -version = "1.10.5" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b91213439dad192326a0d7c6ee3955910425f441d7038e0d6933b0aec5c4517f" +checksum = "38200e5ee88914975b69f657f0801b6f6dccafd44fd9326302a4aaeecfacb1d8" dependencies = [ "aho-corasick", "memchr", - "regex-automata 0.4.7", - "regex-syntax 0.8.4", + "regex-automata 0.4.8", + "regex-syntax 0.8.5", ] [[package]] @@ -2734,13 +2762,13 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.7" +version = "0.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38caf58cc5ef2fed281f89292ef23f6365465ed9a41b7a7754eb4e26496c92df" +checksum = "368758f23274712b504848e9d5a6f010445cc8b87a7cdb4d7cbee666c1288da3" dependencies = [ "aho-corasick", "memchr", - "regex-syntax 0.8.4", + "regex-syntax 0.8.5", ] [[package]] @@ -2751,9 +2779,9 @@ checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" [[package]] name = "regex-syntax" -version = "0.8.4" +version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b" +checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" [[package]] name = "reopen" @@ -2780,7 +2808,7 @@ dependencies = [ "h2 0.3.26", "http 0.2.12", "http-body 0.4.6", - "hyper 0.14.29", + "hyper 0.14.30", "hyper-tls 0.5.0", "ipnet", "js-sys", @@ -2795,7 +2823,7 @@ dependencies = [ "serde_json", "serde_urlencoded", "sync_wrapper 0.1.2", - "system-configuration", + "system-configuration 0.5.1", "tokio", "tokio-native-tls", "tower-service", @@ -2803,14 +2831,14 @@ dependencies = [ "wasm-bindgen", "wasm-bindgen-futures", "web-sys", - "winreg 0.50.0", + "winreg", ] [[package]] name = "reqwest" -version = "0.12.5" +version = "0.12.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7d6d2a27d57148378eb5e111173f4276ad26340ecc5c49a4a2152167a2d6a37" +checksum = "f713147fbe92361e52392c73b8c9e48c04c6625bce969ef54dc901e58e042a7b" dependencies = [ "async-compression", "base64 0.22.1", @@ -2820,11 +2848,11 @@ dependencies = [ "encoding_rs", "futures-core", "futures-util", - "h2 0.4.5", + "h2 0.4.6", "http 1.1.0", - "http-body 1.0.0", + "http-body 1.0.1", "http-body-util", - "hyper 1.4.0", + "hyper 1.4.1", "hyper-rustls", "hyper-tls 0.6.0", "hyper-util", @@ -2836,12 +2864,12 @@ dependencies = [ "once_cell", "percent-encoding", "pin-project-lite", - "rustls-pemfile 2.1.2", + "rustls-pemfile 2.2.0", "serde", "serde_json", "serde_urlencoded", "sync_wrapper 1.0.1", - "system-configuration", + "system-configuration 0.6.1", "tokio", "tokio-native-tls", "tokio-socks", @@ -2852,7 +2880,7 @@ dependencies = [ "wasm-bindgen-futures", "wasm-streams", "web-sys", - "winreg 0.52.0", + "windows-registry", ] [[package]] @@ -2966,7 +2994,7 @@ dependencies = [ "either", "futures", "http 0.2.12", - "hyper 0.14.29", + "hyper 0.14.30", "indexmap", "log", "memchr", @@ -3025,28 +3053,14 @@ checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" [[package]] name = "rustix" -version = "0.37.27" +version = "0.38.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fea8ca367a3a01fe35e6943c400addf443c0f57670e6ec51196f71a4b8762dd2" -dependencies = [ - "bitflags 1.3.2", - "errno", - "io-lifetimes", - "libc", - "linux-raw-sys 0.3.8", - "windows-sys 0.48.0", -] - -[[package]] -name = "rustix" -version = "0.38.34" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70dc5ec042f7a43c4a73241207cecc9873a06d45debb38b329f8541d85c2730f" +checksum = "8acb788b847c24f28525660c4d7758620a7210875711f79e7f663cc152726811" dependencies = [ "bitflags 2.6.0", "errno", "libc", - "linux-raw-sys 0.4.14", + "linux-raw-sys", "windows-sys 0.52.0", ] @@ -3064,13 +3078,13 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.11" +version = "0.23.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4828ea528154ae444e5a642dbb7d5623354030dc9822b83fd9bb79683c7399d0" +checksum = "415d9944693cb90382053259f89fbb077ea730ad7273047ec63b19bc9b160ba8" dependencies = [ "once_cell", "rustls-pki-types", - "rustls-webpki 0.102.5", + "rustls-webpki 0.102.8", "subtle", "zeroize", ] @@ -3086,19 +3100,18 @@ dependencies = [ [[package]] name = "rustls-pemfile" -version = "2.1.2" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29993a25686778eb88d4189742cd713c9bce943bc54251a33509dc63cbacf73d" +checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50" dependencies = [ - "base64 0.22.1", "rustls-pki-types", ] [[package]] name = "rustls-pki-types" -version = "1.7.0" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "976295e77ce332211c0d24d92c0e83e50f5c5f046d11082cea19f3df13a3562d" +checksum = "0e696e35370c65c9c541198af4543ccd580cf17fc25d8e05c5a242b202488c55" [[package]] name = "rustls-webpki" @@ -3112,9 +3125,9 @@ dependencies = [ [[package]] name = "rustls-webpki" -version = "0.102.5" +version = "0.102.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9a6fccd794a42c2c105b513a2f62bc3fd8f3ba57a4593677ceb0bd035164d78" +checksum = "64ca1bc8749bd4cf37b5ce386cc146580777b4e8572c7b97baf22c83f444bee9" dependencies = [ "ring", "rustls-pki-types", @@ -3144,11 +3157,11 @@ dependencies = [ [[package]] name = "schannel" -version = "0.1.23" +version = "0.1.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fbc91545643bcf3a0bbb6569265615222618bdf33ce4ffbbd13c4bbd4c093534" +checksum = "01227be5826fa0690321a2ba6c5cd57a19cf3f6a09e76973b58e61de6ab9d1c1" dependencies = [ - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -3184,9 +3197,9 @@ dependencies = [ [[package]] name = "security-framework" -version = "2.11.0" +version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c627723fd09706bacdb5cf41499e95098555af3c3c29d014dc3c458ef6be11c0" +checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" dependencies = [ "bitflags 2.6.0", "core-foundation", @@ -3197,9 +3210,9 @@ dependencies = [ [[package]] name = "security-framework-sys" -version = "2.11.0" +version = "2.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "317936bbbd05227752583946b9e66d7ce3b489f84e11a94a510b4437fef407d7" +checksum = "ea4a292869320c0272d7bc55a5a6aafaff59b4f63404a003887b679a2e05b4b6" dependencies = [ "core-foundation-sys", "libc", @@ -3213,9 +3226,9 @@ checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b" [[package]] name = "serde" -version = "1.0.204" +version = "1.0.210" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc76f558e0cbb2a839d37354c575f1dc3fdc6546b5be373ba43d95f231bf7c12" +checksum = "c8e3592472072e6e22e0a54d5904d9febf8508f65fb8552499a1abc7d1078c3a" dependencies = [ "serde_derive", ] @@ -3232,9 +3245,9 @@ dependencies = [ [[package]] name = "serde_derive" -version = "1.0.204" +version = "1.0.210" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0cd7e117be63d3c3678776753929474f3b04a43a080c744d6b0ae2a8c28e222" +checksum = "243902eda00fad750862fc144cea25caca5e20d615af0a81bee94ca738f1df1f" dependencies = [ "proc-macro2", "quote", @@ -3243,20 +3256,21 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.120" +version = "1.0.128" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e0d21c9a8cae1235ad58a00c11cb40d4b1e5c784f1ef2c537876ed6ffd8b7c5" +checksum = "6ff5456707a1de34e7e37f2a6fd3d3f808c318259cbd01ab6377795054b483d8" dependencies = [ "itoa", + "memchr", "ryu", "serde", ] [[package]] name = "serde_spanned" -version = "0.6.6" +version = "0.6.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79e674e01f999af37c49f70a6ede167a8a60b2503e56c5599532a65baa5969a0" +checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1" dependencies = [ "serde", ] @@ -3304,6 +3318,12 @@ dependencies = [ "lazy_static", ] +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + [[package]] name = "signal-hook" version = "0.3.17" @@ -3356,16 +3376,6 @@ version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" -[[package]] -name = "socket2" -version = "0.4.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f7916fc008ca5542385b89a3d3ce689953c143e9304a9bf8beec1de48994c0d" -dependencies = [ - "libc", - "winapi", -] - [[package]] name = "socket2" version = "0.5.7" @@ -3400,17 +3410,23 @@ dependencies = [ "memchr", ] +[[package]] +name = "stable_deref_trait" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" + [[package]] name = "stacker" -version = "0.1.15" +version = "0.1.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c886bd4480155fd3ef527d45e9ac8dd7118a898a46530b7b94c3e21866259fce" +checksum = "799c883d55abdb5e98af1a7b3f23b9b6de8ecada0ecac058672d7635eb48ca7b" dependencies = [ "cc", "cfg-if", "libc", "psm", - "winapi", + "windows-sys 0.59.0", ] [[package]] @@ -3436,9 +3452,9 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "syn" -version = "2.0.70" +version = "2.0.79" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f0209b68b3613b093e0ec905354eccaedcfe83b8cb37cbdeae64026c3064c16" +checksum = "89132cd0bf050864e1d38dc3bbc07a0eb8e7530af26344d3d2bbbef83499f590" dependencies = [ "proc-macro2", "quote", @@ -3456,6 +3472,20 @@ name = "sync_wrapper" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7065abeca94b6a8a577f9bd45aa0867a2238b74e8eb67cf10d492bc39351394" +dependencies = [ + "futures-core", +] + +[[package]] +name = "synstructure" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] [[package]] name = "syslog" @@ -3478,7 +3508,18 @@ checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7" dependencies = [ "bitflags 1.3.2", "core-foundation", - "system-configuration-sys", + "system-configuration-sys 0.5.0", +] + +[[package]] +name = "system-configuration" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" +dependencies = [ + "bitflags 2.6.0", + "core-foundation", + "system-configuration-sys 0.6.0", ] [[package]] @@ -3491,32 +3532,43 @@ dependencies = [ "libc", ] +[[package]] +name = "system-configuration-sys" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "tempfile" -version = "3.10.1" +version = "3.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85b77fafb263dd9d05cbeac119526425676db3784113aa9295c88498cbf8bff1" +checksum = "f0f2c9fc62d0beef6951ccffd757e241266a2c833136efbe35af6cd2567dca5b" dependencies = [ "cfg-if", - "fastrand 2.1.0", - "rustix 0.38.34", - "windows-sys 0.52.0", + "fastrand", + "once_cell", + "rustix", + "windows-sys 0.59.0", ] [[package]] name = "thiserror" -version = "1.0.61" +version = "1.0.64" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c546c80d6be4bc6a00c0f01730c08df82eaa7a7a61f11d656526506112cc1709" +checksum = "d50af8abc119fb8bb6dbabcfa89656f46f84aa0ac7688088608076ad2b459a84" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.61" +version = "1.0.64" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46c3384250002a6d5af4d114f2845d37b57521033f30d5c3f46c4d70e1197533" +checksum = "08904e7672f5eb876eaaf87e0ce17857500934f4981c4a0ab2b4aa98baac7fc3" dependencies = [ "proc-macro2", "quote", @@ -3575,6 +3627,16 @@ dependencies = [ "time-core", ] +[[package]] +name = "tinystr" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f" +dependencies = [ + "displaydoc", + "zerovec", +] + [[package]] name = "tinyvec" version = "1.8.0" @@ -3592,28 +3654,27 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.38.0" +version = "1.40.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba4f4a02a7a80d6f274636f0aa95c7e383b912d41fe721a31f29e29698585a4a" +checksum = "e2b070231665d27ad9ec9b8df639893f46727666c6767db40317fbe920a5d998" dependencies = [ "backtrace", "bytes", "libc", "mio", - "num_cpus", "parking_lot", "pin-project-lite", "signal-hook-registry", - "socket2 0.5.7", + "socket2", "tokio-macros", - "windows-sys 0.48.0", + "windows-sys 0.52.0", ] [[package]] name = "tokio-macros" -version = "2.3.0" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f5ae998a069d4b5aba8ee9dad856af7d520c3699e6159b185c2acd48155d39a" +checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752" dependencies = [ "proc-macro2", "quote", @@ -3646,16 +3707,16 @@ version = "0.26.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c7bc40d0e5a97695bb96e27995cd3a08538541b0a846f65bba7a359f36700d4" dependencies = [ - "rustls 0.23.11", + "rustls 0.23.14", "rustls-pki-types", "tokio", ] [[package]] name = "tokio-socks" -version = "0.5.1" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51165dfa029d2a65969413a6cc96f354b86b464498702f174a4efa13608fd8c0" +checksum = "0d4770b8024672c1101b3f6733eab95b18007dbe0847a8afe341fcf79e06043f" dependencies = [ "either", "futures-util", @@ -3665,9 +3726,9 @@ dependencies = [ [[package]] name = "tokio-stream" -version = "0.1.15" +version = "0.1.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "267ac89e0bec6e691e5813911606935d77c476ff49024f98abcea3e7b15e37af" +checksum = "4f4e6ce100d0eb49a2734f8c0812bcd324cf357d21810932c5df6b96ef2b86f1" dependencies = [ "futures-core", "pin-project-lite", @@ -3688,9 +3749,9 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.11" +version = "0.7.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9cf6b47b3771c49ac75ad09a6162f53ad4b8088b76ac60e8ec1455b31a189fe1" +checksum = "61e7c3654c13bcd040d4a03abee2c75b1d14a37b423cf5a813ceae1cc903ec6a" dependencies = [ "bytes", "futures-core", @@ -3701,9 +3762,9 @@ dependencies = [ [[package]] name = "toml" -version = "0.8.14" +version = "0.8.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f49eb2ab21d2f26bd6db7bf383edc527a7ebaee412d17af4d40fdccd442f335" +checksum = "a1ed1f98e3fdc28d6d910e6737ae6ab1a93bf1985935a1193e68f93eeb68d24e" dependencies = [ "serde", "serde_spanned", @@ -3713,18 +3774,18 @@ dependencies = [ [[package]] name = "toml_datetime" -version = "0.6.6" +version = "0.6.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4badfd56924ae69bcc9039335b2e017639ce3f9b001c393c1b2d1ef846ce2cbf" +checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" dependencies = [ "serde", ] [[package]] name = "toml_edit" -version = "0.22.15" +version = "0.22.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d59a3a72298453f564e2b111fa896f8d07fabb36f51f06d7e875fc5e0b5a3ef1" +checksum = "4ae48d6208a266e853d946088ed816055e556cc6028c5e8e2b84d9fa5dd7c7f5" dependencies = [ "indexmap", "serde", @@ -3745,32 +3806,11 @@ dependencies = [ "sha2", ] -[[package]] -name = "tower" -version = "0.4.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c" -dependencies = [ - "futures-core", - "futures-util", - "pin-project", - "pin-project-lite", - "tokio", - "tower-layer", - "tower-service", -] - -[[package]] -name = "tower-layer" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c20c8dbed6283a09604c3e69b4b7eeb54e298b8a600d4d5ecb5ad39de609f1d0" - [[package]] name = "tower-service" -version = "0.3.2" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" [[package]] name = "tracing" @@ -3876,9 +3916,9 @@ dependencies = [ [[package]] name = "ucd-trie" -version = "0.1.6" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed646292ffc8188ef8ea4d1e0e0150fb15a5c2e12ad9b8fc191ae7a8a7f3c4b9" +checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" [[package]] name = "uncased" @@ -3892,30 +3932,30 @@ dependencies = [ [[package]] name = "unicode-bidi" -version = "0.3.15" +version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08f95100a766bf4f8f28f90d77e0a5461bbdb219042e7679bebe79004fed8d75" +checksum = "5ab17db44d7388991a428b2ee655ce0c212e862eff1768a455c58f9aad6e7893" [[package]] name = "unicode-ident" -version = "1.0.12" +version = "1.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" +checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe" [[package]] name = "unicode-normalization" -version = "0.1.23" +version = "0.1.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a56d1686db2308d901306f92a263857ef59ea39678a5458e7cb17f01415101f5" +checksum = "5033c97c4262335cded6d6fc3e5c18ab755e1a3dc96376350f3d8e9f009ad956" dependencies = [ "tinyvec", ] [[package]] name = "unicode-xid" -version = "0.2.4" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f962df74c8c05a667b5ee8bcf162993134c104e96440b663c8daa176dc772d8c" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" [[package]] name = "untrusted" @@ -3941,11 +3981,23 @@ version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" +[[package]] +name = "utf16_iter" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246" + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + [[package]] name = "uuid" -version = "1.9.1" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5de17fd2f7da591098415cff336e12965a28061ddace43b59cb3c430179c9439" +checksum = "81dfa00651efa65069b0b6b651f4aaa31ba9e3c3ce0137aaad053604ee7e0314" dependencies = [ "getrandom", ] @@ -3974,7 +4026,7 @@ dependencies = [ "chrono-tz", "cookie", "cookie_store", - "dashmap 6.0.1", + "dashmap 6.1.0", "data-encoding", "data-url", "diesel", @@ -4003,7 +4055,7 @@ dependencies = [ "pico-args", "rand", "regex", - "reqwest 0.12.5", + "reqwest 0.12.8", "ring", "rmpv", "rocket", @@ -4032,15 +4084,9 @@ checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" [[package]] name = "version_check" -version = "0.9.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" - -[[package]] -name = "waker-fn" -version = "1.2.0" +version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "317211a0dc0ceedd78fb2ca9a44aed3d7b9b26f81870d485c07122b4350673b7" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" [[package]] name = "walkdir" @@ -4069,19 +4115,20 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasm-bindgen" -version = "0.2.92" +version = "0.2.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4be2531df63900aeb2bca0daaaddec08491ee64ceecbee5076636a3b026795a8" +checksum = "128d1e363af62632b8eb57219c8fd7877144af57558fb2ef0368d0087bddeb2e" dependencies = [ "cfg-if", + "once_cell", "wasm-bindgen-macro", ] [[package]] name = "wasm-bindgen-backend" -version = "0.2.92" +version = "0.2.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "614d787b966d3989fa7bb98a654e369c762374fd3213d212cfc0251257e747da" +checksum = "cb6dd4d3ca0ddffd1dd1c9c04f94b868c37ff5fac97c30b97cff2d74fce3a358" dependencies = [ "bumpalo", "log", @@ -4094,9 +4141,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.42" +version = "0.4.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76bc14366121efc8dbb487ab05bcc9d346b3b5ec0eaa76e46594cabbe51762c0" +checksum = "cc7ec4f8827a71586374db3e87abdb5a2bb3a15afed140221307c3ec06b1f63b" dependencies = [ "cfg-if", "js-sys", @@ -4106,9 +4153,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.92" +version = "0.2.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1f8823de937b71b9460c0c34e25f3da88250760bec0ebac694b49997550d726" +checksum = "e79384be7f8f5a9dd5d7167216f022090cf1f9ec128e6e6a482a2cb5c5422c56" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -4116,9 +4163,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.92" +version = "0.2.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" +checksum = "26c6ab57572f7a24a4985830b120de1594465e5d500f24afe89e16b4e833ef68" dependencies = [ "proc-macro2", "quote", @@ -4129,15 +4176,15 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.92" +version = "0.2.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96" +checksum = "65fc09f10666a9f147042251e0dda9c18f166ff7de300607007e96bdebc1068d" [[package]] name = "wasm-streams" -version = "0.4.0" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b65dc4c90b63b118468cf747d8bf3566c1913ef60be765b5730ead9e0a3ba129" +checksum = "4e072d4e72f700fb3443d8fe94a39315df013eef1104903cdb0a2abd322bbecd" dependencies = [ "futures-util", "js-sys", @@ -4148,9 +4195,19 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.69" +version = "0.3.72" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77afa9a11836342370f4817622a2f0f418b134426d91a82dfb48f532d2ec13ef" +checksum = "f6488b90108c040df0fe62fa815cbdee25124641df01814dd7282749234c6112" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" dependencies = [ "js-sys", "wasm-bindgen", @@ -4177,13 +4234,13 @@ dependencies = [ [[package]] name = "which" -version = "6.0.1" +version = "6.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8211e4f58a2b2805adfbefbc07bab82958fc91e3836339b1ab7ae32465dce0d7" +checksum = "b4ee928febd44d98f2f459a4a79bd4d928591333a494a10a868418ac1b39cf1f" dependencies = [ "either", "home", - "rustix 0.38.34", + "rustix", "winsafe", ] @@ -4211,11 +4268,11 @@ checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" [[package]] name = "winapi-util" -version = "0.1.8" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d4cc384e1e73b93bafa6fb4f1df8c41695c8a91cf9c4c64358067d15a7b6c6b" +checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" dependencies = [ - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -4252,6 +4309,36 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows-registry" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e400001bb720a623c1c69032f8e3e4cf09984deec740f007dd2b03ec864804b0" +dependencies = [ + "windows-result", + "windows-strings", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-result" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d1043d8214f791817bab27572aaa8af63732e11bf84aa21a45a78d6c317ae0e" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-strings" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10" +dependencies = [ + "windows-result", + "windows-targets 0.52.6", +] + [[package]] name = "windows-sys" version = "0.48.0" @@ -4270,6 +4357,15 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + [[package]] name = "windows-targets" version = "0.48.5" @@ -4393,9 +4489,9 @@ checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] name = "winnow" -version = "0.6.13" +version = "0.6.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59b5e5f6c299a3c7890b876a2a587f3115162487e704907d9b6cd29473052ba1" +checksum = "36c1fec1a2bb5866f07c25f68c26e565c4c200aebb96d7e55710c19d3e8ac49b" dependencies = [ "memchr", ] @@ -4410,22 +4506,24 @@ dependencies = [ "windows-sys 0.48.0", ] -[[package]] -name = "winreg" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a277a57398d4bfa075df44f501a17cfdf8542d224f0d36095a2adc7aee4ef0a5" -dependencies = [ - "cfg-if", - "windows-sys 0.48.0", -] - [[package]] name = "winsafe" version = "0.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d135d17ab770252ad95e9a872d365cf3090e3be864a34ab46f48555993efc904" +[[package]] +name = "write16" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1890f4022759daae28ed4fe62859b1236caebfc61ede2f63ed4e695f3f6d936" + +[[package]] +name = "writeable" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" + [[package]] name = "yansi" version = "1.0.1" @@ -4435,6 +4533,30 @@ dependencies = [ "is-terminal", ] +[[package]] +name = "yoke" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c5b1314b079b0930c31e3af543d8ee1757b1951ae1e1565ec704403a7240ca5" +dependencies = [ + "serde", + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28cc31741b18cb6f1d5ff12f5b7523e3d6eb0852bbbad19d73905511d9849b95" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + [[package]] name = "yubico" version = "0.11.0" @@ -4457,6 +4579,7 @@ version = "0.7.35" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" dependencies = [ + "byteorder", "zerocopy-derive", ] @@ -4471,8 +4594,51 @@ dependencies = [ "syn", ] +[[package]] +name = "zerofrom" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91ec111ce797d0e0784a1116d0ddcdbea84322cd79e5d5ad173daeba4f93ab55" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ea7b4a3637ea8669cedf0f1fd5c286a17f3de97b8dd5a70a6c167a1730e63a5" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + [[package]] name = "zeroize" version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" + +[[package]] +name = "zerovec" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa2b893d79df23bfb12d5461018d408ea19dfafe76c2c7ef6d4eba614f8ff079" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] diff --git a/Cargo.toml b/Cargo.toml index 7d266e53..9f1873c7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,7 +3,7 @@ name = "vaultwarden" version = "1.0.0" authors = ["Daniel García "] edition = "2021" -rust-version = "1.78.0" +rust-version = "1.79.0" resolver = "2" repository = "https://github.com/dani-garcia/vaultwarden" @@ -18,23 +18,23 @@ build = "build.rs" enable_syslog = [] mysql = ["diesel/mysql", "diesel_migrations/mysql"] postgresql = ["diesel/postgres", "diesel_migrations/postgres"] -sqlite = ["diesel/sqlite", "diesel_migrations/sqlite", "libsqlite3-sys"] +sqlite = ["diesel/sqlite", "diesel_migrations/sqlite", "dep:libsqlite3-sys"] # Enable to use a vendored and statically linked openssl vendored_openssl = ["openssl/vendored"] # Enable MiMalloc memory allocator to replace the default malloc # This can improve performance for Alpine builds -enable_mimalloc = ["mimalloc"] +enable_mimalloc = ["dep:mimalloc"] # This is a development dependency, and should only be used during development! # It enables the usage of the diesel_logger crate, which is able to output the generated queries. # You also need to set an env variable `QUERY_LOGGER=1` to fully activate this so you do not have to re-compile # if you want to turn off the logging for a specific run. -query_logger = ["diesel_logger"] +query_logger = ["dep:diesel_logger"] # Enable unstable features, requires nightly # Currently only used to enable rusts official ip support unstable = [] -[target."cfg(not(windows))".dependencies] +[target."cfg(unix)".dependencies] # Logging syslog = "6.1.1" @@ -48,7 +48,7 @@ tracing = { version = "0.1.40", features = ["log"] } # Needed to have lettre and dotenvy = { version = "0.15.7", default-features = false } # Lazy initialization -once_cell = "1.19.0" +once_cell = "1.20.2" # Numerical libraries num-traits = "0.2.19" @@ -63,34 +63,34 @@ rocket_ws = { version ="0.1.1" } rmpv = "1.3.0" # MessagePack library # Concurrent HashMap used for WebSocket messaging and favicons -dashmap = "6.0.1" +dashmap = "6.1.0" # Async futures -futures = "0.3.30" -tokio = { version = "1.38.0", features = ["rt-multi-thread", "fs", "io-util", "parking_lot", "time", "signal", "net"] } +futures = "0.3.31" +tokio = { version = "1.40.0", features = ["rt-multi-thread", "fs", "io-util", "parking_lot", "time", "signal", "net"] } # A generic serialization/deserialization framework -serde = { version = "1.0.204", features = ["derive"] } -serde_json = "1.0.120" +serde = { version = "1.0.210", features = ["derive"] } +serde_json = "1.0.128" # A safe, extensible ORM and Query builder -diesel = { version = "2.2.1", features = ["chrono", "r2d2", "numeric"] } +diesel = { version = "2.2.4", features = ["chrono", "r2d2", "numeric"] } diesel_migrations = "2.2.0" diesel_logger = { version = "0.3.0", optional = true } # Bundled/Static SQLite -libsqlite3-sys = { version = "0.28.0", features = ["bundled"], optional = true } +libsqlite3-sys = { version = "0.30.1", features = ["bundled"], optional = true } # Crypto-related libraries rand = { version = "0.8.5", features = ["small_rng"] } ring = "0.17.8" # UUID generation -uuid = { version = "1.9.1", features = ["v4"] } +uuid = { version = "1.10.0", features = ["v4"] } # Date and time libraries chrono = { version = "0.4.38", features = ["clock", "serde"], default-features = false } -chrono-tz = "0.9.0" +chrono-tz = "0.10.0" time = "0.3.36" # Job scheduler @@ -115,32 +115,32 @@ webauthn-rs = "0.3.2" url = "2.5.2" # Email libraries -lettre = { version = "0.11.7", features = ["smtp-transport", "sendmail-transport", "builder", "serde", "tokio1-native-tls", "hostname", "tracing", "tokio1"], default-features = false } +lettre = { version = "0.11.9", features = ["smtp-transport", "sendmail-transport", "builder", "serde", "tokio1-native-tls", "hostname", "tracing", "tokio1"], default-features = false } percent-encoding = "2.3.1" # URL encoding library used for URL's in the emails -email_address = "0.2.5" +email_address = "0.2.9" # HTML Template library -handlebars = { version = "5.1.2", features = ["dir_source"] } +handlebars = { version = "6.1.0", features = ["dir_source"] } # HTTP client (Used for favicons, version check, DUO and HIBP API) -reqwest = { version = "0.12.5", features = ["native-tls-alpn", "stream", "json", "gzip", "brotli", "socks", "cookies"] } +reqwest = { version = "0.12.8", features = ["native-tls-alpn", "stream", "json", "gzip", "brotli", "socks", "cookies"] } hickory-resolver = "0.24.1" # Favicon extraction libraries html5gum = "0.5.7" -regex = { version = "1.10.5", features = ["std", "perf", "unicode-perl"], default-features = false } +regex = { version = "1.11.0", features = ["std", "perf", "unicode-perl"], default-features = false } data-url = "0.3.1" -bytes = "1.6.0" +bytes = "1.7.2" # Cache function results (Used for version check and favicon fetching) -cached = { version = "0.52.0", features = ["async"] } +cached = { version = "0.53.1", features = ["async"] } # Used for custom short lived cookie jar during favicon extraction cookie = "0.18.1" cookie_store = "0.21.0" # Used by U2F, JWT and PostgreSQL -openssl = "0.10.64" +openssl = "0.10.66" # CLI argument parsing pico-args = "0.5.0" @@ -155,7 +155,7 @@ semver = "1.0.23" # Allow overriding the default memory allocator # Mainly used for the musl builds, since the default musl malloc is very slow mimalloc = { version = "0.1.43", features = ["secure"], default-features = false, optional = true } -which = "6.0.1" +which = "6.0.3" # Argon2 library with support for the PHC format argon2 = "0.5.3" @@ -198,33 +198,46 @@ lto = "thin" codegen-units = 16 # Linting config +# https://doc.rust-lang.org/rustc/lints/groups.html [lints.rust] # Forbid unsafe_code = "forbid" non_ascii_idents = "forbid" # Deny +deprecated_in_future = "deny" future_incompatible = { level = "deny", priority = -1 } +keyword_idents = { level = "deny", priority = -1 } +let_underscore = { level = "deny", priority = -1 } noop_method_call = "deny" +refining_impl_trait = { level = "deny", priority = -1 } rust_2018_idioms = { level = "deny", priority = -1 } rust_2021_compatibility = { level = "deny", priority = -1 } +# rust_2024_compatibility = { level = "deny", priority = -1 } # Enable once we are at MSRV 1.81.0 +single_use_lifetimes = "deny" trivial_casts = "deny" trivial_numeric_casts = "deny" unused = { level = "deny", priority = -1 } unused_import_braces = "deny" unused_lifetimes = "deny" -deprecated_in_future = "deny" +unused_qualifications = "deny" +variant_size_differences = "deny" +# The lints below are part of the rust_2024_compatibility group +static-mut-refs = "deny" +unsafe-op-in-unsafe-fn = "deny" +# https://rust-lang.github.io/rust-clippy/stable/index.html [lints.clippy] -# Allow -# We need this since Rust v1.76+, since it has some bugs -# https://github.com/rust-lang/rust-clippy/issues/12016 -blocks_in_conditions = "allow" +# Warn +dbg_macro = "warn" +todo = "warn" # Deny +case_sensitive_file_extension_comparisons = "deny" cast_lossless = "deny" clone_on_ref_ptr = "deny" equatable_if_let = "deny" +filter_map_next = "deny" float_cmp_const = "deny" inefficient_to_string = "deny" iter_on_empty_collections = "deny" @@ -234,13 +247,18 @@ macro_use_imports = "deny" manual_assert = "deny" manual_instant_elapsed = "deny" manual_string_new = "deny" +match_on_vec_items = "deny" match_wildcard_for_single_variants = "deny" mem_forget = "deny" +needless_continue = "deny" needless_lifetimes = "deny" +option_option = "deny" string_add_assign = "deny" string_to_string = "deny" unnecessary_join = "deny" unnecessary_self_imports = "deny" +unnested_or_patterns = "deny" unused_async = "deny" +unused_self = "deny" verbose_file_reads = "deny" zero_sized_map_values = "deny" diff --git a/SECURITY.md b/SECURITY.md index 95d87b78..0917981c 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -39,7 +39,11 @@ Thank you for helping keep Vaultwarden and our users safe! # How to contact us -- You can contact us on Matrix https://matrix.to/#/#vaultwarden:matrix.org (user: `@danig:matrix.org`) -- You can send an ![security-contact](/.github/security-contact.gif) to report a security issue. - - If you want to send an encrypted email you can use the following GPG key:
- https://keyserver.ubuntu.com/pks/lookup?search=0xB9B7A108373276BF3C0406F9FC8A7D14C3CD543A&fingerprint=on&op=index +- You can contact us on Matrix https://matrix.to/#/#vaultwarden:matrix.org (users: `@danig:matrix.org` and/or `@blackdex:matrix.org`) +- You can send an ![security-contact](/.github/security-contact.gif) to report a security issue.
+ If you want to send an encrypted email you can use the following GPG key: 13BB3A34C9E380258CE43D595CB150B31F6426BC
+ It can be found on several public GPG key servers.
+ * https://keys.openpgp.org/search?q=security%40vaultwarden.org + * https://keys.mailvelope.com/pks/lookup?op=get&search=security%40vaultwarden.org + * https://pgpkeys.eu/pks/lookup?search=security%40vaultwarden.org&fingerprint=on&op=index + * https://keyserver.ubuntu.com/pks/lookup?search=security%40vaultwarden.org&fingerprint=on&op=index diff --git a/docker/DockerSettings.yaml b/docker/DockerSettings.yaml index 807a6632..9d91b4c6 100644 --- a/docker/DockerSettings.yaml +++ b/docker/DockerSettings.yaml @@ -1,10 +1,11 @@ --- -vault_version: "v2024.5.1b" -vault_image_digest: "sha256:1a867b4b175e85fc8602314bd83bc263c76c49787031704f16a2915567725375" -# Cross Compile Docker Helper Scripts v1.4.0 +vault_version: "v2024.6.2c" +vault_image_digest: "sha256:409ab328ca931439cb916b388a4bb784bd44220717aaf74cf71620c23e34fc2b" +# Cross Compile Docker Helper Scripts v1.5.0 # We use the linux/amd64 platform shell scripts since there is no difference between the different platform scripts -xx_image_digest: "sha256:0cd3f05c72d6c9b038eb135f91376ee1169ef3a330d34e418e65e2a5c2e9c0d4" -rust_version: 1.79.0 # Rust version to be used +# https://github.com/tonistiigi/xx | https://hub.docker.com/r/tonistiigi/xx/tags +xx_image_digest: "sha256:1978e7a58a1777cb0ef0dde76bad60b7914b21da57cfa88047875e4f364297aa" +rust_version: 1.81.0 # Rust version to be used debian_version: bookworm # Debian release name to be used alpine_version: "3.20" # Alpine version to be used # For which platforms/architectures will we try to build images diff --git a/docker/Dockerfile.alpine b/docker/Dockerfile.alpine index e4a392f8..67b14e39 100644 --- a/docker/Dockerfile.alpine +++ b/docker/Dockerfile.alpine @@ -1,4 +1,5 @@ # syntax=docker/dockerfile:1 +# check=skip=FromPlatformFlagConstDisallowed,RedundantTargetPlatform # This file was generated using a Jinja2 template. # Please make your changes in `DockerSettings.yaml` or `Dockerfile.j2` and then `make` @@ -18,27 +19,27 @@ # - From https://hub.docker.com/r/vaultwarden/web-vault/tags, # click the tag name to view the digest of the image it currently points to. # - From the command line: -# $ docker pull docker.io/vaultwarden/web-vault:v2024.5.1b -# $ docker image inspect --format "{{.RepoDigests}}" docker.io/vaultwarden/web-vault:v2024.5.1b -# [docker.io/vaultwarden/web-vault@sha256:1a867b4b175e85fc8602314bd83bc263c76c49787031704f16a2915567725375] +# $ docker pull docker.io/vaultwarden/web-vault:v2024.6.2c +# $ docker image inspect --format "{{.RepoDigests}}" docker.io/vaultwarden/web-vault:v2024.6.2c +# [docker.io/vaultwarden/web-vault@sha256:409ab328ca931439cb916b388a4bb784bd44220717aaf74cf71620c23e34fc2b] # # - Conversely, to get the tag name from the digest: -# $ docker image inspect --format "{{.RepoTags}}" docker.io/vaultwarden/web-vault@sha256:1a867b4b175e85fc8602314bd83bc263c76c49787031704f16a2915567725375 -# [docker.io/vaultwarden/web-vault:v2024.5.1b] +# $ docker image inspect --format "{{.RepoTags}}" docker.io/vaultwarden/web-vault@sha256:409ab328ca931439cb916b388a4bb784bd44220717aaf74cf71620c23e34fc2b +# [docker.io/vaultwarden/web-vault:v2024.6.2c] # -FROM --platform=linux/amd64 docker.io/vaultwarden/web-vault@sha256:1a867b4b175e85fc8602314bd83bc263c76c49787031704f16a2915567725375 as vault +FROM --platform=linux/amd64 docker.io/vaultwarden/web-vault@sha256:409ab328ca931439cb916b388a4bb784bd44220717aaf74cf71620c23e34fc2b AS vault ########################## ALPINE BUILD IMAGES ########################## ## NOTE: The Alpine Base Images do not support other platforms then linux/amd64 ## And for Alpine we define all build images here, they will only be loaded when actually used -FROM --platform=linux/amd64 ghcr.io/blackdex/rust-musl:x86_64-musl-stable-1.79.0 as build_amd64 -FROM --platform=linux/amd64 ghcr.io/blackdex/rust-musl:aarch64-musl-stable-1.79.0 as build_arm64 -FROM --platform=linux/amd64 ghcr.io/blackdex/rust-musl:armv7-musleabihf-stable-1.79.0 as build_armv7 -FROM --platform=linux/amd64 ghcr.io/blackdex/rust-musl:arm-musleabi-stable-1.79.0 as build_armv6 +FROM --platform=linux/amd64 ghcr.io/blackdex/rust-musl:x86_64-musl-stable-1.81.0 AS build_amd64 +FROM --platform=linux/amd64 ghcr.io/blackdex/rust-musl:aarch64-musl-stable-1.81.0 AS build_arm64 +FROM --platform=linux/amd64 ghcr.io/blackdex/rust-musl:armv7-musleabihf-stable-1.81.0 AS build_armv7 +FROM --platform=linux/amd64 ghcr.io/blackdex/rust-musl:arm-musleabi-stable-1.81.0 AS build_armv6 ########################## BUILD IMAGE ########################## # hadolint ignore=DL3006 -FROM --platform=linux/amd64 build_${TARGETARCH}${TARGETVARIANT} as build +FROM --platform=linux/amd64 build_${TARGETARCH}${TARGETVARIANT} AS build ARG TARGETARCH ARG TARGETVARIANT ARG TARGETPLATFORM @@ -142,7 +143,6 @@ RUN mkdir /data && \ VOLUME /data EXPOSE 80 -EXPOSE 3012 # Copies the files from the context (Rocket.toml file and web-vault) # and the binary from the "build" stage to the current stage diff --git a/docker/Dockerfile.debian b/docker/Dockerfile.debian index 84ae6ff7..f1e52770 100644 --- a/docker/Dockerfile.debian +++ b/docker/Dockerfile.debian @@ -1,4 +1,5 @@ # syntax=docker/dockerfile:1 +# check=skip=FromPlatformFlagConstDisallowed,RedundantTargetPlatform # This file was generated using a Jinja2 template. # Please make your changes in `DockerSettings.yaml` or `Dockerfile.j2` and then `make` @@ -18,24 +19,24 @@ # - From https://hub.docker.com/r/vaultwarden/web-vault/tags, # click the tag name to view the digest of the image it currently points to. # - From the command line: -# $ docker pull docker.io/vaultwarden/web-vault:v2024.5.1b -# $ docker image inspect --format "{{.RepoDigests}}" docker.io/vaultwarden/web-vault:v2024.5.1b -# [docker.io/vaultwarden/web-vault@sha256:1a867b4b175e85fc8602314bd83bc263c76c49787031704f16a2915567725375] +# $ docker pull docker.io/vaultwarden/web-vault:v2024.6.2c +# $ docker image inspect --format "{{.RepoDigests}}" docker.io/vaultwarden/web-vault:v2024.6.2c +# [docker.io/vaultwarden/web-vault@sha256:409ab328ca931439cb916b388a4bb784bd44220717aaf74cf71620c23e34fc2b] # # - Conversely, to get the tag name from the digest: -# $ docker image inspect --format "{{.RepoTags}}" docker.io/vaultwarden/web-vault@sha256:1a867b4b175e85fc8602314bd83bc263c76c49787031704f16a2915567725375 -# [docker.io/vaultwarden/web-vault:v2024.5.1b] +# $ docker image inspect --format "{{.RepoTags}}" docker.io/vaultwarden/web-vault@sha256:409ab328ca931439cb916b388a4bb784bd44220717aaf74cf71620c23e34fc2b +# [docker.io/vaultwarden/web-vault:v2024.6.2c] # -FROM --platform=linux/amd64 docker.io/vaultwarden/web-vault@sha256:1a867b4b175e85fc8602314bd83bc263c76c49787031704f16a2915567725375 as vault +FROM --platform=linux/amd64 docker.io/vaultwarden/web-vault@sha256:409ab328ca931439cb916b388a4bb784bd44220717aaf74cf71620c23e34fc2b AS vault ########################## Cross Compile Docker Helper Scripts ########################## ## We use the linux/amd64 no matter which Build Platform, since these are all bash scripts ## And these bash scripts do not have any significant difference if at all -FROM --platform=linux/amd64 docker.io/tonistiigi/xx@sha256:0cd3f05c72d6c9b038eb135f91376ee1169ef3a330d34e418e65e2a5c2e9c0d4 AS xx +FROM --platform=linux/amd64 docker.io/tonistiigi/xx@sha256:1978e7a58a1777cb0ef0dde76bad60b7914b21da57cfa88047875e4f364297aa AS xx ########################## BUILD IMAGE ########################## # hadolint ignore=DL3006 -FROM --platform=$BUILDPLATFORM docker.io/library/rust:1.79.0-slim-bookworm as build +FROM --platform=$BUILDPLATFORM docker.io/library/rust:1.81.0-slim-bookworm AS build COPY --from=xx / / ARG TARGETARCH ARG TARGETVARIANT @@ -185,7 +186,6 @@ RUN mkdir /data && \ VOLUME /data EXPOSE 80 -EXPOSE 3012 # Copies the files from the context (Rocket.toml file and web-vault) # and the binary from the "build" stage to the current stage diff --git a/docker/Dockerfile.j2 b/docker/Dockerfile.j2 index d71b4ccc..372be95e 100644 --- a/docker/Dockerfile.j2 +++ b/docker/Dockerfile.j2 @@ -1,4 +1,5 @@ # syntax=docker/dockerfile:1 +# check=skip=FromPlatformFlagConstDisallowed,RedundantTargetPlatform # This file was generated using a Jinja2 template. # Please make your changes in `DockerSettings.yaml` or `Dockerfile.j2` and then `make` @@ -26,7 +27,7 @@ # $ docker image inspect --format "{{ '{{' }}.RepoTags}}" docker.io/vaultwarden/web-vault@{{ vault_image_digest }} # [docker.io/vaultwarden/web-vault:{{ vault_version }}] # -FROM --platform=linux/amd64 docker.io/vaultwarden/web-vault@{{ vault_image_digest }} as vault +FROM --platform=linux/amd64 docker.io/vaultwarden/web-vault@{{ vault_image_digest }} AS vault {% if base == "debian" %} ########################## Cross Compile Docker Helper Scripts ########################## @@ -38,13 +39,13 @@ FROM --platform=linux/amd64 docker.io/tonistiigi/xx@{{ xx_image_digest }} AS xx ## NOTE: The Alpine Base Images do not support other platforms then linux/amd64 ## And for Alpine we define all build images here, they will only be loaded when actually used {% for arch in build_stage_image[base].arch_image %} -FROM --platform={{ build_stage_image[base].platform }} {{ build_stage_image[base].arch_image[arch] }} as build_{{ arch }} +FROM --platform={{ build_stage_image[base].platform }} {{ build_stage_image[base].arch_image[arch] }} AS build_{{ arch }} {% endfor %} {% endif %} ########################## BUILD IMAGE ########################## # hadolint ignore=DL3006 -FROM --platform={{ build_stage_image[base].platform }} {{ build_stage_image[base].image }} as build +FROM --platform={{ build_stage_image[base].platform }} {{ build_stage_image[base].image }} AS build {% if base == "debian" %} COPY --from=xx / / {% endif %} @@ -229,7 +230,6 @@ RUN mkdir /data && \ VOLUME /data EXPOSE 80 -EXPOSE 3012 # Copies the files from the context (Rocket.toml file and web-vault) # and the binary from the "build" stage to the current stage diff --git a/docker/start.sh b/docker/start.sh index e9a932e4..4fac4514 100755 --- a/docker/start.sh +++ b/docker/start.sh @@ -1,5 +1,9 @@ #!/bin/sh +if [ -n "${UMASK}" ]; then + umask "${UMASK}" +fi + if [ -r /etc/vaultwarden.sh ]; then . /etc/vaultwarden.sh elif [ -r /etc/bitwarden_rs.sh ]; then diff --git a/migrations/mysql/2024-06-05-131359_add_2fa_duo_store/down.sql b/migrations/mysql/2024-06-05-131359_add_2fa_duo_store/down.sql new file mode 100644 index 00000000..7af867a2 --- /dev/null +++ b/migrations/mysql/2024-06-05-131359_add_2fa_duo_store/down.sql @@ -0,0 +1 @@ +DROP TABLE twofactor_duo_ctx; \ No newline at end of file diff --git a/migrations/mysql/2024-06-05-131359_add_2fa_duo_store/up.sql b/migrations/mysql/2024-06-05-131359_add_2fa_duo_store/up.sql new file mode 100644 index 00000000..29091791 --- /dev/null +++ b/migrations/mysql/2024-06-05-131359_add_2fa_duo_store/up.sql @@ -0,0 +1,8 @@ +CREATE TABLE twofactor_duo_ctx ( + state VARCHAR(64) NOT NULL, + user_email VARCHAR(255) NOT NULL, + nonce VARCHAR(64) NOT NULL, + exp BIGINT NOT NULL, + + PRIMARY KEY (state) +); \ No newline at end of file diff --git a/migrations/mysql/2024-09-04-091351_use_device_type_for_mails/down.sql b/migrations/mysql/2024-09-04-091351_use_device_type_for_mails/down.sql new file mode 100644 index 00000000..dd0394ee --- /dev/null +++ b/migrations/mysql/2024-09-04-091351_use_device_type_for_mails/down.sql @@ -0,0 +1 @@ +ALTER TABLE `twofactor_incomplete` DROP COLUMN `device_type`; diff --git a/migrations/mysql/2024-09-04-091351_use_device_type_for_mails/up.sql b/migrations/mysql/2024-09-04-091351_use_device_type_for_mails/up.sql new file mode 100644 index 00000000..a8fc7dfd --- /dev/null +++ b/migrations/mysql/2024-09-04-091351_use_device_type_for_mails/up.sql @@ -0,0 +1 @@ +ALTER TABLE twofactor_incomplete ADD COLUMN device_type INTEGER NOT NULL DEFAULT 14; -- 14 = Unknown Browser diff --git a/migrations/postgresql/2024-06-05-131359_add_2fa_duo_store/down.sql b/migrations/postgresql/2024-06-05-131359_add_2fa_duo_store/down.sql new file mode 100644 index 00000000..0b5d4cd8 --- /dev/null +++ b/migrations/postgresql/2024-06-05-131359_add_2fa_duo_store/down.sql @@ -0,0 +1 @@ +DROP TABLE twofactor_duo_ctx; diff --git a/migrations/postgresql/2024-06-05-131359_add_2fa_duo_store/up.sql b/migrations/postgresql/2024-06-05-131359_add_2fa_duo_store/up.sql new file mode 100644 index 00000000..ebc8be1b --- /dev/null +++ b/migrations/postgresql/2024-06-05-131359_add_2fa_duo_store/up.sql @@ -0,0 +1,8 @@ +CREATE TABLE twofactor_duo_ctx ( + state VARCHAR(64) NOT NULL, + user_email VARCHAR(255) NOT NULL, + nonce VARCHAR(64) NOT NULL, + exp BIGINT NOT NULL, + + PRIMARY KEY (state) +); \ No newline at end of file diff --git a/migrations/postgresql/2024-09-04-091351_use_device_type_for_mails/down.sql b/migrations/postgresql/2024-09-04-091351_use_device_type_for_mails/down.sql new file mode 100644 index 00000000..72fc20e8 --- /dev/null +++ b/migrations/postgresql/2024-09-04-091351_use_device_type_for_mails/down.sql @@ -0,0 +1 @@ +ALTER TABLE twofactor_incomplete DROP COLUMN device_type; diff --git a/migrations/postgresql/2024-09-04-091351_use_device_type_for_mails/up.sql b/migrations/postgresql/2024-09-04-091351_use_device_type_for_mails/up.sql new file mode 100644 index 00000000..a8fc7dfd --- /dev/null +++ b/migrations/postgresql/2024-09-04-091351_use_device_type_for_mails/up.sql @@ -0,0 +1 @@ +ALTER TABLE twofactor_incomplete ADD COLUMN device_type INTEGER NOT NULL DEFAULT 14; -- 14 = Unknown Browser diff --git a/migrations/sqlite/2024-06-05-131359_add_2fa_duo_store/down.sql b/migrations/sqlite/2024-06-05-131359_add_2fa_duo_store/down.sql new file mode 100644 index 00000000..7af867a2 --- /dev/null +++ b/migrations/sqlite/2024-06-05-131359_add_2fa_duo_store/down.sql @@ -0,0 +1 @@ +DROP TABLE twofactor_duo_ctx; \ No newline at end of file diff --git a/migrations/sqlite/2024-06-05-131359_add_2fa_duo_store/up.sql b/migrations/sqlite/2024-06-05-131359_add_2fa_duo_store/up.sql new file mode 100644 index 00000000..40d8e52f --- /dev/null +++ b/migrations/sqlite/2024-06-05-131359_add_2fa_duo_store/up.sql @@ -0,0 +1,8 @@ +CREATE TABLE twofactor_duo_ctx ( + state TEXT NOT NULL, + user_email TEXT NOT NULL, + nonce TEXT NOT NULL, + exp INTEGER NOT NULL, + + PRIMARY KEY (state) +); diff --git a/migrations/sqlite/2024-09-04-091351_use_device_type_for_mails/down.sql b/migrations/sqlite/2024-09-04-091351_use_device_type_for_mails/down.sql new file mode 100644 index 00000000..dd0394ee --- /dev/null +++ b/migrations/sqlite/2024-09-04-091351_use_device_type_for_mails/down.sql @@ -0,0 +1 @@ +ALTER TABLE `twofactor_incomplete` DROP COLUMN `device_type`; diff --git a/migrations/sqlite/2024-09-04-091351_use_device_type_for_mails/up.sql b/migrations/sqlite/2024-09-04-091351_use_device_type_for_mails/up.sql new file mode 100644 index 00000000..a8fc7dfd --- /dev/null +++ b/migrations/sqlite/2024-09-04-091351_use_device_type_for_mails/up.sql @@ -0,0 +1 @@ +ALTER TABLE twofactor_incomplete ADD COLUMN device_type INTEGER NOT NULL DEFAULT 14; -- 14 = Unknown Browser diff --git a/rust-toolchain.toml b/rust-toolchain.toml index 58c631f0..e2df5b62 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -1,4 +1,4 @@ [toolchain] -channel = "1.79.0" +channel = "1.81.0" components = [ "rustfmt", "clippy" ] profile = "minimal" diff --git a/src/api/admin.rs b/src/api/admin.rs index 58a056b6..cc902e39 100644 --- a/src/api/admin.rs +++ b/src/api/admin.rs @@ -1,4 +1,5 @@ use once_cell::sync::Lazy; +use reqwest::Method; use serde::de::DeserializeOwned; use serde_json::Value; use std::env; @@ -17,13 +18,14 @@ use crate::{ core::{log_event, two_factor}, unregister_push_device, ApiResult, EmptyResult, JsonResult, Notify, }, - auth::{decode_admin, encode_jwt, generate_admin_claims, ClientIp}, + auth::{decode_admin, encode_jwt, generate_admin_claims, ClientIp, Secure}, config::ConfigBuilder, db::{backup_database, get_sql_server_version, models::*, DbConn, DbConnType}, error::{Error, MapResult}, + http_client::make_http_request, mail, util::{ - container_base_image, format_naive_datetime_local, get_display_size, get_reqwest_client, + container_base_image, format_naive_datetime_local, get_display_size, get_web_vault_version, is_running_in_container, NumberOrString, }, CONFIG, VERSION, @@ -168,7 +170,12 @@ struct LoginForm { } #[post("/", data = "")] -fn post_admin_login(data: Form, cookies: &CookieJar<'_>, ip: ClientIp) -> Result { +fn post_admin_login( + data: Form, + cookies: &CookieJar<'_>, + ip: ClientIp, + secure: Secure, +) -> Result { let data = data.into_inner(); let redirect = data.redirect; @@ -190,9 +197,10 @@ fn post_admin_login(data: Form, cookies: &CookieJar<'_>, ip: ClientIp let cookie = Cookie::build((COOKIE_NAME, jwt)) .path(admin_path()) - .max_age(rocket::time::Duration::minutes(CONFIG.admin_session_lifetime())) + .max_age(time::Duration::minutes(CONFIG.admin_session_lifetime())) .same_site(SameSite::Strict) - .http_only(true); + .http_only(true) + .secure(secure.https); cookies.add(cookie); if let Some(redirect) = redirect { @@ -290,7 +298,7 @@ async fn invite_user(data: Json, _token: AdminToken, mut conn: DbCon async fn _generate_invite(user: &User, conn: &mut DbConn) -> EmptyResult { if CONFIG.mail_enabled() { - mail::send_invite(&user.email, &user.uuid, None, None, &CONFIG.invitation_org_name(), None).await + mail::send_invite(user, None, None, &CONFIG.invitation_org_name(), None).await } else { let invitation = Invitation::new(&user.email); invitation.save(conn).await @@ -466,7 +474,7 @@ async fn resend_user_invite(uuid: &str, _token: AdminToken, mut conn: DbConn) -> } if CONFIG.mail_enabled() { - mail::send_invite(&user.email, &user.uuid, None, None, &CONFIG.invitation_org_name(), None).await + mail::send_invite(&user, None, None, &CONFIG.invitation_org_name(), None).await } else { Ok(()) } @@ -568,11 +576,6 @@ async fn delete_organization(uuid: &str, _token: AdminToken, mut conn: DbConn) - org.delete(&mut conn).await } -#[derive(Deserialize)] -struct WebVaultVersion { - version: String, -} - #[derive(Deserialize)] struct GitRelease { tag_name: String, @@ -594,15 +597,15 @@ struct TimeApi { } async fn get_json_api(url: &str) -> Result { - let json_api = get_reqwest_client(); - - Ok(json_api.get(url).send().await?.error_for_status()?.json::().await?) + Ok(make_http_request(Method::GET, url)?.send().await?.error_for_status()?.json::().await?) } async fn has_http_access() -> bool { - let http_access = get_reqwest_client(); - - match http_access.head("https://github.com/dani-garcia/vaultwarden").send().await { + let req = match make_http_request(Method::HEAD, "https://github.com/dani-garcia/vaultwarden") { + Ok(r) => r, + Err(_) => return false, + }; + match req.send().await { Ok(r) => r.status().is_success(), _ => false, } @@ -672,18 +675,6 @@ async fn diagnostics(_token: AdminToken, ip_header: IpHeader, mut conn: DbConn) use chrono::prelude::*; use std::net::ToSocketAddrs; - // Get current running versions - let web_vault_version: WebVaultVersion = - match std::fs::read_to_string(format!("{}/{}", CONFIG.web_vault_folder(), "vw-version.json")) { - Ok(s) => serde_json::from_str(&s)?, - _ => match std::fs::read_to_string(format!("{}/{}", CONFIG.web_vault_folder(), "version.json")) { - Ok(s) => serde_json::from_str(&s)?, - _ => WebVaultVersion { - version: String::from("Version file missing"), - }, - }, - }; - // Execute some environment checks let running_within_container = is_running_in_container(); let has_http_access = has_http_access().await; @@ -703,13 +694,16 @@ async fn diagnostics(_token: AdminToken, ip_header: IpHeader, mut conn: DbConn) let ip_header_name = &ip_header.0.unwrap_or_default(); + // Get current running versions + let web_vault_version = get_web_vault_version(); + let diagnostics_json = json!({ "dns_resolved": dns_resolved, "current_release": VERSION, "latest_release": latest_release, "latest_commit": latest_commit, "web_vault_enabled": &CONFIG.web_vault_enabled(), - "web_vault_version": web_vault_version.version.trim_start_matches('v'), + "web_vault_version": web_vault_version, "latest_web_build": latest_web_build, "running_within_container": running_within_container, "container_base_image": if running_within_container { container_base_image() } else { "Not applicable" }, @@ -723,8 +717,8 @@ async fn diagnostics(_token: AdminToken, ip_header: IpHeader, mut conn: DbConn) "db_version": get_sql_server_version(&mut conn).await, "admin_url": format!("{}/diagnostics", admin_url()), "overrides": &CONFIG.get_overrides().join(", "), - "host_arch": std::env::consts::ARCH, - "host_os": std::env::consts::OS, + "host_arch": env::consts::ARCH, + "host_os": env::consts::OS, "server_time_local": Local::now().format("%Y-%m-%d %H:%M:%S %Z").to_string(), "server_time": Utc::now().format("%Y-%m-%d %H:%M:%S UTC").to_string(), // Run the server date/time check as late as possible to minimize the time difference "ntp_time": get_ntp_time(has_http_access).await, // Run the ntp check as late as possible to minimize the time difference @@ -743,18 +737,27 @@ fn get_diagnostics_config(_token: AdminToken) -> Json { #[post("/config", data = "")] fn post_config(data: Json, _token: AdminToken) -> EmptyResult { let data: ConfigBuilder = data.into_inner(); - CONFIG.update_config(data) + if let Err(e) = CONFIG.update_config(data) { + err!(format!("Unable to save config: {e:?}")) + } + Ok(()) } #[post("/config/delete")] fn delete_config(_token: AdminToken) -> EmptyResult { - CONFIG.delete_user_config() + if let Err(e) = CONFIG.delete_user_config() { + err!(format!("Unable to delete config: {e:?}")) + } + Ok(()) } #[post("/config/backup_db")] -async fn backup_db(_token: AdminToken, mut conn: DbConn) -> EmptyResult { +async fn backup_db(_token: AdminToken, mut conn: DbConn) -> ApiResult { if *CAN_BACKUP { - backup_database(&mut conn).await + match backup_database(&mut conn).await { + Ok(f) => Ok(format!("Backup to '{f}' was successful")), + Err(e) => err!(format!("Backup was unsuccessful {e}")), + } } else { err!("Can't back up current DB (Only SQLite supports this feature)"); } diff --git a/src/api/core/accounts.rs b/src/api/core/accounts.rs index a747f3ec..f9822629 100644 --- a/src/api/core/accounts.rs +++ b/src/api/core/accounts.rs @@ -1,5 +1,5 @@ use crate::db::DbPool; -use chrono::Utc; +use chrono::{SecondsFormat, Utc}; use rocket::serde::json::Json; use serde_json::Value; @@ -112,7 +112,7 @@ async fn is_email_2fa_required(org_user_uuid: Option, conn: &mut DbConn) return true; } if org_user_uuid.is_some() { - return OrgPolicy::is_enabled_by_org(&org_user_uuid.unwrap(), OrgPolicyType::TwoFactorAuthentication, conn) + return OrgPolicy::is_enabled_for_member(&org_user_uuid.unwrap(), OrgPolicyType::TwoFactorAuthentication, conn) .await; } false @@ -223,7 +223,7 @@ pub async fn _register(data: Json, mut conn: DbConn) -> JsonResult } if verified_by_invite && is_email_2fa_required(data.organization_user_id, &mut conn).await { - let _ = email::activate_email_2fa(&user, &mut conn).await; + email::activate_email_2fa(&user, &mut conn).await.ok(); } } @@ -232,7 +232,7 @@ pub async fn _register(data: Json, mut conn: DbConn) -> JsonResult // accept any open emergency access invitations if !CONFIG.mail_enabled() && CONFIG.emergency_access_allowed() { for mut emergency_invite in EmergencyAccess::find_all_invited_by_grantee_email(&user.email, &mut conn).await { - let _ = emergency_invite.accept_invite(&user.uuid, &user.email, &mut conn).await; + emergency_invite.accept_invite(&user.uuid, &user.email, &mut conn).await.ok(); } } @@ -490,7 +490,7 @@ async fn post_rotatekey(data: Json, headers: Headers, mut conn: DbConn, // Bitwarden does not process the import if there is one item invalid. // Since we check for the size of the encrypted note length, we need to do that here to pre-validate it. // TODO: See if we can optimize the whole cipher adding/importing and prevent duplicate code and checks. - Cipher::validate_notes(&data.ciphers)?; + Cipher::validate_cipher_data(&data.ciphers)?; let user_uuid = &headers.user.uuid; @@ -1038,7 +1038,7 @@ async fn put_device_token(uuid: &str, data: Json, headers: Headers, m return Ok(()); } else { // Try to unregister already registered device - let _ = unregister_push_device(device.push_uuid).await; + unregister_push_device(device.push_uuid).await.ok(); } // clear the push_uuid device.push_uuid = None; @@ -1123,7 +1123,7 @@ async fn post_auth_request( "requestIpAddress": auth_request.request_ip, "key": null, "masterPasswordHash": null, - "creationDate": auth_request.creation_date.and_utc(), + "creationDate": auth_request.creation_date.and_utc().to_rfc3339_opts(SecondsFormat::Micros, true), "responseDate": null, "requestApproved": false, "origin": CONFIG.domain_origin(), @@ -1140,7 +1140,9 @@ async fn get_auth_request(uuid: &str, mut conn: DbConn) -> JsonResult { } }; - let response_date_utc = auth_request.response_date.map(|response_date| response_date.and_utc()); + let response_date_utc = auth_request + .response_date + .map(|response_date| response_date.and_utc().to_rfc3339_opts(SecondsFormat::Micros, true)); Ok(Json(json!( { @@ -1150,7 +1152,7 @@ async fn get_auth_request(uuid: &str, mut conn: DbConn) -> JsonResult { "requestIpAddress": auth_request.request_ip, "key": auth_request.enc_key, "masterPasswordHash": auth_request.master_password_hash, - "creationDate": auth_request.creation_date.and_utc(), + "creationDate": auth_request.creation_date.and_utc().to_rfc3339_opts(SecondsFormat::Micros, true), "responseDate": response_date_utc, "requestApproved": auth_request.approved, "origin": CONFIG.domain_origin(), @@ -1195,7 +1197,9 @@ async fn put_auth_request( nt.send_auth_response(&auth_request.user_uuid, &auth_request.uuid, data.device_identifier, &mut conn).await; } - let response_date_utc = auth_request.response_date.map(|response_date| response_date.and_utc()); + let response_date_utc = auth_request + .response_date + .map(|response_date| response_date.and_utc().to_rfc3339_opts(SecondsFormat::Micros, true)); Ok(Json(json!( { @@ -1205,7 +1209,7 @@ async fn put_auth_request( "requestIpAddress": auth_request.request_ip, "key": auth_request.enc_key, "masterPasswordHash": auth_request.master_password_hash, - "creationDate": auth_request.creation_date.and_utc(), + "creationDate": auth_request.creation_date.and_utc().to_rfc3339_opts(SecondsFormat::Micros, true), "responseDate": response_date_utc, "requestApproved": auth_request.approved, "origin": CONFIG.domain_origin(), @@ -1227,7 +1231,9 @@ async fn get_auth_request_response(uuid: &str, code: &str, mut conn: DbConn) -> err!("Access code invalid doesn't exist") } - let response_date_utc = auth_request.response_date.map(|response_date| response_date.and_utc()); + let response_date_utc = auth_request + .response_date + .map(|response_date| response_date.and_utc().to_rfc3339_opts(SecondsFormat::Micros, true)); Ok(Json(json!( { @@ -1237,7 +1243,7 @@ async fn get_auth_request_response(uuid: &str, code: &str, mut conn: DbConn) -> "requestIpAddress": auth_request.request_ip, "key": auth_request.enc_key, "masterPasswordHash": auth_request.master_password_hash, - "creationDate": auth_request.creation_date.and_utc(), + "creationDate": auth_request.creation_date.and_utc().to_rfc3339_opts(SecondsFormat::Micros, true), "responseDate": response_date_utc, "requestApproved": auth_request.approved, "origin": CONFIG.domain_origin(), @@ -1255,7 +1261,7 @@ async fn get_auth_requests(headers: Headers, mut conn: DbConn) -> JsonResult { .iter() .filter(|request| request.approved.is_none()) .map(|request| { - let response_date_utc = request.response_date.map(|response_date| response_date.and_utc()); + let response_date_utc = request.response_date.map(|response_date| response_date.and_utc().to_rfc3339_opts(SecondsFormat::Micros, true)); json!({ "id": request.uuid, @@ -1264,7 +1270,7 @@ async fn get_auth_requests(headers: Headers, mut conn: DbConn) -> JsonResult { "requestIpAddress": request.request_ip, "key": request.enc_key, "masterPasswordHash": request.master_password_hash, - "creationDate": request.creation_date.and_utc(), + "creationDate": request.creation_date.and_utc().to_rfc3339_opts(SecondsFormat::Micros, true), "responseDate": response_date_utc, "requestApproved": request.approved, "origin": CONFIG.domain_origin(), diff --git a/src/api/core/ciphers.rs b/src/api/core/ciphers.rs index c2c78b33..da718942 100644 --- a/src/api/core/ciphers.rs +++ b/src/api/core/ciphers.rs @@ -208,6 +208,7 @@ pub struct CipherData { // Folder id is not included in import folder_id: Option, // TODO: Some of these might appear all the time, no need for Option + #[serde(alias = "organizationID")] pub organization_id: Option, key: Option, @@ -232,7 +233,7 @@ pub struct CipherData { favorite: Option, reprompt: Option, - password_history: Option, + pub password_history: Option, // These are used during key rotation // 'Attachments' is unused, contains map of {id: filename} @@ -377,8 +378,9 @@ pub async fn update_cipher_from_data( } if let Some(note) = &data.notes { - if note.len() > 10_000 { - err!("The field Notes exceeds the maximum encrypted value length of 10000 characters.") + let max_note_size = CONFIG._max_note_size(); + if note.len() > max_note_size { + err!(format!("The field Notes exceeds the maximum encrypted value length of {max_note_size} characters.")) } } @@ -561,7 +563,7 @@ async fn post_ciphers_import( // Bitwarden does not process the import if there is one item invalid. // Since we check for the size of the encrypted note length, we need to do that here to pre-validate it. // TODO: See if we can optimize the whole cipher adding/importing and prevent duplicate code and checks. - Cipher::validate_notes(&data.ciphers)?; + Cipher::validate_cipher_data(&data.ciphers)?; // Read and create the folders let existing_folders: Vec = @@ -701,6 +703,7 @@ async fn put_cipher_partial( #[derive(Deserialize)] #[serde(rename_all = "camelCase")] struct CollectionsAdminData { + #[serde(alias = "CollectionIds")] collection_ids: Vec, } diff --git a/src/api/core/mod.rs b/src/api/core/mod.rs index 9da0e886..ee5db190 100644 --- a/src/api/core/mod.rs +++ b/src/api/core/mod.rs @@ -12,6 +12,7 @@ pub use accounts::purge_auth_requests; pub use ciphers::{purge_trashed_ciphers, CipherData, CipherSyncData, CipherSyncType}; pub use emergency_access::{emergency_notification_reminder_job, emergency_request_timeout_job}; pub use events::{event_cleanup_job, log_event, log_user_event}; +use reqwest::Method; pub use sends::purge_sends; pub fn routes() -> Vec { @@ -53,7 +54,8 @@ use crate::{ auth::Headers, db::DbConn, error::Error, - util::{get_reqwest_client, parse_experimental_client_feature_flags}, + http_client::make_http_request, + util::parse_experimental_client_feature_flags, }; #[derive(Debug, Serialize, Deserialize)] @@ -139,9 +141,7 @@ async fn hibp_breach(username: &str) -> JsonResult { ); if let Some(api_key) = crate::CONFIG.hibp_api_key() { - let hibp_client = get_reqwest_client(); - - let res = hibp_client.get(&url).header("hibp-api-key", api_key).send().await?; + let res = make_http_request(Method::GET, &url)?.header("hibp-api-key", api_key).send().await?; // If we get a 404, return a 404, it means no breached accounts if res.status() == 404 { @@ -190,6 +190,8 @@ fn config() -> Json { parse_experimental_client_feature_flags(&crate::CONFIG.experimental_client_feature_flags()); // Force the new key rotation feature feature_states.insert("key-rotation-improvements".to_string(), true); + feature_states.insert("flexible-collections-v-1".to_string(), false); + Json(json!({ // Note: The clients use this version to handle backwards compatibility concerns // This means they expect a version that closely matches the Bitwarden server version @@ -200,8 +202,10 @@ fn config() -> Json { "gitHash": option_env!("GIT_REV"), "server": { "name": "Vaultwarden", - "url": "https://github.com/dani-garcia/vaultwarden", - "version": crate::VERSION + "url": "https://github.com/dani-garcia/vaultwarden" + }, + "settings": { + "disableUserRegistration": !crate::CONFIG.signups_allowed() && crate::CONFIG.signups_domains_whitelist().is_empty(), }, "environment": { "vault": domain, diff --git a/src/api/core/organizations.rs b/src/api/core/organizations.rs index 204dd56f..afd2d388 100644 --- a/src/api/core/organizations.rs +++ b/src/api/core/organizations.rs @@ -2,6 +2,7 @@ use num_traits::FromPrimitive; use rocket::serde::json::Json; use rocket::Route; use serde_json::Value; +use std::collections::{HashMap, HashSet}; use crate::{ api::{ @@ -39,6 +40,7 @@ pub fn routes() -> Vec { delete_organization_collection, post_organization_collection_delete, bulk_delete_organization_collections, + post_bulk_collections, get_org_details, get_org_users, send_invite, @@ -65,6 +67,7 @@ pub fn routes() -> Vec { import, post_org_keys, get_organization_keys, + get_organization_public_key, bulk_public_keys, deactivate_organization_user, bulk_deactivate_organization_user, @@ -355,7 +358,7 @@ async fn get_org_collections_details(org_id: &str, headers: ManagerHeadersLoose, Vec::with_capacity(0) }; - let mut json_object = col.to_json(); + let mut json_object = col.to_json_details(&headers.user.uuid, None, &mut conn).await; json_object["assigned"] = json!(assigned); json_object["users"] = json!(users); json_object["groups"] = json!(groups); @@ -506,7 +509,7 @@ async fn post_organization_collection_update( CollectionUser::save(&org_user.user_uuid, col_id, user.read_only, user.hide_passwords, &mut conn).await?; } - Ok(Json(collection.to_json())) + Ok(Json(collection.to_json_details(&headers.user.uuid, None, &mut conn).await)) } #[delete("/organizations//collections//user/")] @@ -677,7 +680,7 @@ async fn get_org_collection_detail( let assigned = Collection::can_access_collection(&user_org, &collection.uuid, &mut conn).await; - let mut json_object = collection.to_json(); + let mut json_object = collection.to_json_details(&headers.user.uuid, None, &mut conn).await; json_object["assigned"] = json!(assigned); json_object["users"] = json!(users); json_object["groups"] = json!(groups); @@ -749,12 +752,19 @@ struct OrgIdData { } #[get("/ciphers/organization-details?")] -async fn get_org_details(data: OrgIdData, headers: Headers, mut conn: DbConn) -> Json { - Json(json!({ +async fn get_org_details(data: OrgIdData, headers: Headers, mut conn: DbConn) -> JsonResult { + if UserOrganization::find_confirmed_by_user_and_org(&headers.user.uuid, &data.organization_id, &mut conn) + .await + .is_none() + { + err_code!("Resource not found.", rocket::http::Status::NotFound.code); + } + + Ok(Json(json!({ "data": _get_org_details(&data.organization_id, &headers.host, &headers.user.uuid, &mut conn).await, "object": "list", "continuationToken": null, - })) + }))) } async fn _get_org_details(org_id: &str, host: &str, user_uuid: &str, conn: &mut DbConn) -> Value { @@ -844,7 +854,8 @@ struct InviteData { groups: Vec, r#type: NumberOrString, collections: Option>, - access_all: Option, + #[serde(default)] + access_all: bool, } #[post("/organizations//users/invite", data = "")] @@ -896,7 +907,7 @@ async fn send_invite(org_id: &str, data: Json, headers: AdminHeaders }; let mut new_user = UserOrganization::new(user.uuid.clone(), String::from(org_id)); - let access_all = data.access_all.unwrap_or(false); + let access_all = data.access_all; new_user.access_all = access_all; new_user.atype = new_type; new_user.status = user_org_status; @@ -945,8 +956,7 @@ async fn send_invite(org_id: &str, data: Json, headers: AdminHeaders }; mail::send_invite( - &email, - &user.uuid, + &user, Some(String::from(org_id)), Some(new_user.uuid), &org_name, @@ -997,14 +1007,6 @@ async fn reinvite_user(org_id: &str, user_org: &str, headers: AdminHeaders, mut } async fn _reinvite_user(org_id: &str, user_org: &str, invited_by_email: &str, conn: &mut DbConn) -> EmptyResult { - if !CONFIG.invitations_allowed() { - err!("Invitations are not allowed.") - } - - if !CONFIG.mail_enabled() { - err!("SMTP is not configured.") - } - let user_org = match UserOrganization::find_by_uuid(user_org, conn).await { Some(user_org) => user_org, None => err!("The user hasn't been invited to the organization."), @@ -1019,6 +1021,10 @@ async fn _reinvite_user(org_id: &str, user_org: &str, invited_by_email: &str, co None => err!("User not found."), }; + if !CONFIG.invitations_allowed() && user.password_hash.is_empty() { + err!("Invitations are not allowed.") + } + let org_name = match Organization::find_by_uuid(org_id, conn).await { Some(org) => org.name, None => err!("Error looking up organization."), @@ -1026,17 +1032,21 @@ async fn _reinvite_user(org_id: &str, user_org: &str, invited_by_email: &str, co if CONFIG.mail_enabled() { mail::send_invite( - &user.email, - &user.uuid, + &user, Some(org_id.to_string()), Some(user_org.uuid), &org_name, Some(invited_by_email.to_string()), ) .await?; - } else { + } else if user.password_hash.is_empty() { let invitation = Invitation::new(&user.email); invitation.save(conn).await?; + } else { + let _ = Invitation::take(&user.email, conn).await; + let mut user_org = user_org; + user_org.status = UserOrgStatus::Accepted as i32; + user_org.save(conn).await?; } Ok(()) @@ -1296,6 +1306,7 @@ struct EditUserData { r#type: NumberOrString, collections: Option>, groups: Option>, + #[serde(default)] access_all: bool, } @@ -1585,7 +1596,7 @@ async fn post_org_import( // Bitwarden does not process the import if there is one item invalid. // Since we check for the size of the encrypted note length, we need to do that here to pre-validate it. // TODO: See if we can optimize the whole cipher adding/importing and prevent duplicate code and checks. - Cipher::validate_notes(&data.ciphers)?; + Cipher::validate_cipher_data(&data.ciphers)?; let mut collections = Vec::new(); for coll in data.collections { @@ -1628,6 +1639,66 @@ async fn post_org_import( user.update_revision(&mut conn).await } +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +#[allow(dead_code)] +struct BulkCollectionsData { + organization_id: String, + cipher_ids: Vec, + collection_ids: HashSet, + remove_collections: bool, +} + +// This endpoint is only reachable via the organization view, therefor this endpoint is located here +// Also Bitwarden does not send out Notifications for these changes, it only does this for individual cipher collection updates +#[post("/ciphers/bulk-collections", data = "")] +async fn post_bulk_collections(data: Json, headers: Headers, mut conn: DbConn) -> EmptyResult { + let data: BulkCollectionsData = data.into_inner(); + + // This feature does not seem to be active on all the clients + // To prevent future issues, add a check to block a call when this is set to true + if data.remove_collections { + err!("Bulk removing of collections is not yet implemented") + } + + // Get all the collection available to the user in one query + // Also filter based upon the provided collections + let user_collections: HashMap = + Collection::find_by_organization_and_user_uuid(&data.organization_id, &headers.user.uuid, &mut conn) + .await + .into_iter() + .filter_map(|c| { + if data.collection_ids.contains(&c.uuid) { + Some((c.uuid.clone(), c)) + } else { + None + } + }) + .collect(); + + // Verify if all the collections requested exists and are writeable for the user, else abort + for collection_uuid in &data.collection_ids { + match user_collections.get(collection_uuid) { + Some(collection) if collection.is_writable_by_user(&headers.user.uuid, &mut conn).await => (), + _ => err_code!("Resource not found", "User does not have access to a collection", 404), + } + } + + for cipher_id in data.cipher_ids.iter() { + // Only act on existing cipher uuid's + // Do not abort the operation just ignore it, it could be a cipher was just deleted for example + if let Some(cipher) = Cipher::find_by_uuid_and_org(cipher_id, &data.organization_id, &mut conn).await { + if cipher.is_write_accessible_to_user(&headers.user.uuid, &mut conn).await { + for collection in &data.collection_ids { + CollectionCipher::save(&cipher.uuid, collection, &mut conn).await?; + } + } + }; + } + + Ok(()) +} + #[get("/organizations//policies")] async fn list_policies(org_id: &str, _headers: AdminHeaders, mut conn: DbConn) -> Json { let policies = OrgPolicy::find_by_org(org_id, &mut conn).await; @@ -1642,7 +1713,14 @@ async fn list_policies(org_id: &str, _headers: AdminHeaders, mut conn: DbConn) - #[get("/organizations//policies/token?")] async fn list_policies_token(org_id: &str, token: &str, mut conn: DbConn) -> JsonResult { - let invite = crate::auth::decode_invite(token)?; + // web-vault 2024.6.2 seems to send these values and cause logs to output errors + // Catch this and prevent errors in the logs + // TODO: CleanUp after 2024.6.x is not used anymore. + if org_id == "undefined" && token == "undefined" { + return Ok(Json(json!({}))); + } + + let invite = decode_invite(token)?; let invite_org_id = match invite.org_id { Some(invite_org_id) => invite_org_id, @@ -1702,6 +1780,38 @@ async fn put_policy( None => err!("Invalid or unsupported policy type"), }; + // Bitwarden only allows the Reset Password policy when Single Org policy is enabled + // Vaultwarden encouraged to use multiple orgs instead of groups because groups were not available in the past + // Now that groups are available we can enforce this option when wanted. + // We put this behind a config option to prevent breaking current installation. + // Maybe we want to enable this by default in the future, but currently it is disabled by default. + if CONFIG.enforce_single_org_with_reset_pw_policy() { + if pol_type_enum == OrgPolicyType::ResetPassword && data.enabled { + let single_org_policy_enabled = + match OrgPolicy::find_by_org_and_type(org_id, OrgPolicyType::SingleOrg, &mut conn).await { + Some(p) => p.enabled, + None => false, + }; + + if !single_org_policy_enabled { + err!("Single Organization policy is not enabled. It is mandatory for this policy to be enabled.") + } + } + + // Also prevent the Single Org Policy to be disabled if the Reset Password policy is enabled + if pol_type_enum == OrgPolicyType::SingleOrg && !data.enabled { + let reset_pw_policy_enabled = + match OrgPolicy::find_by_org_and_type(org_id, OrgPolicyType::ResetPassword, &mut conn).await { + Some(p) => p.enabled, + None => false, + }; + + if reset_pw_policy_enabled { + err!("Account recovery policy is enabled. It is not allowed to disable this policy.") + } + } + } + // When enabling the TwoFactorAuthentication policy, revoke all members that do not have 2FA if pol_type_enum == OrgPolicyType::TwoFactorAuthentication && data.enabled { two_factor::enforce_2fa_policy_for_org( @@ -1925,8 +2035,7 @@ async fn import(org_id: &str, data: Json, headers: Headers, mut c }; mail::send_invite( - &user_data.email, - &user.uuid, + &user, Some(String::from(org_id)), Some(new_org_user.uuid), &org_name, @@ -2196,13 +2305,14 @@ async fn _restore_organization_user( } #[get("/organizations//groups")] -async fn get_groups(org_id: &str, _headers: ManagerHeadersLoose, mut conn: DbConn) -> JsonResult { +async fn get_groups(org_id: &str, headers: ManagerHeadersLoose, mut conn: DbConn) -> JsonResult { let groups: Vec = if CONFIG.org_groups_enabled() { // Group::find_by_organization(&org_id, &mut conn).await.iter().map(Group::to_json).collect::() let groups = Group::find_by_organization(org_id, &mut conn).await; let mut groups_json = Vec::with_capacity(groups.len()); + for g in groups { - groups_json.push(g.to_json_details(&mut conn).await) + groups_json.push(g.to_json_details(&headers.org_user.atype, &mut conn).await) } groups_json } else { @@ -2222,7 +2332,8 @@ async fn get_groups(org_id: &str, _headers: ManagerHeadersLoose, mut conn: DbCon #[serde(rename_all = "camelCase")] struct GroupRequest { name: String, - access_all: Option, + #[serde(default)] + access_all: bool, external_id: Option, collections: Vec, users: Vec, @@ -2230,17 +2341,12 @@ struct GroupRequest { impl GroupRequest { pub fn to_group(&self, organizations_uuid: &str) -> Group { - Group::new( - String::from(organizations_uuid), - self.name.clone(), - self.access_all.unwrap_or(false), - self.external_id.clone(), - ) + Group::new(String::from(organizations_uuid), self.name.clone(), self.access_all, self.external_id.clone()) } pub fn update_group(&self, mut group: Group) -> Group { group.name.clone_from(&self.name); - group.access_all = self.access_all.unwrap_or(false); + group.access_all = self.access_all; // Group Updates do not support changing the external_id // These input fields are in a disabled state, and can only be updated/added via ldap_import @@ -2394,7 +2500,7 @@ async fn add_update_group( } #[get("/organizations/<_org_id>/groups//details")] -async fn get_group_details(_org_id: &str, group_id: &str, _headers: AdminHeaders, mut conn: DbConn) -> JsonResult { +async fn get_group_details(_org_id: &str, group_id: &str, headers: AdminHeaders, mut conn: DbConn) -> JsonResult { if !CONFIG.org_groups_enabled() { err!("Group support is disabled"); } @@ -2404,7 +2510,7 @@ async fn get_group_details(_org_id: &str, group_id: &str, _headers: AdminHeaders _ => err!("Group could not be found!"), }; - Ok(Json(group.to_json_details(&mut conn).await)) + Ok(Json(group.to_json_details(&(headers.org_user_type as i32), &mut conn).await)) } #[post("/organizations//groups//delete")] @@ -2680,20 +2786,29 @@ struct OrganizationUserResetPasswordRequest { key: String, } -#[get("/organizations//keys")] -async fn get_organization_keys(org_id: &str, mut conn: DbConn) -> JsonResult { +// Upstrem reports this is the renamed endpoint instead of `/keys` +// But the clients do not seem to use this at all +// Just add it here in case they will +#[get("/organizations//public-key")] +async fn get_organization_public_key(org_id: &str, _headers: Headers, mut conn: DbConn) -> JsonResult { let org = match Organization::find_by_uuid(org_id, &mut conn).await { Some(organization) => organization, None => err!("Organization not found"), }; Ok(Json(json!({ - "object": "organizationKeys", + "object": "organizationPublicKey", "publicKey": org.public_key, - "privateKey": org.private_key, }))) } +// Obsolete - Renamed to public-key (2023.8), left for backwards compatibility with older clients +// https://github.com/bitwarden/server/blob/25dc0c9178e3e3584074bbef0d4be827b7c89415/src/Api/AdminConsole/Controllers/OrganizationsController.cs#L463-L468 +#[get("/organizations//keys")] +async fn get_organization_keys(org_id: &str, headers: Headers, conn: DbConn) -> JsonResult { + get_organization_public_key(org_id, headers, conn).await +} + #[put("/organizations//users//reset-password", data = "")] async fn put_reset_password( org_id: &str, diff --git a/src/api/core/public.rs b/src/api/core/public.rs index 0cdcbb63..737d30dd 100644 --- a/src/api/core/public.rs +++ b/src/api/core/public.rs @@ -1,6 +1,6 @@ use chrono::Utc; use rocket::{ - request::{self, FromRequest, Outcome}, + request::{FromRequest, Outcome}, serde::json::Json, Request, Route, }; @@ -123,15 +123,8 @@ async fn ldap_import(data: Json, token: PublicToken, mut conn: Db None => err!("Error looking up organization"), }; - mail::send_invite( - &user_data.email, - &user.uuid, - Some(org_id.clone()), - Some(new_org_user.uuid), - &org_name, - Some(org_email), - ) - .await?; + mail::send_invite(&user, Some(org_id.clone()), Some(new_org_user.uuid), &org_name, Some(org_email)) + .await?; } } } @@ -199,7 +192,7 @@ pub struct PublicToken(String); impl<'r> FromRequest<'r> for PublicToken { type Error = &'static str; - async fn from_request(request: &'r Request<'_>) -> request::Outcome { + async fn from_request(request: &'r Request<'_>) -> Outcome { let headers = request.headers(); // Get access_token let access_token: &str = match headers.get_one("Authorization") { diff --git a/src/api/core/sends.rs b/src/api/core/sends.rs index 27aea95a..a7e5bcf0 100644 --- a/src/api/core/sends.rs +++ b/src/api/core/sends.rs @@ -349,7 +349,15 @@ async fn post_send_file_v2(data: Json, headers: Headers, mut conn: DbC }))) } -// https://github.com/bitwarden/server/blob/d0c793c95181dfb1b447eb450f85ba0bfd7ef643/src/Api/Controllers/SendsController.cs#L243 +#[derive(Deserialize)] +#[allow(non_snake_case)] +pub struct SendFileData { + id: String, + size: u64, + fileName: String, +} + +// https://github.com/bitwarden/server/blob/66f95d1c443490b653e5a15d32977e2f5a3f9e32/src/Api/Tools/Controllers/SendsController.cs#L250 #[post("/sends//file/", format = "multipart/form-data", data = "")] async fn post_send_file_v2_data( send_uuid: &str, @@ -367,15 +375,55 @@ async fn post_send_file_v2_data( err!("Send not found. Unable to save the file.") }; + if send.atype != SendType::File as i32 { + err!("Send is not a file type send."); + } + let Some(send_user_id) = &send.user_uuid else { - err!("Sends are only supported for users at the moment") + err!("Sends are only supported for users at the moment.") }; + if send_user_id != &headers.user.uuid { - err!("Send doesn't belong to user"); + err!("Send doesn't belong to user."); + } + + let Ok(send_data) = serde_json::from_str::(&send.data) else { + err!("Unable to decode send data as json.") + }; + + match data.data.raw_name() { + Some(raw_file_name) if raw_file_name.dangerous_unsafe_unsanitized_raw() == send_data.fileName => (), + Some(raw_file_name) => err!( + "Send file name does not match.", + format!( + "Expected file name '{}' got '{}'", + send_data.fileName, + raw_file_name.dangerous_unsafe_unsanitized_raw() + ) + ), + _ => err!("Send file name does not match or is not provided."), + } + + if file_id != send_data.id { + err!("Send file does not match send data.", format!("Expected id {} got {file_id}", send_data.id)); + } + + let Some(size) = data.data.len().to_u64() else { + err!("Send file size overflow."); + }; + + if size != send_data.size { + err!("Send file size does not match.", format!("Expected a file size of {} got {size}", send_data.size)); } let folder_path = tokio::fs::canonicalize(&CONFIG.sends_folder()).await?.join(send_uuid); let file_path = folder_path.join(file_id); + + // Check if the file already exists, if that is the case do not overwrite it + if tokio::fs::metadata(&file_path).await.is_ok() { + err!("Send file has already been uploaded.", format!("File {file_path:?} already exists")) + } + tokio::fs::create_dir_all(&folder_path).await?; if let Err(_err) = data.data.persist_to(&file_path).await { diff --git a/src/api/core/two_factor/duo.rs b/src/api/core/two_factor/duo.rs index c5bfa9e5..6de2935d 100644 --- a/src/api/core/two_factor/duo.rs +++ b/src/api/core/two_factor/duo.rs @@ -15,7 +15,7 @@ use crate::{ DbConn, }, error::MapResult, - util::get_reqwest_client, + http_client::make_http_request, CONFIG, }; @@ -210,10 +210,7 @@ async fn duo_api_request(method: &str, path: &str, params: &str, data: &DuoData) let m = Method::from_str(method).unwrap_or_default(); - let client = get_reqwest_client(); - - client - .request(m, &url) + make_http_request(m, &url)? .basic_auth(username, Some(password)) .header(header::USER_AGENT, "vaultwarden:Duo/1.0 (Rust)") .header(header::DATE, date) @@ -255,7 +252,7 @@ async fn get_user_duo_data(uuid: &str, conn: &mut DbConn) -> DuoStatus { } // let (ik, sk, ak, host) = get_duo_keys(); -async fn get_duo_keys_email(email: &str, conn: &mut DbConn) -> ApiResult<(String, String, String, String)> { +pub(crate) async fn get_duo_keys_email(email: &str, conn: &mut DbConn) -> ApiResult<(String, String, String, String)> { let data = match User::find_by_mail(email, conn).await { Some(u) => get_user_duo_data(&u.uuid, conn).await.data(), _ => DuoData::global(), @@ -284,10 +281,6 @@ fn sign_duo_values(key: &str, email: &str, ikey: &str, prefix: &str, expire: i64 } pub async fn validate_duo_login(email: &str, response: &str, conn: &mut DbConn) -> EmptyResult { - // email is as entered by the user, so it needs to be normalized before - // comparison with auth_user below. - let email = &email.to_lowercase(); - let split: Vec<&str> = response.split(':').collect(); if split.len() != 2 { err!( diff --git a/src/api/core/two_factor/duo_oidc.rs b/src/api/core/two_factor/duo_oidc.rs new file mode 100644 index 00000000..d252df91 --- /dev/null +++ b/src/api/core/two_factor/duo_oidc.rs @@ -0,0 +1,498 @@ +use chrono::Utc; +use data_encoding::HEXLOWER; +use jsonwebtoken::{Algorithm, DecodingKey, EncodingKey, Header, Validation}; +use reqwest::{header, StatusCode}; +use ring::digest::{digest, Digest, SHA512_256}; +use serde::Serialize; +use std::collections::HashMap; + +use crate::{ + api::{core::two_factor::duo::get_duo_keys_email, EmptyResult}, + crypto, + db::{ + models::{EventType, TwoFactorDuoContext}, + DbConn, DbPool, + }, + error::Error, + http_client::make_http_request, + CONFIG, +}; +use url::Url; + +// The location on this service that Duo should redirect users to. For us, this is a bridge +// built in to the Bitwarden clients. +// See: https://github.com/bitwarden/clients/blob/main/apps/web/src/connectors/duo-redirect.ts +const DUO_REDIRECT_LOCATION: &str = "duo-redirect-connector.html"; + +// Number of seconds that a JWT we generate for Duo should be valid for. +const JWT_VALIDITY_SECS: i64 = 300; + +// Number of seconds that a Duo context stored in the database should be valid for. +const CTX_VALIDITY_SECS: i64 = 300; + +// Expected algorithm used by Duo to sign JWTs. +const DUO_RESP_SIGNATURE_ALG: Algorithm = Algorithm::HS512; + +// Signature algorithm we're using to sign JWTs for Duo. Must be either HS512 or HS256. +const JWT_SIGNATURE_ALG: Algorithm = Algorithm::HS512; + +// Size of random strings for state and nonce. Must be at least 16 characters and at most 1024 characters. +// If increasing this above 64, also increase the size of the twofactor_duo_ctx.state and +// twofactor_duo_ctx.nonce database columns for postgres and mariadb. +const STATE_LENGTH: usize = 64; + +// client_assertion payload for health checks and obtaining MFA results. +#[derive(Debug, Serialize, Deserialize)] +struct ClientAssertion { + pub iss: String, + pub sub: String, + pub aud: String, + pub exp: i64, + pub jti: String, + pub iat: i64, +} + +// authorization request payload sent with clients to Duo for MFA +#[derive(Debug, Serialize, Deserialize)] +struct AuthorizationRequest { + pub response_type: String, + pub scope: String, + pub exp: i64, + pub client_id: String, + pub redirect_uri: String, + pub state: String, + pub duo_uname: String, + pub iss: String, + pub aud: String, + pub nonce: String, +} + +// Duo service health check responses +#[derive(Debug, Serialize, Deserialize)] +#[serde(untagged)] +enum HealthCheckResponse { + HealthOK { + stat: String, + }, + HealthFail { + message: String, + message_detail: String, + }, +} + +// Outer structure of response when exchanging authz code for MFA results +#[derive(Debug, Serialize, Deserialize)] +struct IdTokenResponse { + id_token: String, // IdTokenClaims + access_token: String, + expires_in: i64, + token_type: String, +} + +// Inner structure of IdTokenResponse.id_token +#[derive(Debug, Serialize, Deserialize)] +struct IdTokenClaims { + preferred_username: String, + nonce: String, +} + +// Duo OIDC Authorization Client +// See https://duo.com/docs/oauthapi +struct DuoClient { + client_id: String, // Duo Client ID (DuoData.ik) + client_secret: String, // Duo Client Secret (DuoData.sk) + api_host: String, // Duo API hostname (DuoData.host) + redirect_uri: String, // URL in this application clients should call for MFA verification +} + +impl DuoClient { + // Construct a new DuoClient + fn new(client_id: String, client_secret: String, api_host: String, redirect_uri: String) -> DuoClient { + DuoClient { + client_id, + client_secret, + api_host, + redirect_uri, + } + } + + // Generate a client assertion for health checks and authorization code exchange. + fn new_client_assertion(&self, url: &str) -> ClientAssertion { + let now = Utc::now().timestamp(); + let jwt_id = crypto::get_random_string_alphanum(STATE_LENGTH); + + ClientAssertion { + iss: self.client_id.clone(), + sub: self.client_id.clone(), + aud: url.to_string(), + exp: now + JWT_VALIDITY_SECS, + jti: jwt_id, + iat: now, + } + } + + // Given a serde-serializable struct, attempt to encode it as a JWT + fn encode_duo_jwt(&self, jwt_payload: T) -> Result { + match jsonwebtoken::encode( + &Header::new(JWT_SIGNATURE_ALG), + &jwt_payload, + &EncodingKey::from_secret(self.client_secret.as_bytes()), + ) { + Ok(token) => Ok(token), + Err(e) => err!(format!("Error encoding Duo JWT: {e:?}")), + } + } + + // "required" health check to verify the integration is configured and Duo's services + // are up. + // https://duo.com/docs/oauthapi#health-check + async fn health_check(&self) -> Result<(), Error> { + let health_check_url: String = format!("https://{}/oauth/v1/health_check", self.api_host); + + let jwt_payload = self.new_client_assertion(&health_check_url); + + let token = match self.encode_duo_jwt(jwt_payload) { + Ok(token) => token, + Err(e) => return Err(e), + }; + + let mut post_body = HashMap::new(); + post_body.insert("client_assertion", token); + post_body.insert("client_id", self.client_id.clone()); + + let res = match make_http_request(reqwest::Method::POST, &health_check_url)? + .header(header::USER_AGENT, "vaultwarden:Duo/2.0 (Rust)") + .form(&post_body) + .send() + .await + { + Ok(r) => r, + Err(e) => err!(format!("Error requesting Duo health check: {e:?}")), + }; + + let response: HealthCheckResponse = match res.json::().await { + Ok(r) => r, + Err(e) => err!(format!("Duo health check response decode error: {e:?}")), + }; + + let health_stat: String = match response { + HealthCheckResponse::HealthOK { + stat, + } => stat, + HealthCheckResponse::HealthFail { + message, + message_detail, + } => err!(format!("Duo health check FAIL response, msg: {}, detail: {}", message, message_detail)), + }; + + if health_stat != "OK" { + err!(format!("Duo health check failed, got OK-like body with stat {health_stat}")); + } + + Ok(()) + } + + // Constructs the URL for the authorization request endpoint on Duo's service. + // Clients are sent here to continue authentication. + // https://duo.com/docs/oauthapi#authorization-request + fn make_authz_req_url(&self, duo_username: &str, state: String, nonce: String) -> Result { + let now = Utc::now().timestamp(); + + let jwt_payload = AuthorizationRequest { + response_type: String::from("code"), + scope: String::from("openid"), + exp: now + JWT_VALIDITY_SECS, + client_id: self.client_id.clone(), + redirect_uri: self.redirect_uri.clone(), + state, + duo_uname: String::from(duo_username), + iss: self.client_id.clone(), + aud: format!("https://{}", self.api_host), + nonce, + }; + + let token = match self.encode_duo_jwt(jwt_payload) { + Ok(token) => token, + Err(e) => return Err(e), + }; + + let authz_endpoint = format!("https://{}/oauth/v1/authorize", self.api_host); + let mut auth_url = match Url::parse(authz_endpoint.as_str()) { + Ok(url) => url, + Err(e) => err!(format!("Error parsing Duo authorization URL: {e:?}")), + }; + + { + let mut query_params = auth_url.query_pairs_mut(); + query_params.append_pair("response_type", "code"); + query_params.append_pair("client_id", self.client_id.as_str()); + query_params.append_pair("request", token.as_str()); + } + + let final_auth_url = auth_url.to_string(); + Ok(final_auth_url) + } + + // Exchange the authorization code obtained from an access token provided by the user + // for the result of the MFA and validate. + // See: https://duo.com/docs/oauthapi#access-token (under Response Format) + async fn exchange_authz_code_for_result( + &self, + duo_code: &str, + duo_username: &str, + nonce: &str, + ) -> Result<(), Error> { + if duo_code.is_empty() { + err!("Empty Duo authorization code") + } + + let token_url = format!("https://{}/oauth/v1/token", self.api_host); + + let jwt_payload = self.new_client_assertion(&token_url); + + let token = match self.encode_duo_jwt(jwt_payload) { + Ok(token) => token, + Err(e) => return Err(e), + }; + + let mut post_body = HashMap::new(); + post_body.insert("grant_type", String::from("authorization_code")); + post_body.insert("code", String::from(duo_code)); + + // Must be the same URL that was supplied in the authorization request for the supplied duo_code + post_body.insert("redirect_uri", self.redirect_uri.clone()); + + post_body + .insert("client_assertion_type", String::from("urn:ietf:params:oauth:client-assertion-type:jwt-bearer")); + post_body.insert("client_assertion", token); + + let res = match make_http_request(reqwest::Method::POST, &token_url)? + .header(header::USER_AGENT, "vaultwarden:Duo/2.0 (Rust)") + .form(&post_body) + .send() + .await + { + Ok(r) => r, + Err(e) => err!(format!("Error exchanging Duo code: {e:?}")), + }; + + let status_code = res.status(); + if status_code != StatusCode::OK { + err!(format!("Failure response from Duo: {}", status_code)) + } + + let response: IdTokenResponse = match res.json::().await { + Ok(r) => r, + Err(e) => err!(format!("Error decoding ID token response: {e:?}")), + }; + + let mut validation = Validation::new(DUO_RESP_SIGNATURE_ALG); + validation.set_required_spec_claims(&["exp", "aud", "iss"]); + validation.set_audience(&[&self.client_id]); + validation.set_issuer(&[token_url.as_str()]); + + let token_data = match jsonwebtoken::decode::( + &response.id_token, + &DecodingKey::from_secret(self.client_secret.as_bytes()), + &validation, + ) { + Ok(c) => c, + Err(e) => err!(format!("Failed to decode Duo token {e:?}")), + }; + + let matching_nonces = crypto::ct_eq(nonce, &token_data.claims.nonce); + let matching_usernames = crypto::ct_eq(duo_username, &token_data.claims.preferred_username); + + if !(matching_nonces && matching_usernames) { + err!("Error validating Duo authorization, nonce or username mismatch.") + }; + + Ok(()) + } +} + +struct DuoAuthContext { + pub state: String, + pub user_email: String, + pub nonce: String, + pub exp: i64, +} + +// Given a state string, retrieve the associated Duo auth context and +// delete the retrieved state from the database. +async fn extract_context(state: &str, conn: &mut DbConn) -> Option { + let ctx: TwoFactorDuoContext = match TwoFactorDuoContext::find_by_state(state, conn).await { + Some(c) => c, + None => return None, + }; + + if ctx.exp < Utc::now().timestamp() { + ctx.delete(conn).await.ok(); + return None; + } + + // Copy the context data, so that we can delete the context from + // the database before returning. + let ret_ctx = DuoAuthContext { + state: ctx.state.clone(), + user_email: ctx.user_email.clone(), + nonce: ctx.nonce.clone(), + exp: ctx.exp, + }; + + ctx.delete(conn).await.ok(); + Some(ret_ctx) +} + +// Task to clean up expired Duo authentication contexts that may have accumulated in the database. +pub async fn purge_duo_contexts(pool: DbPool) { + debug!("Purging Duo authentication contexts"); + if let Ok(mut conn) = pool.get().await { + TwoFactorDuoContext::purge_expired_duo_contexts(&mut conn).await; + } else { + error!("Failed to get DB connection while purging expired Duo authentications") + } +} + +// Construct the url that Duo should redirect users to. +fn make_callback_url(client_name: &str) -> Result { + // Get the location of this application as defined in the config. + let base = match Url::parse(&format!("{}/", CONFIG.domain())) { + Ok(url) => url, + Err(e) => err!(format!("Error parsing configured domain URL (check your domain configuration): {e:?}")), + }; + + // Add the client redirect bridge location + let mut callback = match base.join(DUO_REDIRECT_LOCATION) { + Ok(url) => url, + Err(e) => err!(format!("Error constructing Duo redirect URL (check your domain configuration): {e:?}")), + }; + + // Add the 'client' string with the authenticating device type. The callback connector uses this + // information to figure out how it should handle certain clients. + { + let mut query_params = callback.query_pairs_mut(); + query_params.append_pair("client", client_name); + } + Ok(callback.to_string()) +} + +// Pre-redirect first stage of the Duo OIDC authentication flow. +// Returns the "AuthUrl" that should be returned to clients for MFA. +pub async fn get_duo_auth_url( + email: &str, + client_id: &str, + device_identifier: &String, + conn: &mut DbConn, +) -> Result { + let (ik, sk, _, host) = get_duo_keys_email(email, conn).await?; + + let callback_url = match make_callback_url(client_id) { + Ok(url) => url, + Err(e) => return Err(e), + }; + + let client = DuoClient::new(ik, sk, host, callback_url); + + match client.health_check().await { + Ok(()) => {} + Err(e) => return Err(e), + }; + + // Generate random OAuth2 state and OIDC Nonce + let state: String = crypto::get_random_string_alphanum(STATE_LENGTH); + let nonce: String = crypto::get_random_string_alphanum(STATE_LENGTH); + + // Bind the nonce to the device that's currently authing by hashing the nonce and device id + // and sending the result as the OIDC nonce. + let d: Digest = digest(&SHA512_256, format!("{nonce}{device_identifier}").as_bytes()); + let hash: String = HEXLOWER.encode(d.as_ref()); + + match TwoFactorDuoContext::save(state.as_str(), email, nonce.as_str(), CTX_VALIDITY_SECS, conn).await { + Ok(()) => client.make_authz_req_url(email, state, hash), + Err(e) => err!(format!("Error saving Duo authentication context: {e:?}")), + } +} + +// Post-redirect second stage of the Duo OIDC authentication flow. +// Exchanges an authorization code for the MFA result with Duo's API and validates the result. +pub async fn validate_duo_login( + email: &str, + two_factor_token: &str, + client_id: &str, + device_identifier: &str, + conn: &mut DbConn, +) -> EmptyResult { + // Result supplied to us by clients in the form "|" + let split: Vec<&str> = two_factor_token.split('|').collect(); + if split.len() != 2 { + err!( + "Invalid response length", + ErrorEvent { + event: EventType::UserFailedLogIn2fa + } + ); + } + + let code = split[0]; + let state = split[1]; + + let (ik, sk, _, host) = get_duo_keys_email(email, conn).await?; + + // Get the context by the state reported by the client. If we don't have one, + // it means the context is either missing or expired. + let ctx = match extract_context(state, conn).await { + Some(c) => c, + None => { + err!( + "Error validating duo authentication", + ErrorEvent { + event: EventType::UserFailedLogIn2fa + } + ) + } + }; + + // Context validation steps + let matching_usernames = crypto::ct_eq(email, &ctx.user_email); + + // Probably redundant, but we're double-checking them anyway. + let matching_states = crypto::ct_eq(state, &ctx.state); + let unexpired_context = ctx.exp > Utc::now().timestamp(); + + if !(matching_usernames && matching_states && unexpired_context) { + err!( + "Error validating duo authentication", + ErrorEvent { + event: EventType::UserFailedLogIn2fa + } + ) + } + + let callback_url = match make_callback_url(client_id) { + Ok(url) => url, + Err(e) => return Err(e), + }; + + let client = DuoClient::new(ik, sk, host, callback_url); + + match client.health_check().await { + Ok(()) => {} + Err(e) => return Err(e), + }; + + let d: Digest = digest(&SHA512_256, format!("{}{}", ctx.nonce, device_identifier).as_bytes()); + let hash: String = HEXLOWER.encode(d.as_ref()); + + match client.exchange_authz_code_for_result(code, email, hash.as_str()).await { + Ok(_) => Ok(()), + Err(_) => { + err!( + "Error validating duo authentication", + ErrorEvent { + event: EventType::UserFailedLogIn2fa + } + ) + } + } +} diff --git a/src/api/core/two_factor/email.rs b/src/api/core/two_factor/email.rs index a4a69240..293c0671 100644 --- a/src/api/core/two_factor/email.rs +++ b/src/api/core/two_factor/email.rs @@ -24,7 +24,10 @@ pub fn routes() -> Vec { #[derive(Deserialize)] #[serde(rename_all = "camelCase")] struct SendEmailLoginData { + // DeviceIdentifier: String, // Currently not used + #[serde(alias = "Email")] email: String, + #[serde(alias = "MasterPasswordHash")] master_password_hash: String, } @@ -289,7 +292,7 @@ impl EmailTokenData { } pub fn from_json(string: &str) -> Result { - let res: Result = serde_json::from_str(string); + let res: Result = serde_json::from_str(string); match res { Ok(x) => Ok(x), Err(_) => err!("Could not decode EmailTokenData from string"), diff --git a/src/api/core/two_factor/mod.rs b/src/api/core/two_factor/mod.rs index 2fbcfb3b..e3795eb8 100644 --- a/src/api/core/two_factor/mod.rs +++ b/src/api/core/two_factor/mod.rs @@ -19,6 +19,7 @@ use crate::{ pub mod authenticator; pub mod duo; +pub mod duo_oidc; pub mod email; pub mod protected_actions; pub mod webauthn; @@ -268,10 +269,24 @@ pub async fn send_incomplete_2fa_notifications(pool: DbPool) { "User {} did not complete a 2FA login within the configured time limit. IP: {}", user.email, login.ip_address ); - mail::send_incomplete_2fa_login(&user.email, &login.ip_address, &login.login_time, &login.device_name) - .await - .expect("Error sending incomplete 2FA email"); - login.delete(&mut conn).await.expect("Error deleting incomplete 2FA record"); + match mail::send_incomplete_2fa_login( + &user.email, + &login.ip_address, + &login.login_time, + &login.device_name, + &DeviceType::from_i32(login.device_type).to_string(), + ) + .await + { + Ok(_) => { + if let Err(e) = login.delete(&mut conn).await { + error!("Error deleting incomplete 2FA record: {e:#?}"); + } + } + Err(e) => { + error!("Error sending incomplete 2FA email: {e:#?}"); + } + } } } diff --git a/src/api/core/two_factor/protected_actions.rs b/src/api/core/two_factor/protected_actions.rs index 8bfc59c1..1a1d59c8 100644 --- a/src/api/core/two_factor/protected_actions.rs +++ b/src/api/core/two_factor/protected_actions.rs @@ -42,7 +42,7 @@ impl ProtectedActionData { } pub fn from_json(string: &str) -> Result { - let res: Result = serde_json::from_str(string); + let res: Result = serde_json::from_str(string); match res { Ok(x) => Ok(x), Err(_) => err!("Could not decode ProtectedActionData from string"), diff --git a/src/api/core/two_factor/yubikey.rs b/src/api/core/two_factor/yubikey.rs index 2eff3b6f..b2940353 100644 --- a/src/api/core/two_factor/yubikey.rs +++ b/src/api/core/two_factor/yubikey.rs @@ -49,7 +49,7 @@ fn parse_yubikeys(data: &EnableYubikeyData) -> Vec { data_keys.iter().filter_map(|e| e.as_ref().cloned()).collect() } -fn jsonify_yubikeys(yubikeys: Vec) -> serde_json::Value { +fn jsonify_yubikeys(yubikeys: Vec) -> Value { let mut result = Value::Object(serde_json::Map::new()); for (i, key) in yubikeys.into_iter().enumerate() { diff --git a/src/api/icons.rs b/src/api/icons.rs index 94fab3f8..6afbaa9f 100644 --- a/src/api/icons.rs +++ b/src/api/icons.rs @@ -1,6 +1,7 @@ use std::{ + collections::HashMap, net::IpAddr, - sync::{Arc, Mutex}, + sync::Arc, time::{Duration, SystemTime}, }; @@ -22,7 +23,8 @@ use html5gum::{Emitter, HtmlString, InfallibleTokenizer, Readable, StringReader, use crate::{ error::Error, - util::{get_reqwest_client_builder, Cached, CustomDnsResolver, CustomResolverError}, + http_client::{get_reqwest_client_builder, should_block_address, CustomHttpClientError}, + util::Cached, CONFIG, }; @@ -53,7 +55,6 @@ static CLIENT: Lazy = Lazy::new(|| { .timeout(icon_download_timeout) .pool_max_idle_per_host(5) // Configure the Hyper Pool to only have max 5 idle connections .pool_idle_timeout(pool_idle_timeout) // Configure the Hyper Pool to timeout after 10 seconds - .dns_resolver(CustomDnsResolver::instance()) .default_headers(default_headers.clone()) .build() .expect("Failed to build client") @@ -69,7 +70,8 @@ fn icon_external(domain: &str) -> Option { return None; } - if is_domain_blacklisted(domain) { + if should_block_address(domain) { + warn!("Blocked address: {}", domain); return None; } @@ -99,6 +101,15 @@ async fn icon_internal(domain: &str) -> Cached<(ContentType, Vec)> { ); } + if should_block_address(domain) { + warn!("Blocked address: {}", domain); + return Cached::ttl( + (ContentType::new("image", "png"), FALLBACK_ICON.to_vec()), + CONFIG.icon_cache_negttl(), + true, + ); + } + match get_icon(domain).await { Some((icon, icon_type)) => { Cached::ttl((ContentType::new("image", icon_type), icon), CONFIG.icon_cache_ttl(), true) @@ -144,30 +155,6 @@ fn is_valid_domain(domain: &str) -> bool { true } -pub fn is_domain_blacklisted(domain: &str) -> bool { - let Some(config_blacklist) = CONFIG.icon_blacklist_regex() else { - return false; - }; - - // Compiled domain blacklist - static COMPILED_BLACKLIST: Mutex> = Mutex::new(None); - let mut guard = COMPILED_BLACKLIST.lock().unwrap(); - - // If the stored regex is up to date, use it - if let Some((value, regex)) = &*guard { - if value == &config_blacklist { - return regex.is_match(domain); - } - } - - // If we don't have a regex stored, or it's not up to date, recreate it - let regex = Regex::new(&config_blacklist).unwrap(); - let is_match = regex.is_match(domain); - *guard = Some((config_blacklist, regex)); - - is_match -} - async fn get_icon(domain: &str) -> Option<(Vec, String)> { let path = format!("{}/{}.png", CONFIG.icon_cache_folder(), domain); @@ -195,9 +182,9 @@ async fn get_icon(domain: &str) -> Option<(Vec, String)> { Some((icon.to_vec(), icon_type.unwrap_or("x-icon").to_string())) } Err(e) => { - // If this error comes from the custom resolver, this means this is a blacklisted domain + // If this error comes from the custom resolver, this means this is a blocked domain // or non global IP, don't save the miss file in this case to avoid leaking it - if let Some(error) = CustomResolverError::downcast_ref(&e) { + if let Some(error) = CustomHttpClientError::downcast_ref(&e) { warn!("{error}"); return None; } @@ -353,7 +340,7 @@ async fn get_icon_url(domain: &str) -> Result { // First check the domain as given during the request for HTTPS. let resp = match get_page(&ssldomain).await { - Err(e) if CustomResolverError::downcast_ref(&e).is_none() => { + Err(e) if CustomHttpClientError::downcast_ref(&e).is_none() => { // If we get an error that is not caused by the blacklist, we retry with HTTP match get_page(&httpdomain).await { mut sub_resp @ Err(_) => { @@ -460,6 +447,9 @@ async fn get_page_with_referer(url: &str, referer: &str) -> Result u8 { + static PRIORITY_MAP: Lazy> = + Lazy::new(|| [(".png", 10), (".jpg", 20), (".jpeg", 20)].into_iter().collect()); + // Check if there is a dimension set let (width, height) = parse_sizes(sizes); @@ -484,13 +474,9 @@ fn get_icon_priority(href: &str, sizes: &str) -> u8 { 200 } } else { - // Change priority by file extension - if href.ends_with(".png") { - 10 - } else if href.ends_with(".jpg") || href.ends_with(".jpeg") { - 20 - } else { - 30 + match href.rsplit_once('.') { + Some((_, extension)) => PRIORITY_MAP.get(&*extension.to_ascii_lowercase()).copied().unwrap_or(30), + None => 30, } } } @@ -637,7 +623,7 @@ use cookie_store::CookieStore; pub struct Jar(std::sync::RwLock); impl reqwest::cookie::CookieStore for Jar { - fn set_cookies(&self, cookie_headers: &mut dyn Iterator, url: &url::Url) { + fn set_cookies(&self, cookie_headers: &mut dyn Iterator, url: &url::Url) { use cookie::{Cookie as RawCookie, ParseError as RawCookieParseError}; use time::Duration; @@ -656,7 +642,7 @@ impl reqwest::cookie::CookieStore for Jar { cookie_store.store_response_cookies(cookies, url); } - fn cookies(&self, url: &url::Url) -> Option { + fn cookies(&self, url: &url::Url) -> Option { let cookie_store = self.0.read().unwrap(); let s = cookie_store .get_request_values(url) @@ -668,7 +654,7 @@ impl reqwest::cookie::CookieStore for Jar { return None; } - header::HeaderValue::from_maybe_shared(Bytes::from(s)).ok() + HeaderValue::from_maybe_shared(Bytes::from(s)).ok() } } diff --git a/src/api/identity.rs b/src/api/identity.rs index fbf8d506..4244d68d 100644 --- a/src/api/identity.rs +++ b/src/api/identity.rs @@ -12,7 +12,7 @@ use crate::{ core::{ accounts::{PreloginData, RegisterData, _prelogin, _register}, log_user_event, - two_factor::{authenticator, duo, email, enforce_2fa_policy, webauthn, yubikey}, + two_factor::{authenticator, duo, duo_oidc, email, enforce_2fa_policy, webauthn, yubikey}, }, push::register_push_device, ApiResult, EmptyResult, JsonResult, @@ -135,6 +135,18 @@ async fn _refresh_login(data: ConnectData, conn: &mut DbConn) -> JsonResult { Ok(Json(result)) } +#[derive(Default, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +struct MasterPasswordPolicy { + min_complexity: u8, + min_length: u32, + require_lower: bool, + require_upper: bool, + require_numbers: bool, + require_special: bool, + enforce_on_login: bool, +} + async fn _password_login( data: ConnectData, user_uuid: &mut Option, @@ -253,7 +265,7 @@ async fn _password_login( let twofactor_token = twofactor_auth(&user, &data, &mut device, ip, conn).await?; if CONFIG.mail_enabled() && new_device { - if let Err(e) = mail::send_new_device_logged_in(&user.email, &ip.ip.to_string(), &now, &device.name).await { + if let Err(e) = mail::send_new_device_logged_in(&user.email, &ip.ip.to_string(), &now, &device).await { error!("Error sending new device email: {:#?}", e); if CONFIG.require_device_email() { @@ -282,6 +294,36 @@ async fn _password_login( let (access_token, expires_in) = device.refresh_tokens(&user, scope_vec); device.save(conn).await?; + // Fetch all valid Master Password Policies and merge them into one with all true's and larges numbers as one policy + let master_password_policies: Vec = + OrgPolicy::find_accepted_and_confirmed_by_user_and_active_policy( + &user.uuid, + OrgPolicyType::MasterPassword, + conn, + ) + .await + .into_iter() + .filter_map(|p| serde_json::from_str(&p.data).ok()) + .collect(); + + let master_password_policy = if !master_password_policies.is_empty() { + let mut mpp_json = json!(master_password_policies.into_iter().reduce(|acc, policy| { + MasterPasswordPolicy { + min_complexity: acc.min_complexity.max(policy.min_complexity), + min_length: acc.min_length.max(policy.min_length), + require_lower: acc.require_lower || policy.require_lower, + require_upper: acc.require_upper || policy.require_upper, + require_numbers: acc.require_numbers || policy.require_numbers, + require_special: acc.require_special || policy.require_special, + enforce_on_login: acc.enforce_on_login || policy.enforce_on_login, + } + })); + mpp_json["object"] = json!("masterPasswordPolicy"); + mpp_json + } else { + json!({"object": "masterPasswordPolicy"}) + }; + let mut result = json!({ "access_token": access_token, "expires_in": expires_in, @@ -297,9 +339,7 @@ async fn _password_login( "KdfParallelism": user.client_kdf_parallelism, "ResetMasterPassword": false, // TODO: Same as above "ForcePasswordReset": false, - "MasterPasswordPolicy": { - "object": "masterPasswordPolicy", - }, + "MasterPasswordPolicy": master_password_policy, "scope": scope, "unofficialServer": true, @@ -381,7 +421,7 @@ async fn _user_api_key_login( if CONFIG.mail_enabled() && new_device { let now = Utc::now().naive_utc(); - if let Err(e) = mail::send_new_device_logged_in(&user.email, &ip.ip.to_string(), &now, &device.name).await { + if let Err(e) = mail::send_new_device_logged_in(&user.email, &ip.ip.to_string(), &now, &device).await { error!("Error sending new device email: {:#?}", e); if CONFIG.require_device_email() { @@ -495,14 +535,16 @@ async fn twofactor_auth( return Ok(None); } - TwoFactorIncomplete::mark_incomplete(&user.uuid, &device.uuid, &device.name, ip, conn).await?; + TwoFactorIncomplete::mark_incomplete(&user.uuid, &device.uuid, &device.name, device.atype, ip, conn).await?; let twofactor_ids: Vec<_> = twofactors.iter().map(|tf| tf.atype).collect(); let selected_id = data.two_factor_provider.unwrap_or(twofactor_ids[0]); // If we aren't given a two factor provider, assume the first one let twofactor_code = match data.two_factor_token { Some(ref code) => code, - None => err_json!(_json_err_twofactor(&twofactor_ids, &user.uuid, conn).await?, "2FA token not provided"), + None => { + err_json!(_json_err_twofactor(&twofactor_ids, &user.uuid, data, conn).await?, "2FA token not provided") + } }; let selected_twofactor = twofactors.into_iter().find(|tf| tf.atype == selected_id && tf.enabled); @@ -519,7 +561,23 @@ async fn twofactor_auth( Some(TwoFactorType::Webauthn) => webauthn::validate_webauthn_login(&user.uuid, twofactor_code, conn).await?, Some(TwoFactorType::YubiKey) => yubikey::validate_yubikey_login(twofactor_code, &selected_data?).await?, Some(TwoFactorType::Duo) => { - duo::validate_duo_login(data.username.as_ref().unwrap().trim(), twofactor_code, conn).await? + match CONFIG.duo_use_iframe() { + true => { + // Legacy iframe prompt flow + duo::validate_duo_login(&user.email, twofactor_code, conn).await? + } + false => { + // OIDC based flow + duo_oidc::validate_duo_login( + &user.email, + twofactor_code, + data.client_id.as_ref().unwrap(), + data.device_identifier.as_ref().unwrap(), + conn, + ) + .await? + } + } } Some(TwoFactorType::Email) => { email::validate_email_code_str(&user.uuid, twofactor_code, &selected_data?, conn).await? @@ -532,7 +590,7 @@ async fn twofactor_auth( } _ => { err_json!( - _json_err_twofactor(&twofactor_ids, &user.uuid, conn).await?, + _json_err_twofactor(&twofactor_ids, &user.uuid, data, conn).await?, "2FA Remember token not provided" ) } @@ -560,7 +618,12 @@ fn _selected_data(tf: Option) -> ApiResult { tf.map(|t| t.data).map_res("Two factor doesn't exist") } -async fn _json_err_twofactor(providers: &[i32], user_uuid: &str, conn: &mut DbConn) -> ApiResult { +async fn _json_err_twofactor( + providers: &[i32], + user_uuid: &str, + data: &ConnectData, + conn: &mut DbConn, +) -> ApiResult { let mut result = json!({ "error" : "invalid_grant", "error_description" : "Two factor required.", @@ -588,12 +651,30 @@ async fn _json_err_twofactor(providers: &[i32], user_uuid: &str, conn: &mut DbCo None => err!("User does not exist"), }; - let (signature, host) = duo::generate_duo_signature(&email, conn).await?; - - result["TwoFactorProviders2"][provider.to_string()] = json!({ - "Host": host, - "Signature": signature, - }); + match CONFIG.duo_use_iframe() { + true => { + // Legacy iframe prompt flow + let (signature, host) = duo::generate_duo_signature(&email, conn).await?; + result["TwoFactorProviders2"][provider.to_string()] = json!({ + "Host": host, + "Signature": signature, + }) + } + false => { + // OIDC based flow + let auth_url = duo_oidc::get_duo_auth_url( + &email, + data.client_id.as_ref().unwrap(), + data.device_identifier.as_ref().unwrap(), + conn, + ) + .await?; + + result["TwoFactorProviders2"][provider.to_string()] = json!({ + "AuthUrl": auth_url, + }) + } + } } Some(tf_type @ TwoFactorType::YubiKey) => { diff --git a/src/api/mod.rs b/src/api/mod.rs index d5281bda..27a3775f 100644 --- a/src/api/mod.rs +++ b/src/api/mod.rs @@ -20,7 +20,7 @@ pub use crate::api::{ core::two_factor::send_incomplete_2fa_notifications, core::{emergency_notification_reminder_job, emergency_request_timeout_job}, core::{event_cleanup_job, events_routes as core_events_routes}, - icons::{is_domain_blacklisted, routes as icons_routes}, + icons::routes as icons_routes, identity::routes as identity_routes, notifications::routes as notifications_routes, notifications::{AnonymousNotify, Notify, UpdateType, WS_ANONYMOUS_SUBSCRIPTIONS, WS_USERS}, diff --git a/src/api/notifications.rs b/src/api/notifications.rs index e4707399..8c925e37 100644 --- a/src/api/notifications.rs +++ b/src/api/notifications.rs @@ -428,7 +428,7 @@ impl WebSocketUsers { let (user_uuid, collection_uuids, revision_date) = if let Some(collection_uuids) = collection_uuids { ( Value::Nil, - Value::Array(collection_uuids.into_iter().map(|v| v.into()).collect::>()), + Value::Array(collection_uuids.into_iter().map(|v| v.into()).collect::>()), serialize_date(Utc::now().naive_utc()), ) } else { diff --git a/src/api/push.rs b/src/api/push.rs index 607fb7ea..eaf304f9 100644 --- a/src/api/push.rs +++ b/src/api/push.rs @@ -1,11 +1,14 @@ -use reqwest::header::{ACCEPT, AUTHORIZATION, CONTENT_TYPE}; +use reqwest::{ + header::{ACCEPT, AUTHORIZATION, CONTENT_TYPE}, + Method, +}; use serde_json::Value; use tokio::sync::RwLock; use crate::{ api::{ApiResult, EmptyResult, UpdateType}, db::models::{Cipher, Device, Folder, Send, User}, - util::get_reqwest_client, + http_client::make_http_request, CONFIG, }; @@ -50,8 +53,7 @@ async fn get_auth_push_token() -> ApiResult { ("client_secret", &client_secret), ]; - let res = match get_reqwest_client() - .post(&format!("{}/connect/token", CONFIG.push_identity_uri())) + let res = match make_http_request(Method::POST, &format!("{}/connect/token", CONFIG.push_identity_uri()))? .form(¶ms) .send() .await @@ -104,8 +106,7 @@ pub async fn register_push_device(device: &mut Device, conn: &mut crate::db::DbC let auth_push_token = get_auth_push_token().await?; let auth_header = format!("Bearer {}", &auth_push_token); - if let Err(e) = get_reqwest_client() - .post(CONFIG.push_relay_uri() + "/push/register") + if let Err(e) = make_http_request(Method::POST, &(CONFIG.push_relay_uri() + "/push/register"))? .header(CONTENT_TYPE, "application/json") .header(ACCEPT, "application/json") .header(AUTHORIZATION, auth_header) @@ -132,8 +133,7 @@ pub async fn unregister_push_device(push_uuid: Option) -> EmptyResult { let auth_header = format!("Bearer {}", &auth_push_token); - match get_reqwest_client() - .delete(CONFIG.push_relay_uri() + "/push/" + &push_uuid.unwrap()) + match make_http_request(Method::DELETE, &(CONFIG.push_relay_uri() + "/push/" + &push_uuid.unwrap()))? .header(AUTHORIZATION, auth_header) .send() .await @@ -266,8 +266,15 @@ async fn send_to_push_relay(notification_data: Value) { let auth_header = format!("Bearer {}", &auth_push_token); - if let Err(e) = get_reqwest_client() - .post(CONFIG.push_relay_uri() + "/push/send") + let req = match make_http_request(Method::POST, &(CONFIG.push_relay_uri() + "/push/send")) { + Ok(r) => r, + Err(e) => { + error!("An error occurred while sending a send update to the push relay: {}", e); + return; + } + }; + + if let Err(e) = req .header(ACCEPT, "application/json") .header(CONTENT_TYPE, "application/json") .header(AUTHORIZATION, &auth_header) diff --git a/src/auth.rs b/src/auth.rs index c8060a28..b1a743da 100644 --- a/src/auth.rs +++ b/src/auth.rs @@ -1,13 +1,18 @@ // JWT Handling // use chrono::{TimeDelta, Utc}; +use jsonwebtoken::{errors::ErrorKind, Algorithm, DecodingKey, EncodingKey, Header}; use num_traits::FromPrimitive; use once_cell::sync::{Lazy, OnceCell}; - -use jsonwebtoken::{errors::ErrorKind, Algorithm, DecodingKey, EncodingKey, Header}; use openssl::rsa::Rsa; use serde::de::DeserializeOwned; use serde::ser::Serialize; +use std::{ + env, + fs::File, + io::{Read, Write}, + net::IpAddr, +}; use crate::{error::Error, CONFIG}; @@ -30,28 +35,37 @@ static JWT_FILE_DOWNLOAD_ISSUER: Lazy = Lazy::new(|| format!("{}|file_do static PRIVATE_RSA_KEY: OnceCell = OnceCell::new(); static PUBLIC_RSA_KEY: OnceCell = OnceCell::new(); -pub fn initialize_keys() -> Result<(), crate::error::Error> { - let mut priv_key_buffer = Vec::with_capacity(2048); +pub fn initialize_keys() -> Result<(), Error> { + fn read_key(create_if_missing: bool) -> Result<(Rsa, Vec), Error> { + let mut priv_key_buffer = Vec::with_capacity(2048); - let priv_key = { - let mut priv_key_file = - File::options().create(true).truncate(false).read(true).write(true).open(CONFIG.private_rsa_key())?; + let mut priv_key_file = File::options() + .create(create_if_missing) + .truncate(false) + .read(true) + .write(create_if_missing) + .open(CONFIG.private_rsa_key())?; #[allow(clippy::verbose_file_reads)] let bytes_read = priv_key_file.read_to_end(&mut priv_key_buffer)?; - if bytes_read > 0 { + let rsa_key = if bytes_read > 0 { Rsa::private_key_from_pem(&priv_key_buffer[..bytes_read])? - } else { + } else if create_if_missing { // Only create the key if the file doesn't exist or is empty - let rsa_key = openssl::rsa::Rsa::generate(2048)?; + let rsa_key = Rsa::generate(2048)?; priv_key_buffer = rsa_key.private_key_to_pem()?; priv_key_file.write_all(&priv_key_buffer)?; - info!("Private key created correctly."); + info!("Private key '{}' created correctly", CONFIG.private_rsa_key()); rsa_key - } - }; + } else { + err!("Private key does not exist or invalid format", CONFIG.private_rsa_key()); + }; + + Ok((rsa_key, priv_key_buffer)) + } + let (priv_key, priv_key_buffer) = read_key(true).or_else(|_| read_key(false))?; let pub_key_buffer = priv_key.public_key_to_pem()?; let enc = EncodingKey::from_rsa_pem(&priv_key_buffer)?; @@ -379,8 +393,6 @@ impl<'r> FromRequest<'r> for Host { referer.to_string() } else { // Try to guess from the headers - use std::env; - let protocol = if let Some(proto) = headers.get_one("X-Forwarded-Proto") { proto } else if env::var("ROCKET_TLS").is_ok() { @@ -805,11 +817,6 @@ impl<'r> FromRequest<'r> for OwnerHeaders { // // Client IP address detection // -use std::{ - fs::File, - io::{Read, Write}, - net::IpAddr, -}; pub struct ClientIp { pub ip: IpAddr, @@ -842,6 +849,35 @@ impl<'r> FromRequest<'r> for ClientIp { } } +pub struct Secure { + pub https: bool, +} + +#[rocket::async_trait] +impl<'r> FromRequest<'r> for Secure { + type Error = (); + + async fn from_request(request: &'r Request<'_>) -> Outcome { + let headers = request.headers(); + + // Try to guess from the headers + let protocol = match headers.get_one("X-Forwarded-Proto") { + Some(proto) => proto, + None => { + if env::var("ROCKET_TLS").is_ok() { + "https" + } else { + "http" + } + } + }; + + Outcome::Success(Secure { + https: protocol == "https", + }) + } +} + pub struct WsAccessTokenHeader { pub access_token: Option, } diff --git a/src/config.rs b/src/config.rs index 489a229d..1a384701 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,6 +1,9 @@ use std::env::consts::EXE_SUFFIX; use std::process::exit; -use std::sync::RwLock; +use std::sync::{ + atomic::{AtomicBool, Ordering}, + RwLock, +}; use job_scheduler_ng::Schedule; use once_cell::sync::Lazy; @@ -17,6 +20,8 @@ static CONFIG_FILE: Lazy = Lazy::new(|| { get_env("CONFIG_FILE").unwrap_or_else(|| format!("{data_folder}/config.json")) }); +pub static SKIP_CONFIG_VALIDATION: AtomicBool = AtomicBool::new(false); + pub static CONFIG: Lazy = Lazy::new(|| { Config::load().unwrap_or_else(|e| { println!("Error loading config:\n {e:?}\n"); @@ -146,6 +151,12 @@ macro_rules! make_config { config.signups_domains_whitelist = config.signups_domains_whitelist.trim().to_lowercase(); config.org_creation_users = config.org_creation_users.trim().to_lowercase(); + + // Copy the values from the deprecated flags to the new ones + if config.http_request_block_regex.is_none() { + config.http_request_block_regex = config.icon_blacklist_regex.clone(); + } + config } } @@ -325,7 +336,7 @@ macro_rules! make_config { } } }}; - ( @build $value:expr, $config:expr, gen, $default_fn:expr ) => {{ + ( @build $value:expr, $config:expr, generated, $default_fn:expr ) => {{ let f: &dyn Fn(&ConfigItems) -> _ = &$default_fn; f($config) }}; @@ -343,10 +354,10 @@ macro_rules! make_config { // } // // Where action applied when the value wasn't provided and can be: -// def: Use a default value -// auto: Value is auto generated based on other values -// option: Value is optional -// gen: Value is always autogenerated and it's original value ignored +// def: Use a default value +// auto: Value is auto generated based on other values +// option: Value is optional +// generated: Value is always autogenerated and it's original value ignored make_config! { folders { /// Data folder |> Main data folder @@ -409,7 +420,9 @@ make_config! { /// Auth Request cleanup schedule |> Cron schedule of the job that cleans old auth requests from the auth request. /// Defaults to every minute. Set blank to disable this job. auth_request_purge_schedule: String, false, def, "30 * * * * *".to_string(); - + /// Duo Auth context cleanup schedule |> Cron schedule of the job that cleans expired Duo contexts from the database. Does nothing if Duo MFA is disabled or set to use the legacy iframe prompt. + /// Defaults to once every minute. Set blank to disable this job. + duo_context_purge_schedule: String, false, def, "30 * * * * *".to_string(); }, /// General settings @@ -507,7 +520,7 @@ make_config! { /// Set to the string "none" (without quotes), to disable any headers and just use the remote IP ip_header: String, true, def, "X-Real-IP".to_string(); /// Internal IP header property, used to avoid recomputing each time - _ip_header_enabled: bool, false, gen, |c| &c.ip_header.trim().to_lowercase() != "none"; + _ip_header_enabled: bool, false, generated, |c| &c.ip_header.trim().to_lowercase() != "none"; /// Icon service |> The predefined icon services are: internal, bitwarden, duckduckgo, google. /// To specify a custom icon service, set a URL template with exactly one instance of `{}`, /// which is replaced with the domain. For example: `https://icon.example.com/domain/{}`. @@ -516,9 +529,9 @@ make_config! { /// corresponding icon at the external service. icon_service: String, false, def, "internal".to_string(); /// _icon_service_url - _icon_service_url: String, false, gen, |c| generate_icon_service_url(&c.icon_service); + _icon_service_url: String, false, generated, |c| generate_icon_service_url(&c.icon_service); /// _icon_service_csp - _icon_service_csp: String, false, gen, |c| generate_icon_service_csp(&c.icon_service, &c._icon_service_url); + _icon_service_csp: String, false, generated, |c| generate_icon_service_csp(&c.icon_service, &c._icon_service_url); /// Icon redirect code |> The HTTP status code to use for redirects to an external icon service. /// The supported codes are 301 (legacy permanent), 302 (legacy temporary), 307 (temporary), and 308 (permanent). /// Temporary redirects are useful while testing different icon services, but once a service @@ -531,12 +544,18 @@ make_config! { icon_cache_negttl: u64, true, def, 259_200; /// Icon download timeout |> Number of seconds when to stop attempting to download an icon. icon_download_timeout: u64, true, def, 10; - /// Icon blacklist Regex |> Any domains or IPs that match this regex won't be fetched by the icon service. + + /// [Deprecated] Icon blacklist Regex |> Use `http_request_block_regex` instead + icon_blacklist_regex: String, false, option; + /// [Deprecated] Icon blacklist non global IPs |> Use `http_request_block_non_global_ips` instead + icon_blacklist_non_global_ips: bool, false, def, true; + + /// Block HTTP domains/IPs by Regex |> Any domains or IPs that match this regex won't be fetched by the internal HTTP client. /// Useful to hide other servers in the local network. Check the WIKI for more details - icon_blacklist_regex: String, true, option; - /// Icon blacklist non global IPs |> Any IP which is not defined as a global IP will be blacklisted. + http_request_block_regex: String, true, option; + /// Block non global IPs |> Enabling this will cause the internal HTTP client to refuse to connect to any non global IP address. /// Useful to secure your internal environment: See https://en.wikipedia.org/wiki/Reserved_IP_addresses for a list of IPs which it will block - icon_blacklist_non_global_ips: bool, true, def, true; + http_request_block_non_global_ips: bool, true, auto, |c| c.icon_blacklist_non_global_ips; /// Disable Two-Factor remember |> Enabling this would force the users to use a second factor to login every time. /// Note that the checkbox would still be present, but ignored. @@ -564,8 +583,9 @@ make_config! { use_syslog: bool, false, def, false; /// Log file path log_file: String, false, option; - /// Log level - log_level: String, false, def, "Info".to_string(); + /// Log level |> Valid values are "trace", "debug", "info", "warn", "error" and "off" + /// For a specific module append it as a comma separated value "info,path::to::module=debug" + log_level: String, false, def, "info".to_string(); /// Enable DB WAL |> Turning this off might lead to worse performance, but might help if using vaultwarden on some exotic filesystems, /// that do not support WAL. Please make sure you read project wiki on the topic before changing this setting. @@ -603,7 +623,18 @@ make_config! { admin_session_lifetime: i64, true, def, 20; /// Enable groups (BETA!) (Know the risks!) |> Enables groups support for organizations (Currently contains known issues!). - org_groups_enabled: bool, false, def, false; + org_groups_enabled: bool, false, def, false; + + /// Increase note size limit (Know the risks!) |> Sets the secure note size limit to 100_000 instead of the default 10_000. + /// WARNING: This could cause issues with clients. Also exports will not work on Bitwarden servers! + increase_note_size_limit: bool, true, def, false; + /// Generated max_note_size value to prevent if..else matching during every check + _max_note_size: usize, false, generated, |c| if c.increase_note_size_limit {100_000} else {10_000}; + + /// Enforce Single Org with Reset Password Policy |> Enforce that the Single Org policy is enabled before setting the Reset Password policy + /// Bitwarden enforces this by default. In Vaultwarden we encouraged to use multiple organizations because groups were not available. + /// Setting this to true will enforce the Single Org Policy to be enabled before you can enable the Reset Password policy. + enforce_single_org_with_reset_pw_policy: bool, false, def, false; }, /// Yubikey settings @@ -622,6 +653,8 @@ make_config! { duo: _enable_duo { /// Enabled _enable_duo: bool, true, def, true; + /// Attempt to use deprecated iframe-based Traditional Prompt (Duo WebSDK 2) + duo_use_iframe: bool, false, def, false; /// Integration Key duo_ikey: String, true, option; /// Secret Key @@ -667,7 +700,7 @@ make_config! { /// Embed images as email attachments. smtp_embed_images: bool, true, def, true; /// _smtp_img_src - _smtp_img_src: String, false, gen, |c| generate_smtp_img_src(c.smtp_embed_images, &c.domain); + _smtp_img_src: String, false, generated, |c| generate_smtp_img_src(c.smtp_embed_images, &c.domain); /// Enable SMTP debugging (Know the risks!) |> DANGEROUS: Enabling this will output very detailed SMTP messages. This could contain sensitive information like passwords and usernames! Only enable this during troubleshooting! smtp_debug: bool, false, def, false; /// Accept Invalid Certs (Know the risks!) |> DANGEROUS: Allow invalid certificates. This option introduces significant vulnerabilities to man-in-the-middle attacks! @@ -899,12 +932,12 @@ fn validate_config(cfg: &ConfigItems) -> Result<(), Error> { err!("To use email 2FA as automatic fallback, email 2fa has to be enabled!"); } - // Check if the icon blacklist regex is valid - if let Some(ref r) = cfg.icon_blacklist_regex { + // Check if the HTTP request block regex is valid + if let Some(ref r) = cfg.http_request_block_regex { let validate_regex = regex::Regex::new(r); match validate_regex { Ok(_) => (), - Err(e) => err!(format!("`ICON_BLACKLIST_REGEX` is invalid: {e:#?}")), + Err(e) => err!(format!("`HTTP_REQUEST_BLOCK_REGEX` is invalid: {e:#?}")), } } @@ -984,6 +1017,11 @@ fn validate_config(cfg: &ConfigItems) -> Result<(), Error> { _ => {} } } + + if cfg.increase_note_size_limit { + println!("[WARNING] Secure Note size limit is increased to 100_000!"); + println!("[WARNING] This could cause issues with clients. Also exports will not work on Bitwarden servers!."); + } Ok(()) } @@ -1072,7 +1110,9 @@ impl Config { // Fill any missing with defaults let config = builder.build(); - validate_config(&config)?; + if !SKIP_CONFIG_VALIDATION.load(Ordering::Relaxed) { + validate_config(&config)?; + } Ok(Config { inner: RwLock::new(Inner { @@ -1192,7 +1232,7 @@ impl Config { } pub fn private_rsa_key(&self) -> String { - format!("{}.pem", CONFIG.rsa_key_filename()) + format!("{}.pem", self.rsa_key_filename()) } pub fn mail_enabled(&self) -> bool { let inner = &self.inner.read().unwrap().config; @@ -1223,12 +1263,8 @@ impl Config { token.is_some() && !token.unwrap().trim().is_empty() } - pub fn render_template( - &self, - name: &str, - data: &T, - ) -> Result { - if CONFIG.reload_templates() { + pub fn render_template(&self, name: &str, data: &T) -> Result { + if self.reload_templates() { warn!("RELOADING TEMPLATES"); let hb = load_templates(CONFIG.templates_folder()); hb.render(name, data).map_err(Into::into) @@ -1265,7 +1301,6 @@ where hb.set_strict_mode(true); // Register helpers hb.register_helper("case", Box::new(case_helper)); - hb.register_helper("jsesc", Box::new(js_escape_helper)); hb.register_helper("to_json", Box::new(to_json)); macro_rules! reg { @@ -1323,14 +1358,7 @@ where // And then load user templates to overwrite the defaults // Use .hbs extension for the files // Templates get registered with their relative name - hb.register_templates_directory( - path, - DirectorySourceOptions { - tpl_extension: ".hbs".to_owned(), - ..Default::default() - }, - ) - .unwrap(); + hb.register_templates_directory(path, DirectorySourceOptions::default()).unwrap(); hb } @@ -1353,32 +1381,6 @@ fn case_helper<'reg, 'rc>( } } -fn js_escape_helper<'reg, 'rc>( - h: &Helper<'rc>, - _r: &'reg Handlebars<'_>, - _ctx: &'rc Context, - _rc: &mut RenderContext<'reg, 'rc>, - out: &mut dyn Output, -) -> HelperResult { - let param = - h.param(0).ok_or_else(|| RenderErrorReason::Other(String::from("Param not found for helper \"jsesc\"")))?; - - let no_quote = h.param(1).is_some(); - - let value = param - .value() - .as_str() - .ok_or_else(|| RenderErrorReason::Other(String::from("Param for helper \"jsesc\" is not a String")))?; - - let mut escaped_value = value.replace('\\', "").replace('\'', "\\x22").replace('\"', "\\x27"); - if !no_quote { - escaped_value = format!(""{escaped_value}""); - } - - out.write(&escaped_value)?; - Ok(()) -} - fn to_json<'reg, 'rc>( h: &Helper<'rc>, _r: &'reg Handlebars<'_>, diff --git a/src/db/mod.rs b/src/db/mod.rs index 824b3c71..fe1ab79b 100644 --- a/src/db/mod.rs +++ b/src/db/mod.rs @@ -300,19 +300,17 @@ pub trait FromDb { impl FromDb for Vec { type Output = Vec; - #[allow(clippy::wrong_self_convention)] #[inline(always)] fn from_db(self) -> Self::Output { - self.into_iter().map(crate::db::FromDb::from_db).collect() + self.into_iter().map(FromDb::from_db).collect() } } impl FromDb for Option { type Output = Option; - #[allow(clippy::wrong_self_convention)] #[inline(always)] fn from_db(self) -> Self::Output { - self.map(crate::db::FromDb::from_db) + self.map(FromDb::from_db) } } @@ -368,23 +366,31 @@ pub mod models; /// Creates a back-up of the sqlite database /// MySQL/MariaDB and PostgreSQL are not supported. -pub async fn backup_database(conn: &mut DbConn) -> Result<(), Error> { +pub async fn backup_database(conn: &mut DbConn) -> Result { db_run! {@raw conn: postgresql, mysql { let _ = conn; err!("PostgreSQL and MySQL/MariaDB do not support this backup feature"); } sqlite { - use std::path::Path; - let db_url = CONFIG.database_url(); - let db_path = Path::new(&db_url).parent().unwrap().to_string_lossy(); - let file_date = chrono::Utc::now().format("%Y%m%d_%H%M%S").to_string(); - diesel::sql_query(format!("VACUUM INTO '{db_path}/db_{file_date}.sqlite3'")).execute(conn)?; - Ok(()) + backup_sqlite_database(conn) } } } +#[cfg(sqlite)] +pub fn backup_sqlite_database(conn: &mut diesel::sqlite::SqliteConnection) -> Result { + use diesel::RunQueryDsl; + let db_url = CONFIG.database_url(); + let db_path = std::path::Path::new(&db_url).parent().unwrap(); + let backup_file = db_path + .join(format!("db_{}.sqlite3", chrono::Utc::now().format("%Y%m%d_%H%M%S"))) + .to_string_lossy() + .into_owned(); + diesel::sql_query(format!("VACUUM INTO '{backup_file}'")).execute(conn)?; + Ok(backup_file) +} + /// Get the SQL Server version pub async fn get_sql_server_version(conn: &mut DbConn) -> String { db_run! {@raw conn: diff --git a/src/db/models/cipher.rs b/src/db/models/cipher.rs index 446749d4..06aa69c5 100644 --- a/src/db/models/cipher.rs +++ b/src/db/models/cipher.rs @@ -1,6 +1,6 @@ use crate::util::LowerCase; use crate::CONFIG; -use chrono::{NaiveDateTime, TimeDelta, Utc}; +use chrono::{DateTime, NaiveDateTime, TimeDelta, Utc}; use serde_json::Value; use super::{ @@ -79,21 +79,39 @@ impl Cipher { } } - pub fn validate_notes(cipher_data: &[CipherData]) -> EmptyResult { + pub fn validate_cipher_data(cipher_data: &[CipherData]) -> EmptyResult { let mut validation_errors = serde_json::Map::new(); + let max_note_size = CONFIG._max_note_size(); + let max_note_size_msg = + format!("The field Notes exceeds the maximum encrypted value length of {} characters.", &max_note_size); for (index, cipher) in cipher_data.iter().enumerate() { + // Validate the note size and if it is exceeded return a warning if let Some(note) = &cipher.notes { - if note.len() > 10_000 { - validation_errors.insert( - format!("Ciphers[{index}].Notes"), - serde_json::to_value([ - "The field Notes exceeds the maximum encrypted value length of 10000 characters.", - ]) - .unwrap(), - ); + if note.len() > max_note_size { + validation_errors + .insert(format!("Ciphers[{index}].Notes"), serde_json::to_value([&max_note_size_msg]).unwrap()); + } + } + + // Validate the password history if it contains `null` values and if so, return a warning + if let Some(Value::Array(password_history)) = &cipher.password_history { + for pwh in password_history { + if let Value::Object(pwo) = pwh { + if pwo.get("password").is_some_and(|p| !p.is_string()) { + validation_errors.insert( + format!("Ciphers[{index}].Notes"), + serde_json::to_value([ + "The password history contains a `null` value. Only strings are allowed.", + ]) + .unwrap(), + ); + break; + } + } } } } + if !validation_errors.is_empty() { let err_json = json!({ "message": "The model state is invalid.", @@ -155,27 +173,68 @@ impl Cipher { .as_ref() .and_then(|s| { serde_json::from_str::>>(s) - .inspect_err(|e| warn!("Error parsing fields {:?}", e)) + .inspect_err(|e| warn!("Error parsing fields {e:?} for {}", self.uuid)) .ok() }) - .map(|d| d.into_iter().map(|d| d.data).collect()) + .map(|d| { + d.into_iter() + .map(|mut f| { + // Check if the `type` key is a number, strings break some clients + // The fallback type is the hidden type `1`. this should prevent accidental data disclosure + // If not try to convert the string value to a number and fallback to `1` + // If it is both not a number and not a string, fallback to `1` + match f.data.get("type") { + Some(t) if t.is_number() => {} + Some(t) if t.is_string() => { + let type_num = &t.as_str().unwrap_or("1").parse::().unwrap_or(1); + f.data["type"] = json!(type_num); + } + _ => { + f.data["type"] = json!(1); + } + } + f.data + }) + .collect() + }) .unwrap_or_default(); + let password_history_json: Vec<_> = self .password_history .as_ref() .and_then(|s| { serde_json::from_str::>>(s) - .inspect_err(|e| warn!("Error parsing password history {:?}", e)) + .inspect_err(|e| warn!("Error parsing password history {e:?} for {}", self.uuid)) .ok() }) - .map(|d| d.into_iter().map(|d| d.data).collect()) + .map(|d| { + // Check every password history item if they are valid and return it. + // If a password field has the type `null` skip it, it breaks newer Bitwarden clients + // A second check is done to verify the lastUsedDate exists and is a valid DateTime string, if not the epoch start time will be used + d.into_iter() + .filter_map(|d| match d.data.get("password") { + Some(p) if p.is_string() => Some(d.data), + _ => None, + }) + .map(|d| match d.get("lastUsedDate").and_then(|l| l.as_str()) { + Some(l) if DateTime::parse_from_rfc3339(l).is_ok() => d, + _ => { + let mut d = d; + d["lastUsedDate"] = json!("1970-01-01T00:00:00.000Z"); + d + } + }) + .collect() + }) .unwrap_or_default(); // Get the type_data or a default to an empty json object '{}'. // If not passing an empty object, mobile clients will crash. - let mut type_data_json = serde_json::from_str::>(&self.data) - .map(|d| d.data) - .unwrap_or_else(|_| Value::Object(serde_json::Map::new())); + let mut type_data_json = + serde_json::from_str::>(&self.data).map(|d| d.data).unwrap_or_else(|_| { + warn!("Error parsing data field for {}", self.uuid); + Value::Object(serde_json::Map::new()) + }); // NOTE: This was marked as *Backwards Compatibility Code*, but as of January 2021 this is still being used by upstream // Set the first element of the Uris array as Uri, this is needed several (mobile) clients. @@ -189,10 +248,15 @@ impl Cipher { } } - // Fix secure note issues when data is `{}` + // Fix secure note issues when data is invalid // This breaks at least the native mobile clients - if self.atype == 2 && (self.data.eq("{}") || self.data.to_ascii_lowercase().eq("{\"type\":null}")) { - type_data_json = json!({"type": 0}); + if self.atype == 2 { + match type_data_json { + Value::Object(ref t) if t.get("type").is_some_and(|t| t.is_number()) => {} + _ => { + type_data_json = json!({"type": 0}); + } + } } // Clone the type_data and add some default value. @@ -200,7 +264,7 @@ impl Cipher { // NOTE: This was marked as *Backwards Compatibility Code*, but as of January 2021 this is still being used by upstream // data_json should always contain the following keys with every atype - data_json["fields"] = Value::Array(fields_json.clone()); + data_json["fields"] = json!([fields_json]); data_json["name"] = json!(self.name); data_json["notes"] = json!(self.notes); data_json["passwordHistory"] = Value::Array(password_history_json.clone()); @@ -620,6 +684,17 @@ impl Cipher { }} } + pub async fn find_by_uuid_and_org(cipher_uuid: &str, org_uuid: &str, conn: &mut DbConn) -> Option { + db_run! {conn: { + ciphers::table + .filter(ciphers::uuid.eq(cipher_uuid)) + .filter(ciphers::organization_uuid.eq(org_uuid)) + .first::(conn) + .ok() + .from_db() + }} + } + // Find all ciphers accessible or visible to the specified user. // // "Accessible" means the user has read access to the cipher, either via diff --git a/src/db/models/collection.rs b/src/db/models/collection.rs index 3ba6c516..7fb17c66 100644 --- a/src/db/models/collection.rs +++ b/src/db/models/collection.rs @@ -78,28 +78,46 @@ impl Collection { cipher_sync_data: Option<&crate::api::core::CipherSyncData>, conn: &mut DbConn, ) -> Value { - let (read_only, hide_passwords) = if let Some(cipher_sync_data) = cipher_sync_data { + let (read_only, hide_passwords, can_manage) = if let Some(cipher_sync_data) = cipher_sync_data { match cipher_sync_data.user_organizations.get(&self.org_uuid) { - Some(uo) if uo.has_full_access() => (false, false), - Some(_) => { + // Only for Manager types Bitwarden returns true for the can_manage option + // Owners and Admins always have false, but they can manage all collections anyway + Some(uo) if uo.has_full_access() => (false, false, uo.atype == UserOrgType::Manager), + Some(uo) => { + // Only let a manager manage collections when the have full read/write access + let is_manager = uo.atype == UserOrgType::Manager; if let Some(uc) = cipher_sync_data.user_collections.get(&self.uuid) { - (uc.read_only, uc.hide_passwords) + (uc.read_only, uc.hide_passwords, is_manager && !uc.read_only && !uc.hide_passwords) } else if let Some(cg) = cipher_sync_data.user_collections_groups.get(&self.uuid) { - (cg.read_only, cg.hide_passwords) + (cg.read_only, cg.hide_passwords, is_manager && !cg.read_only && !cg.hide_passwords) } else { - (false, false) + (false, false, false) } } - _ => (true, true), + _ => (true, true, false), } } else { - (!self.is_writable_by_user(user_uuid, conn).await, self.hide_passwords_for_user(user_uuid, conn).await) + match UserOrganization::find_confirmed_by_user_and_org(user_uuid, &self.org_uuid, conn).await { + Some(ou) if ou.has_full_access() => (false, false, ou.atype == UserOrgType::Manager), + Some(ou) => { + let is_manager = ou.atype == UserOrgType::Manager; + let read_only = !self.is_writable_by_user(user_uuid, conn).await; + let hide_passwords = self.hide_passwords_for_user(user_uuid, conn).await; + (read_only, hide_passwords, is_manager && !read_only && !hide_passwords) + } + _ => ( + !self.is_writable_by_user(user_uuid, conn).await, + self.hide_passwords_for_user(user_uuid, conn).await, + false, + ), + } }; let mut json_object = self.to_json(); json_object["object"] = json!("collectionDetails"); json_object["readOnly"] = json!(read_only); json_object["hidePasswords"] = json!(hide_passwords); + json_object["manage"] = json!(can_manage); json_object } diff --git a/src/db/models/device.rs b/src/db/models/device.rs index 60c63589..8feab49d 100644 --- a/src/db/models/device.rs +++ b/src/db/models/device.rs @@ -16,7 +16,7 @@ db_object! { pub user_uuid: String, pub name: String, - pub atype: i32, // https://github.com/bitwarden/server/blob/master/src/Core/Enums/DeviceType.cs + pub atype: i32, // https://github.com/bitwarden/server/blob/dcc199bcce4aa2d5621f6fab80f1b49d8b143418/src/Core/Enums/DeviceType.cs pub push_uuid: Option, pub push_token: Option, @@ -267,6 +267,9 @@ pub enum DeviceType { SafariExtension = 20, Sdk = 21, Server = 22, + WindowsCLI = 23, + MacOsCLI = 24, + LinuxCLI = 25, } impl fmt::Display for DeviceType { @@ -278,23 +281,26 @@ impl fmt::Display for DeviceType { DeviceType::FirefoxExtension => write!(f, "Firefox Extension"), DeviceType::OperaExtension => write!(f, "Opera Extension"), DeviceType::EdgeExtension => write!(f, "Edge Extension"), - DeviceType::WindowsDesktop => write!(f, "Windows Desktop"), - DeviceType::MacOsDesktop => write!(f, "MacOS Desktop"), - DeviceType::LinuxDesktop => write!(f, "Linux Desktop"), - DeviceType::ChromeBrowser => write!(f, "Chrome Browser"), - DeviceType::FirefoxBrowser => write!(f, "Firefox Browser"), - DeviceType::OperaBrowser => write!(f, "Opera Browser"), - DeviceType::EdgeBrowser => write!(f, "Edge Browser"), + DeviceType::WindowsDesktop => write!(f, "Windows"), + DeviceType::MacOsDesktop => write!(f, "macOS"), + DeviceType::LinuxDesktop => write!(f, "Linux"), + DeviceType::ChromeBrowser => write!(f, "Chrome"), + DeviceType::FirefoxBrowser => write!(f, "Firefox"), + DeviceType::OperaBrowser => write!(f, "Opera"), + DeviceType::EdgeBrowser => write!(f, "Edge"), DeviceType::IEBrowser => write!(f, "Internet Explorer"), DeviceType::UnknownBrowser => write!(f, "Unknown Browser"), - DeviceType::AndroidAmazon => write!(f, "Android Amazon"), + DeviceType::AndroidAmazon => write!(f, "Android"), DeviceType::Uwp => write!(f, "UWP"), - DeviceType::SafariBrowser => write!(f, "Safari Browser"), - DeviceType::VivaldiBrowser => write!(f, "Vivaldi Browser"), + DeviceType::SafariBrowser => write!(f, "Safari"), + DeviceType::VivaldiBrowser => write!(f, "Vivaldi"), DeviceType::VivaldiExtension => write!(f, "Vivaldi Extension"), DeviceType::SafariExtension => write!(f, "Safari Extension"), DeviceType::Sdk => write!(f, "SDK"), DeviceType::Server => write!(f, "Server"), + DeviceType::WindowsCLI => write!(f, "Windows CLI"), + DeviceType::MacOsCLI => write!(f, "macOS CLI"), + DeviceType::LinuxCLI => write!(f, "Linux CLI"), } } } @@ -325,6 +331,9 @@ impl DeviceType { 20 => DeviceType::SafariExtension, 21 => DeviceType::Sdk, 22 => DeviceType::Server, + 23 => DeviceType::WindowsCLI, + 24 => DeviceType::MacOsCLI, + 25 => DeviceType::LinuxCLI, _ => DeviceType::UnknownBrowser, } } diff --git a/src/db/models/emergency_access.rs b/src/db/models/emergency_access.rs index ecfe86fe..f4f3b9a9 100644 --- a/src/db/models/emergency_access.rs +++ b/src/db/models/emergency_access.rs @@ -26,7 +26,7 @@ db_object! { } } -/// Local methods +// Local methods impl EmergencyAccess { pub fn new(grantor_uuid: String, email: String, status: i32, atype: i32, wait_time_days: i32) -> Self { @@ -89,7 +89,7 @@ impl EmergencyAccess { Some(user) => user, None => { // remove outstanding invitations which should not exist - let _ = Self::delete_all_by_grantee_email(email, conn).await; + Self::delete_all_by_grantee_email(email, conn).await.ok(); return None; } } diff --git a/src/db/models/group.rs b/src/db/models/group.rs index f6ccc710..6e2db088 100644 --- a/src/db/models/group.rs +++ b/src/db/models/group.rs @@ -1,3 +1,7 @@ +use super::{User, UserOrgType, UserOrganization}; +use crate::api::EmptyResult; +use crate::db::DbConn; +use crate::error::MapResult; use chrono::{NaiveDateTime, Utc}; use serde_json::Value; @@ -69,7 +73,7 @@ impl Group { }) } - pub async fn to_json_details(&self, conn: &mut DbConn) -> Value { + pub async fn to_json_details(&self, user_org_type: &i32, conn: &mut DbConn) -> Value { let collections_groups: Vec = CollectionGroup::find_by_group(&self.uuid, conn) .await .iter() @@ -77,7 +81,8 @@ impl Group { json!({ "id": entry.collections_uuid, "readOnly": entry.read_only, - "hidePasswords": entry.hide_passwords + "hidePasswords": entry.hide_passwords, + "manage": *user_org_type == UserOrgType::Manager && !entry.read_only && !entry.hide_passwords }) }) .collect(); @@ -122,13 +127,6 @@ impl GroupUser { } } -use crate::db::DbConn; - -use crate::api::EmptyResult; -use crate::error::MapResult; - -use super::{User, UserOrganization}; - /// Database methods impl Group { pub async fn save(&mut self, conn: &mut DbConn) -> EmptyResult { diff --git a/src/db/models/mod.rs b/src/db/models/mod.rs index 0379141a..c336cb1a 100644 --- a/src/db/models/mod.rs +++ b/src/db/models/mod.rs @@ -12,6 +12,7 @@ mod org_policy; mod organization; mod send; mod two_factor; +mod two_factor_duo_context; mod two_factor_incomplete; mod user; @@ -29,5 +30,6 @@ pub use self::org_policy::{OrgPolicy, OrgPolicyErr, OrgPolicyType}; pub use self::organization::{Organization, OrganizationApiKey, UserOrgStatus, UserOrgType, UserOrganization}; pub use self::send::{Send, SendType}; pub use self::two_factor::{TwoFactor, TwoFactorType}; +pub use self::two_factor_duo_context::TwoFactorDuoContext; pub use self::two_factor_incomplete::TwoFactorIncomplete; pub use self::user::{Invitation, User, UserKdfType, UserStampException}; diff --git a/src/db/models/org_policy.rs b/src/db/models/org_policy.rs index d1e8aa0f..23e583b4 100644 --- a/src/db/models/org_policy.rs +++ b/src/db/models/org_policy.rs @@ -342,9 +342,11 @@ impl OrgPolicy { false } - pub async fn is_enabled_by_org(org_uuid: &str, policy_type: OrgPolicyType, conn: &mut DbConn) -> bool { - if let Some(policy) = OrgPolicy::find_by_org_and_type(org_uuid, policy_type, conn).await { - return policy.enabled; + pub async fn is_enabled_for_member(org_user_uuid: &str, policy_type: OrgPolicyType, conn: &mut DbConn) -> bool { + if let Some(membership) = UserOrganization::find_by_uuid(org_user_uuid, conn).await { + if let Some(policy) = OrgPolicy::find_by_org_and_type(&membership.org_uuid, policy_type, conn).await { + return policy.enabled; + } } false } diff --git a/src/db/models/organization.rs b/src/db/models/organization.rs index f378ba40..e59c1b05 100644 --- a/src/db/models/organization.rs +++ b/src/db/models/organization.rs @@ -1,9 +1,13 @@ use chrono::{NaiveDateTime, Utc}; use num_traits::FromPrimitive; use serde_json::Value; -use std::cmp::Ordering; +use std::{ + cmp::Ordering, + collections::{HashMap, HashSet}, +}; use super::{CollectionUser, Group, GroupUser, OrgPolicy, OrgPolicyType, TwoFactor, User}; +use crate::db::models::{Collection, CollectionGroup}; use crate::CONFIG; db_object! { @@ -112,7 +116,7 @@ impl PartialOrd for UserOrgType { } fn ge(&self, other: &i32) -> bool { - matches!(self.partial_cmp(other), Some(Ordering::Greater) | Some(Ordering::Equal)) + matches!(self.partial_cmp(other), Some(Ordering::Greater | Ordering::Equal)) } } @@ -135,7 +139,7 @@ impl PartialOrd for i32 { } fn le(&self, other: &UserOrgType) -> bool { - matches!(self.partial_cmp(other), Some(Ordering::Less) | Some(Ordering::Equal) | None) + matches!(self.partial_cmp(other), Some(Ordering::Less | Ordering::Equal) | None) } } @@ -156,11 +160,12 @@ impl Organization { "id": self.uuid, "identifier": null, // not supported by us "name": self.name, - "seats": 10, // The value doesn't matter, we don't check server-side - // "maxAutoscaleSeats": null, // The value doesn't matter, we don't check server-side - "maxCollections": 10, // The value doesn't matter, we don't check server-side - "maxStorageGb": 10, // The value doesn't matter, we don't check server-side + "seats": null, + "maxAutoscaleSeats": null, + "maxCollections": null, + "maxStorageGb": i16::MAX, // The value doesn't matter, we don't check server-side "use2fa": true, + "useCustomPermissions": false, "useDirectory": false, // Is supported, but this value isn't checked anywhere (yet) "useEvents": CONFIG.org_events_enabled(), "useGroups": CONFIG.org_groups_enabled(), @@ -182,8 +187,7 @@ impl Organization { "businessTaxNumber": null, "billingEmail": self.billing_email, - "plan": "TeamsAnnually", - "planType": 5, // TeamsAnnually plan + "planType": 6, // Custom plan "usersGetPremium": true, "object": "organization", }) @@ -369,8 +373,9 @@ impl UserOrganization { "id": self.org_uuid, "identifier": null, // Not supported "name": org.name, - "seats": 10, // The value doesn't matter, we don't check server-side - "maxCollections": 10, // The value doesn't matter, we don't check server-side + "seats": null, + "maxAutoscaleSeats": null, + "maxCollections": null, "usersGetPremium": true, "use2fa": true, "useDirectory": false, // Is supported, but this value isn't checked anywhere (yet) @@ -392,12 +397,14 @@ impl UserOrganization { "useCustomPermissions": false, "useActivateAutofillPolicy": false, + "organizationUserId": self.uuid, "providerId": null, "providerName": null, "providerType": null, "familySponsorshipFriendlyName": null, "familySponsorshipAvailable": false, - "planProductType": 0, + "planProductType": 3, + "productTierType": 3, // Enterprise tier "keyConnectorEnabled": false, "keyConnectorUrl": null, "familySponsorshipLastSyncDate": null, @@ -410,7 +417,7 @@ impl UserOrganization { "permissions": permissions, - "maxStorageGb": 10, // The value doesn't matter, we don't check server-side + "maxStorageGb": i16::MAX, // The value doesn't matter, we don't check server-side // These are per user "userId": self.user_uuid, @@ -450,27 +457,79 @@ impl UserOrganization { }; let collections: Vec = if include_collections { - CollectionUser::find_by_organization_and_user_uuid(&self.org_uuid, &self.user_uuid, conn) + // Get all collections for the user here already to prevent more queries + let cu: HashMap = + CollectionUser::find_by_organization_and_user_uuid(&self.org_uuid, &self.user_uuid, conn) + .await + .into_iter() + .map(|cu| (cu.collection_uuid.clone(), cu)) + .collect(); + + // Get all collection groups for this user to prevent there inclusion + let cg: HashSet = CollectionGroup::find_by_user(&self.user_uuid, conn) .await - .iter() - .map(|cu| { - json!({ - "id": cu.collection_uuid, - "readOnly": cu.read_only, - "hidePasswords": cu.hide_passwords, - }) + .into_iter() + .map(|cg| cg.collections_uuid) + .collect(); + + Collection::find_by_organization_and_user_uuid(&self.org_uuid, &self.user_uuid, conn) + .await + .into_iter() + .filter_map(|c| { + let (read_only, hide_passwords, can_manage) = if self.has_full_access() { + (false, false, self.atype == UserOrgType::Manager) + } else if let Some(cu) = cu.get(&c.uuid) { + ( + cu.read_only, + cu.hide_passwords, + self.atype == UserOrgType::Manager && !cu.read_only && !cu.hide_passwords, + ) + // If previous checks failed it might be that this user has access via a group, but we should not return those elements here + // Those are returned via a special group endpoint + } else if cg.contains(&c.uuid) { + return None; + } else { + (true, true, false) + }; + + Some(json!({ + "id": c.uuid, + "readOnly": read_only, + "hidePasswords": hide_passwords, + "manage": can_manage, + })) }) .collect() } else { Vec::with_capacity(0) }; + let permissions = json!({ + // TODO: Add support for Custom User Roles + // See: https://bitwarden.com/help/article/user-types-access-control/#custom-role + "accessEventLogs": false, + "accessImportExport": false, + "accessReports": false, + "createNewCollections": false, + "editAnyCollection": false, + "deleteAnyCollection": false, + "editAssignedCollections": false, + "deleteAssignedCollections": false, + "manageGroups": false, + "managePolicies": false, + "manageSso": false, // Not supported + "manageUsers": false, + "manageResetPassword": false, + "manageScim": false // Not supported (Not AGPLv3 Licensed) + }); + json!({ "id": self.uuid, "userId": self.user_uuid, "name": user.name, "email": user.email, "externalId": self.external_id, + "avatarColor": user.avatar_color, "groups": groups, "collections": collections, @@ -479,6 +538,13 @@ impl UserOrganization { "accessAll": self.access_all, "twoFactorEnabled": twofactor_enabled, "resetPasswordEnrolled": self.reset_password_key.is_some(), + "hasMasterPassword": !user.password_hash.is_empty(), + + "permissions": permissions, + + "ssoBound": false, // Not supported + "usesKeyConnector": false, // Not supported + "accessSecretsManager": false, // Not supported (Not AGPLv3 Licensed) "object": "organizationUserUserDetails", }) @@ -592,7 +658,7 @@ impl UserOrganization { } pub async fn find_by_email_and_org(email: &str, org_id: &str, conn: &mut DbConn) -> Option { - if let Some(user) = super::User::find_by_mail(email, conn).await { + if let Some(user) = User::find_by_mail(email, conn).await { if let Some(user_org) = UserOrganization::find_by_user_and_org(&user.uuid, org_id, conn).await { return Some(user_org); } @@ -734,6 +800,19 @@ impl UserOrganization { }} } + pub async fn find_confirmed_by_user_and_org(user_uuid: &str, org_uuid: &str, conn: &mut DbConn) -> Option { + db_run! { conn: { + users_organizations::table + .filter(users_organizations::user_uuid.eq(user_uuid)) + .filter(users_organizations::org_uuid.eq(org_uuid)) + .filter( + users_organizations::status.eq(UserOrgStatus::Confirmed as i32) + ) + .first::(conn) + .ok().from_db() + }} + } + pub async fn find_by_user(user_uuid: &str, conn: &mut DbConn) -> Vec { db_run! { conn: { users_organizations::table diff --git a/src/db/models/two_factor_duo_context.rs b/src/db/models/two_factor_duo_context.rs new file mode 100644 index 00000000..3e742d35 --- /dev/null +++ b/src/db/models/two_factor_duo_context.rs @@ -0,0 +1,84 @@ +use chrono::Utc; + +use crate::{api::EmptyResult, db::DbConn, error::MapResult}; + +db_object! { + #[derive(Identifiable, Queryable, Insertable, AsChangeset)] + #[diesel(table_name = twofactor_duo_ctx)] + #[diesel(primary_key(state))] + pub struct TwoFactorDuoContext { + pub state: String, + pub user_email: String, + pub nonce: String, + pub exp: i64, + } +} + +impl TwoFactorDuoContext { + pub async fn find_by_state(state: &str, conn: &mut DbConn) -> Option { + db_run! { + conn: { + twofactor_duo_ctx::table + .filter(twofactor_duo_ctx::state.eq(state)) + .first::(conn) + .ok() + .from_db() + } + } + } + + pub async fn save(state: &str, user_email: &str, nonce: &str, ttl: i64, conn: &mut DbConn) -> EmptyResult { + // A saved context should never be changed, only created or deleted. + let exists = Self::find_by_state(state, conn).await; + if exists.is_some() { + return Ok(()); + }; + + let exp = Utc::now().timestamp() + ttl; + + db_run! { + conn: { + diesel::insert_into(twofactor_duo_ctx::table) + .values(( + twofactor_duo_ctx::state.eq(state), + twofactor_duo_ctx::user_email.eq(user_email), + twofactor_duo_ctx::nonce.eq(nonce), + twofactor_duo_ctx::exp.eq(exp) + )) + .execute(conn) + .map_res("Error saving context to twofactor_duo_ctx") + } + } + } + + pub async fn find_expired(conn: &mut DbConn) -> Vec { + let now = Utc::now().timestamp(); + db_run! { + conn: { + twofactor_duo_ctx::table + .filter(twofactor_duo_ctx::exp.lt(now)) + .load::(conn) + .expect("Error finding expired contexts in twofactor_duo_ctx") + .from_db() + } + } + } + + pub async fn delete(&self, conn: &mut DbConn) -> EmptyResult { + db_run! { + conn: { + diesel::delete( + twofactor_duo_ctx::table + .filter(twofactor_duo_ctx::state.eq(&self.state))) + .execute(conn) + .map_res("Error deleting from twofactor_duo_ctx") + } + } + } + + pub async fn purge_expired_duo_contexts(conn: &mut DbConn) { + for context in Self::find_expired(conn).await { + context.delete(conn).await.ok(); + } + } +} diff --git a/src/db/models/two_factor_incomplete.rs b/src/db/models/two_factor_incomplete.rs index 49f7691f..12813eb5 100644 --- a/src/db/models/two_factor_incomplete.rs +++ b/src/db/models/two_factor_incomplete.rs @@ -13,6 +13,7 @@ db_object! { // must complete 2FA login before being added into the devices table. pub device_uuid: String, pub device_name: String, + pub device_type: i32, pub login_time: NaiveDateTime, pub ip_address: String, } @@ -23,6 +24,7 @@ impl TwoFactorIncomplete { user_uuid: &str, device_uuid: &str, device_name: &str, + device_type: i32, ip: &ClientIp, conn: &mut DbConn, ) -> EmptyResult { @@ -44,6 +46,7 @@ impl TwoFactorIncomplete { twofactor_incomplete::user_uuid.eq(user_uuid), twofactor_incomplete::device_uuid.eq(device_uuid), twofactor_incomplete::device_name.eq(device_name), + twofactor_incomplete::device_type.eq(device_type), twofactor_incomplete::login_time.eq(Utc::now().naive_utc()), twofactor_incomplete::ip_address.eq(ip.ip.to_string()), )) diff --git a/src/db/models/user.rs b/src/db/models/user.rs index a02b694d..d91c91c1 100644 --- a/src/db/models/user.rs +++ b/src/db/models/user.rs @@ -144,14 +144,14 @@ impl User { pub fn check_valid_recovery_code(&self, recovery_code: &str) -> bool { if let Some(ref totp_recover) = self.totp_recover { - crate::crypto::ct_eq(recovery_code, totp_recover.to_lowercase()) + crypto::ct_eq(recovery_code, totp_recover.to_lowercase()) } else { false } } pub fn check_valid_api_key(&self, key: &str) -> bool { - matches!(self.api_key, Some(ref api_key) if crate::crypto::ct_eq(api_key, key)) + matches!(self.api_key, Some(ref api_key) if crypto::ct_eq(api_key, key)) } /// Set the password hash generated diff --git a/src/db/schemas/mysql/schema.rs b/src/db/schemas/mysql/schema.rs index 0fb286a4..fa84ed05 100644 --- a/src/db/schemas/mysql/schema.rs +++ b/src/db/schemas/mysql/schema.rs @@ -169,11 +169,21 @@ table! { user_uuid -> Text, device_uuid -> Text, device_name -> Text, + device_type -> Integer, login_time -> Timestamp, ip_address -> Text, } } +table! { + twofactor_duo_ctx (state) { + state -> Text, + user_email -> Text, + nonce -> Text, + exp -> BigInt, + } +} + table! { users (uuid) { uuid -> Text, diff --git a/src/db/schemas/postgresql/schema.rs b/src/db/schemas/postgresql/schema.rs index 26bf4b68..d1ea4b02 100644 --- a/src/db/schemas/postgresql/schema.rs +++ b/src/db/schemas/postgresql/schema.rs @@ -169,11 +169,21 @@ table! { user_uuid -> Text, device_uuid -> Text, device_name -> Text, + device_type -> Integer, login_time -> Timestamp, ip_address -> Text, } } +table! { + twofactor_duo_ctx (state) { + state -> Text, + user_email -> Text, + nonce -> Text, + exp -> BigInt, + } +} + table! { users (uuid) { uuid -> Text, diff --git a/src/db/schemas/sqlite/schema.rs b/src/db/schemas/sqlite/schema.rs index 26bf4b68..d1ea4b02 100644 --- a/src/db/schemas/sqlite/schema.rs +++ b/src/db/schemas/sqlite/schema.rs @@ -169,11 +169,21 @@ table! { user_uuid -> Text, device_uuid -> Text, device_name -> Text, + device_type -> Integer, login_time -> Timestamp, ip_address -> Text, } } +table! { + twofactor_duo_ctx (state) { + state -> Text, + user_email -> Text, + nonce -> Text, + exp -> BigInt, + } +} + table! { users (uuid) { uuid -> Text, diff --git a/src/error.rs b/src/error.rs index afb1dc83..1061a08d 100644 --- a/src/error.rs +++ b/src/error.rs @@ -2,6 +2,7 @@ // Error generator macro // use crate::db::models::EventType; +use crate::http_client::CustomHttpClientError; use std::error::Error as StdError; macro_rules! make_error { @@ -68,6 +69,10 @@ make_error! { Empty(Empty): _no_source, _serialize, // Used to represent err! calls Simple(String): _no_source, _api_error, + + // Used in our custom http client to handle non-global IPs and blocked domains + CustomHttpClient(CustomHttpClientError): _has_source, _api_error, + // Used for special return values, like 2FA errors Json(Value): _no_source, _serialize, Db(DieselErr): _has_source, _api_error, @@ -204,7 +209,7 @@ use rocket::http::{ContentType, Status}; use rocket::request::Request; use rocket::response::{self, Responder, Response}; -impl<'r> Responder<'r, 'static> for Error { +impl Responder<'_, 'static> for Error { fn respond_to(self, _: &Request<'_>) -> response::Result<'static> { match self.error { ErrorKind::Empty(_) => {} // Don't print the error in this situation diff --git a/src/http_client.rs b/src/http_client.rs new file mode 100644 index 00000000..9feba366 --- /dev/null +++ b/src/http_client.rs @@ -0,0 +1,246 @@ +use std::{ + fmt, + net::{IpAddr, SocketAddr}, + str::FromStr, + sync::{Arc, Mutex}, + time::Duration, +}; + +use hickory_resolver::{system_conf::read_system_conf, TokioAsyncResolver}; +use once_cell::sync::Lazy; +use regex::Regex; +use reqwest::{ + dns::{Name, Resolve, Resolving}, + header, Client, ClientBuilder, +}; +use url::Host; + +use crate::{util::is_global, CONFIG}; + +pub fn make_http_request(method: reqwest::Method, url: &str) -> Result { + let Ok(url) = url::Url::parse(url) else { + err!("Invalid URL"); + }; + let Some(host) = url.host() else { + err!("Invalid host"); + }; + + should_block_host(host)?; + + static INSTANCE: Lazy = Lazy::new(|| get_reqwest_client_builder().build().expect("Failed to build client")); + + Ok(INSTANCE.request(method, url)) +} + +pub fn get_reqwest_client_builder() -> ClientBuilder { + let mut headers = header::HeaderMap::new(); + headers.insert(header::USER_AGENT, header::HeaderValue::from_static("Vaultwarden")); + + let redirect_policy = reqwest::redirect::Policy::custom(|attempt| { + if attempt.previous().len() >= 5 { + return attempt.error("Too many redirects"); + } + + let Some(host) = attempt.url().host() else { + return attempt.error("Invalid host"); + }; + + if let Err(e) = should_block_host(host) { + return attempt.error(e); + } + + attempt.follow() + }); + + Client::builder() + .default_headers(headers) + .redirect(redirect_policy) + .dns_resolver(CustomDnsResolver::instance()) + .timeout(Duration::from_secs(10)) +} + +pub fn should_block_address(domain_or_ip: &str) -> bool { + if let Ok(ip) = IpAddr::from_str(domain_or_ip) { + if should_block_ip(ip) { + return true; + } + } + + should_block_address_regex(domain_or_ip) +} + +fn should_block_ip(ip: IpAddr) -> bool { + if !CONFIG.http_request_block_non_global_ips() { + return false; + } + + !is_global(ip) +} + +fn should_block_address_regex(domain_or_ip: &str) -> bool { + let Some(block_regex) = CONFIG.http_request_block_regex() else { + return false; + }; + + static COMPILED_REGEX: Mutex> = Mutex::new(None); + let mut guard = COMPILED_REGEX.lock().unwrap(); + + // If the stored regex is up to date, use it + if let Some((value, regex)) = &*guard { + if value == &block_regex { + return regex.is_match(domain_or_ip); + } + } + + // If we don't have a regex stored, or it's not up to date, recreate it + let regex = Regex::new(&block_regex).unwrap(); + let is_match = regex.is_match(domain_or_ip); + *guard = Some((block_regex, regex)); + + is_match +} + +fn should_block_host(host: Host<&str>) -> Result<(), CustomHttpClientError> { + let (ip, host_str): (Option, String) = match host { + Host::Ipv4(ip) => (Some(ip.into()), ip.to_string()), + Host::Ipv6(ip) => (Some(ip.into()), ip.to_string()), + Host::Domain(d) => (None, d.to_string()), + }; + + if let Some(ip) = ip { + if should_block_ip(ip) { + return Err(CustomHttpClientError::NonGlobalIp { + domain: None, + ip, + }); + } + } + + if should_block_address_regex(&host_str) { + return Err(CustomHttpClientError::Blocked { + domain: host_str, + }); + } + + Ok(()) +} + +#[derive(Debug, Clone)] +pub enum CustomHttpClientError { + Blocked { + domain: String, + }, + NonGlobalIp { + domain: Option, + ip: IpAddr, + }, +} + +impl CustomHttpClientError { + pub fn downcast_ref(e: &dyn std::error::Error) -> Option<&Self> { + let mut source = e.source(); + + while let Some(err) = source { + source = err.source(); + if let Some(err) = err.downcast_ref::() { + return Some(err); + } + } + None + } +} + +impl fmt::Display for CustomHttpClientError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Blocked { + domain, + } => write!(f, "Blocked domain: {domain} matched HTTP_REQUEST_BLOCK_REGEX"), + Self::NonGlobalIp { + domain: Some(domain), + ip, + } => write!(f, "IP {ip} for domain '{domain}' is not a global IP!"), + Self::NonGlobalIp { + domain: None, + ip, + } => write!(f, "IP {ip} is not a global IP!"), + } + } +} + +impl std::error::Error for CustomHttpClientError {} + +#[derive(Debug, Clone)] +enum CustomDnsResolver { + Default(), + Hickory(Arc), +} +type BoxError = Box; + +impl CustomDnsResolver { + fn instance() -> Arc { + static INSTANCE: Lazy> = Lazy::new(CustomDnsResolver::new); + Arc::clone(&*INSTANCE) + } + + fn new() -> Arc { + match read_system_conf() { + Ok((config, opts)) => { + let resolver = TokioAsyncResolver::tokio(config.clone(), opts.clone()); + Arc::new(Self::Hickory(Arc::new(resolver))) + } + Err(e) => { + warn!("Error creating Hickory resolver, falling back to default: {e:?}"); + Arc::new(Self::Default()) + } + } + } + + // Note that we get an iterator of addresses, but we only grab the first one for convenience + async fn resolve_domain(&self, name: &str) -> Result, BoxError> { + pre_resolve(name)?; + + let result = match self { + Self::Default() => tokio::net::lookup_host(name).await?.next(), + Self::Hickory(r) => r.lookup_ip(name).await?.iter().next().map(|a| SocketAddr::new(a, 0)), + }; + + if let Some(addr) = &result { + post_resolve(name, addr.ip())?; + } + + Ok(result) + } +} + +fn pre_resolve(name: &str) -> Result<(), CustomHttpClientError> { + if should_block_address(name) { + return Err(CustomHttpClientError::Blocked { + domain: name.to_string(), + }); + } + + Ok(()) +} + +fn post_resolve(name: &str, ip: IpAddr) -> Result<(), CustomHttpClientError> { + if should_block_ip(ip) { + Err(CustomHttpClientError::NonGlobalIp { + domain: Some(name.to_string()), + ip, + }) + } else { + Ok(()) + } +} + +impl Resolve for CustomDnsResolver { + fn resolve(&self, name: Name) -> Resolving { + let this = self.clone(); + Box::pin(async move { + let name = name.as_str(); + let result = this.resolve_domain(name).await?; + Ok::(Box::new(result.into_iter())) + }) + } +} diff --git a/src/mail.rs b/src/mail.rs index 151554a1..b33efd95 100644 --- a/src/mail.rs +++ b/src/mail.rs @@ -17,6 +17,7 @@ use crate::{ encode_jwt, generate_delete_claims, generate_emergency_access_invite_claims, generate_invite_claims, generate_verify_email_claims, }, + db::models::{Device, DeviceType, User}, error::Error, CONFIG, }; @@ -229,37 +230,51 @@ pub async fn send_single_org_removed_from_org(address: &str, org_name: &str) -> } pub async fn send_invite( - address: &str, - uuid: &str, + user: &User, org_id: Option, org_user_id: Option, org_name: &str, invited_by_email: Option, ) -> EmptyResult { let claims = generate_invite_claims( - uuid.to_string(), - String::from(address), + user.uuid.clone(), + user.email.clone(), org_id.clone(), org_user_id.clone(), invited_by_email, ); let invite_token = encode_jwt(&claims); + let mut query = url::Url::parse("https://query.builder").unwrap(); + { + let mut query_params = query.query_pairs_mut(); + query_params + .append_pair("email", &user.email) + .append_pair("organizationName", org_name) + .append_pair("organizationId", org_id.as_deref().unwrap_or("_")) + .append_pair("organizationUserId", org_user_id.as_deref().unwrap_or("_")) + .append_pair("token", &invite_token); + if user.private_key.is_some() { + query_params.append_pair("orgUserHasExistingUser", "true"); + } + } + + let query_string = match query.query() { + None => err!(format!("Failed to build invite URL query parameters")), + Some(query) => query, + }; + // `url.Url` would place the anchor `#` after the query parameters + let url = format!("{}/#/accept-organization/?{}", CONFIG.domain(), query_string); let (subject, body_html, body_text) = get_text( "email/send_org_invite", json!({ - "url": CONFIG.domain(), + "url": url, "img_src": CONFIG._smtp_img_src(), - "org_id": org_id.as_deref().unwrap_or("_"), - "org_user_id": org_user_id.as_deref().unwrap_or("_"), - "email": percent_encode(address.as_bytes(), NON_ALPHANUMERIC).to_string(), - "org_name_encoded": percent_encode(org_name.as_bytes(), NON_ALPHANUMERIC).to_string(), "org_name": org_name, - "token": invite_token, }), )?; - send_email(address, &subject, body_html, body_text).await + send_email(&user.email, &subject, body_html, body_text).await } pub async fn send_emergency_access_invite( @@ -427,9 +442,8 @@ pub async fn send_invite_confirmed(address: &str, org_name: &str) -> EmptyResult send_email(address, &subject, body_html, body_text).await } -pub async fn send_new_device_logged_in(address: &str, ip: &str, dt: &NaiveDateTime, device: &str) -> EmptyResult { +pub async fn send_new_device_logged_in(address: &str, ip: &str, dt: &NaiveDateTime, device: &Device) -> EmptyResult { use crate::util::upcase_first; - let device = upcase_first(device); let fmt = "%A, %B %_d, %Y at %r %Z"; let (subject, body_html, body_text) = get_text( @@ -438,7 +452,8 @@ pub async fn send_new_device_logged_in(address: &str, ip: &str, dt: &NaiveDateTi "url": CONFIG.domain(), "img_src": CONFIG._smtp_img_src(), "ip": ip, - "device": device, + "device_name": upcase_first(&device.name), + "device_type": DeviceType::from_i32(device.atype).to_string(), "datetime": crate::util::format_naive_datetime_local(dt, fmt), }), )?; @@ -446,9 +461,14 @@ pub async fn send_new_device_logged_in(address: &str, ip: &str, dt: &NaiveDateTi send_email(address, &subject, body_html, body_text).await } -pub async fn send_incomplete_2fa_login(address: &str, ip: &str, dt: &NaiveDateTime, device: &str) -> EmptyResult { +pub async fn send_incomplete_2fa_login( + address: &str, + ip: &str, + dt: &NaiveDateTime, + device_name: &str, + device_type: &str, +) -> EmptyResult { use crate::util::upcase_first; - let device = upcase_first(device); let fmt = "%A, %B %_d, %Y at %r %Z"; let (subject, body_html, body_text) = get_text( @@ -457,7 +477,8 @@ pub async fn send_incomplete_2fa_login(address: &str, ip: &str, dt: &NaiveDateTi "url": CONFIG.domain(), "img_src": CONFIG._smtp_img_src(), "ip": ip, - "device": device, + "device_name": upcase_first(device_name), + "device_type": device_type, "datetime": crate::util::format_naive_datetime_local(dt, fmt), "time_limit": CONFIG.incomplete_2fa_time_limit(), }), diff --git a/src/main.rs b/src/main.rs index 73085901..a0b40a84 100644 --- a/src/main.rs +++ b/src/main.rs @@ -26,6 +26,7 @@ extern crate diesel; extern crate diesel_migrations; use std::{ + collections::HashMap, fs::{canonicalize, create_dir_all}, panic, path::Path, @@ -39,6 +40,9 @@ use tokio::{ io::{AsyncBufReadExt, BufReader}, }; +#[cfg(unix)] +use tokio::signal::unix::SignalKind; + #[macro_use] mod error; mod api; @@ -47,16 +51,18 @@ mod config; mod crypto; #[macro_use] mod db; +mod http_client; mod mail; mod ratelimit; mod util; +use crate::api::core::two_factor::duo_oidc::purge_duo_contexts; use crate::api::purge_auth_requests; use crate::api::{WS_ANONYMOUS_SUBSCRIPTIONS, WS_USERS}; pub use config::CONFIG; pub use error::{Error, MapResult}; use rocket::data::{Limits, ToByteUnit}; -use std::sync::Arc; +use std::sync::{atomic::Ordering, Arc}; pub use util::is_running_in_container; #[rocket::main] @@ -64,19 +70,11 @@ async fn main() -> Result<(), Error> { parse_args(); launch_info(); - use log::LevelFilter as LF; - let level = LF::from_str(&CONFIG.log_level()).unwrap_or_else(|_| { - let valid_log_levels = LF::iter().map(|lvl| lvl.as_str().to_lowercase()).collect::>().join(", "); - println!("Log level must be one of the following: {valid_log_levels}"); - exit(1); - }); - init_logging(level).ok(); - - let extra_debug = matches!(level, LF::Trace | LF::Debug); + let level = init_logging()?; check_data_folder().await; - auth::initialize_keys().unwrap_or_else(|_| { - error!("Error creating keys, exiting..."); + auth::initialize_keys().unwrap_or_else(|e| { + error!("Error creating private key '{}'\n{e:?}\nExiting Vaultwarden!", CONFIG.private_rsa_key()); exit(1); }); check_web_vault(); @@ -88,8 +86,9 @@ async fn main() -> Result<(), Error> { let pool = create_db_pool().await; schedule_jobs(pool.clone()); - crate::db::models::TwoFactor::migrate_u2f_to_webauthn(&mut pool.get().await.unwrap()).await.unwrap(); + db::models::TwoFactor::migrate_u2f_to_webauthn(&mut pool.get().await.unwrap()).await.unwrap(); + let extra_debug = matches!(level, log::LevelFilter::Trace | log::LevelFilter::Debug); launch_rocket(pool, extra_debug).await // Blocks until program termination. } @@ -101,10 +100,12 @@ USAGE: FLAGS: -h, --help Prints help information - -v, --version Prints the app version + -v, --version Prints the app and web-vault version COMMAND: hash [--preset {bitwarden|owasp}] Generate an Argon2id PHC ADMIN_TOKEN + backup Create a backup of the SQLite database + You can also send the USR1 signal to trigger a backup PRESETS: m= t= p= bitwarden (default) 64MiB, 3 Iterations, 4 Threads @@ -119,11 +120,14 @@ fn parse_args() { let version = VERSION.unwrap_or("(Version info from Git not present)"); if pargs.contains(["-h", "--help"]) { - println!("vaultwarden {version}"); + println!("Vaultwarden {version}"); print!("{HELP}"); exit(0); } else if pargs.contains(["-v", "--version"]) { - println!("vaultwarden {version}"); + config::SKIP_CONFIG_VALIDATION.store(true, Ordering::Relaxed); + let web_vault_version = util::get_web_vault_version(); + println!("Vaultwarden {version}"); + println!("Web-Vault {web_vault_version}"); exit(0); } @@ -167,7 +171,7 @@ fn parse_args() { } let argon2 = Argon2::new(Argon2id, V0x13, argon2_params.build().unwrap()); - let salt = SaltString::encode_b64(&crate::crypto::get_random_bytes::<32>()).unwrap(); + let salt = SaltString::encode_b64(&crypto::get_random_bytes::<32>()).unwrap(); let argon2_timer = tokio::time::Instant::now(); if let Ok(password_hash) = argon2.hash_password(password.as_bytes(), &salt) { @@ -178,13 +182,47 @@ fn parse_args() { argon2_timer.elapsed() ); } else { - error!("Unable to generate Argon2id PHC hash."); + println!("Unable to generate Argon2id PHC hash."); exit(1); } + } else if command == "backup" { + match backup_sqlite() { + Ok(f) => { + println!("Backup to '{f}' was successful"); + exit(0); + } + Err(e) => { + println!("Backup failed. {e:?}"); + exit(1); + } + } } exit(0); } } + +fn backup_sqlite() -> Result { + #[cfg(sqlite)] + { + use crate::db::{backup_sqlite_database, DbConnType}; + if DbConnType::from_url(&CONFIG.database_url()).map(|t| t == DbConnType::sqlite).unwrap_or(false) { + use diesel::Connection; + let url = CONFIG.database_url(); + + // Establish a connection to the sqlite database + let mut conn = diesel::sqlite::SqliteConnection::establish(&url)?; + let backup_file = backup_sqlite_database(&mut conn)?; + Ok(backup_file) + } else { + err_silent!("The database type is not SQLite. Backups only works for SQLite databases") + } + } + #[cfg(not(sqlite))] + { + err_silent!("The 'sqlite' feature is not enabled. Backups only works for SQLite databases") + } +} + fn launch_info() { println!( "\ @@ -210,7 +248,38 @@ fn launch_info() { ); } -fn init_logging(level: log::LevelFilter) -> Result<(), fern::InitError> { +fn init_logging() -> Result { + let levels = log::LevelFilter::iter().map(|lvl| lvl.as_str().to_lowercase()).collect::>().join("|"); + let log_level_rgx_str = format!("^({levels})((,[^,=]+=({levels}))*)$"); + let log_level_rgx = regex::Regex::new(&log_level_rgx_str)?; + let config_str = CONFIG.log_level().to_lowercase(); + + let (level, levels_override) = if let Some(caps) = log_level_rgx.captures(&config_str) { + let level = caps + .get(1) + .and_then(|m| log::LevelFilter::from_str(m.as_str()).ok()) + .ok_or(Error::new("Failed to parse global log level".to_string(), ""))?; + + let levels_override: Vec<(&str, log::LevelFilter)> = caps + .get(2) + .map(|m| { + m.as_str() + .split(',') + .collect::>() + .into_iter() + .flat_map(|s| match s.split('=').collect::>()[..] { + [log, lvl_str] => log::LevelFilter::from_str(lvl_str).ok().map(|lvl| (log, lvl)), + _ => None, + }) + .collect() + }) + .ok_or(Error::new("Failed to parse overrides".to_string(), ""))?; + + (level, levels_override) + } else { + err!(format!("LOG_LEVEL should follow the format info,vaultwarden::api::icons=debug, invalid: {config_str}")) + }; + // Depending on the main log level we either want to disable or enable logging for hickory. // Else if there are timeouts it will clutter the logs since hickory uses warn for this. let hickory_level = if level >= log::LevelFilter::Debug { @@ -241,47 +310,61 @@ fn init_logging(level: log::LevelFilter) -> Result<(), fern::InitError> { log::LevelFilter::Warn }; - let mut logger = fern::Dispatch::new() - .level(level) + // Enable smtp debug logging only specifically for smtp when need. + // This can contain sensitive information we do not want in the default debug/trace logging. + let smtp_log_level = if CONFIG.smtp_debug() { + log::LevelFilter::Debug + } else { + log::LevelFilter::Off + }; + + let mut default_levels = HashMap::from([ // Hide unknown certificate errors if using self-signed - .level_for("rustls::session", log::LevelFilter::Off) + ("rustls::session", log::LevelFilter::Off), // Hide failed to close stream messages - .level_for("hyper::server", log::LevelFilter::Warn) + ("hyper::server", log::LevelFilter::Warn), // Silence Rocket `_` logs - .level_for("_", rocket_underscore_level) - .level_for("rocket::response::responder::_", rocket_underscore_level) - .level_for("rocket::server::_", rocket_underscore_level) - .level_for("vaultwarden::api::admin::_", rocket_underscore_level) - .level_for("vaultwarden::api::notifications::_", rocket_underscore_level) + ("_", rocket_underscore_level), + ("rocket::response::responder::_", rocket_underscore_level), + ("rocket::server::_", rocket_underscore_level), + ("vaultwarden::api::admin::_", rocket_underscore_level), + ("vaultwarden::api::notifications::_", rocket_underscore_level), // Silence Rocket logs - .level_for("rocket::launch", log::LevelFilter::Error) - .level_for("rocket::launch_", log::LevelFilter::Error) - .level_for("rocket::rocket", log::LevelFilter::Warn) - .level_for("rocket::server", log::LevelFilter::Warn) - .level_for("rocket::fairing::fairings", log::LevelFilter::Warn) - .level_for("rocket::shield::shield", log::LevelFilter::Warn) - .level_for("hyper::proto", log::LevelFilter::Off) - .level_for("hyper::client", log::LevelFilter::Off) + ("rocket::launch", log::LevelFilter::Error), + ("rocket::launch_", log::LevelFilter::Error), + ("rocket::rocket", log::LevelFilter::Warn), + ("rocket::server", log::LevelFilter::Warn), + ("rocket::fairing::fairings", log::LevelFilter::Warn), + ("rocket::shield::shield", log::LevelFilter::Warn), + ("hyper::proto", log::LevelFilter::Off), + ("hyper::client", log::LevelFilter::Off), // Filter handlebars logs - .level_for("handlebars::render", handlebars_level) + ("handlebars::render", handlebars_level), // Prevent cookie_store logs - .level_for("cookie_store", log::LevelFilter::Off) + ("cookie_store", log::LevelFilter::Off), // Variable level for hickory used by reqwest - .level_for("hickory_resolver::name_server::name_server", hickory_level) - .level_for("hickory_proto::xfer", hickory_level) - .level_for("diesel_logger", diesel_logger_level) - .chain(std::io::stdout()); + ("hickory_resolver::name_server::name_server", hickory_level), + ("hickory_proto::xfer", hickory_level), + ("diesel_logger", diesel_logger_level), + // SMTP + ("lettre::transport::smtp", smtp_log_level), + ]); + + for (path, level) in levels_override.into_iter() { + let _ = default_levels.insert(path, level); + } - // Enable smtp debug logging only specifically for smtp when need. - // This can contain sensitive information we do not want in the default debug/trace logging. - if CONFIG.smtp_debug() { + if Some(&log::LevelFilter::Debug) == default_levels.get("lettre::transport::smtp") { println!( "[WARNING] SMTP Debugging is enabled (SMTP_DEBUG=true). Sensitive information could be disclosed via logs!\n\ [WARNING] Only enable SMTP_DEBUG during troubleshooting!\n" ); - logger = logger.level_for("lettre::transport::smtp", log::LevelFilter::Debug) - } else { - logger = logger.level_for("lettre::transport::smtp", log::LevelFilter::Off) + } + + let mut logger = fern::Dispatch::new().level(level).chain(std::io::stdout()); + + for (path, level) in default_levels { + logger = logger.level_for(path.to_string(), level); } if CONFIG.extended_logging() { @@ -303,22 +386,24 @@ fn init_logging(level: log::LevelFilter) -> Result<(), fern::InitError> { { logger = logger.chain(fern::log_file(log_file)?); } - #[cfg(not(windows))] + #[cfg(unix)] { - const SIGHUP: i32 = tokio::signal::unix::SignalKind::hangup().as_raw_value(); + const SIGHUP: i32 = SignalKind::hangup().as_raw_value(); let path = Path::new(&log_file); logger = logger.chain(fern::log_reopen1(path, [SIGHUP])?); } } - #[cfg(not(windows))] + #[cfg(unix)] { if cfg!(feature = "enable_syslog") || CONFIG.use_syslog() { logger = chain_syslog(logger); } } - logger.apply()?; + if let Err(err) = logger.apply() { + err!(format!("Failed to activate logger: {err}")) + } // Catch panics and log them instead of default output to StdErr panic::set_hook(Box::new(|info| { @@ -356,10 +441,10 @@ fn init_logging(level: log::LevelFilter) -> Result<(), fern::InitError> { } })); - Ok(()) + Ok(level) } -#[cfg(not(windows))] +#[cfg(unix)] fn chain_syslog(logger: fern::Dispatch) -> fern::Dispatch { let syslog_fmt = syslog::Formatter3164 { facility: syslog::Facility::LOG_USER, @@ -513,11 +598,27 @@ async fn launch_rocket(pool: db::DbPool, extra_debug: bool) -> Result<(), Error> tokio::spawn(async move { tokio::signal::ctrl_c().await.expect("Error setting Ctrl-C handler"); - info!("Exiting vaultwarden!"); + info!("Exiting Vaultwarden!"); CONFIG.shutdown(); }); - let _ = instance.launch().await?; + #[cfg(unix)] + { + tokio::spawn(async move { + let mut signal_user1 = tokio::signal::unix::signal(SignalKind::user_defined1()).unwrap(); + loop { + // If we need more signals to act upon, we might want to use select! here. + // With only one item to listen for this is enough. + let _ = signal_user1.recv().await; + match backup_sqlite() { + Ok(f) => info!("Backup to '{f}' was successful"), + Err(e) => error!("Backup failed. {e:?}"), + } + } + }); + } + + instance.launch().await?; info!("Vaultwarden process exited!"); Ok(()) @@ -584,6 +685,13 @@ fn schedule_jobs(pool: db::DbPool) { })); } + // Clean unused, expired Duo authentication contexts. + if !CONFIG.duo_context_purge_schedule().is_empty() && CONFIG._enable_duo() && !CONFIG.duo_use_iframe() { + sched.add(Job::new(CONFIG.duo_context_purge_schedule().parse().unwrap(), || { + runtime.spawn(purge_duo_contexts(pool.clone())); + })); + } + // Cleanup the event table of records x days old. if CONFIG.org_events_enabled() && !CONFIG.event_cleanup_schedule().is_empty() diff --git a/src/static/scripts/admin.js b/src/static/scripts/admin.js index b35f3fb1..b194a91d 100644 --- a/src/static/scripts/admin.js +++ b/src/static/scripts/admin.js @@ -49,8 +49,8 @@ function _post(url, successMsg, errMsg, body, reload_page = true) { }).then(respText => { try { const respJson = JSON.parse(respText); - if (respJson.ErrorModel && respJson.ErrorModel.Message) { - return respJson.ErrorModel.Message; + if (respJson.errorModel && respJson.errorModel.message) { + return respJson.errorModel.message; } else { return Promise.reject({ body: `${respStatus} - ${respStatusText}\n\nUnknown error`, error: true }); } @@ -98,7 +98,7 @@ const showActiveTheme = (theme, focus = false) => { const themeSwitcherText = document.querySelector("#bd-theme-text"); const activeThemeIcon = document.querySelector(".theme-icon-active use"); const btnToActive = document.querySelector(`[data-bs-theme-value="${theme}"]`); - const svgOfActiveBtn = btnToActive.querySelector("span use").innerText; + const svgOfActiveBtn = btnToActive.querySelector("span use").textContent; document.querySelectorAll("[data-bs-theme-value]").forEach(element => { element.classList.remove("active"); @@ -107,7 +107,7 @@ const showActiveTheme = (theme, focus = false) => { btnToActive.classList.add("active"); btnToActive.setAttribute("aria-pressed", "true"); - activeThemeIcon.innerText = svgOfActiveBtn; + activeThemeIcon.textContent = svgOfActiveBtn; const themeSwitcherLabel = `${themeSwitcherText.textContent} (${btnToActive.dataset.bsThemeValue})`; themeSwitcher.setAttribute("aria-label", themeSwitcherLabel); diff --git a/src/static/scripts/admin_diagnostics.js b/src/static/scripts/admin_diagnostics.js index 9f2aca66..6a178e4b 100644 --- a/src/static/scripts/admin_diagnostics.js +++ b/src/static/scripts/admin_diagnostics.js @@ -117,7 +117,7 @@ async function generateSupportString(event, dj) { supportString += `\n**Environment settings which are overridden:** ${dj.overrides}\n`; supportString += "\n\n```json\n" + JSON.stringify(configJson, undefined, 2) + "\n```\n\n"; - document.getElementById("support-string").innerText = supportString; + document.getElementById("support-string").textContent = supportString; document.getElementById("support-string").classList.remove("d-none"); document.getElementById("copy-support").classList.remove("d-none"); } @@ -126,7 +126,7 @@ function copyToClipboard(event) { event.preventDefault(); event.stopPropagation(); - const supportStr = document.getElementById("support-string").innerText; + const supportStr = document.getElementById("support-string").textContent; const tmpCopyEl = document.createElement("textarea"); tmpCopyEl.setAttribute("id", "copy-support-string"); @@ -201,7 +201,7 @@ function checkDns(dns_resolved) { function init(dj) { // Time check - document.getElementById("time-browser-string").innerText = browserUTC; + document.getElementById("time-browser-string").textContent = browserUTC; // Check if we were able to fetch a valid NTP Time // If so, compare both browser and server with NTP @@ -217,7 +217,7 @@ function init(dj) { // Domain check const browserURL = location.href.toLowerCase(); - document.getElementById("domain-browser-string").innerText = browserURL; + document.getElementById("domain-browser-string").textContent = browserURL; checkDomain(browserURL, dj.admin_url.toLowerCase()); // Version check @@ -229,7 +229,7 @@ function init(dj) { // onLoad events document.addEventListener("DOMContentLoaded", (event) => { - const diag_json = JSON.parse(document.getElementById("diagnostics_json").innerText); + const diag_json = JSON.parse(document.getElementById("diagnostics_json").textContent); init(diag_json); const btnGenSupport = document.getElementById("gen-support"); diff --git a/src/static/scripts/admin_settings.js b/src/static/scripts/admin_settings.js index ffdd778b..3d61a508 100644 --- a/src/static/scripts/admin_settings.js +++ b/src/static/scripts/admin_settings.js @@ -122,7 +122,7 @@ function submitTestEmailOnEnter() { function colorRiskSettings() { const risk_items = document.getElementsByClassName("col-form-label"); Array.from(risk_items).forEach((el) => { - if (el.innerText.toLowerCase().includes("risks") ) { + if (el.textContent.toLowerCase().includes("risks") ) { el.parentElement.className += " alert-danger"; } }); diff --git a/src/static/scripts/admin_users.js b/src/static/scripts/admin_users.js index 8b569296..c2462521 100644 --- a/src/static/scripts/admin_users.js +++ b/src/static/scripts/admin_users.js @@ -198,7 +198,8 @@ userOrgTypeDialog.addEventListener("show.bs.modal", function(event) { const orgName = event.relatedTarget.dataset.vwOrgName; const orgUuid = event.relatedTarget.dataset.vwOrgUuid; - document.getElementById("userOrgTypeDialogTitle").innerHTML = `Update User Type:
Organization: ${orgName}
User: ${userEmail}`; + document.getElementById("userOrgTypeDialogOrgName").textContent = orgName; + document.getElementById("userOrgTypeDialogUserEmail").textContent = userEmail; document.getElementById("userOrgTypeUserUuid").value = userUuid; document.getElementById("userOrgTypeOrgUuid").value = orgUuid; document.getElementById(`userOrgType${userOrgTypeName}`).checked = true; @@ -206,7 +207,8 @@ userOrgTypeDialog.addEventListener("show.bs.modal", function(event) { // Prevent accidental submission of the form with valid elements after the modal has been hidden. userOrgTypeDialog.addEventListener("hide.bs.modal", function() { - document.getElementById("userOrgTypeDialogTitle").innerHTML = ""; + document.getElementById("userOrgTypeDialogOrgName").textContent = ""; + document.getElementById("userOrgTypeDialogUserEmail").textContent = ""; document.getElementById("userOrgTypeUserUuid").value = ""; document.getElementById("userOrgTypeOrgUuid").value = ""; }, false); diff --git a/src/static/scripts/datatables.css b/src/static/scripts/datatables.css index 83e4f44b..878e2347 100644 --- a/src/static/scripts/datatables.css +++ b/src/static/scripts/datatables.css @@ -4,10 +4,10 @@ * * To rebuild or modify this file with the latest versions of the included * software please visit: - * https://datatables.net/download/#bs5/dt-2.0.7 + * https://datatables.net/download/#bs5/dt-2.0.8 * * Included libraries: - * DataTables 2.0.7 + * DataTables 2.0.8 */ @charset "UTF-8"; diff --git a/src/static/scripts/datatables.js b/src/static/scripts/datatables.js index 88d0b627..3d22cbde 100644 --- a/src/static/scripts/datatables.js +++ b/src/static/scripts/datatables.js @@ -4,20 +4,20 @@ * * To rebuild or modify this file with the latest versions of the included * software please visit: - * https://datatables.net/download/#bs5/dt-2.0.7 + * https://datatables.net/download/#bs5/dt-2.0.8 * * Included libraries: - * DataTables 2.0.7 + * DataTables 2.0.8 */ -/*! DataTables 2.0.7 +/*! DataTables 2.0.8 * © SpryMedia Ltd - datatables.net/license */ /** * @summary DataTables * @description Paginate, search and order HTML tables - * @version 2.0.7 + * @version 2.0.8 * @author SpryMedia Ltd * @contact www.datatables.net * @copyright SpryMedia Ltd. @@ -563,7 +563,7 @@ * * @type string */ - builder: "bs5/dt-2.0.7", + builder: "bs5/dt-2.0.8", /** @@ -7572,6 +7572,16 @@ order = opts.order, // applied, current, index (original - compatibility with 1.9) page = opts.page; // all, current + if ( _fnDataSource( settings ) == 'ssp' ) { + // In server-side processing mode, most options are irrelevant since + // rows not shown don't exist and the index order is the applied order + // Removed is a special case - for consistency just return an empty + // array + return search === 'removed' ? + [] : + _range( 0, displayMaster.length ); + } + if ( page == 'current' ) { // Current page implies that order=current and filter=applied, since it is // fairly senseless otherwise, regardless of what order and search actually @@ -8243,7 +8253,7 @@ _api_register( _child_obj+'.isShown()', function () { var ctx = this.context; - if ( ctx.length && this.length ) { + if ( ctx.length && this.length && ctx[0].aoData[ this[0] ] ) { // _detailsShown as false or undefined will fall through to return false return ctx[0].aoData[ this[0] ]._detailsShow || false; } @@ -8266,7 +8276,7 @@ // can be an array of these items, comma separated list, or an array of comma // separated lists - var __re_column_selector = /^([^:]+):(name|title|visIdx|visible)$/; + var __re_column_selector = /^([^:]+)?:(name|title|visIdx|visible)$/; // r1 and r2 are redundant - but it means that the parameters match for the @@ -8338,17 +8348,24 @@ switch( match[2] ) { case 'visIdx': case 'visible': - var idx = parseInt( match[1], 10 ); - // Visible index given, convert to column index - if ( idx < 0 ) { - // Counting from the right - var visColumns = columns.map( function (col,i) { - return col.bVisible ? i : null; - } ); - return [ visColumns[ visColumns.length + idx ] ]; + if (match[1]) { + var idx = parseInt( match[1], 10 ); + // Visible index given, convert to column index + if ( idx < 0 ) { + // Counting from the right + var visColumns = columns.map( function (col,i) { + return col.bVisible ? i : null; + } ); + return [ visColumns[ visColumns.length + idx ] ]; + } + // Counting from the left + return [ _fnVisibleToColumnIndex( settings, idx ) ]; } - // Counting from the left - return [ _fnVisibleToColumnIndex( settings, idx ) ]; + + // `:visible` on its own + return columns.map( function (col, i) { + return col.bVisible ? i : null; + } ); case 'name': // match by name. `names` is column index complete and in order @@ -9623,7 +9640,7 @@ * @type string * @default Version number */ - DataTable.version = "2.0.7"; + DataTable.version = "2.0.8"; /** * Private data store, containing all of the settings objects that are diff --git a/src/static/templates/admin/organizations.hbs b/src/static/templates/admin/organizations.hbs index 46547b28..654f904e 100644 --- a/src/static/templates/admin/organizations.hbs +++ b/src/static/templates/admin/organizations.hbs @@ -44,7 +44,7 @@ Events: {{event_count}} -
+
{{/each}} diff --git a/src/static/templates/admin/users.hbs b/src/static/templates/admin/users.hbs index 1765876a..09efc113 100644 --- a/src/static/templates/admin/users.hbs +++ b/src/static/templates/admin/users.hbs @@ -54,14 +54,14 @@ {{/if}} -
+
{{#each organizations}} - + {{/each}}
- + {{#if twoFactorEnabled}}
{{/if}} @@ -109,7 +109,9 @@