diff --git a/.env.template b/.env.template index 67f531fc..03990820 100644 --- a/.env.template +++ b/.env.template @@ -372,16 +372,22 @@ ## Note that clients cache the /api/config endpoint for about 1 hour and it could take some time before they are enabled or disabled! ## ## The following flags are available: -## - "inline-menu-positioning-improvements": Enable the use of inline menu password generator and identity suggestions in the browser extension. -## - "inline-menu-totp": Enable the use of inline menu TOTP codes in the browser extension. -## - "ssh-agent": Enable SSH agent support on Desktop. (Needs desktop >=2024.12.0) -## - "ssh-key-vault-item": Enable the creation and use of SSH key vault items. (Needs clients >=2024.12.0) -## - "pm-25373-windows-biometrics-v2": Enable the new implementation of biometrics on Windows. (Needs desktop >= 2025.11.0) -## - "export-attachments": Enable support for exporting attachments (Clients >=2025.4.0) -## - "anon-addy-self-host-alias": Enable configuring self-hosted Anon Addy alias generator. (Needs Android >=2025.3.0, iOS >=2025.4.0) -## - "simple-login-self-host-alias": Enable configuring self-hosted Simple Login alias generator. (Needs Android >=2025.3.0, iOS >=2025.4.0) -## - "mutual-tls": Enable the use of mutual TLS on Android (Client >= 2025.2.0) -# EXPERIMENTAL_CLIENT_FEATURE_FLAGS=fido2-vault-credentials +## - "pm-5594-safari-account-switching": Enable account switching in Safari. (Safari >= 2026.2.0) +## - "ssh-agent": Enable SSH agent support on Desktop. (Desktop >= 2024.12.0) +## - "ssh-agent-v2": Enable newer SSH agent support. (Desktop >= 2026.2.1) +## - "ssh-key-vault-item": Enable the creation and use of SSH key vault items. (Clients >= 2024.12.0) +## - "pm-25373-windows-biometrics-v2": Enable the new implementation of biometrics on Windows. (Desktop >= 2025.11.0) +## - "anon-addy-self-host-alias": Enable configuring self-hosted Anon Addy alias generator. (Android >= 2025.3.0, iOS >= 2025.4.0) +## - "simple-login-self-host-alias": Enable configuring self-hosted Simple Login alias generator. (Android >= 2025.3.0, iOS >= 2025.4.0) +## - "mutual-tls": Enable the use of mutual TLS on Android (Clients >= 2025.2.0) +## - "cxp-import-mobile": Enable the import via CXP on iOS (Clients >= 2025.9.2) +## - "cxp-export-mobile": Enable the export via CXP on iOS (Clients >= 2025.9.2) +## - "pm-30529-webauthn-related-origins": +## - "desktop-ui-migration-milestone-1": Special feature flag for desktop UI (Desktop >= 2026.2.0) +## - "desktop-ui-migration-milestone-2": Special feature flag for desktop UI (Desktop >= 2026.2.0) +## - "desktop-ui-migration-milestone-3": Special feature flag for desktop UI (Desktop >= 2026.2.0) +## - "desktop-ui-migration-milestone-4": Special feature flag for desktop UI (Desktop >= 2026.2.0) +# EXPERIMENTAL_CLIENT_FEATURE_FLAGS= ## Require new device emails. When a user logs in an email is required to be sent. ## If sending the email fails the login attempt will fail!! diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 2e3468f4..6269e595 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -62,7 +62,7 @@ jobs: # Checkout the repo - name: "Checkout" - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 #v6.0.0 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false fetch-depth: 0 @@ -85,32 +85,23 @@ jobs: # End Determine rust-toolchain version - # Only install the clippy and rustfmt components on the default rust-toolchain - - name: "Install rust-toolchain version" - uses: dtolnay/rust-toolchain@efa25f7f19611383d5b0ccf2d1c8914531636bf9 # master @ Feb 13, 2026, 3:46 AM GMT+1 - if: ${{ matrix.channel == 'rust-toolchain' }} - with: - toolchain: "${{steps.toolchain.outputs.RUST_TOOLCHAIN}}" - components: clippy, rustfmt - # End Uses the rust-toolchain file to determine version - - - # 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@efa25f7f19611383d5b0ccf2d1c8914531636bf9 # master @ Feb 13, 2026, 3:46 AM GMT+1 - if: ${{ matrix.channel != 'rust-toolchain' }} - with: - toolchain: "${{steps.toolchain.outputs.RUST_TOOLCHAIN}}" - # End Install the MSRV channel to be used - - # Set the current matrix toolchain version as default - - name: "Set toolchain ${{steps.toolchain.outputs.RUST_TOOLCHAIN}} as default" + - name: "Install toolchain ${{steps.toolchain.outputs.RUST_TOOLCHAIN}} as default" env: + CHANNEL: ${{ matrix.channel }} RUST_TOOLCHAIN: ${{steps.toolchain.outputs.RUST_TOOLCHAIN}} run: | # Remove the rust-toolchain.toml rm rust-toolchain.toml - # Set the default + + # Install the correct toolchain version + rustup toolchain install "${RUST_TOOLCHAIN}" --profile minimal --no-self-update + + # If this matrix is the `rust-toolchain` flow, also install rustfmt and clippy + if [[ "${CHANNEL}" == 'rust-toolchain' ]]; then + rustup component add --toolchain "${RUST_TOOLCHAIN}" rustfmt clippy + fi + + # Set as the default toolchain rustup default "${RUST_TOOLCHAIN}" # Show environment @@ -122,7 +113,7 @@ jobs: # Enable Rust Caching - name: Rust Caching - uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2 + uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1 with: # Use a custom prefix-key to force a fresh start. This is sometimes needed with bigger changes. # Like changing the build host from Ubuntu 20.04 to 22.04 for example. diff --git a/.github/workflows/check-templates.yml b/.github/workflows/check-templates.yml index a8415dde..57b53bf4 100644 --- a/.github/workflows/check-templates.yml +++ b/.github/workflows/check-templates.yml @@ -20,7 +20,7 @@ jobs: steps: # Checkout the repo - name: "Checkout" - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 #v6.0.0 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false # End Checkout the repo diff --git a/.github/workflows/hadolint.yml b/.github/workflows/hadolint.yml index 1e0fa48c..2b476904 100644 --- a/.github/workflows/hadolint.yml +++ b/.github/workflows/hadolint.yml @@ -20,7 +20,7 @@ jobs: steps: # Start Docker Buildx - name: Setup Docker Buildx - uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0 + uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0 # https://github.com/moby/buildkit/issues/3969 # Also set max parallelism to 2, the default of 4 breaks GitHub Actions and causes OOMKills with: @@ -40,7 +40,7 @@ jobs: # End Download hadolint # Checkout the repo - name: Checkout - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 #v6.0.0 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false # End Checkout the repo diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index fc117e30..5b72da3a 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -20,23 +20,27 @@ defaults: run: shell: bash -env: - # The *_REPO variables need to be configured as repository variables - # Append `/settings/variables/actions` to your repo url - # DOCKERHUB_REPO needs to be 'index.docker.io//' - # Check for Docker hub credentials in secrets - HAVE_DOCKERHUB_LOGIN: ${{ vars.DOCKERHUB_REPO != '' && secrets.DOCKERHUB_USERNAME != '' && secrets.DOCKERHUB_TOKEN != '' }} - # GHCR_REPO needs to be 'ghcr.io//' - # Check for Github credentials in secrets - HAVE_GHCR_LOGIN: ${{ vars.GHCR_REPO != '' && github.repository_owner != '' && secrets.GITHUB_TOKEN != '' }} - # QUAY_REPO needs to be 'quay.io//' - # Check for Quay.io credentials in secrets - HAVE_QUAY_LOGIN: ${{ vars.QUAY_REPO != '' && secrets.QUAY_USERNAME != '' && secrets.QUAY_TOKEN != '' }} +# A "release" environment must be created in the repository settings +# (Settings > Environments > New environment) with the following +# variables and secrets configured as needed. +# +# Variables (only set the ones for registries you want to push to): +# DOCKERHUB_REPO: 'index.docker.io//' +# QUAY_REPO: 'quay.io//' +# GHCR_REPO: 'ghcr.io//' +# +# Secrets (only required when the corresponding *_REPO variable is set): +# DOCKERHUB_REPO => DOCKERHUB_USERNAME, DOCKERHUB_TOKEN +# QUAY_REPO => QUAY_USERNAME, QUAY_TOKEN +# GITHUB_TOKEN is provided automatically jobs: docker-build: name: Build Vaultwarden containers if: ${{ github.repository == 'dani-garcia/vaultwarden' }} + environment: + name: release + deployment: false permissions: packages: write # Needed to upload packages and artifacts contents: read @@ -54,13 +58,13 @@ jobs: steps: - name: Initialize QEMU binfmt support - uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3.7.0 + uses: docker/setup-qemu-action@ce360397dd3f832beb865e1373c09c0e9f86d70a # v4.0.0 with: platforms: "arm64,arm" # Start Docker Buildx - name: Setup Docker Buildx - uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0 + uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0 # https://github.com/moby/buildkit/issues/3969 # Also set max parallelism to 2, the default of 4 breaks GitHub Actions and causes OOMKills with: @@ -73,7 +77,7 @@ jobs: # Checkout the repo - name: Checkout - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 #v6.0.0 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 # We need fetch-depth of 0 so we also get all the tag metadata with: persist-credentials: false @@ -102,14 +106,14 @@ jobs: # Login to Docker Hub - name: Login to Docker Hub - uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0 + uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - if: ${{ env.HAVE_DOCKERHUB_LOGIN == 'true' }} + if: ${{ vars.DOCKERHUB_REPO != '' }} - name: Add registry for DockerHub - if: ${{ env.HAVE_DOCKERHUB_LOGIN == 'true' }} + if: ${{ vars.DOCKERHUB_REPO != '' }} env: DOCKERHUB_REPO: ${{ vars.DOCKERHUB_REPO }} run: | @@ -117,15 +121,15 @@ jobs: # Login to GitHub Container Registry - name: Login to GitHub Container Registry - uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0 + uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0 with: registry: ghcr.io username: ${{ github.repository_owner }} password: ${{ secrets.GITHUB_TOKEN }} - if: ${{ env.HAVE_GHCR_LOGIN == 'true' }} + if: ${{ vars.GHCR_REPO != '' }} - name: Add registry for ghcr.io - if: ${{ env.HAVE_GHCR_LOGIN == 'true' }} + if: ${{ vars.GHCR_REPO != '' }} env: GHCR_REPO: ${{ vars.GHCR_REPO }} run: | @@ -133,15 +137,15 @@ jobs: # Login to Quay.io - name: Login to Quay.io - uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0 + uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0 with: registry: quay.io username: ${{ secrets.QUAY_USERNAME }} password: ${{ secrets.QUAY_TOKEN }} - if: ${{ env.HAVE_QUAY_LOGIN == 'true' }} + if: ${{ vars.QUAY_REPO != '' }} - name: Add registry for Quay.io - if: ${{ env.HAVE_QUAY_LOGIN == 'true' }} + if: ${{ vars.QUAY_REPO != '' }} env: QUAY_REPO: ${{ vars.QUAY_REPO }} run: | @@ -155,7 +159,7 @@ jobs: run: | # # Check if there is a GitHub Container Registry Login and use it for caching - if [[ -n "${HAVE_GHCR_LOGIN}" ]]; then + if [[ -n "${GHCR_REPO}" ]]; then echo "BAKE_CACHE_FROM=type=registry,ref=${GHCR_REPO}-buildcache:${BASE_IMAGE}-${NORMALIZED_ARCH}" | tee -a "${GITHUB_ENV}" echo "BAKE_CACHE_TO=type=registry,ref=${GHCR_REPO}-buildcache:${BASE_IMAGE}-${NORMALIZED_ARCH},compression=zstd,mode=max" | tee -a "${GITHUB_ENV}" else @@ -181,7 +185,7 @@ jobs: - name: Bake ${{ matrix.base_image }} containers id: bake_vw - uses: docker/bake-action@5be5f02ff8819ecd3092ea6b2e6261c31774f2b4 # v6.10.0 + uses: docker/bake-action@82490499d2e5613fcead7e128237ef0b0ea210f7 # v7.0.0 env: BASE_TAGS: "${{ steps.determine-version.outputs.BASE_TAGS }}" SOURCE_COMMIT: "${{ env.SOURCE_COMMIT }}" @@ -218,7 +222,7 @@ jobs: touch "${RUNNER_TEMP}/digests/${digest#sha256:}" - name: Upload digest - uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 with: name: digests-${{ env.NORMALIZED_ARCH }}-${{ matrix.base_image }} path: ${{ runner.temp }}/digests/* @@ -233,12 +237,12 @@ jobs: # Upload artifacts to Github Actions and Attest the binaries - name: Attest binaries - uses: actions/attest-build-provenance@96278af6caaf10aea03fd8d33a09a777ca52d62f # v3.2.0 + uses: actions/attest@59d89421af93a897026c735860bf21b6eb4f7b26 # v4.1.0 with: subject-path: vaultwarden-${{ env.NORMALIZED_ARCH }} - name: Upload binaries as artifacts - uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 with: name: vaultwarden-${{ env.SOURCE_VERSION }}-linux-${{ env.NORMALIZED_ARCH }}-${{ matrix.base_image }} path: vaultwarden-${{ env.NORMALIZED_ARCH }} @@ -247,6 +251,9 @@ jobs: name: Merge manifests runs-on: ubuntu-latest needs: docker-build + environment: + name: release + deployment: false permissions: packages: write # Needed to upload packages and artifacts attestations: write # Needed to generate an artifact attestation for a build @@ -257,7 +264,7 @@ jobs: steps: - name: Download digests - uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0 + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: path: ${{ runner.temp }}/digests pattern: digests-*-${{ matrix.base_image }} @@ -265,14 +272,14 @@ jobs: # Login to Docker Hub - name: Login to Docker Hub - uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0 + uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - if: ${{ env.HAVE_DOCKERHUB_LOGIN == 'true' }} + if: ${{ vars.DOCKERHUB_REPO != '' }} - name: Add registry for DockerHub - if: ${{ env.HAVE_DOCKERHUB_LOGIN == 'true' }} + if: ${{ vars.DOCKERHUB_REPO != '' }} env: DOCKERHUB_REPO: ${{ vars.DOCKERHUB_REPO }} run: | @@ -280,15 +287,15 @@ jobs: # Login to GitHub Container Registry - name: Login to GitHub Container Registry - uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0 + uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0 with: registry: ghcr.io username: ${{ github.repository_owner }} password: ${{ secrets.GITHUB_TOKEN }} - if: ${{ env.HAVE_GHCR_LOGIN == 'true' }} + if: ${{ vars.GHCR_REPO != '' }} - name: Add registry for ghcr.io - if: ${{ env.HAVE_GHCR_LOGIN == 'true' }} + if: ${{ vars.GHCR_REPO != '' }} env: GHCR_REPO: ${{ vars.GHCR_REPO }} run: | @@ -296,15 +303,15 @@ jobs: # Login to Quay.io - name: Login to Quay.io - uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0 + uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0 with: registry: quay.io username: ${{ secrets.QUAY_USERNAME }} password: ${{ secrets.QUAY_TOKEN }} - if: ${{ env.HAVE_QUAY_LOGIN == 'true' }} + if: ${{ vars.QUAY_REPO != '' }} - name: Add registry for Quay.io - if: ${{ env.HAVE_QUAY_LOGIN == 'true' }} + if: ${{ vars.QUAY_REPO != '' }} env: QUAY_REPO: ${{ vars.QUAY_REPO }} run: | @@ -357,24 +364,24 @@ jobs: # Attest container images - name: Attest - docker.io - ${{ matrix.base_image }} - if: ${{ env.HAVE_DOCKERHUB_LOGIN == 'true' && env.DIGEST_SHA != ''}} - uses: actions/attest-build-provenance@96278af6caaf10aea03fd8d33a09a777ca52d62f # v3.2.0 + if: ${{ vars.DOCKERHUB_REPO != '' && env.DIGEST_SHA != ''}} + uses: actions/attest@59d89421af93a897026c735860bf21b6eb4f7b26 # v4.1.0 with: subject-name: ${{ vars.DOCKERHUB_REPO }} subject-digest: ${{ env.DIGEST_SHA }} push-to-registry: true - name: Attest - ghcr.io - ${{ matrix.base_image }} - if: ${{ env.HAVE_GHCR_LOGIN == 'true' && env.DIGEST_SHA != ''}} - uses: actions/attest-build-provenance@96278af6caaf10aea03fd8d33a09a777ca52d62f # v3.2.0 + if: ${{ vars.GHCR_REPO != '' && env.DIGEST_SHA != ''}} + uses: actions/attest@59d89421af93a897026c735860bf21b6eb4f7b26 # v4.1.0 with: subject-name: ${{ vars.GHCR_REPO }} subject-digest: ${{ env.DIGEST_SHA }} push-to-registry: true - name: Attest - quay.io - ${{ matrix.base_image }} - if: ${{ env.HAVE_QUAY_LOGIN == 'true' && env.DIGEST_SHA != ''}} - uses: actions/attest-build-provenance@96278af6caaf10aea03fd8d33a09a777ca52d62f # v3.2.0 + if: ${{ vars.QUAY_REPO != '' && env.DIGEST_SHA != ''}} + uses: actions/attest@59d89421af93a897026c735860bf21b6eb4f7b26 # v4.1.0 with: subject-name: ${{ vars.QUAY_REPO }} subject-digest: ${{ env.DIGEST_SHA }} diff --git a/.github/workflows/trivy.yml b/.github/workflows/trivy.yml index 13cd2e24..c9e02cf9 100644 --- a/.github/workflows/trivy.yml +++ b/.github/workflows/trivy.yml @@ -33,12 +33,12 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 #v6.0.0 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false - name: Run Trivy vulnerability scanner - uses: aquasecurity/trivy-action@c1824fd6edce30d7ab345a9989de00bbd46ef284 # 0.34.0 + uses: aquasecurity/trivy-action@57a97c7e7821a5776cebc9bb87c984fa69cba8f1 # v0.35.0 env: TRIVY_DB_REPOSITORY: docker.io/aquasec/trivy-db:2,public.ecr.aws/aquasecurity/trivy-db:2,ghcr.io/aquasecurity/trivy-db:2 TRIVY_JAVA_DB_REPOSITORY: docker.io/aquasec/trivy-java-db:1,public.ecr.aws/aquasecurity/trivy-java-db:1,ghcr.io/aquasecurity/trivy-java-db:1 @@ -50,6 +50,6 @@ jobs: severity: CRITICAL,HIGH - name: Upload Trivy scan results to GitHub Security tab - uses: github/codeql-action/upload-sarif@9e907b5e64f6b83e7804b09294d44122997950d6 # v4.32.3 + uses: github/codeql-action/upload-sarif@c10b8064de6f491fea524254123dbe5e09572f13 # v4.35.1 with: sarif_file: 'trivy-results.sarif' diff --git a/.github/workflows/typos.yml b/.github/workflows/typos.yml index 5726a6fc..d7b645e0 100644 --- a/.github/workflows/typos.yml +++ b/.github/workflows/typos.yml @@ -16,11 +16,11 @@ jobs: steps: # Checkout the repo - name: Checkout - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 #v6.0.0 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false # End Checkout the repo # When this version is updated, do not forget to update this in `.pre-commit-config.yaml` too - name: Spell Check Repo - uses: crate-ci/typos@57b11c6b7e54c402ccd9cda953f1072ec4f78e33 # v1.43.5 + uses: crate-ci/typos@631208b7aac2daa8b707f55e7331f9112b0e062d # v1.44.0 diff --git a/.github/workflows/zizmor.yml b/.github/workflows/zizmor.yml index 4051a8b2..4bd40db3 100644 --- a/.github/workflows/zizmor.yml +++ b/.github/workflows/zizmor.yml @@ -19,12 +19,12 @@ jobs: security-events: write # To write the security report steps: - name: Checkout repository - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 #v6.0.0 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false - name: Run zizmor - uses: zizmorcore/zizmor-action@0dce2577a4760a2749d8cfb7a84b7d5585ebcb7d # v0.5.0 + uses: zizmorcore/zizmor-action@71321a20a9ded102f6e9ce5718a2fcec2c4f70d8 # v0.5.2 with: # intentionally not scanning the entire repository, # since it contains integration tests. diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 54f09b8b..3a151637 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -53,6 +53,6 @@ repos: - "cd docker && make" # When this version is updated, do not forget to update this in `.github/workflows/typos.yaml` too - repo: https://github.com/crate-ci/typos - rev: 57b11c6b7e54c402ccd9cda953f1072ec4f78e33 # v1.43.5 + rev: 631208b7aac2daa8b707f55e7331f9112b0e062d # v1.44.0 hooks: - id: typos diff --git a/Cargo.lock b/Cargo.lock index a776541d..b03e6170 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -76,15 +76,6 @@ version = "1.0.102" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" -[[package]] -name = "ar_archive_writer" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7eb93bbb63b9c227414f6eb3a0adfddca591a8ce1e9b60661bb08969b87e340b" -dependencies = [ - "object", -] - [[package]] name = "argon2" version = "0.5.3" @@ -161,9 +152,9 @@ dependencies = [ [[package]] name = "async-compression" -version = "0.4.40" +version = "0.4.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d67d43201f4d20c78bcda740c142ca52482d81da80681533d33bf3f0596c8e2" +checksum = "d0f9ee0f6e02ffd7ad5816e9464499fba7b3effd01123b515c41d1697c43dad1" dependencies = [ "compression-codecs", "compression-core", @@ -360,9 +351,9 @@ checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" [[package]] name = "aws-config" -version = "1.8.14" +version = "1.8.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a8fc176d53d6fe85017f230405e3255cedb4a02221cb55ed6d76dccbbb099b2" +checksum = "11493b0bad143270fb8ad284a096dd529ba91924c5409adeac856cc1bf047dbc" dependencies = [ "aws-credential-types", "aws-runtime", @@ -380,7 +371,7 @@ dependencies = [ "fastrand", "hex", "http 1.4.0", - "ring", + "sha1", "time", "tokio", "tracing", @@ -390,9 +381,9 @@ dependencies = [ [[package]] name = "aws-credential-types" -version = "1.2.13" +version = "1.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d203b0bf2626dcba8665f5cd0871d7c2c0930223d6b6be9097592fea21242d0" +checksum = "8f20799b373a1be121fe3005fba0c2090af9411573878f224df44b42727fcaf7" dependencies = [ "aws-smithy-async", "aws-smithy-runtime-api", @@ -402,9 +393,9 @@ dependencies = [ [[package]] name = "aws-runtime" -version = "1.7.1" +version = "1.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ede2ddc593e6c8acc6ce3358c28d6677a6dc49b65ba4b37a2befe14a11297e75" +checksum = "5fc0651c57e384202e47153c1260b84a9936e19803d747615edf199dc3b98d17" dependencies = [ "aws-credential-types", "aws-sigv4", @@ -427,9 +418,9 @@ dependencies = [ [[package]] name = "aws-sdk-sso" -version = "1.95.0" +version = "1.97.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00c5ff27c6ba2cbd95e6e26e2e736676fdf6bcf96495b187733f521cfe4ce448" +checksum = "9aadc669e184501caaa6beafb28c6267fc1baef0810fb58f9b205485ca3f2567" dependencies = [ "aws-credential-types", "aws-runtime", @@ -451,9 +442,9 @@ dependencies = [ [[package]] name = "aws-sdk-ssooidc" -version = "1.97.0" +version = "1.99.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d186f1e5a3694a188e5a0640b3115ccc6e084d104e16fd6ba968dca072ffef8" +checksum = "1342a7db8f358d3de0aed2007a0b54e875458e39848d54cc1d46700b2bfcb0a8" dependencies = [ "aws-credential-types", "aws-runtime", @@ -475,9 +466,9 @@ dependencies = [ [[package]] name = "aws-sdk-sts" -version = "1.99.0" +version = "1.101.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9acba7c62f3d4e2408fa998a3a8caacd8b9a5b5549cf36e2372fbdae329d5449" +checksum = "ab41ad64e4051ecabeea802d6a17845a91e83287e1dd249e6963ea1ba78c428a" dependencies = [ "aws-credential-types", "aws-runtime", @@ -500,9 +491,9 @@ dependencies = [ [[package]] name = "aws-sigv4" -version = "1.4.1" +version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37411f8e0f4bea0c3ca0958ce7f18f6439db24d555dbd809787262cd00926aa9" +checksum = "b0b660013a6683ab23797778e21f1f854744fdf05f68204b4cca4c8c04b5d1f4" dependencies = [ "aws-credential-types", "aws-smithy-http", @@ -522,9 +513,9 @@ dependencies = [ [[package]] name = "aws-smithy-async" -version = "1.2.13" +version = "1.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5cc50d0f63e714784b84223abd7abbc8577de8c35d699e0edd19f0a88a08ae13" +checksum = "2ffcaf626bdda484571968400c326a244598634dc75fd451325a54ad1a59acfc" dependencies = [ "futures-util", "pin-project-lite", @@ -533,9 +524,9 @@ dependencies = [ [[package]] name = "aws-smithy-http" -version = "0.63.5" +version = "0.63.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d619373d490ad70966994801bc126846afaa0d1ee920697a031f0cf63f2568e7" +checksum = "ba1ab2dc1c2c3749ead27180d333c42f11be8b0e934058fb4b2258ee8dbe5231" dependencies = [ "aws-smithy-runtime-api", "aws-smithy-types", @@ -554,27 +545,27 @@ dependencies = [ [[package]] name = "aws-smithy-json" -version = "0.62.4" +version = "0.62.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "27b3a779093e18cad88bbae08dc4261e1d95018c4c5b9356a52bcae7c0b6e9bb" +checksum = "9648b0bb82a2eedd844052c6ad2a1a822d1f8e3adee5fbf668366717e428856a" dependencies = [ "aws-smithy-types", ] [[package]] name = "aws-smithy-observability" -version = "0.2.5" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d3f39d5bb871aaf461d59144557f16d5927a5248a983a40654d9cf3b9ba183b" +checksum = "a06c2315d173edbf1920da8ba3a7189695827002e4c0fc961973ab1c54abca9c" dependencies = [ "aws-smithy-runtime-api", ] [[package]] name = "aws-smithy-query" -version = "0.60.14" +version = "0.60.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05f76a580e3d8f8961e5d48763214025a2af65c2fa4cd1fb7f270a0e107a71b0" +checksum = "1a56d79744fb3edb5d722ef79d86081e121d3b9422cb209eb03aea6aa4f21ebd" dependencies = [ "aws-smithy-types", "urlencoding", @@ -582,9 +573,9 @@ dependencies = [ [[package]] name = "aws-smithy-runtime" -version = "1.10.2" +version = "1.10.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22ccf7f6eba8b2dcf8ce9b74806c6c185659c311665c4bf8d6e71ebd454db6bf" +checksum = "028999056d2d2fd58a697232f9eec4a643cf73a71cf327690a7edad1d2af2110" dependencies = [ "aws-smithy-async", "aws-smithy-http", @@ -606,9 +597,9 @@ dependencies = [ [[package]] name = "aws-smithy-runtime-api" -version = "1.11.5" +version = "1.11.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4af6e5def28be846479bbeac55aa4603d6f7986fc5da4601ba324dd5d377516" +checksum = "876ab3c9c29791ba4ba02b780a3049e21ec63dabda09268b175272c3733a79e6" dependencies = [ "aws-smithy-async", "aws-smithy-types", @@ -623,9 +614,9 @@ dependencies = [ [[package]] name = "aws-smithy-types" -version = "1.4.5" +version = "1.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ca2734c16913a45343b37313605d84e7d8b34a4611598ce1d25b35860a2bed3" +checksum = "9d73dbfbaa8e4bc57b9045137680b958d274823509a360abfd8e1d514d40c95c" dependencies = [ "base64-simd", "bytes", @@ -646,18 +637,18 @@ dependencies = [ [[package]] name = "aws-smithy-xml" -version = "0.60.14" +version = "0.60.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b53543b4b86ed43f051644f704a98c7291b3618b67adf057ee77a366fa52fcaa" +checksum = "0ce02add1aa3677d022f8adf81dcbe3046a95f17a1b1e8979c145cd21d3d22b3" dependencies = [ "xmlparser", ] [[package]] name = "aws-types" -version = "1.3.13" +version = "1.3.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0470cc047657c6e286346bdf10a8719d26efd6a91626992e0e64481e44323e96" +checksum = "47c8323699dd9b3c8d5b3c13051ae9cdef58fd179957c882f8374dd8725962d9" dependencies = [ "aws-credential-types", "aws-smithy-async", @@ -815,12 +806,6 @@ version = "3.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" -[[package]] -name = "bytecount" -version = "0.6.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "175812e0be2bccb6abe50bb8d566126198344f707e304f45c648fd8f2cc0365e" - [[package]] name = "bytemuck" version = "1.25.0" @@ -851,17 +836,18 @@ dependencies = [ [[package]] name = "cached" -version = "0.56.0" +version = "0.59.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "801927ee168e17809ab8901d9f01f700cd7d8d6a6527997fee44e4b0327a253c" +checksum = "53b6f5d101f0f6322c8646a45b7c581a673e476329040d97565815c2461dd0c4" dependencies = [ "ahash", "async-trait", "cached_proc_macro", "cached_proc_macro_types", "futures", - "hashbrown 0.15.5", + "hashbrown 0.16.1", "once_cell", + "parking_lot", "thiserror 2.0.18", "tokio", "web-time", @@ -869,9 +855,9 @@ dependencies = [ [[package]] name = "cached_proc_macro" -version = "0.25.0" +version = "0.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9225bdcf4e4a9a4c08bf16607908eb2fbf746828d5e0b5e019726dbf6571f201" +checksum = "8ebcf9c75f17a17d55d11afc98e46167d4790a263f428891b8705ab2f793eca3" dependencies = [ "darling 0.20.11", "proc-macro2", @@ -885,37 +871,6 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ade8366b8bd5ba243f0a58f036cc0ca8a2f069cff1a2351ef1cac6b083e16fc0" -[[package]] -name = "camino" -version = "1.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e629a66d692cb9ff1a1c664e41771b3dcaf961985a9774c0eb0bd1b51cf60a48" -dependencies = [ - "serde_core", -] - -[[package]] -name = "cargo-platform" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e35af189006b9c0f00a064685c727031e3ed2d8020f7ba284d78cc2671bd36ea" -dependencies = [ - "serde", -] - -[[package]] -name = "cargo_metadata" -version = "0.14.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4acbb09d9ee8e23699b9634375c72795d095bf268439da88562cf9b501f181fa" -dependencies = [ - "camino", - "cargo-platform", - "semver", - "serde", - "serde_json", -] - [[package]] name = "cbc" version = "0.1.2" @@ -927,9 +882,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.56" +version = "1.2.58" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aebf35691d1bfb0ac386a69bac2fde4dd276fb618cf8bf4f5318fe285e821bb2" +checksum = "e1e928d4b69e3077709075a938a05ffbedfa53a84c8f766efbf8220bb1ff60e1" dependencies = [ "find-msvc-tools", "jobserver", @@ -962,9 +917,9 @@ dependencies = [ [[package]] name = "chrono" -version = "0.4.43" +version = "0.4.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fac4744fb15ae8337dc853fee7fb3f4e48c0fbaa23d0afe49c447b4fab126118" +checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0" dependencies = [ "iana-time-zone", "js-sys", @@ -984,16 +939,6 @@ dependencies = [ "phf 0.12.1", ] -[[package]] -name = "chumsky" -version = "0.9.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8eebd66744a15ded14960ab4ccdbfb51ad3b81f51f3f04a80adac98c985396c9" -dependencies = [ - "hashbrown 0.14.5", - "stacker", -] - [[package]] name = "cipher" version = "0.4.4" @@ -1281,6 +1226,16 @@ dependencies = [ "darling_macro 0.21.3", ] +[[package]] +name = "darling" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d" +dependencies = [ + "darling_core 0.23.0", + "darling_macro 0.23.0", +] + [[package]] name = "darling_core" version = "0.20.11" @@ -1309,6 +1264,19 @@ dependencies = [ "syn", ] +[[package]] +name = "darling_core" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9865a50f7c335f53564bb694ef660825eb8610e0a53d3e11bf1b0d3df31e03b0" +dependencies = [ + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn", +] + [[package]] name = "darling_macro" version = "0.20.11" @@ -1332,16 +1300,14 @@ dependencies = [ ] [[package]] -name = "dashmap" -version = "5.5.3" +name = "darling_macro" +version = "0.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "978747c1d849a7d2ee5e8adc0159961c48fb7e5db2f06af6723b80123bb53856" +checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" dependencies = [ - "cfg-if", - "hashbrown 0.14.5", - "lock_api", - "once_cell", - "parking_lot_core", + "darling_core 0.23.0", + "quote", + "syn", ] [[package]] @@ -1494,9 +1460,9 @@ dependencies = [ [[package]] name = "diesel" -version = "2.3.6" +version = "2.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9b6c2fc184a6fb6ebcf5f9a5e3bbfa84d8fd268cdfcce4ed508979a6259494d" +checksum = "f4ae09a41a4b89f94ec1e053623da8340d996bc32c6517d325a9daad9b239358" dependencies = [ "bigdecimal", "bitflags", @@ -1740,12 +1706,6 @@ dependencies = [ "syn", ] -[[package]] -name = "env_home" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7f84e12ccf0a7ddc17a6c41c93326024c42920d7ee630d04950e6926645c0fe" - [[package]] name = "equivalent" version = "1.0.2" @@ -1762,15 +1722,6 @@ dependencies = [ "windows-sys 0.61.2", ] -[[package]] -name = "error-chain" -version = "0.12.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d2f06b9cac1506ece98fe3231e3cc9c4410ec3d5b1f24ae1c8946f0742cdefc" -dependencies = [ - "version_check", -] - [[package]] name = "event-listener" version = "2.5.3" @@ -2057,20 +2008,20 @@ dependencies = [ "cfg-if", "js-sys", "libc", - "r-efi", + "r-efi 5.3.0", "wasip2", "wasm-bindgen", ] [[package]] name = "getrandom" -version = "0.4.1" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "139ef39800118c7683f2fd3c98c1b23c09ae076556b435f8e9064ae108aaeeec" +checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" dependencies = [ "cfg-if", "libc", - "r-efi", + "r-efi 6.0.0", "rand_core 0.10.0", "wasip2", "wasip3", @@ -2101,7 +2052,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9efcab3c1958580ff1f25a2a41be1668f7603d849bb63af523b208a3cc1223b8" dependencies = [ "cfg-if", - "dashmap 6.1.0", + "dashmap", "futures-sink", "futures-timer", "futures-util", @@ -2210,8 +2161,6 @@ version = "0.15.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" dependencies = [ - "allocator-api2", - "equivalent", "foldhash 0.1.5", ] @@ -2458,7 +2407,7 @@ dependencies = [ "http 1.4.0", "hyper 1.8.1", "hyper-util", - "rustls 0.23.36", + "rustls 0.23.37", "rustls-native-certs", "rustls-pki-types", "tokio", @@ -2484,7 +2433,7 @@ dependencies = [ "libc", "percent-encoding", "pin-project-lite", - "socket2 0.6.2", + "socket2 0.6.3", "system-configuration", "tokio", "tower-service", @@ -2671,27 +2620,28 @@ dependencies = [ [[package]] name = "ipconfig" -version = "0.3.2" +version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b58db92f96b720de98181bbbe63c831e87005ab460c1bf306eb2622b4707997f" +checksum = "4d40460c0ce33d6ce4b0630ad68ff63d6661961c48b6dba35e5a4d81cfb48222" dependencies = [ - "socket2 0.5.10", + "socket2 0.6.3", "widestring", - "windows-sys 0.48.0", - "winreg", + "windows-registry", + "windows-result", + "windows-sys 0.61.2", ] [[package]] name = "ipnet" -version = "2.11.0" +version = "2.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" +checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" [[package]] name = "iri-string" -version = "0.7.10" +version = "0.7.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c91338f0783edbd6195decb37bae672fd3b165faffb89bf7b9e6942f8b1a731a" +checksum = "d8e7418f59cc01c88316161279a7f665217ae316b388e58a0d10e29f54f1e5eb" dependencies = [ "memchr", "serde", @@ -2719,9 +2669,9 @@ dependencies = [ [[package]] name = "itoa" -version = "1.0.17" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" [[package]] name = "jetscii" @@ -2731,9 +2681,9 @@ checksum = "47f142fe24a9c9944451e8349de0a56af5f3e7226dc46f3ed4d4ecc0b85af75e" [[package]] name = "jiff" -version = "0.2.21" +version = "0.2.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3e3d65f018c6ae946ab16e80944b97096ed73c35b221d1c478a6c81d8f57940" +checksum = "1a3546dc96b6d42c5f24902af9e2538e82e39ad350b0c766eb3fbf2d8f3d8359" dependencies = [ "jiff-static", "jiff-tzdb-platform", @@ -2746,9 +2696,9 @@ dependencies = [ [[package]] name = "jiff-static" -version = "0.2.21" +version = "0.2.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a17c2b211d863c7fde02cbea8a3c1a439b98e109286554f2860bdded7ff83818" +checksum = "2a8c8b344124222efd714b73bb41f8b5120b27a7cc1c75593a6ff768d9d05aa4" dependencies = [ "proc-macro2", "quote", @@ -2757,9 +2707,9 @@ dependencies = [ [[package]] name = "jiff-tzdb" -version = "0.1.5" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68971ebff725b9e2ca27a601c5eb38a4c5d64422c4cbab0c535f248087eda5c2" +checksum = "c900ef84826f1338a557697dc8fc601df9ca9af4ac137c7fb61d4c6f2dfd3076" [[package]] name = "jiff-tzdb-platform" @@ -2793,10 +2743,12 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.88" +version = "0.3.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7e709f3e3d22866f9c25b3aff01af289b18422cc8b4262fb19103ee80fe513d" +checksum = "cc4c90f45aa2e6eacbe8645f77fdea542ac97a494bcd117a67df9ff4d611f995" dependencies = [ + "cfg-if", + "futures-util", "once_cell", "wasm-bindgen", ] @@ -2874,14 +2826,13 @@ checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" [[package]] name = "lettre" -version = "0.11.19" +version = "0.11.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e13e10e8818f8b2a60f52cb127041d388b89f3a96a62be9ceaffa22262fef7f" +checksum = "471816f3e24b85e820dee02cde962379ea1a669e5242f19c61bcbcffedf4c4fb" dependencies = [ "async-std", "async-trait", "base64 0.22.1", - "chumsky", "email-encoding", "email_address", "fastrand", @@ -2894,10 +2845,10 @@ dependencies = [ "nom 8.0.0", "percent-encoding", "quoted_printable", - "rustls 0.23.36", + "rustls 0.23.37", "rustls-native-certs", "serde", - "socket2 0.6.2", + "socket2 0.6.3", "tokio", "tokio-rustls 0.26.4", "tracing", @@ -2906,9 +2857,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.182" +version = "0.2.183" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6800badb6cb2082ffd7b6a67e6125bb39f18782f793520caee8cb8846be06112" +checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d" [[package]] name = "libm" @@ -2928,9 +2879,9 @@ dependencies = [ [[package]] name = "libsqlite3-sys" -version = "0.35.0" +version = "0.36.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "133c182a6a2c87864fe97778797e46c7e999672690dc9fa3ee8e241aa4a9c13f" +checksum = "95b4103cffefa72eb8428cb6b47d6627161e51c2739fc5e3b734584157bc642a" dependencies = [ "cc", "pkg-config", @@ -3063,21 +3014,6 @@ version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" -[[package]] -name = "mini-moka" -version = "0.10.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c325dfab65f261f386debee8b0969da215b3fa0037e74c8a1234db7ba986d803" -dependencies = [ - "crossbeam-channel", - "crossbeam-utils", - "dashmap 5.5.3", - "skeptic", - "smallvec", - "tagptr", - "triomphe", -] - [[package]] name = "minimal-lexical" version = "0.2.1" @@ -3096,9 +3032,9 @@ dependencies = [ [[package]] name = "mio" -version = "1.1.1" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" +checksum = "50b7e5b27aa02a74bac8c3f23f448f8d87ff11f92d3aac1a6ed369ee08cc56c1" dependencies = [ "libc", "wasi", @@ -3107,14 +3043,17 @@ dependencies = [ [[package]] name = "moka" -version = "0.12.13" +version = "0.12.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4ac832c50ced444ef6be0767a008b02c106a909ba79d1d830501e94b96f6b7e" +checksum = "957228ad12042ee839f93c8f257b62b4c0ab5eaae1d4fa60de53b27c9d7c5046" dependencies = [ + "async-lock", "crossbeam-channel", "crossbeam-epoch", "crossbeam-utils", "equivalent", + "event-listener 5.4.1", + "futures-util", "parking_lot", "portable-atomic", "smallvec", @@ -3214,9 +3153,9 @@ dependencies = [ [[package]] name = "num-conv" -version = "0.2.0" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf97ec579c3c42f953ef76dbf8d55ac91fb219dde70e49aa4a6b7d74e9919050" +checksum = "c6673768db2d862beb9b39a78fdcb1a69439615d5794a1be50caa9bc92c81967" [[package]] name = "num-derive" @@ -3313,15 +3252,6 @@ dependencies = [ "url", ] -[[package]] -name = "object" -version = "0.37.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe" -dependencies = [ - "memchr", -] - [[package]] name = "oid-registry" version = "0.7.1" @@ -3333,9 +3263,9 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.21.3" +version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" dependencies = [ "critical-section", "portable-atomic", @@ -3403,9 +3333,9 @@ dependencies = [ [[package]] name = "openssl" -version = "0.10.75" +version = "0.10.76" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08838db121398ad17ab8531ce9de97b244589089e290a384c900cb9ff7434328" +checksum = "951c002c75e16ea2c65b8c7e4d3d51d5530d8dfa7d060b4776828c88cfb18ecf" dependencies = [ "bitflags", "cfg-if", @@ -3444,9 +3374,9 @@ dependencies = [ [[package]] name = "openssl-sys" -version = "0.9.111" +version = "0.9.112" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82cab2d520aa75e3c58898289429321eb788c3106963d0dc886ec7a5f4adc321" +checksum = "57d55af3b3e226502be1526dfdba67ab0e9c96fc293004e79576b2b9edb0dbdb" dependencies = [ "cc", "libc", @@ -3725,9 +3655,9 @@ checksum = "5be167a7af36ee22fe3115051bc51f6e6c7054c9348e28deb4f49bd6f705a315" [[package]] name = "pin-project-lite" -version = "0.2.16" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" [[package]] name = "pin-utils" @@ -3737,9 +3667,9 @@ checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" [[package]] name = "piper" -version = "0.2.4" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96c8c490f422ef9a4efd2cb5b42b76c8613d7e7dfc1caf667b8a3350a5acc066" +checksum = "c835479a4443ded371d6c535cbfd8d31ad92c5d23ae9770a61bc155e4992a3c1" dependencies = [ "atomic-waker", "fastrand", @@ -3812,9 +3742,9 @@ checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" [[package]] name = "portable-atomic-util" -version = "0.2.5" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a9db96d7fa8782dd8c15ce32ffe8680bbd1e978a43bf51a34d39483540495f5" +checksum = "091397be61a01d4be58e7841595bd4bfedb15f1cd54977d79b8271e94ed799a3" dependencies = [ "portable-atomic", ] @@ -3915,16 +3845,6 @@ version = "2.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "33cb294fe86a74cbcf50d4445b37da762029549ebeea341421c7c70370f86cac" -[[package]] -name = "psm" -version = "0.1.30" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3852766467df634d74f0b2d7819bf8dc483a0eb2e3b0f50f756f9cfe8b0d18d8" -dependencies = [ - "ar_archive_writer", - "cc", -] - [[package]] name = "publicsuffix" version = "2.3.0" @@ -3935,17 +3855,6 @@ dependencies = [ "psl-types", ] -[[package]] -name = "pulldown-cmark" -version = "0.9.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57206b407293d2bcd3af849ce869d52068623f19e1b5ff8e8778e3309439682b" -dependencies = [ - "bitflags", - "memchr", - "unicase", -] - [[package]] name = "quanta" version = "0.12.6" @@ -3999,8 +3908,8 @@ dependencies = [ "quinn-proto", "quinn-udp", "rustc-hash", - "rustls 0.23.36", - "socket2 0.6.2", + "rustls 0.23.37", + "socket2 0.6.3", "thiserror 2.0.18", "tokio", "tracing", @@ -4009,9 +3918,9 @@ dependencies = [ [[package]] name = "quinn-proto" -version = "0.11.13" +version = "0.11.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1906b49b0c3bc04b5fe5d86a77925ae6524a19b816ae38ce1e426255f1d8a31" +checksum = "434b42fec591c96ef50e21e886936e66d3cc3f737104fdb9b737c40ffb94c098" dependencies = [ "bytes", "getrandom 0.3.4", @@ -4019,7 +3928,7 @@ dependencies = [ "rand 0.9.2", "ring", "rustc-hash", - "rustls 0.23.36", + "rustls 0.23.37", "rustls-pki-types", "slab", "thiserror 2.0.18", @@ -4037,25 +3946,25 @@ dependencies = [ "cfg_aliases", "libc", "once_cell", - "socket2 0.6.2", + "socket2 0.6.3", "tracing", "windows-sys 0.60.2", ] [[package]] name = "quote" -version = "1.0.44" +version = "1.0.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" dependencies = [ "proc-macro2", ] [[package]] name = "quoted_printable" -version = "0.5.1" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "640c9bd8497b02465aeef5375144c26062e0dcd5939dfcbb0f5db76cb8c17c73" +checksum = "478e0585659a122aa407eb7e3c0e1fa51b1d8a870038bd29f0cf4a8551eea972" [[package]] name = "r-efi" @@ -4063,6 +3972,12 @@ version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + [[package]] name = "r2d2" version = "0.8.10" @@ -4102,7 +4017,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bc266eb313df6c5c09c1c7b1fbe2510961e5bcd3add930c1e31f7ed9da0feff8" dependencies = [ "chacha20", - "getrandom 0.4.1", + "getrandom 0.4.2", "rand_core 0.10.0", ] @@ -4219,9 +4134,9 @@ checksum = "cab834c73d247e67f4fae452806d17d3c7501756d98c8808d7c9c7aa7d18f973" [[package]] name = "regex-syntax" -version = "0.8.9" +version = "0.8.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a96887878f22d7bad8a3b6dc5b7440e0ada9a245242924394987b21cf2210a4c" +checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" [[package]] name = "reopen" @@ -4294,7 +4209,7 @@ dependencies = [ "percent-encoding", "pin-project-lite", "quinn", - "rustls 0.23.36", + "rustls 0.23.37", "rustls-native-certs", "rustls-pki-types", "serde", @@ -4522,9 +4437,9 @@ dependencies = [ [[package]] name = "rustc-hash" -version = "2.1.1" +version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" +checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe" [[package]] name = "rustc_version" @@ -4571,15 +4486,15 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.36" +version = "0.23.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c665f33d38cea657d9614f766881e4d510e0eda4239891eea56b4cadcf01801b" +checksum = "758025cb5fccfd3bc2fd74708fd4682be41d99e5dff73c377c0646c6012c73a4" dependencies = [ "log", "once_cell", "ring", "rustls-pki-types", - "rustls-webpki 0.103.9", + "rustls-webpki 0.103.10", "subtle", "zeroize", ] @@ -4627,9 +4542,9 @@ dependencies = [ [[package]] name = "rustls-webpki" -version = "0.103.9" +version = "0.103.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7df23109aa6c1567d1c575b9952556388da57401e4ace1d15f79eedad0d8f53" +checksum = "df33b2b81ac578cabaf06b89b0631153a3f416b0a886e8a7a1707fb51abbd1ef" dependencies = [ "ring", "rustls-pki-types", @@ -4668,9 +4583,9 @@ dependencies = [ [[package]] name = "schannel" -version = "0.1.28" +version = "0.1.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "891d81b926048e76efe18581bf793546b4c0eaf8448d72be8de2bbee5fd166e1" +checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939" dependencies = [ "windows-sys 0.61.2", ] @@ -4783,10 +4698,6 @@ name = "semver" version = "1.0.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" -dependencies = [ - "serde", - "serde_core", -] [[package]] name = "serde" @@ -4882,9 +4793,9 @@ dependencies = [ [[package]] name = "serde_spanned" -version = "1.0.4" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8bbf91e5a4d6315eee45e704372590b30e260ee83af6639d64557f51b067776" +checksum = "876ac351060d4f882bb1032b6369eb0aef79ad9df1ea8bc404874d8cc3d0cd98" dependencies = [ "serde_core", ] @@ -4903,9 +4814,9 @@ dependencies = [ [[package]] name = "serde_with" -version = "3.16.1" +version = "3.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fa237f2807440d238e0364a218270b98f767a00d3dada77b1c53ae88940e2e7" +checksum = "dd5414fad8e6907dbdd5bc441a50ae8d6e26151a03b1de04d89a5576de61d01f" dependencies = [ "base64 0.22.1", "chrono", @@ -4922,11 +4833,11 @@ dependencies = [ [[package]] name = "serde_with_macros" -version = "3.16.1" +version = "3.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52a8e3ca0ca629121f70ab50f95249e5a6f925cc0f6ffe8256c45b728875706c" +checksum = "d3db8978e608f1fe7357e211969fd9abdcae80bac1ba7a3369bb7eb6b404eb65" dependencies = [ - "darling 0.21.3", + "darling 0.23.0", "proc-macro2", "quote", "syn", @@ -5001,9 +4912,9 @@ dependencies = [ [[package]] name = "simd-adler32" -version = "0.3.8" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2" +checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" [[package]] name = "simple_asn1" @@ -5023,21 +4934,6 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b2aa850e253778c88a04c3d7323b043aeda9d3e30d5971937c1855769763678e" -[[package]] -name = "skeptic" -version = "0.13.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16d23b015676c90a0f01c197bfdc786c20342c73a0afdda9025adb0bc42940a8" -dependencies = [ - "bytecount", - "cargo_metadata", - "error-chain", - "glob", - "pulldown-cmark", - "tempfile", - "walkdir", -] - [[package]] name = "slab" version = "0.4.12" @@ -5062,12 +4958,12 @@ dependencies = [ [[package]] name = "socket2" -version = "0.6.2" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86f4aa3ad99f2088c990dfa82d367e19cb29268ed67c574d10d0a4bfe71f07e0" +checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" dependencies = [ "libc", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -5122,19 +5018,6 @@ version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" -[[package]] -name = "stacker" -version = "0.1.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08d74a23609d509411d10e2176dc2a4346e3b4aea2e7b1869f19fdedbc71c013" -dependencies = [ - "cc", - "cfg-if", - "libc", - "psm", - "windows-sys 0.59.0", -] - [[package]] name = "state" version = "0.6.0" @@ -5241,12 +5124,12 @@ checksum = "7b2093cf4c8eb1e67749a6762251bc9cd836b6fc171623bd0a9d324d37af2417" [[package]] name = "tempfile" -version = "3.25.0" +version = "3.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0136791f7c95b1f6dd99f9cc786b91bb81c3800b639b3478e561ddb7be95e5f1" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" dependencies = [ "fastrand", - "getrandom 0.4.1", + "getrandom 0.4.2", "once_cell", "rustix", "windows-sys 0.61.2", @@ -5364,9 +5247,9 @@ dependencies = [ [[package]] name = "tinyvec" -version = "1.10.0" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa" +checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3" dependencies = [ "tinyvec_macros", ] @@ -5379,9 +5262,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.49.0" +version = "1.50.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72a2903cd7736441aac9df9d7688bd0ce48edccaadf181c3b90be801e81d3d86" +checksum = "27ad5e34374e03cfffefc301becb44e9dc3c17584f414349ebe29ed26661822d" dependencies = [ "bytes", "libc", @@ -5389,16 +5272,16 @@ dependencies = [ "parking_lot", "pin-project-lite", "signal-hook-registry", - "socket2 0.6.2", + "socket2 0.6.3", "tokio-macros", "windows-sys 0.61.2", ] [[package]] name = "tokio-macros" -version = "2.6.0" +version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" +checksum = "5c55a2eff8b69ce66c84f85e1da1c233edc36ceb85a2058d11b0d6a3c7e7569c" dependencies = [ "proc-macro2", "quote", @@ -5421,7 +5304,7 @@ version = "0.26.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" dependencies = [ - "rustls 0.23.36", + "rustls 0.23.37", "tokio", ] @@ -5481,10 +5364,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf92845e79fc2e2def6a5d828f0801e29a2f8acc037becc5ab08595c7d5e9863" dependencies = [ "serde_core", - "serde_spanned 1.0.4", + "serde_spanned 1.1.0", "toml_datetime 0.7.5+spec-1.1.0", "toml_parser", - "winnow 0.7.14", + "winnow 0.7.15", ] [[package]] @@ -5516,16 +5399,16 @@ dependencies = [ "serde_spanned 0.6.9", "toml_datetime 0.6.11", "toml_write", - "winnow 0.7.14", + "winnow 0.7.15", ] [[package]] name = "toml_parser" -version = "1.0.9+spec-1.1.0" +version = "1.1.0+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "702d4415e08923e7e1ef96cd5727c0dfed80b4d2fa25db9647fe5eb6f7c5a4c4" +checksum = "2334f11ee363607eb04df9b8fc8a13ca1715a72ba8662a26ac285c98aabb4011" dependencies = [ - "winnow 0.7.14", + "winnow 1.0.0", ] [[package]] @@ -5642,9 +5525,9 @@ dependencies = [ [[package]] name = "tracing-subscriber" -version = "0.3.22" +version = "0.3.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f30143827ddab0d256fd843b7a66d164e9f271cfa0dde49142c5ca0ca291f1e" +checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319" dependencies = [ "matchers", "nu-ansi-term", @@ -5658,12 +5541,6 @@ dependencies = [ "tracing-log", ] -[[package]] -name = "triomphe" -version = "0.1.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd69c5aa8f924c7519d6372789a74eac5b94fb0f8fcf0d4a97eb0bfc3e785f39" - [[package]] name = "try-lock" version = "0.2.5" @@ -5720,12 +5597,6 @@ dependencies = [ "version_check", ] -[[package]] -name = "unicase" -version = "2.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142" - [[package]] name = "unicode-ident" version = "1.0.24" @@ -5734,9 +5605,9 @@ checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" [[package]] name = "unicode-segmentation" -version = "1.12.0" +version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" +checksum = "9629274872b2bfaf8d66f5f15725007f635594914870f65218920345aa11aa8c" [[package]] name = "unicode-xid" @@ -5783,11 +5654,11 @@ checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" [[package]] name = "uuid" -version = "1.21.0" +version = "1.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b672338555252d43fd2240c714dc444b8c6fb0a5c5335e65a07bba7742735ddb" +checksum = "5ac8b6f42ead25368cf5b098aeb3dc8a1a2c05a3eee8a9a1a68c640edbfc79d9" dependencies = [ - "getrandom 0.4.1", + "getrandom 0.4.2", "js-sys", "serde_core", "wasm-bindgen", @@ -5821,7 +5692,7 @@ dependencies = [ "chrono-tz", "cookie", "cookie_store", - "dashmap 6.1.0", + "dashmap", "data-encoding", "data-url", "derive_more", @@ -5845,7 +5716,7 @@ dependencies = [ "log", "macros", "mimalloc", - "mini-moka", + "moka", "num-derive", "num-traits", "once_cell", @@ -5948,9 +5819,9 @@ dependencies = [ [[package]] name = "wasm-bindgen" -version = "0.2.111" +version = "0.2.115" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec1adf1535672f5b7824f817792b1afd731d7e843d2d04ec8f27e8cb51edd8ac" +checksum = "6523d69017b7633e396a89c5efab138161ed5aafcbc8d3e5c5a42ae38f50495a" dependencies = [ "cfg-if", "once_cell", @@ -5961,23 +5832,19 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.61" +version = "0.4.65" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe88540d1c934c4ec8e6db0afa536876c5441289d7f9f9123d4f065ac1250a6b" +checksum = "2d1faf851e778dfa54db7cd438b70758eba9755cb47403f3496edd7c8fc212f0" dependencies = [ - "cfg-if", - "futures-util", "js-sys", - "once_cell", "wasm-bindgen", - "web-sys", ] [[package]] name = "wasm-bindgen-macro" -version = "0.2.111" +version = "0.2.115" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19e638317c08b21663aed4d2b9a2091450548954695ff4efa75bff5fa546b3b1" +checksum = "4e3a6c758eb2f701ed3d052ff5737f5bfe6614326ea7f3bbac7156192dc32e67" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -5985,9 +5852,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.111" +version = "0.2.115" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c64760850114d03d5f65457e96fc988f11f01d38fbaa51b254e4ab5809102af" +checksum = "921de2737904886b52bcbb237301552d05969a6f9c40d261eb0533c8b055fedf" dependencies = [ "bumpalo", "proc-macro2", @@ -5998,9 +5865,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.111" +version = "0.2.115" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60eecd4fe26177cfa3339eb00b4a36445889ba3ad37080c2429879718e20ca41" +checksum = "a93e946af942b58934c604527337bad9ae33ba1d5c6900bbb41c2c07c2364a93" dependencies = [ "unicode-ident", ] @@ -6054,9 +5921,9 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.88" +version = "0.3.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d6bb20ed2d9572df8584f6dc81d68a41a625cadc6f15999d649a70ce7e3597a" +checksum = "84cde8507f4d7cfcb1185b8cb5890c494ffea65edbe1ba82cfd63661c805ed94" dependencies = [ "js-sys", "wasm-bindgen", @@ -6151,13 +6018,11 @@ dependencies = [ [[package]] name = "which" -version = "8.0.0" +version = "8.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3fabb953106c3c8eea8306e4393700d7657561cb43122571b172bbfb7c7ba1d" +checksum = "81995fafaaaf6ae47a7d0cc83c67caf92aeb7e5331650ae6ff856f7c0c60c459" dependencies = [ - "env_home", - "rustix", - "winsafe", + "libc", ] [[package]] @@ -6276,15 +6141,6 @@ dependencies = [ "windows-link", ] -[[package]] -name = "windows-sys" -version = "0.48.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" -dependencies = [ - "windows-targets 0.48.5", -] - [[package]] name = "windows-sys" version = "0.52.0" @@ -6518,28 +6374,18 @@ dependencies = [ [[package]] name = "winnow" -version = "0.7.14" +version = "0.7.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829" +checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" dependencies = [ "memchr", ] [[package]] -name = "winreg" -version = "0.50.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1" -dependencies = [ - "cfg-if", - "windows-sys 0.48.0", -] - -[[package]] -name = "winsafe" -version = "0.0.19" +name = "winnow" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d135d17ab770252ad95e9a872d365cf3090e3be864a34ab46f48555993efc904" +checksum = "a90e88e4667264a994d34e6d1ab2d26d398dcdca8b7f52bec8668957517fc7d8" [[package]] name = "wit-bindgen" @@ -6714,18 +6560,18 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.8.39" +version = "0.8.48" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db6d35d663eadb6c932438e763b262fe1a70987f9ae936e60158176d710cae4a" +checksum = "eed437bf9d6692032087e337407a86f04cd8d6a16a37199ed57949d415bd68e9" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.39" +version = "0.8.48" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4122cd3169e94605190e77839c9a40d40ed048d305bfdc146e7df40ab0f3e517" +checksum = "70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index 93f2697f..c5cb3676 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [workspace.package] edition = "2021" -rust-version = "1.91.0" +rust-version = "1.92.0" license = "AGPL-3.0-only" repository = "https://github.com/dani-garcia/vaultwarden" publish = false @@ -84,7 +84,7 @@ once_cell = "1.20.2" # Async futures futures = "0.3.32" -tokio = { version = "1.49.0", features = ["rt-multi-thread", "fs", "io-util", "parking_lot", "time", "signal", "net"] } +tokio = { version = "1.50.0", features = ["rt-multi-thread", "fs", "io-util", "parking_lot", "time", "signal", "net"] } tokio-util = { version = "0.7.18", features = ["compat"]} # A generic serialization/deserialization framework @@ -93,14 +93,14 @@ serde_json = "1.0.149" # A safe, extensible ORM and Query builder # Currently pinned diesel to v2.3.3 as newer version break MySQL/MariaDB compatibility -diesel = { version = "2.3.6", features = ["chrono", "r2d2", "numeric"] } +diesel = { version = "2.3.7", features = ["chrono", "r2d2", "numeric"] } diesel_migrations = "2.3.1" derive_more = { version = "2.1.1", features = ["from", "into", "as_ref", "deref", "display"] } diesel-derive-newtype = "2.1.2" # Bundled/Static SQLite -libsqlite3-sys = { version = "0.35.0", features = ["bundled"], optional = true } +libsqlite3-sys = { version = "0.36.0", features = ["bundled"], optional = true } # Crypto-related libraries rand = "0.10.0" @@ -108,10 +108,10 @@ ring = "0.17.14" subtle = "2.6.1" # UUID generation -uuid = { version = "1.21.0", features = ["v4"] } +uuid = { version = "1.23.0", features = ["v4"] } # Date and time libraries -chrono = { version = "0.4.43", features = ["clock", "serde"], default-features = false } +chrono = { version = "0.4.44", features = ["clock", "serde"], default-features = false } chrono-tz = "0.10.4" time = "0.3.47" @@ -141,7 +141,7 @@ webauthn-rs-core = "0.5.4" url = "2.5.8" # Email libraries -lettre = { version = "0.11.19", features = ["smtp-transport", "sendmail-transport", "builder", "serde", "hostname", "tracing", "tokio1-rustls", "ring", "rustls-native-certs"], default-features = false } +lettre = { version = "0.11.20", features = ["smtp-transport", "sendmail-transport", "builder", "serde", "hostname", "tracing", "tokio1-rustls", "ring", "rustls-native-certs"], default-features = false } percent-encoding = "2.3.2" # URL encoding library used for URL's in the emails email_address = "0.2.9" @@ -160,14 +160,14 @@ bytes = "1.11.1" svg-hush = "0.9.6" # Cache function results (Used for version check and favicon fetching) -cached = { version = "0.56.0", features = ["async"] } +cached = { version = "0.59.0", features = ["async"] } # Used for custom short lived cookie jar during favicon extraction cookie = "0.18.1" cookie_store = "0.22.1" # Used by U2F, JWT and PostgreSQL -openssl = "0.10.75" +openssl = "0.10.76" # CLI argument parsing pico-args = "0.5.0" @@ -178,7 +178,7 @@ governor = "0.10.4" # OIDC for SSO openidconnect = { version = "4.0.1", features = ["reqwest", "rustls-tls"] } -mini-moka = "0.10.3" +moka = { version = "0.12.15", features = ["future"] } # Check client versions for specific features. semver = "1.0.27" @@ -190,7 +190,7 @@ mimalloc = { version = "0.1.48", features = ["secure"], default-features = false # Prometheus metrics prometheus = { version = "0.13.1", default-features = false, optional = true } -which = "8.0.0" +which = "8.0.2" # Argon2 library with support for the PHC format argon2 = "0.5.3" @@ -205,10 +205,10 @@ grass_compiler = { version = "0.13.4", default-features = false } opendal = { version = "0.55.0", features = ["services-fs"], default-features = false } # For retrieving AWS credentials, including temporary SSO credentials -anyhow = { version = "1.0.101", optional = true } -aws-config = { version = "1.8.14", features = ["behavior-version-latest", "rt-tokio", "credentials-process", "sso"], default-features = false, optional = true } -aws-credential-types = { version = "1.2.13", optional = true } -aws-smithy-runtime-api = { version = "1.11.5", optional = true } +anyhow = { version = "1.0.102", optional = true } +aws-config = { version = "1.8.15", features = ["behavior-version-latest", "rt-tokio", "credentials-process", "sso"], default-features = false, optional = true } +aws-credential-types = { version = "1.2.14", optional = true } +aws-smithy-runtime-api = { version = "1.11.6", optional = true } http = { version = "1.4.0", optional = true } reqsign = { version = "0.16.5", optional = true } diff --git a/docker/DockerSettings.yaml b/docker/DockerSettings.yaml index 7b4a9af7..c679b0da 100644 --- a/docker/DockerSettings.yaml +++ b/docker/DockerSettings.yaml @@ -1,11 +1,11 @@ --- -vault_version: "v2026.1.1" -vault_image_digest: "sha256:062fcf0d5dc37247dae61b0ee1ba5d20f9296e290d7ad1f6114ea5888f5738a7" +vault_version: "v2026.2.0" +vault_image_digest: "sha256:37c8661fa59dcdfbd3baa8366b6e950ef292b15adfeff1f57812b075c1fd3447" # Cross Compile Docker Helper Scripts v1.9.0 # We use the linux/amd64 platform shell scripts since there is no difference between the different platform scripts # https://github.com/tonistiigi/xx | https://hub.docker.com/r/tonistiigi/xx/tags xx_image_digest: "sha256:c64defb9ed5a91eacb37f96ccc3d4cd72521c4bd18d5442905b95e2226b0e707" -rust_version: 1.93.1 # Rust version to be used +rust_version: 1.94.1 # Rust version to be used debian_version: trixie # Debian release name to be used alpine_version: "3.23" # 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 a76d3f59..0a66c8c7 100644 --- a/docker/Dockerfile.alpine +++ b/docker/Dockerfile.alpine @@ -19,23 +19,23 @@ # - 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:v2026.1.1 -# $ docker image inspect --format "{{.RepoDigests}}" docker.io/vaultwarden/web-vault:v2026.1.1 -# [docker.io/vaultwarden/web-vault@sha256:062fcf0d5dc37247dae61b0ee1ba5d20f9296e290d7ad1f6114ea5888f5738a7] +# $ docker pull docker.io/vaultwarden/web-vault:v2026.2.0 +# $ docker image inspect --format "{{.RepoDigests}}" docker.io/vaultwarden/web-vault:v2026.2.0 +# [docker.io/vaultwarden/web-vault@sha256:37c8661fa59dcdfbd3baa8366b6e950ef292b15adfeff1f57812b075c1fd3447] # # - Conversely, to get the tag name from the digest: -# $ docker image inspect --format "{{.RepoTags}}" docker.io/vaultwarden/web-vault@sha256:062fcf0d5dc37247dae61b0ee1ba5d20f9296e290d7ad1f6114ea5888f5738a7 -# [docker.io/vaultwarden/web-vault:v2026.1.1] +# $ docker image inspect --format "{{.RepoTags}}" docker.io/vaultwarden/web-vault@sha256:37c8661fa59dcdfbd3baa8366b6e950ef292b15adfeff1f57812b075c1fd3447 +# [docker.io/vaultwarden/web-vault:v2026.2.0] # -FROM --platform=linux/amd64 docker.io/vaultwarden/web-vault@sha256:062fcf0d5dc37247dae61b0ee1ba5d20f9296e290d7ad1f6114ea5888f5738a7 AS vault +FROM --platform=linux/amd64 docker.io/vaultwarden/web-vault@sha256:37c8661fa59dcdfbd3baa8366b6e950ef292b15adfeff1f57812b075c1fd3447 AS vault ########################## ALPINE BUILD IMAGES ########################## ## NOTE: The Alpine Base Images do not support other platforms then linux/amd64 and linux/arm64 ## And for Alpine we define all build images here, they will only be loaded when actually used -FROM --platform=$BUILDPLATFORM ghcr.io/blackdex/rust-musl:x86_64-musl-stable-1.93.1 AS build_amd64 -FROM --platform=$BUILDPLATFORM ghcr.io/blackdex/rust-musl:aarch64-musl-stable-1.93.1 AS build_arm64 -FROM --platform=$BUILDPLATFORM ghcr.io/blackdex/rust-musl:armv7-musleabihf-stable-1.93.1 AS build_armv7 -FROM --platform=$BUILDPLATFORM ghcr.io/blackdex/rust-musl:arm-musleabi-stable-1.93.1 AS build_armv6 +FROM --platform=$BUILDPLATFORM ghcr.io/blackdex/rust-musl:x86_64-musl-stable-1.94.1 AS build_amd64 +FROM --platform=$BUILDPLATFORM ghcr.io/blackdex/rust-musl:aarch64-musl-stable-1.94.1 AS build_arm64 +FROM --platform=$BUILDPLATFORM ghcr.io/blackdex/rust-musl:armv7-musleabihf-stable-1.94.1 AS build_armv7 +FROM --platform=$BUILDPLATFORM ghcr.io/blackdex/rust-musl:arm-musleabi-stable-1.94.1 AS build_armv6 ########################## BUILD IMAGE ########################## # hadolint ignore=DL3006 diff --git a/docker/Dockerfile.debian b/docker/Dockerfile.debian index d24a22e1..95053018 100644 --- a/docker/Dockerfile.debian +++ b/docker/Dockerfile.debian @@ -19,15 +19,15 @@ # - 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:v2026.1.1 -# $ docker image inspect --format "{{.RepoDigests}}" docker.io/vaultwarden/web-vault:v2026.1.1 -# [docker.io/vaultwarden/web-vault@sha256:062fcf0d5dc37247dae61b0ee1ba5d20f9296e290d7ad1f6114ea5888f5738a7] +# $ docker pull docker.io/vaultwarden/web-vault:v2026.2.0 +# $ docker image inspect --format "{{.RepoDigests}}" docker.io/vaultwarden/web-vault:v2026.2.0 +# [docker.io/vaultwarden/web-vault@sha256:37c8661fa59dcdfbd3baa8366b6e950ef292b15adfeff1f57812b075c1fd3447] # # - Conversely, to get the tag name from the digest: -# $ docker image inspect --format "{{.RepoTags}}" docker.io/vaultwarden/web-vault@sha256:062fcf0d5dc37247dae61b0ee1ba5d20f9296e290d7ad1f6114ea5888f5738a7 -# [docker.io/vaultwarden/web-vault:v2026.1.1] +# $ docker image inspect --format "{{.RepoTags}}" docker.io/vaultwarden/web-vault@sha256:37c8661fa59dcdfbd3baa8366b6e950ef292b15adfeff1f57812b075c1fd3447 +# [docker.io/vaultwarden/web-vault:v2026.2.0] # -FROM --platform=linux/amd64 docker.io/vaultwarden/web-vault@sha256:062fcf0d5dc37247dae61b0ee1ba5d20f9296e290d7ad1f6114ea5888f5738a7 AS vault +FROM --platform=linux/amd64 docker.io/vaultwarden/web-vault@sha256:37c8661fa59dcdfbd3baa8366b6e950ef292b15adfeff1f57812b075c1fd3447 AS vault ########################## Cross Compile Docker Helper Scripts ########################## ## We use the linux/amd64 no matter which Build Platform, since these are all bash scripts @@ -36,7 +36,7 @@ FROM --platform=linux/amd64 docker.io/tonistiigi/xx@sha256:c64defb9ed5a91eacb37f ########################## BUILD IMAGE ########################## # hadolint ignore=DL3006 -FROM --platform=$BUILDPLATFORM docker.io/library/rust:1.93.1-slim-trixie AS build +FROM --platform=$BUILDPLATFORM docker.io/library/rust:1.94.1-slim-trixie AS build COPY --from=xx / / ARG TARGETARCH ARG TARGETVARIANT diff --git a/macros/Cargo.toml b/macros/Cargo.toml index 0a560a74..eb3bd670 100644 --- a/macros/Cargo.toml +++ b/macros/Cargo.toml @@ -13,8 +13,8 @@ path = "src/lib.rs" proc-macro = true [dependencies] -quote = "1.0.44" -syn = "2.0.114" +quote = "1.0.45" +syn = "2.0.117" [lints] workspace = true diff --git a/rust-toolchain.toml b/rust-toolchain.toml index 585747e0..151be09f 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -1,4 +1,4 @@ [toolchain] -channel = "1.93.1" +channel = "1.94.1" components = [ "rustfmt", "clippy" ] profile = "minimal" diff --git a/src/api/admin.rs b/src/api/admin.rs index badfaa3a..1546676f 100644 --- a/src/api/admin.rs +++ b/src/api/admin.rs @@ -32,7 +32,7 @@ use crate::{ mail, util::{ container_base_image, format_naive_datetime_local, get_active_web_release, get_display_size, - is_running_in_container, NumberOrString, + is_running_in_container, parse_experimental_client_feature_flags, FeatureFlagFilter, NumberOrString, }, CONFIG, VERSION, }; @@ -472,7 +472,7 @@ async fn deauth_user(user_id: UserId, _token: AdminToken, conn: DbConn, nt: Noti } Device::delete_all_by_user(&user.uuid, &conn).await?; - user.reset_security_stamp(); + user.reset_security_stamp(&conn).await?; user.save(&conn).await } @@ -480,14 +480,15 @@ async fn deauth_user(user_id: UserId, _token: AdminToken, conn: DbConn, nt: Noti #[post("/users//disable", format = "application/json")] async fn disable_user(user_id: UserId, _token: AdminToken, conn: DbConn, nt: Notify<'_>) -> EmptyResult { let mut user = get_user_or_404(&user_id, &conn).await?; - Device::delete_all_by_user(&user.uuid, &conn).await?; - user.reset_security_stamp(); + user.reset_security_stamp(&conn).await?; user.enabled = false; let save_result = user.save(&conn).await; nt.send_logout(&user, None, &conn).await; + Device::delete_all_by_user(&user.uuid, &conn).await?; + save_result } @@ -637,7 +638,6 @@ use cached::proc_macro::cached; /// Cache this function to prevent API call rate limit. Github only allows 60 requests per hour, and we use 3 here already /// It will cache this function for 600 seconds (10 minutes) which should prevent the exhaustion of the rate limit /// Any cache will be lost if Vaultwarden is restarted -use std::time::Duration; // Needed for cached #[cached(time = 600, sync_writes = "default")] async fn get_release_info(has_http_access: bool) -> (String, String, String) { // If the HTTP Check failed, do not even attempt to check for new versions since we were not able to connect with github.com anyway. @@ -734,6 +734,13 @@ async fn diagnostics(_token: AdminToken, ip_header: IpHeader, conn: DbConn) -> A let ip_header_name = &ip_header.0.unwrap_or_default(); + let invalid_feature_flags: Vec = parse_experimental_client_feature_flags( + &CONFIG.experimental_client_feature_flags(), + FeatureFlagFilter::InvalidOnly, + ) + .into_keys() + .collect(); + let diagnostics_json = json!({ "dns_resolved": dns_resolved, "current_release": VERSION, @@ -756,6 +763,7 @@ async fn diagnostics(_token: AdminToken, ip_header: IpHeader, conn: DbConn) -> A "db_version": get_sql_server_version(&conn).await, "admin_url": format!("{}/diagnostics", admin_url()), "overrides": &CONFIG.get_overrides().join(", "), + "invalid_feature_flags": invalid_feature_flags, "host_arch": env::consts::ARCH, "host_os": env::consts::OS, "tz_env": env::var("TZ").unwrap_or_default(), diff --git a/src/api/core/accounts.rs b/src/api/core/accounts.rs index 51ebbf03..8841c184 100644 --- a/src/api/core/accounts.rs +++ b/src/api/core/accounts.rs @@ -22,7 +22,7 @@ use crate::{ DbConn, }, mail, - util::{format_date, NumberOrString}, + util::{deser_opt_nonempty_str, format_date, NumberOrString}, CONFIG, }; @@ -33,7 +33,6 @@ use rocket::{ pub fn routes() -> Vec { routes![ - register, profile, put_profile, post_profile, @@ -107,7 +106,6 @@ pub struct RegisterData { name: Option, - #[allow(dead_code)] organization_user_id: Option, // Used only from the register/finish endpoint @@ -168,11 +166,6 @@ async fn is_email_2fa_required(member_id: Option, conn: &DbConn) - false } -#[post("/accounts/register", data = "")] -async fn register(data: Json, conn: DbConn) -> JsonResult { - _register(data, false, conn).await -} - pub async fn _register(data: Json, email_verification: bool, conn: DbConn) -> JsonResult { let mut data: RegisterData = data.into_inner(); let email = data.email.to_lowercase(); @@ -302,7 +295,7 @@ pub async fn _register(data: Json, email_verification: bool, conn: set_kdf_data(&mut user, &data.kdf)?; - user.set_password(&data.master_password_hash, Some(data.key), true, None); + user.set_password(&data.master_password_hash, Some(data.key), true, None, &conn).await?; user.password_hint = password_hint; // Add extra fields if present @@ -370,7 +363,9 @@ async fn post_set_password(data: Json, headers: Headers, conn: Some(data.key), false, Some(vec![String::from("revision_date")]), // We need to allow revision-date to use the old security_timestamp - ); + &conn, + ) + .await?; user.password_hint = password_hint; if let Some(keys) = data.keys { @@ -380,14 +375,12 @@ async fn post_set_password(data: Json, headers: Headers, conn: if let Some(identifier) = data.org_identifier { if identifier != crate::sso::FAKE_IDENTIFIER && identifier != crate::api::admin::FAKE_ADMIN_UUID { - let org = match Organization::find_by_uuid(&identifier.into(), &conn).await { - None => err!("Failed to retrieve the associated organization"), - Some(org) => org, + let Some(org) = Organization::find_by_uuid(&identifier.into(), &conn).await else { + err!("Failed to retrieve the associated organization") }; - let membership = match Membership::find_by_user_and_org(&user.uuid, &org.uuid, &conn).await { - None => err!("Failed to retrieve the invitation"), - Some(org) => org, + let Some(membership) = Membership::find_by_user_and_org(&user.uuid, &org.uuid, &conn).await else { + err!("Failed to retrieve the invitation") }; accept_org_invite(&user, membership, None, &conn).await?; @@ -538,14 +531,16 @@ async fn post_password(data: Json, headers: Headers, conn: DbCon String::from("get_public_keys"), String::from("get_api_webauthn"), ]), - ); + &conn, + ) + .await?; let save_result = user.save(&conn).await; // Prevent logging out the client where the user requested this endpoint from. // If you do logout the user it will causes issues at the client side. // Adding the device uuid will prevent this. - nt.send_logout(&user, Some(headers.device.uuid.clone()), &conn).await; + nt.send_logout(&user, Some(&headers.device), &conn).await; save_result } @@ -585,7 +580,6 @@ fn set_kdf_data(user: &mut User, data: &KDFData) -> EmptyResult { Ok(()) } -#[allow(dead_code)] #[derive(Deserialize)] #[serde(rename_all = "camelCase")] struct AuthenticationData { @@ -594,7 +588,6 @@ struct AuthenticationData { master_password_authentication_hash: String, } -#[allow(dead_code)] #[derive(Deserialize)] #[serde(rename_all = "camelCase")] struct UnlockData { @@ -603,11 +596,12 @@ struct UnlockData { master_key_wrapped_user_key: String, } -#[allow(dead_code)] #[derive(Deserialize)] #[serde(rename_all = "camelCase")] struct ChangeKdfData { + #[allow(dead_code)] new_master_password_hash: String, + #[allow(dead_code)] key: String, authentication_data: AuthenticationData, unlock_data: UnlockData, @@ -639,10 +633,12 @@ async fn post_kdf(data: Json, headers: Headers, conn: DbConn, nt: Some(data.unlock_data.master_key_wrapped_user_key), true, None, - ); + &conn, + ) + .await?; let save_result = user.save(&conn).await; - nt.send_logout(&user, Some(headers.device.uuid.clone()), &conn).await; + nt.send_logout(&user, Some(&headers.device), &conn).await; save_result } @@ -653,6 +649,7 @@ struct UpdateFolderData { // There is a bug in 2024.3.x which adds a `null` item. // To bypass this we allow a Option here, but skip it during the updates // See: https://github.com/bitwarden/clients/issues/8453 + #[serde(default, deserialize_with = "deser_opt_nonempty_str")] id: Option, name: String, } @@ -906,14 +903,16 @@ async fn post_rotatekey(data: Json, headers: Headers, conn: DbConn, nt: Some(data.account_unlock_data.master_password_unlock_data.master_key_encrypted_user_key), true, None, - ); + &conn, + ) + .await?; let save_result = user.save(&conn).await; // Prevent logging out the client where the user requested this endpoint from. // If you do logout the user it will causes issues at the client side. // Adding the device uuid will prevent this. - nt.send_logout(&user, Some(headers.device.uuid.clone()), &conn).await; + nt.send_logout(&user, Some(&headers.device), &conn).await; save_result } @@ -925,12 +924,13 @@ async fn post_sstamp(data: Json, headers: Headers, conn: DbCo data.validate(&user, true, &conn).await?; - Device::delete_all_by_user(&user.uuid, &conn).await?; - user.reset_security_stamp(); + user.reset_security_stamp(&conn).await?; let save_result = user.save(&conn).await; nt.send_logout(&user, None, &conn).await; + Device::delete_all_by_user(&user.uuid, &conn).await?; + save_result } @@ -1048,7 +1048,7 @@ async fn post_email(data: Json, headers: Headers, conn: DbConn, user.email_new = None; user.email_new_token = None; - user.set_password(&data.new_master_password_hash, Some(data.key), true, None); + user.set_password(&data.new_master_password_hash, Some(data.key), true, None, &conn).await?; let save_result = user.save(&conn).await; @@ -1260,7 +1260,7 @@ struct SecretVerificationRequest { pub async fn kdf_upgrade(user: &mut User, pwd_hash: &str, conn: &DbConn) -> ApiResult<()> { if user.password_iterations < CONFIG.password_iterations() { user.password_iterations = CONFIG.password_iterations(); - user.set_password(pwd_hash, None, false, None); + user.set_password(pwd_hash, None, false, None, conn).await?; if let Err(e) = user.save(conn).await { error!("Error updating user: {e:#?}"); @@ -1334,6 +1334,11 @@ impl<'r> FromRequest<'r> for KnownDevice { async fn from_request(req: &'r Request<'_>) -> Outcome { let email = if let Some(email_b64) = req.headers().get_one("X-Request-Email") { + // Bitwarden seems to send padded Base64 strings since 2026.2.1 + // Since these values are not streamed and Headers are always split by newlines + // we can safely ignore padding here and remove any '=' appended. + let email_b64 = email_b64.trim_end_matches('='); + let Ok(email_bytes) = data_encoding::BASE64URL_NOPAD.decode(email_b64.as_bytes()) else { return Outcome::Error((Status::BadRequest, "X-Request-Email value failed to decode as base64url")); }; diff --git a/src/api/core/ciphers.rs b/src/api/core/ciphers.rs index d5f244f4..6d4e1f41 100644 --- a/src/api/core/ciphers.rs +++ b/src/api/core/ciphers.rs @@ -11,10 +11,10 @@ use rocket::{ use serde_json::Value; use crate::auth::ClientVersion; -use crate::util::{save_temp_file, NumberOrString}; +use crate::util::{deser_opt_nonempty_str, save_temp_file, NumberOrString}; use crate::{ api::{self, core::log_event, EmptyResult, JsonResult, Notify, PasswordOrOtpData, UpdateType}, - auth::Headers, + auth::{Headers, OrgIdGuard, OwnerHeaders}, config::PathType, crypto, db::{ @@ -86,7 +86,8 @@ pub fn routes() -> Vec { restore_cipher_put_admin, restore_cipher_selected, restore_cipher_selected_admin, - delete_all, + purge_org_vault, + purge_personal_vault, move_cipher_selected, move_cipher_selected_put, put_collections2_update, @@ -247,6 +248,7 @@ pub struct CipherData { // Id is optional as it is included only in bulk share pub id: Option, // Folder id is not included in import + #[serde(default, deserialize_with = "deser_opt_nonempty_str")] pub folder_id: Option, // TODO: Some of these might appear all the time, no need for Option #[serde(alias = "organizationID")] @@ -296,6 +298,7 @@ pub struct CipherData { #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] pub struct PartialCipherData { + #[serde(default, deserialize_with = "deser_opt_nonempty_str")] folder_id: Option, favorite: bool, } @@ -425,7 +428,7 @@ pub async fn update_cipher_from_data( let transfer_cipher = cipher.organization_uuid.is_none() && data.organization_id.is_some(); if let Some(org_id) = data.organization_id { - match Membership::find_by_user_and_org(&headers.user.uuid, &org_id, conn).await { + match Membership::find_confirmed_by_user_and_org(&headers.user.uuid, &org_id, conn).await { None => err!("You don't have permission to add item to organization"), Some(member) => { if shared_to_collections.is_some() @@ -715,9 +718,13 @@ async fn put_cipher_partial( let data: PartialCipherData = data.into_inner(); let Some(cipher) = Cipher::find_by_uuid(&cipher_id, &conn).await else { - err!("Cipher doesn't exist") + err!("Cipher does not exist") }; + if !cipher.is_accessible_to_user(&headers.user.uuid, &conn).await { + err!("Cipher does not exist", "Cipher is not accessible for the current user") + } + if let Some(ref folder_id) = data.folder_id { if Folder::find_by_uuid_and_user(folder_id, &headers.user.uuid, &conn).await.is_none() { err!("Invalid folder", "Folder does not exist or belongs to another user"); @@ -1564,6 +1571,7 @@ async fn restore_cipher_selected( #[derive(Deserialize)] #[serde(rename_all = "camelCase")] struct MoveCipherData { + #[serde(default, deserialize_with = "deser_opt_nonempty_str")] folder_id: Option, ids: Vec, } @@ -1638,67 +1646,75 @@ struct OrganizationIdData { org_id: OrganizationId, } +// Use the OrgIdGuard here, to ensure there an organization id present. +// If there is no organization id present, it should be forwarded to purge_personal_vault. +// This guard needs to be the first argument, else OwnerHeaders will be triggered which will logout the user. #[post("/ciphers/purge?", data = "")] -async fn delete_all( - organization: Option, +async fn purge_org_vault( + _org_id_guard: OrgIdGuard, + organization: OrganizationIdData, data: Json, - headers: Headers, + headers: OwnerHeaders, conn: DbConn, nt: Notify<'_>, ) -> EmptyResult { + if organization.org_id != headers.org_id { + err!("Organization not found", "Organization id's do not match"); + } + let data: PasswordOrOtpData = data.into_inner(); - let mut user = headers.user; + let user = headers.user; data.validate(&user, true, &conn).await?; - match organization { - Some(org_data) => { - // Organization ID in query params, purging organization vault - match Membership::find_by_user_and_org(&user.uuid, &org_data.org_id, &conn).await { - None => err!("You don't have permission to purge the organization vault"), - Some(member) => { - if member.atype == MembershipType::Owner { - Cipher::delete_all_by_organization(&org_data.org_id, &conn).await?; - nt.send_user_update(UpdateType::SyncVault, &user, &headers.device.push_uuid, &conn).await; - - log_event( - EventType::OrganizationPurgedVault as i32, - &org_data.org_id, - &org_data.org_id, - &user.uuid, - headers.device.atype, - &headers.ip.ip, - &conn, - ) - .await; - - Ok(()) - } else { - err!("You don't have permission to purge the organization vault"); - } - } - } - } - None => { - // No organization ID in query params, purging user vault - // Delete ciphers and their attachments - for cipher in Cipher::find_owned_by_user(&user.uuid, &conn).await { - cipher.delete(&conn).await?; - } - - // Delete folders - for f in Folder::find_by_user(&user.uuid, &conn).await { - f.delete(&conn).await?; - } - - user.update_revision(&conn).await?; + match Membership::find_confirmed_by_user_and_org(&user.uuid, &organization.org_id, &conn).await { + Some(member) if member.atype == MembershipType::Owner => { + Cipher::delete_all_by_organization(&organization.org_id, &conn).await?; nt.send_user_update(UpdateType::SyncVault, &user, &headers.device.push_uuid, &conn).await; + log_event( + EventType::OrganizationPurgedVault as i32, + &organization.org_id, + &organization.org_id, + &user.uuid, + headers.device.atype, + &headers.ip.ip, + &conn, + ) + .await; + Ok(()) } + _ => err!("You don't have permission to purge the organization vault"), } } +#[post("/ciphers/purge", data = "")] +async fn purge_personal_vault( + data: Json, + headers: Headers, + conn: DbConn, + nt: Notify<'_>, +) -> EmptyResult { + let data: PasswordOrOtpData = data.into_inner(); + let mut user = headers.user; + + data.validate(&user, true, &conn).await?; + + for cipher in Cipher::find_owned_by_user(&user.uuid, &conn).await { + cipher.delete(&conn).await?; + } + + for f in Folder::find_by_user(&user.uuid, &conn).await { + f.delete(&conn).await?; + } + + user.update_revision(&conn).await?; + nt.send_user_update(UpdateType::SyncVault, &user, &headers.device.push_uuid, &conn).await; + + Ok(()) +} + #[derive(PartialEq)] pub enum CipherDeleteOptions { SoftSingle, @@ -1976,8 +1992,11 @@ impl CipherSyncData { } // Generate a HashMap with the Organization UUID as key and the Membership record - let members: HashMap = - Membership::find_by_user(user_id, conn).await.into_iter().map(|m| (m.org_uuid.clone(), m)).collect(); + let members: HashMap = Membership::find_confirmed_by_user(user_id, conn) + .await + .into_iter() + .map(|m| (m.org_uuid.clone(), m)) + .collect(); // Generate a HashMap with the User_Collections UUID as key and the CollectionUser record let user_collections: HashMap = CollectionUser::find_by_user(user_id, conn) diff --git a/src/api/core/emergency_access.rs b/src/api/core/emergency_access.rs index 1897f995..29a15c8d 100644 --- a/src/api/core/emergency_access.rs +++ b/src/api/core/emergency_access.rs @@ -653,7 +653,7 @@ async fn password_emergency_access( }; // change grantor_user password - grantor_user.set_password(new_master_password_hash, Some(data.key), true, None); + grantor_user.set_password(new_master_password_hash, Some(data.key), true, None, &conn).await?; grantor_user.save(&conn).await?; // Disable TwoFactor providers since they will otherwise block logins diff --git a/src/api/core/events.rs b/src/api/core/events.rs index 2f33a407..d1612255 100644 --- a/src/api/core/events.rs +++ b/src/api/core/events.rs @@ -240,7 +240,7 @@ async fn _log_user_event( ip: &IpAddr, conn: &DbConn, ) { - let memberships = Membership::find_by_user(user_id, conn).await; + let memberships = Membership::find_confirmed_by_user(user_id, conn).await; let mut events: Vec = Vec::with_capacity(memberships.len() + 1); // We need an event per org and one without an org // Upstream saves the event also without any org_id. diff --git a/src/api/core/folders.rs b/src/api/core/folders.rs index dc971a13..1b3fd714 100644 --- a/src/api/core/folders.rs +++ b/src/api/core/folders.rs @@ -8,6 +8,7 @@ use crate::{ models::{Folder, FolderId}, DbConn, }, + util::deser_opt_nonempty_str, }; pub fn routes() -> Vec { @@ -38,6 +39,7 @@ async fn get_folder(folder_id: FolderId, headers: Headers, conn: DbConn) -> Json #[serde(rename_all = "camelCase")] pub struct FolderData { pub name: String, + #[serde(default, deserialize_with = "deser_opt_nonempty_str")] pub id: Option, } diff --git a/src/api/core/mod.rs b/src/api/core/mod.rs index dc7f4628..038b9a6d 100644 --- a/src/api/core/mod.rs +++ b/src/api/core/mod.rs @@ -59,7 +59,8 @@ use crate::{ error::Error, http_client::make_http_request, mail, - util::parse_experimental_client_feature_flags, + util::{parse_experimental_client_feature_flags, FeatureFlagFilter}, + CONFIG, }; #[derive(Debug, Serialize, Deserialize)] @@ -136,7 +137,7 @@ async fn put_eq_domains(data: Json, headers: Headers, conn: DbC #[get("/hibp/breach?")] async fn hibp_breach(username: &str, _headers: Headers) -> JsonResult { let username: String = url::form_urlencoded::byte_serialize(username.as_bytes()).collect(); - if let Some(api_key) = crate::CONFIG.hibp_api_key() { + if let Some(api_key) = CONFIG.hibp_api_key() { let url = format!( "https://haveibeenpwned.com/api/v3/breachedaccount/{username}?truncateResponse=false&includeUnverified=false" ); @@ -197,19 +198,17 @@ fn get_api_webauthn(_headers: Headers) -> Json { #[get("/config")] fn config() -> Json { - let domain = crate::CONFIG.domain(); + let domain = CONFIG.domain(); // Official available feature flags can be found here: - // Server (v2025.6.2): https://github.com/bitwarden/server/blob/d094be3267f2030bd0dc62106bc6871cf82682f5/src/Core/Constants.cs#L103 - // Client (web-v2025.6.1): https://github.com/bitwarden/clients/blob/747c2fd6a1c348a57a76e4a7de8128466ffd3c01/libs/common/src/enums/feature-flag.enum.ts#L12 - // Android (v2025.6.0): https://github.com/bitwarden/android/blob/b5b022caaad33390c31b3021b2c1205925b0e1a2/app/src/main/kotlin/com/x8bit/bitwarden/data/platform/manager/model/FlagKey.kt#L22 - // iOS (v2025.6.0): https://github.com/bitwarden/ios/blob/ff06d9c6cc8da89f78f37f376495800201d7261a/BitwardenShared/Core/Platform/Models/Enum/FeatureFlag.swift#L7 - let mut feature_states = - parse_experimental_client_feature_flags(&crate::CONFIG.experimental_client_feature_flags()); - feature_states.insert("duo-redirect".to_string(), true); - feature_states.insert("email-verification".to_string(), true); - feature_states.insert("unauth-ui-refresh".to_string(), true); - feature_states.insert("enable-pm-flight-recorder".to_string(), true); - feature_states.insert("mobile-error-reporting".to_string(), true); + // Server (v2026.2.1): https://github.com/bitwarden/server/blob/0e42725d0837bd1c0dabd864ff621a579959744b/src/Core/Constants.cs#L135 + // Client (v2026.2.1): https://github.com/bitwarden/clients/blob/f96380c3138291a028bdd2c7a5fee540d5c98ba5/libs/common/src/enums/feature-flag.enum.ts#L12 + // Android (v2026.2.1): https://github.com/bitwarden/android/blob/6902c19c0093fa476bbf74ccaa70c9f14afbb82f/core/src/main/kotlin/com/bitwarden/core/data/manager/model/FlagKey.kt#L31 + // iOS (v2026.2.1): https://github.com/bitwarden/ios/blob/cdd9ba1770ca2ffc098d02d12cc3208e3a830454/BitwardenShared/Core/Platform/Models/Enum/FeatureFlag.swift#L7 + let feature_states = parse_experimental_client_feature_flags( + &CONFIG.experimental_client_feature_flags(), + FeatureFlagFilter::ValidOnly, + ); + // Add default feature_states here if needed, currently no features are needed by default. Json(json!({ // Note: The clients use this version to handle backwards compatibility concerns @@ -225,7 +224,7 @@ fn config() -> Json { "url": "https://github.com/dani-garcia/vaultwarden" }, "settings": { - "disableUserRegistration": crate::CONFIG.is_signup_disabled() + "disableUserRegistration": CONFIG.is_signup_disabled() }, "environment": { "vault": domain, @@ -278,7 +277,7 @@ async fn accept_org_invite( member.save(conn).await?; - if crate::CONFIG.mail_enabled() { + if CONFIG.mail_enabled() { let org = match Organization::find_by_uuid(&member.org_uuid, conn).await { Some(org) => org, None => err!("Organization not found."), diff --git a/src/api/core/organizations.rs b/src/api/core/organizations.rs index f173f90f..9a5079cb 100644 --- a/src/api/core/organizations.rs +++ b/src/api/core/organizations.rs @@ -36,12 +36,9 @@ pub fn routes() -> Vec { get_org_collections_details, get_org_collection_detail, get_collection_users, - put_collection_users, put_organization, post_organization, post_organization_collections, - delete_organization_collection_member, - post_organization_collection_delete_member, post_bulk_access_collections, post_organization_collection_update, put_organization_collection_update, @@ -64,28 +61,20 @@ pub fn routes() -> Vec { put_member, delete_member, bulk_delete_member, - post_delete_member, post_org_import, list_policies, list_policies_token, get_master_password_policy, get_policy, put_policy, - get_organization_tax, + put_policy_vnext, get_plans, - get_plans_all, - get_plans_tax_rates, - import, post_org_keys, get_organization_keys, get_organization_public_key, bulk_public_keys, - deactivate_member, - bulk_deactivate_members, revoke_member, bulk_revoke_members, - activate_member, - bulk_activate_members, restore_member, bulk_restore_members, get_groups, @@ -100,10 +89,6 @@ pub fn routes() -> Vec { bulk_delete_groups, get_group_members, put_group_members, - get_user_groups, - post_user_groups, - put_user_groups, - delete_group_member, post_delete_group_member, put_reset_password_enrollment, get_reset_password_details, @@ -146,6 +131,24 @@ struct FullCollectionData { external_id: Option, } +impl FullCollectionData { + pub async fn validate(&self, org_id: &OrganizationId, conn: &DbConn) -> EmptyResult { + let org_groups = Group::find_by_organization(org_id, conn).await; + let org_group_ids: HashSet<&GroupId> = org_groups.iter().map(|c| &c.uuid).collect(); + if let Some(e) = self.groups.iter().find(|g| !org_group_ids.contains(&g.id)) { + err!("Invalid group", format!("Group {} does not belong to organization {}!", e.id, org_id)) + } + + let org_memberships = Membership::find_by_org(org_id, conn).await; + let org_membership_ids: HashSet<&MembershipId> = org_memberships.iter().map(|m| &m.uuid).collect(); + if let Some(e) = self.users.iter().find(|m| !org_membership_ids.contains(&m.id)) { + err!("Invalid member", format!("Member {} does not belong to organization {}!", e.id, org_id)) + } + + Ok(()) + } +} + #[derive(Deserialize)] #[serde(rename_all = "camelCase")] struct CollectionGroupData { @@ -248,30 +251,30 @@ async fn post_delete_organization( } #[post("/organizations//leave")] -async fn leave_organization(org_id: OrganizationId, headers: Headers, conn: DbConn) -> EmptyResult { - match Membership::find_by_user_and_org(&headers.user.uuid, &org_id, &conn).await { - None => err!("User not part of organization"), - Some(member) => { - if member.atype == MembershipType::Owner - && Membership::count_confirmed_by_org_and_type(&org_id, MembershipType::Owner, &conn).await <= 1 - { - err!("The last owner can't leave") - } - - log_event( - EventType::OrganizationUserLeft as i32, - &member.uuid, - &org_id, - &headers.user.uuid, - headers.device.atype, - &headers.ip.ip, - &conn, - ) - .await; +async fn leave_organization(org_id: OrganizationId, headers: OrgMemberHeaders, conn: DbConn) -> EmptyResult { + if headers.membership.status != MembershipStatus::Confirmed as i32 { + err!("You need to be a Member of the Organization to call this endpoint") + } + let membership = headers.membership; - member.delete(&conn).await - } + if membership.atype == MembershipType::Owner + && Membership::count_confirmed_by_org_and_type(&org_id, MembershipType::Owner, &conn).await <= 1 + { + err!("The last owner can't leave") } + + log_event( + EventType::OrganizationUserLeft as i32, + &membership.uuid, + &org_id, + &headers.user.uuid, + headers.device.atype, + &headers.ip.ip, + &conn, + ) + .await; + + membership.delete(&conn).await } #[get("/organizations/")] @@ -380,6 +383,11 @@ async fn get_org_collections(org_id: OrganizationId, headers: ManagerHeadersLoos if org_id != headers.membership.org_uuid { err!("Organization not found", "Organization id's do not match"); } + + if !headers.membership.has_full_access() { + err_code!("Resource not found.", "User does not have full access", rocket::http::Status::NotFound.code); + } + Ok(Json(json!({ "data": _get_org_collections(&org_id, &conn).await, "object": "list", @@ -392,7 +400,6 @@ async fn get_org_collections_details(org_id: OrganizationId, headers: ManagerHea if org_id != headers.membership.org_uuid { err!("Organization not found", "Organization id's do not match"); } - let mut data = Vec::new(); let Some(member) = Membership::find_by_user_and_org(&headers.user.uuid, &org_id, &conn).await else { err!("User is not part of organization") @@ -406,7 +413,7 @@ async fn get_org_collections_details(org_id: OrganizationId, headers: ManagerHea Membership::find_confirmed_by_org(&org_id, &conn).await.into_iter().map(|m| (m.uuid, m.atype)).collect(); // check if current user has full access to the organization (either directly or via any group) - let has_full_access_to_org = member.access_all + let has_full_access_to_org = member.has_full_access() || (CONFIG.org_groups_enabled() && GroupUser::has_full_access_by_member(&org_id, &member.uuid, &conn).await); // Get all admins, owners and managers who can manage/access all @@ -424,6 +431,7 @@ async fn get_org_collections_details(org_id: OrganizationId, headers: ManagerHea }) .collect(); + let mut data = Vec::new(); for col in Collection::find_by_organization(&org_id, &conn).await { // check whether the current user has access to the given collection let assigned = has_full_access_to_org @@ -431,6 +439,11 @@ async fn get_org_collections_details(org_id: OrganizationId, headers: ManagerHea || (CONFIG.org_groups_enabled() && GroupUser::has_access_to_collection_by_member(&col.uuid, &member.uuid, &conn).await); + // If the user is a manager, and is not assigned to this collection, skip this and continue with the next collection + if !assigned { + continue; + } + // get the users assigned directly to the given collection let mut users: Vec = col_users .iter() @@ -485,12 +498,13 @@ async fn post_organization_collections( err!("Organization not found", "Organization id's do not match"); } let data: FullCollectionData = data.into_inner(); + data.validate(&org_id, &conn).await?; - let Some(org) = Organization::find_by_uuid(&org_id, &conn).await else { - err!("Can't find organization details") - }; + if headers.membership.atype == MembershipType::Manager && !headers.membership.access_all { + err!("You don't have permission to create collections") + } - let collection = Collection::new(org.uuid, data.name, data.external_id); + let collection = Collection::new(org_id.clone(), data.name, data.external_id); collection.save(&conn).await?; log_event( @@ -506,7 +520,7 @@ async fn post_organization_collections( for group in data.groups { CollectionGroup::new(collection.uuid.clone(), group.id, group.read_only, group.hide_passwords, group.manage) - .save(&conn) + .save(&org_id, &conn) .await?; } @@ -530,10 +544,6 @@ async fn post_organization_collections( .await?; } - if headers.membership.atype == MembershipType::Manager && !headers.membership.access_all { - CollectionUser::save(&headers.membership.user_uuid, &collection.uuid, false, false, false, &conn).await?; - } - Ok(Json(collection.to_json_details(&headers.membership.user_uuid, None, &conn).await)) } @@ -566,6 +576,10 @@ async fn post_bulk_access_collections( err!("Collection not found") }; + if !collection.is_manageable_by_user(&headers.membership.user_uuid, &conn).await { + err!("Collection not found", "The current user isn't a manager for this collection") + } + // update collection modification date collection.save(&conn).await?; @@ -580,10 +594,10 @@ async fn post_bulk_access_collections( ) .await; - CollectionGroup::delete_all_by_collection(&col_id, &conn).await?; + CollectionGroup::delete_all_by_collection(&col_id, &org_id, &conn).await?; for group in &data.groups { CollectionGroup::new(col_id.clone(), group.id.clone(), group.read_only, group.hide_passwords, group.manage) - .save(&conn) + .save(&org_id, &conn) .await?; } @@ -628,6 +642,7 @@ async fn post_organization_collection_update( err!("Organization not found", "Organization id's do not match"); } let data: FullCollectionData = data.into_inner(); + data.validate(&org_id, &conn).await?; if Organization::find_by_uuid(&org_id, &conn).await.is_none() { err!("Can't find organization details") @@ -656,11 +671,11 @@ async fn post_organization_collection_update( ) .await; - CollectionGroup::delete_all_by_collection(&col_id, &conn).await?; + CollectionGroup::delete_all_by_collection(&col_id, &org_id, &conn).await?; for group in data.groups { CollectionGroup::new(col_id.clone(), group.id, group.read_only, group.hide_passwords, group.manage) - .save(&conn) + .save(&org_id, &conn) .await?; } @@ -682,43 +697,6 @@ async fn post_organization_collection_update( Ok(Json(collection.to_json_details(&headers.user.uuid, None, &conn).await)) } -#[delete("/organizations//collections//user/")] -async fn delete_organization_collection_member( - org_id: OrganizationId, - col_id: CollectionId, - member_id: MembershipId, - headers: AdminHeaders, - conn: DbConn, -) -> EmptyResult { - if org_id != headers.org_id { - err!("Organization not found", "Organization id's do not match"); - } - let Some(collection) = Collection::find_by_uuid_and_org(&col_id, &org_id, &conn).await else { - err!("Collection not found", "Collection does not exist or does not belong to this organization") - }; - - match Membership::find_by_uuid_and_org(&member_id, &org_id, &conn).await { - None => err!("User not found in organization"), - Some(member) => { - match CollectionUser::find_by_collection_and_user(&collection.uuid, &member.user_uuid, &conn).await { - None => err!("User not assigned to collection"), - Some(col_user) => col_user.delete(&conn).await, - } - } - } -} - -#[post("/organizations//collections//delete-user/")] -async fn post_organization_collection_delete_member( - org_id: OrganizationId, - col_id: CollectionId, - member_id: MembershipId, - headers: AdminHeaders, - conn: DbConn, -) -> EmptyResult { - delete_organization_collection_member(org_id, col_id, member_id, headers, conn).await -} - async fn _delete_organization_collection( org_id: &OrganizationId, col_id: &CollectionId, @@ -887,41 +865,6 @@ async fn get_collection_users( Ok(Json(json!(member_list))) } -#[put("/organizations//collections//users", data = "")] -async fn put_collection_users( - org_id: OrganizationId, - col_id: CollectionId, - data: Json>, - headers: ManagerHeaders, - conn: DbConn, -) -> EmptyResult { - if org_id != headers.org_id { - err!("Organization not found", "Organization id's do not match"); - } - // Get org and collection, check that collection is from org - if Collection::find_by_uuid_and_org(&col_id, &org_id, &conn).await.is_none() { - err!("Collection not found in Organization") - } - - // Delete all the user-collections - CollectionUser::delete_all_by_collection(&col_id, &conn).await?; - - // And then add all the received ones (except if the user has access_all) - for d in data.iter() { - let Some(user) = Membership::find_by_uuid_and_org(&d.id, &org_id, &conn).await else { - err!("User is not part of organization") - }; - - if user.access_all { - continue; - } - - CollectionUser::save(&user.user_uuid, &col_id, d.read_only, d.hide_passwords, d.manage, &conn).await?; - } - - Ok(()) -} - #[derive(FromForm)] struct OrgIdData { #[field(name = "organizationId")] @@ -1076,6 +1019,24 @@ struct InviteData { permissions: HashMap, } +impl InviteData { + async fn validate(&self, org_id: &OrganizationId, conn: &DbConn) -> EmptyResult { + let org_collections = Collection::find_by_organization(org_id, conn).await; + let org_collection_ids: HashSet<&CollectionId> = org_collections.iter().map(|c| &c.uuid).collect(); + if let Some(e) = self.collections.iter().flatten().find(|c| !org_collection_ids.contains(&c.id)) { + err!("Invalid collection", format!("Collection {} does not belong to organization {}!", e.id, org_id)) + } + + let org_groups = Group::find_by_organization(org_id, conn).await; + let org_group_ids: HashSet<&GroupId> = org_groups.iter().map(|c| &c.uuid).collect(); + if let Some(e) = self.groups.iter().find(|g| !org_group_ids.contains(g)) { + err!("Invalid group", format!("Group {} does not belong to organization {}!", e, org_id)) + } + + Ok(()) + } +} + #[post("/organizations//users/invite", data = "")] async fn send_invite( org_id: OrganizationId, @@ -1087,6 +1048,7 @@ async fn send_invite( err!("Organization not found", "Organization id's do not match"); } let data: InviteData = data.into_inner(); + data.validate(&org_id, &conn).await?; // HACK: We need the raw user-type to be sure custom role is selected to determine the access_all permission // The from_str() will convert the custom role type into a manager role type @@ -1346,20 +1308,20 @@ async fn accept_invite( // skip invitation logic when we were invited via the /admin panel if **member_id != FAKE_ADMIN_UUID { - let Some(mut member) = Membership::find_by_uuid_and_org(member_id, &claims.org_id, &conn).await else { + let Some(mut membership) = Membership::find_by_uuid_and_org(member_id, &claims.org_id, &conn).await else { err!("Error accepting the invitation") }; - let reset_password_key = match OrgPolicy::org_is_reset_password_auto_enroll(&member.org_uuid, &conn).await { + let reset_password_key = match OrgPolicy::org_is_reset_password_auto_enroll(&membership.org_uuid, &conn).await { true if data.reset_password_key.is_none() => err!("Reset password key is required, but not provided."), true => data.reset_password_key, false => None, }; // In case the user was invited before the mail was saved in db. - member.invited_by_email = member.invited_by_email.or(claims.invited_by_email); + membership.invited_by_email = membership.invited_by_email.or(claims.invited_by_email); - accept_org_invite(&headers.user, member, reset_password_key, &conn).await?; + accept_org_invite(&headers.user, membership, reset_password_key, &conn).await?; } else if CONFIG.mail_enabled() { // User was invited from /admin, so they are automatically confirmed let org_name = CONFIG.invitation_org_name(); @@ -1593,9 +1555,8 @@ async fn edit_member( && data.permissions.get("deleteAnyCollection") == Some(&json!(true)) && data.permissions.get("createNewCollections") == Some(&json!(true))); - let mut member_to_edit = match Membership::find_by_uuid_and_org(&member_id, &org_id, &conn).await { - Some(member) => member, - None => err!("The specified user isn't member of the organization"), + let Some(mut member_to_edit) = Membership::find_by_uuid_and_org(&member_id, &org_id, &conn).await else { + err!("The specified user isn't member of the organization") }; if new_type != member_to_edit.atype @@ -1719,17 +1680,6 @@ async fn delete_member( _delete_member(&org_id, &member_id, &headers, &conn, &nt).await } -#[post("/organizations//users//delete")] -async fn post_delete_member( - org_id: OrganizationId, - member_id: MembershipId, - headers: AdminHeaders, - conn: DbConn, - nt: Notify<'_>, -) -> EmptyResult { - _delete_member(&org_id, &member_id, &headers, &conn, &nt).await -} - async fn _delete_member( org_id: &OrganizationId, member_id: &MembershipId, @@ -1923,7 +1873,6 @@ async fn post_org_import( #[derive(Deserialize)] #[serde(rename_all = "camelCase")] -#[allow(dead_code)] struct BulkCollectionsData { organization_id: OrganizationId, cipher_ids: Vec, @@ -1937,6 +1886,10 @@ struct BulkCollectionsData { async fn post_bulk_collections(data: Json, headers: Headers, conn: DbConn) -> EmptyResult { let data: BulkCollectionsData = data.into_inner(); + if Membership::find_confirmed_by_user_and_org(&headers.user.uuid, &data.organization_id, &conn).await.is_none() { + err!("You need to be a Member of the Organization to call this endpoint") + } + // Get all the collection available to the user in one query // Also filter based upon the provided collections let user_collections: HashMap = @@ -2025,7 +1978,7 @@ async fn list_policies_token(org_id: OrganizationId, token: &str, conn: DbConn) // Called during the SSO enrollment. // Return the org policy if it exists, otherwise use the default one. #[get("/organizations//policies/master-password", rank = 1)] -async fn get_master_password_policy(org_id: OrganizationId, _headers: Headers, conn: DbConn) -> JsonResult { +async fn get_master_password_policy(org_id: OrganizationId, _headers: OrgMemberHeaders, conn: DbConn) -> JsonResult { let policy = OrgPolicy::find_by_org_and_type(&org_id, OrgPolicyType::MasterPassword, &conn).await.unwrap_or_else(|| { let (enabled, data) = match CONFIG.sso_master_password_policy_value() { @@ -2182,14 +2135,26 @@ async fn put_policy( Ok(Json(policy.to_json())) } -#[allow(unused_variables)] -#[get("/organizations//tax")] -fn get_organization_tax(org_id: OrganizationId, _headers: Headers) -> Json { - // Prevent a 404 error, which also causes Javascript errors. - // Upstream sends "Only allowed when not self hosted." As an error message. - // If we do the same it will also output this to the log, which is overkill. - // An empty list/data also works fine. - Json(_empty_data_json()) +#[derive(Deserialize)] +struct PolicyDataVnext { + policy: PolicyData, + // Ignore metadata for now as we do not yet support this + // "metadata": { + // "defaultUserCollectionName": "2.xx|xx==|xx=" + // } +} + +#[put("/organizations//policies//vnext", data = "")] +async fn put_policy_vnext( + org_id: OrganizationId, + pol_type: i32, + data: Json, + headers: AdminHeaders, + conn: DbConn, +) -> JsonResult { + let data: PolicyDataVnext = data.into_inner(); + let policy: PolicyData = data.policy; + put_policy(org_id, pol_type, Json(policy), headers, conn).await } #[get("/plans")] @@ -2220,25 +2185,14 @@ fn get_plans() -> Json { })) } -#[get("/plans/all")] -fn get_plans_all() -> Json { - get_plans() -} - -#[get("/plans/sales-tax-rates")] -fn get_plans_tax_rates(_headers: Headers) -> Json { - // Prevent a 404 error, which also causes Javascript errors. - Json(_empty_data_json()) -} - #[get("/organizations/<_org_id>/billing/metadata")] -fn get_billing_metadata(_org_id: OrganizationId, _headers: Headers) -> Json { +fn get_billing_metadata(_org_id: OrganizationId, _headers: OrgMemberHeaders) -> Json { // Prevent a 404 error, which also causes Javascript errors. Json(_empty_data_json()) } #[get("/organizations/<_org_id>/billing/vnext/warnings")] -fn get_billing_warnings(_org_id: OrganizationId, _headers: Headers) -> Json { +fn get_billing_warnings(_org_id: OrganizationId, _headers: OrgMemberHeaders) -> Json { Json(json!({ "freeTrial":null, "inactiveSubscription":null, @@ -2255,174 +2209,12 @@ fn _empty_data_json() -> Value { }) } -#[derive(Deserialize, Debug)] -#[serde(rename_all = "camelCase")] -struct OrgImportGroupData { - #[allow(dead_code)] - name: String, // "GroupName" - #[allow(dead_code)] - external_id: String, // "cn=GroupName,ou=Groups,dc=example,dc=com" - #[allow(dead_code)] - users: Vec, // ["uid=user,ou=People,dc=example,dc=com"] -} - -#[derive(Deserialize, Debug)] -#[serde(rename_all = "camelCase")] -struct OrgImportUserData { - email: String, // "user@maildomain.net" - #[allow(dead_code)] - external_id: String, // "uid=user,ou=People,dc=example,dc=com" - deleted: bool, -} - -#[derive(Deserialize, Debug)] -#[serde(rename_all = "camelCase")] -struct OrgImportData { - #[allow(dead_code)] - groups: Vec, - overwrite_existing: bool, - users: Vec, -} - -/// This function seems to be deprecated -/// It is only used with older directory connectors -/// TODO: Cleanup Tech debt -#[post("/organizations//import", data = "")] -async fn import(org_id: OrganizationId, data: Json, headers: Headers, conn: DbConn) -> EmptyResult { - let data = data.into_inner(); - - // TODO: Currently we aren't storing the externalId's anywhere, so we also don't have a way - // to differentiate between auto-imported users and manually added ones. - // This means that this endpoint can end up removing users that were added manually by an admin, - // as opposed to upstream which only removes auto-imported users. - - // User needs to be admin or owner to use the Directory Connector - match Membership::find_by_user_and_org(&headers.user.uuid, &org_id, &conn).await { - Some(member) if member.atype >= MembershipType::Admin => { /* Okay, nothing to do */ } - Some(_) => err!("User has insufficient permissions to use Directory Connector"), - None => err!("User not part of organization"), - }; - - for user_data in &data.users { - if user_data.deleted { - // If user is marked for deletion and it exists, delete it - if let Some(member) = Membership::find_by_email_and_org(&user_data.email, &org_id, &conn).await { - log_event( - EventType::OrganizationUserRemoved as i32, - &member.uuid, - &org_id, - &headers.user.uuid, - headers.device.atype, - &headers.ip.ip, - &conn, - ) - .await; - - member.delete(&conn).await?; - } - - // If user is not part of the organization, but it exists - } else if Membership::find_by_email_and_org(&user_data.email, &org_id, &conn).await.is_none() { - if let Some(user) = User::find_by_mail(&user_data.email, &conn).await { - let member_status = if CONFIG.mail_enabled() { - MembershipStatus::Invited as i32 - } else { - MembershipStatus::Accepted as i32 // Automatically mark user as accepted if no email invites - }; - - let mut new_member = - Membership::new(user.uuid.clone(), org_id.clone(), Some(headers.user.email.clone())); - new_member.access_all = false; - new_member.atype = MembershipType::User as i32; - new_member.status = member_status; - - if CONFIG.mail_enabled() { - let org_name = match Organization::find_by_uuid(&org_id, &conn).await { - Some(org) => org.name, - None => err!("Error looking up organization"), - }; - - mail::send_invite( - &user, - org_id.clone(), - new_member.uuid.clone(), - &org_name, - Some(headers.user.email.clone()), - ) - .await?; - } - - // Save the member after sending an email - // If sending fails the member will not be saved to the database, and will not result in the admin needing to reinvite the users manually - new_member.save(&conn).await?; - - log_event( - EventType::OrganizationUserInvited as i32, - &new_member.uuid, - &org_id, - &headers.user.uuid, - headers.device.atype, - &headers.ip.ip, - &conn, - ) - .await; - } - } - } - - // If this flag is enabled, any user that isn't provided in the Users list will be removed (by default they will be kept unless they have Deleted == true) - if data.overwrite_existing { - for member in Membership::find_by_org_and_type(&org_id, MembershipType::User, &conn).await { - if let Some(user_email) = User::find_by_uuid(&member.user_uuid, &conn).await.map(|u| u.email) { - if !data.users.iter().any(|u| u.email == user_email) { - log_event( - EventType::OrganizationUserRemoved as i32, - &member.uuid, - &org_id, - &headers.user.uuid, - headers.device.atype, - &headers.ip.ip, - &conn, - ) - .await; - - member.delete(&conn).await?; - } - } - } - } - - Ok(()) -} - -// Pre web-vault v2022.9.x endpoint -#[put("/organizations//users//deactivate")] -async fn deactivate_member( - org_id: OrganizationId, - member_id: MembershipId, - headers: AdminHeaders, - conn: DbConn, -) -> EmptyResult { - _revoke_member(&org_id, &member_id, &headers, &conn).await -} - #[derive(Deserialize, Debug)] #[serde(rename_all = "camelCase")] struct BulkRevokeMembershipIds { ids: Option>, } -// Pre web-vault v2022.9.x endpoint -#[put("/organizations//users/deactivate", data = "")] -async fn bulk_deactivate_members( - org_id: OrganizationId, - data: Json, - headers: AdminHeaders, - conn: DbConn, -) -> JsonResult { - bulk_revoke_members(org_id, data, headers, conn).await -} - #[put("/organizations//users//revoke")] async fn revoke_member( org_id: OrganizationId, @@ -2516,28 +2308,6 @@ async fn _revoke_member( Ok(()) } -// Pre web-vault v2022.9.x endpoint -#[put("/organizations//users//activate")] -async fn activate_member( - org_id: OrganizationId, - member_id: MembershipId, - headers: AdminHeaders, - conn: DbConn, -) -> EmptyResult { - _restore_member(&org_id, &member_id, &headers, &conn).await -} - -// Pre web-vault v2022.9.x endpoint -#[put("/organizations//users/activate", data = "")] -async fn bulk_activate_members( - org_id: OrganizationId, - data: Json, - headers: AdminHeaders, - conn: DbConn, -) -> JsonResult { - bulk_restore_members(org_id, data, headers, conn).await -} - #[put("/organizations//users//restore")] async fn restore_member( org_id: OrganizationId, @@ -2694,6 +2464,23 @@ impl GroupRequest { group } + + /// Validate if all the collections and members belong to the provided organization + pub async fn validate(&self, org_id: &OrganizationId, conn: &DbConn) -> EmptyResult { + let org_collections = Collection::find_by_organization(org_id, conn).await; + let org_collection_ids: HashSet<&CollectionId> = org_collections.iter().map(|c| &c.uuid).collect(); + if let Some(e) = self.collections.iter().find(|c| !org_collection_ids.contains(&c.id)) { + err!("Invalid collection", format!("Collection {} does not belong to organization {}!", e.id, org_id)) + } + + let org_memberships = Membership::find_by_org(org_id, conn).await; + let org_membership_ids: HashSet<&MembershipId> = org_memberships.iter().map(|m| &m.uuid).collect(); + if let Some(e) = self.users.iter().find(|m| !org_membership_ids.contains(m)) { + err!("Invalid member", format!("Member {} does not belong to organization {}!", e, org_id)) + } + + Ok(()) + } } #[derive(Deserialize, Serialize)] @@ -2737,6 +2524,8 @@ async fn post_groups( } let group_request = data.into_inner(); + group_request.validate(&org_id, &conn).await?; + let group = group_request.to_group(&org_id); log_event( @@ -2773,10 +2562,12 @@ async fn put_group( }; let group_request = data.into_inner(); + group_request.validate(&org_id, &conn).await?; + let updated_group = group_request.update_group(group); - CollectionGroup::delete_all_by_group(&group_id, &conn).await?; - GroupUser::delete_all_by_group(&group_id, &conn).await?; + CollectionGroup::delete_all_by_group(&group_id, &org_id, &conn).await?; + GroupUser::delete_all_by_group(&group_id, &org_id, &conn).await?; log_event( EventType::GroupUpdated as i32, @@ -2804,7 +2595,7 @@ async fn add_update_group( for col_selection in collections { let mut collection_group = col_selection.to_collection_group(group.uuid.clone()); - collection_group.save(conn).await?; + collection_group.save(&org_id, conn).await?; } for assigned_member in members { @@ -2897,7 +2688,7 @@ async fn _delete_group( ) .await; - group.delete(conn).await + group.delete(org_id, conn).await } #[delete("/organizations//groups", data = "")] @@ -2956,7 +2747,7 @@ async fn get_group_members( err!("Group could not be found!", "Group uuid is invalid or does not belong to the organization") }; - let group_members: Vec = GroupUser::find_by_group(&group_id, &conn) + let group_members: Vec = GroupUser::find_by_group(&group_id, &org_id, &conn) .await .iter() .map(|entry| entry.users_organizations_uuid.clone()) @@ -2984,9 +2775,15 @@ async fn put_group_members( err!("Group could not be found!", "Group uuid is invalid or does not belong to the organization") }; - GroupUser::delete_all_by_group(&group_id, &conn).await?; - let assigned_members = data.into_inner(); + + let org_memberships = Membership::find_by_org(&org_id, &conn).await; + let org_membership_ids: HashSet<&MembershipId> = org_memberships.iter().map(|m| &m.uuid).collect(); + if let Some(e) = assigned_members.iter().find(|m| !org_membership_ids.contains(m)) { + err!("Invalid member", format!("Member {} does not belong to organization {}!", e, org_id)) + } + + GroupUser::delete_all_by_group(&group_id, &org_id, &conn).await?; for assigned_member in assigned_members { let mut user_entry = GroupUser::new(group_id.clone(), assigned_member.clone()); user_entry.save(&conn).await?; @@ -3006,88 +2803,6 @@ async fn put_group_members( Ok(()) } -#[get("/organizations//users//groups")] -async fn get_user_groups( - org_id: OrganizationId, - member_id: MembershipId, - headers: AdminHeaders, - conn: DbConn, -) -> JsonResult { - if org_id != headers.org_id { - err!("Organization not found", "Organization id's do not match"); - } - if !CONFIG.org_groups_enabled() { - err!("Group support is disabled"); - } - - if Membership::find_by_uuid_and_org(&member_id, &org_id, &conn).await.is_none() { - err!("User could not be found!") - }; - - let user_groups: Vec = - GroupUser::find_by_member(&member_id, &conn).await.iter().map(|entry| entry.groups_uuid.clone()).collect(); - - Ok(Json(json!(user_groups))) -} - -#[derive(Deserialize)] -#[serde(rename_all = "camelCase")] -struct OrganizationUserUpdateGroupsRequest { - group_ids: Vec, -} - -#[post("/organizations//users//groups", data = "")] -async fn post_user_groups( - org_id: OrganizationId, - member_id: MembershipId, - data: Json, - headers: AdminHeaders, - conn: DbConn, -) -> EmptyResult { - put_user_groups(org_id, member_id, data, headers, conn).await -} - -#[put("/organizations//users//groups", data = "")] -async fn put_user_groups( - org_id: OrganizationId, - member_id: MembershipId, - data: Json, - headers: AdminHeaders, - conn: DbConn, -) -> EmptyResult { - if org_id != headers.org_id { - err!("Organization not found", "Organization id's do not match"); - } - if !CONFIG.org_groups_enabled() { - err!("Group support is disabled"); - } - - if Membership::find_by_uuid_and_org(&member_id, &org_id, &conn).await.is_none() { - err!("User could not be found or does not belong to the organization."); - } - - GroupUser::delete_all_by_member(&member_id, &conn).await?; - - let assigned_group_ids = data.into_inner(); - for assigned_group_id in assigned_group_ids.group_ids { - let mut group_user = GroupUser::new(assigned_group_id.clone(), member_id.clone()); - group_user.save(&conn).await?; - } - - log_event( - EventType::OrganizationUserUpdatedGroups as i32, - &member_id, - &org_id, - &headers.user.uuid, - headers.device.atype, - &headers.ip.ip, - &conn, - ) - .await; - - Ok(()) -} - #[post("/organizations//groups//delete-user/")] async fn post_delete_group_member( org_id: OrganizationId, @@ -3095,17 +2810,6 @@ async fn post_delete_group_member( member_id: MembershipId, headers: AdminHeaders, conn: DbConn, -) -> EmptyResult { - delete_group_member(org_id, group_id, member_id, headers, conn).await -} - -#[delete("/organizations//groups//users/")] -async fn delete_group_member( - org_id: OrganizationId, - group_id: GroupId, - member_id: MembershipId, - headers: AdminHeaders, - conn: DbConn, ) -> EmptyResult { if org_id != headers.org_id { err!("Organization not found", "Organization id's do not match"); @@ -3218,7 +2922,8 @@ async fn put_reset_password( let reset_request = data.into_inner(); let mut user = user; - user.set_password(reset_request.new_master_password_hash.as_str(), Some(reset_request.key), true, None); + user.set_password(reset_request.new_master_password_hash.as_str(), Some(reset_request.key), true, None, &conn) + .await?; user.save(&conn).await?; nt.send_logout(&user, None, &conn).await; @@ -3310,15 +3015,20 @@ async fn check_reset_password_applicable(org_id: &OrganizationId, conn: &DbConn) Ok(()) } -#[put("/organizations//users//reset-password-enrollment", data = "")] +#[put("/organizations//users//reset-password-enrollment", data = "")] async fn put_reset_password_enrollment( org_id: OrganizationId, - member_id: MembershipId, - headers: Headers, + user_id: UserId, + headers: OrgMemberHeaders, data: Json, conn: DbConn, ) -> EmptyResult { - let Some(mut member) = Membership::find_by_user_and_org(&headers.user.uuid, &org_id, &conn).await else { + if user_id != headers.user.uuid { + err!("User to enroll isn't member of required organization", "The user_id and acting user do not match"); + } + + let Some(mut membership) = Membership::find_confirmed_by_user_and_org(&headers.user.uuid, &org_id, &conn).await + else { err!("User to enroll isn't member of required organization") }; @@ -3345,16 +3055,17 @@ async fn put_reset_password_enrollment( .await?; } - member.reset_password_key = reset_password_key; - member.save(&conn).await?; + membership.reset_password_key = reset_password_key; + membership.save(&conn).await?; - let log_id = if member.reset_password_key.is_some() { + let event_type = if membership.reset_password_key.is_some() { EventType::OrganizationUserResetPasswordEnroll as i32 } else { EventType::OrganizationUserResetPasswordWithdraw as i32 }; - log_event(log_id, &member_id, &org_id, &headers.user.uuid, headers.device.atype, &headers.ip.ip, &conn).await; + log_event(event_type, &membership.uuid, &org_id, &headers.user.uuid, headers.device.atype, &headers.ip.ip, &conn) + .await; Ok(()) } diff --git a/src/api/core/public.rs b/src/api/core/public.rs index 6a317b96..d757d953 100644 --- a/src/api/core/public.rs +++ b/src/api/core/public.rs @@ -156,7 +156,7 @@ async fn ldap_import(data: Json, token: PublicToken, conn: DbConn } }; - GroupUser::delete_all_by_group(&group_uuid, &conn).await?; + GroupUser::delete_all_by_group(&group_uuid, &org_id, &conn).await?; for ext_id in &group_data.member_external_ids { if let Some(member) = Membership::find_by_external_id_and_org(ext_id, &org_id, &conn).await { diff --git a/src/api/core/two_factor/email.rs b/src/api/core/two_factor/email.rs index 25218069..e7d1aed2 100644 --- a/src/api/core/two_factor/email.rs +++ b/src/api/core/two_factor/email.rs @@ -44,6 +44,9 @@ async fn send_email_login(data: Json, client_headers: Client err!("Email 2FA is disabled") } + // Ratelimit the login + crate::ratelimit::check_limit_login(&client_headers.ip.ip)?; + // Get the user let email = match &data.email { Some(email) if !email.is_empty() => Some(email), diff --git a/src/api/core/two_factor/mod.rs b/src/api/core/two_factor/mod.rs index dfaae77a..3a503a23 100644 --- a/src/api/core/two_factor/mod.rs +++ b/src/api/core/two_factor/mod.rs @@ -1,7 +1,9 @@ use chrono::{TimeDelta, Utc}; use data_encoding::BASE32; +use num_traits::FromPrimitive; use rocket::serde::json::Json; use rocket::Route; +use serde::Deserialize; use serde_json::Value; use crate::{ @@ -9,12 +11,12 @@ use crate::{ core::{log_event, log_user_event}, EmptyResult, JsonResult, PasswordOrOtpData, }, - auth::{ClientHeaders, Headers}, + auth::Headers, crypto, db::{ models::{ DeviceType, EventType, Membership, MembershipType, OrgPolicyType, Organization, OrganizationId, TwoFactor, - TwoFactorIncomplete, User, UserId, + TwoFactorIncomplete, TwoFactorType, User, UserId, }, DbConn, DbPool, }, @@ -31,11 +33,47 @@ pub mod protected_actions; pub mod webauthn; pub mod yubikey; +fn has_global_duo_credentials() -> bool { + CONFIG._enable_duo() && CONFIG.duo_host().is_some() && CONFIG.duo_ikey().is_some() && CONFIG.duo_skey().is_some() +} + +pub fn is_twofactor_provider_usable(provider_type: TwoFactorType, provider_data: Option<&str>) -> bool { + #[derive(Deserialize)] + struct DuoProviderData { + host: String, + ik: String, + sk: String, + } + + match provider_type { + TwoFactorType::Authenticator => true, + TwoFactorType::Email => CONFIG._enable_email_2fa(), + TwoFactorType::Duo | TwoFactorType::OrganizationDuo => { + provider_data + .and_then(|raw| serde_json::from_str::(raw).ok()) + .is_some_and(|duo| !duo.host.is_empty() && !duo.ik.is_empty() && !duo.sk.is_empty()) + || has_global_duo_credentials() + } + TwoFactorType::YubiKey => { + CONFIG._enable_yubico() && CONFIG.yubico_client_id().is_some() && CONFIG.yubico_secret_key().is_some() + } + TwoFactorType::Webauthn => CONFIG.is_webauthn_2fa_supported(), + TwoFactorType::Remember => !CONFIG.disable_2fa_remember(), + TwoFactorType::RecoveryCode => true, + TwoFactorType::U2f + | TwoFactorType::U2fRegisterChallenge + | TwoFactorType::U2fLoginChallenge + | TwoFactorType::EmailVerificationChallenge + | TwoFactorType::WebauthnRegisterChallenge + | TwoFactorType::WebauthnLoginChallenge + | TwoFactorType::ProtectedActions => false, + } +} + pub fn routes() -> Vec { let mut routes = routes![ get_twofactor, get_recover, - recover, disable_twofactor, disable_twofactor_put, get_device_verification_settings, @@ -54,7 +92,13 @@ pub fn routes() -> Vec { #[get("/two-factor")] async fn get_twofactor(headers: Headers, conn: DbConn) -> Json { let twofactors = TwoFactor::find_by_user(&headers.user.uuid, &conn).await; - let twofactors_json: Vec = twofactors.iter().map(TwoFactor::to_json_provider).collect(); + let twofactors_json: Vec = twofactors + .iter() + .filter_map(|tf| { + let provider_type = TwoFactorType::from_i32(tf.atype)?; + is_twofactor_provider_usable(provider_type, Some(&tf.data)).then(|| TwoFactor::to_json_provider(tf)) + }) + .collect(); Json(json!({ "data": twofactors_json, @@ -76,54 +120,6 @@ async fn get_recover(data: Json, headers: Headers, conn: DbCo }))) } -#[derive(Deserialize)] -#[serde(rename_all = "camelCase")] -struct RecoverTwoFactor { - master_password_hash: String, - email: String, - recovery_code: String, -} - -#[post("/two-factor/recover", data = "")] -async fn recover(data: Json, client_headers: ClientHeaders, conn: DbConn) -> JsonResult { - let data: RecoverTwoFactor = data.into_inner(); - - use crate::db::models::User; - - // Get the user - let Some(mut user) = User::find_by_mail(&data.email, &conn).await else { - err!("Username or password is incorrect. Try again.") - }; - - // Check password - if !user.check_valid_password(&data.master_password_hash) { - err!("Username or password is incorrect. Try again.") - } - - // Check if recovery code is correct - if !user.check_valid_recovery_code(&data.recovery_code) { - err!("Recovery code is incorrect. Try again.") - } - - // Remove all twofactors from the user - TwoFactor::delete_all_by_user(&user.uuid, &conn).await?; - enforce_2fa_policy(&user, &user.uuid, client_headers.device_type, &client_headers.ip.ip, &conn).await?; - - log_user_event( - EventType::UserRecovered2fa as i32, - &user.uuid, - client_headers.device_type, - &client_headers.ip.ip, - &conn, - ) - .await; - - // Remove the recovery code, not needed without twofactors - user.totp_recover = None; - user.save(&conn).await?; - Ok(Json(Value::Object(serde_json::Map::new()))) -} - async fn _generate_recover_code(user: &mut User, conn: &DbConn) { if user.totp_recover.is_none() { let totp_recover = crypto::encode_random_bytes::<20>(&BASE32); diff --git a/src/api/core/two_factor/webauthn.rs b/src/api/core/two_factor/webauthn.rs index b10a5ded..0ec0e30e 100644 --- a/src/api/core/two_factor/webauthn.rs +++ b/src/api/core/two_factor/webauthn.rs @@ -108,8 +108,8 @@ impl WebauthnRegistration { #[post("/two-factor/get-webauthn", data = "")] async fn get_webauthn(data: Json, headers: Headers, conn: DbConn) -> JsonResult { - if !CONFIG.domain_set() { - err!("`DOMAIN` environment variable is not set. Webauthn disabled") + if !CONFIG.is_webauthn_2fa_supported() { + err!("Configured `DOMAIN` is not compatible with Webauthn") } let data: PasswordOrOtpData = data.into_inner(); @@ -438,7 +438,7 @@ pub async fn validate_webauthn_login(user_id: &UserId, response: &str, conn: &Db // We need to check for and update the backup_eligible flag when needed. // Vaultwarden did not have knowledge of this flag prior to migrating to webauthn-rs v0.5.x // Because of this we check the flag at runtime and update the registrations and state when needed - check_and_update_backup_eligible(user_id, &rsp, &mut registrations, &mut state, conn).await?; + let backup_flags_updated = check_and_update_backup_eligible(&rsp, &mut registrations, &mut state)?; let authentication_result = WEBAUTHN.finish_passkey_authentication(&rsp, &state)?; @@ -446,7 +446,8 @@ pub async fn validate_webauthn_login(user_id: &UserId, response: &str, conn: &Db if ct_eq(reg.credential.cred_id(), authentication_result.cred_id()) { // If the cred id matches and the credential is updated, Some(true) is returned // In those cases, update the record, else leave it alone - if reg.credential.update_credential(&authentication_result) == Some(true) { + let credential_updated = reg.credential.update_credential(&authentication_result) == Some(true); + if credential_updated || backup_flags_updated { TwoFactor::new(user_id.clone(), TwoFactorType::Webauthn, serde_json::to_string(®istrations)?) .save(conn) .await?; @@ -463,13 +464,11 @@ pub async fn validate_webauthn_login(user_id: &UserId, response: &str, conn: &Db ) } -async fn check_and_update_backup_eligible( - user_id: &UserId, +fn check_and_update_backup_eligible( rsp: &PublicKeyCredential, registrations: &mut Vec, state: &mut PasskeyAuthentication, - conn: &DbConn, -) -> EmptyResult { +) -> Result { // The feature flags from the response // For details see: https://www.w3.org/TR/webauthn-3/#sctn-authenticator-data const FLAG_BACKUP_ELIGIBLE: u8 = 0b0000_1000; @@ -486,16 +485,7 @@ async fn check_and_update_backup_eligible( let rsp_id = rsp.raw_id.as_slice(); for reg in &mut *registrations { if ct_eq(reg.credential.cred_id().as_slice(), rsp_id) { - // Try to update the key, and if needed also update the database, before the actual state check is done if reg.set_backup_eligible(backup_eligible, backup_state) { - TwoFactor::new( - user_id.clone(), - TwoFactorType::Webauthn, - serde_json::to_string(®istrations)?, - ) - .save(conn) - .await?; - // We also need to adjust the current state which holds the challenge used to start the authentication verification // Because Vaultwarden supports multiple keys, we need to loop through the deserialized state and check which key to update let mut raw_state = serde_json::to_value(&state)?; @@ -517,11 +507,12 @@ async fn check_and_update_backup_eligible( } *state = serde_json::from_value(raw_state)?; + return Ok(true); } break; } } } } - Ok(()) + Ok(false) } diff --git a/src/api/icons.rs b/src/api/icons.rs index 35a1de30..da83d0c4 100644 --- a/src/api/icons.rs +++ b/src/api/icons.rs @@ -513,13 +513,11 @@ fn parse_sizes(sizes: &str) -> (u16, u16) { if !sizes.is_empty() { match ICON_SIZE_REGEX.captures(sizes.trim()) { - None => {} - Some(dimensions) => { - if dimensions.len() >= 3 { - width = dimensions[1].parse::().unwrap_or_default(); - height = dimensions[2].parse::().unwrap_or_default(); - } + Some(dimensions) if dimensions.len() >= 3 => { + width = dimensions[1].parse::().unwrap_or_default(); + height = dimensions[2].parse::().unwrap_or_default(); } + _ => {} } } diff --git a/src/api/identity.rs b/src/api/identity.rs index 2053ba5d..c5573bc0 100644 --- a/src/api/identity.rs +++ b/src/api/identity.rs @@ -14,7 +14,10 @@ use crate::{ core::{ accounts::{PreloginData, RegisterData, _prelogin, _register, kdf_upgrade}, log_user_event, - two_factor::{authenticator, duo, duo_oidc, email, enforce_2fa_policy, webauthn, yubikey}, + two_factor::{ + authenticator, duo, duo_oidc, email, enforce_2fa_policy, is_twofactor_provider_usable, webauthn, + yubikey, + }, }, master_password_policy, push::register_push_device, @@ -642,6 +645,19 @@ async fn _user_api_key_login( Value::Null }; + let account_keys = if user.private_key.is_some() { + json!({ + "publicKeyEncryptionKeyPair": { + "wrappedPrivateKey": user.private_key, + "publicKey": user.public_key, + "Object": "publicKeyEncryptionKeyPair" + }, + "Object": "privateKeys" + }) + } else { + Value::Null + }; + // Note: No refresh_token is returned. The CLI just repeats the // client_credentials login flow when the existing token expires. let result = json!({ @@ -656,7 +672,9 @@ async fn _user_api_key_login( "KdfMemory": user.client_kdf_memory, "KdfParallelism": user.client_kdf_parallelism, "ResetMasterPassword": false, // TODO: according to official server seems something like: user.password_hash.is_empty(), but would need testing + "ForcePasswordReset": false, "scope": AuthMethod::UserApiKey.scope(), + "AccountKeys": account_keys, "UserDecryptionOptions": { "HasMasterPassword": has_master_password, "MasterPasswordUnlock": master_password_unlock, @@ -733,8 +751,24 @@ async fn twofactor_auth( 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 twofactor_ids: Vec<_> = twofactors + .iter() + .filter_map(|tf| { + let provider_type = TwoFactorType::from_i32(tf.atype)?; + (tf.enabled && is_twofactor_provider_usable(provider_type, Some(&tf.data))).then_some(tf.atype) + }) + .collect(); + if twofactor_ids.is_empty() { + err!("No enabled and usable two factor providers are available for this account") + } + 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 + if !twofactor_ids.contains(&selected_id) { + err_json!( + _json_err_twofactor(&twofactor_ids, &user.uuid, data, client_version, conn).await?, + "Invalid two factor provider" + ) + } let twofactor_code = match data.two_factor_token { Some(ref code) => code, @@ -751,7 +785,6 @@ async fn twofactor_auth( use crate::crypto::ct_eq; let selected_data = _selected_data(selected_twofactor); - let mut remember = data.two_factor_remember.unwrap_or(0); match TwoFactorType::from_i32(selected_id) { Some(TwoFactorType::Authenticator) => { @@ -783,13 +816,23 @@ async fn twofactor_auth( } Some(TwoFactorType::Remember) => { match device.twofactor_remember { - Some(ref code) if !CONFIG.disable_2fa_remember() && ct_eq(code, twofactor_code) => { - remember = 1; // Make sure we also return the token here, otherwise it will only remember the first time - } + // When a 2FA Remember token is used, check and validate this JWT token, if it is valid, just continue + // If it is invalid we need to trigger the 2FA Login prompt + Some(ref token) + if !CONFIG.disable_2fa_remember() + && (ct_eq(token, twofactor_code) + && auth::decode_2fa_remember(twofactor_code) + .is_ok_and(|t| t.sub == device.uuid && t.user_uuid == user.uuid)) => {} _ => { + // Always delete the current twofactor remember token here if it exists + if device.twofactor_remember.is_some() { + device.delete_twofactor_remember(); + // We need to save here, since we send a err_json!() which prevents saving `device` at a later stage + device.save(true, conn).await?; + } err_json!( _json_err_twofactor(&twofactor_ids, &user.uuid, data, client_version, conn).await?, - "2FA Remember token not provided" + "2FA Remember token not provided or expired" ) } } @@ -820,10 +863,10 @@ async fn twofactor_auth( TwoFactorIncomplete::mark_complete(&user.uuid, &device.uuid, conn).await?; + let remember = data.two_factor_remember.unwrap_or(0); let two_factor = if !CONFIG.disable_2fa_remember() && remember == 1 { Some(device.refresh_twofactor_remember()) } else { - device.delete_twofactor_remember(); None }; Ok(two_factor) @@ -856,7 +899,7 @@ async fn _json_err_twofactor( match TwoFactorType::from_i32(*provider) { Some(TwoFactorType::Authenticator) => { /* Nothing to do for TOTP */ } - Some(TwoFactorType::Webauthn) if CONFIG.domain_set() => { + Some(TwoFactorType::Webauthn) if CONFIG.is_webauthn_2fa_supported() => { let request = webauthn::generate_webauthn_login(user_id, conn).await?; result["TwoFactorProviders2"][provider.to_string()] = request.0; } diff --git a/src/api/notifications.rs b/src/api/notifications.rs index 42157ac3..492fdb19 100644 --- a/src/api/notifications.rs +++ b/src/api/notifications.rs @@ -358,15 +358,16 @@ impl WebSocketUsers { } } - pub async fn send_logout(&self, user: &User, acting_device_id: Option, conn: &DbConn) { + pub async fn send_logout(&self, user: &User, acting_device: Option<&Device>, conn: &DbConn) { // Skip any processing if both WebSockets and Push are not active if *NOTIFICATIONS_DISABLED { return; } + let acting_device_id = acting_device.map(|d| d.uuid.clone()); let data = create_update( vec![("UserId".into(), user.uuid.to_string().into()), ("Date".into(), serialize_date(user.updated_at))], UpdateType::LogOut, - acting_device_id.clone(), + acting_device_id, ); if CONFIG.enable_websocket() { @@ -374,7 +375,7 @@ impl WebSocketUsers { } if CONFIG.push_enabled() { - push_logout(user, acting_device_id.clone(), conn).await; + push_logout(user, acting_device, conn).await; } } diff --git a/src/api/push.rs b/src/api/push.rs index a7e88455..5000869d 100644 --- a/src/api/push.rs +++ b/src/api/push.rs @@ -13,7 +13,7 @@ use tokio::sync::RwLock; use crate::{ api::{ApiResult, EmptyResult, UpdateType}, db::{ - models::{AuthRequestId, Cipher, Device, DeviceId, Folder, PushId, Send, User, UserId}, + models::{AuthRequestId, Cipher, Device, Folder, PushId, Send, User, UserId}, DbConn, }, http_client::make_http_request, @@ -188,15 +188,13 @@ pub async fn push_cipher_update(ut: UpdateType, cipher: &Cipher, device: &Device } } -pub async fn push_logout(user: &User, acting_device_id: Option, conn: &DbConn) { - let acting_device_id: Value = acting_device_id.map(|v| v.to_string().into()).unwrap_or_else(|| Value::Null); - +pub async fn push_logout(user: &User, acting_device: Option<&Device>, conn: &DbConn) { if Device::check_user_has_push_device(&user.uuid, conn).await { tokio::task::spawn(send_to_push_relay(json!({ "userId": user.uuid, "organizationId": (), - "deviceId": acting_device_id, - "identifier": acting_device_id, + "deviceId": acting_device.and_then(|d| d.push_uuid.as_ref()), + "identifier": acting_device.map(|d| &d.uuid), "type": UpdateType::LogOut as i32, "payload": { "userId": user.uuid, diff --git a/src/api/web.rs b/src/api/web.rs index 3ef43d6d..4c2dd96e 100644 --- a/src/api/web.rs +++ b/src/api/web.rs @@ -60,6 +60,7 @@ fn vaultwarden_css() -> Cached> { "mail_2fa_enabled": CONFIG._enable_email_2fa(), "mail_enabled": CONFIG.mail_enabled(), "sends_allowed": CONFIG.sends_allowed(), + "remember_2fa_disabled": CONFIG.disable_2fa_remember(), "password_hints_allowed": CONFIG.password_hints_allowed(), "signup_disabled": CONFIG.is_signup_disabled(), "sso_enabled": CONFIG.sso_enabled(), diff --git a/src/auth.rs b/src/auth.rs index ab41898f..43184369 100644 --- a/src/auth.rs +++ b/src/auth.rs @@ -46,6 +46,7 @@ static JWT_FILE_DOWNLOAD_ISSUER: LazyLock = LazyLock::new(|| format!("{}|file_download", CONFIG.domain_origin())); static JWT_REGISTER_VERIFY_ISSUER: LazyLock = LazyLock::new(|| format!("{}|register_verify", CONFIG.domain_origin())); +static JWT_2FA_REMEMBER_ISSUER: LazyLock = LazyLock::new(|| format!("{}|2faremember", CONFIG.domain_origin())); static PRIVATE_RSA_KEY: OnceLock = OnceLock::new(); static PUBLIC_RSA_KEY: OnceLock = OnceLock::new(); @@ -160,6 +161,10 @@ pub fn decode_register_verify(token: &str) -> Result Result { + decode_jwt(token, JWT_2FA_REMEMBER_ISSUER.to_string()) +} + #[derive(Debug, Serialize, Deserialize)] pub struct LoginJwtClaims { // Not before @@ -440,6 +445,31 @@ pub fn generate_register_verify_claims(email: String, name: Option, veri } } +#[derive(Serialize, Deserialize)] +pub struct TwoFactorRememberClaims { + // Not before + pub nbf: i64, + // Expiration time + pub exp: i64, + // Issuer + pub iss: String, + // Subject + pub sub: DeviceId, + // UserId + pub user_uuid: UserId, +} + +pub fn generate_2fa_remember_claims(device_uuid: DeviceId, user_uuid: UserId) -> TwoFactorRememberClaims { + let time_now = Utc::now(); + TwoFactorRememberClaims { + nbf: time_now.timestamp(), + exp: (time_now + TimeDelta::try_days(30).unwrap()).timestamp(), + iss: JWT_2FA_REMEMBER_ISSUER.to_string(), + sub: device_uuid, + user_uuid, + } +} + #[derive(Debug, Serialize, Deserialize)] pub struct BasicJwtClaims { // Not before @@ -674,10 +704,9 @@ pub struct OrgHeaders { impl OrgHeaders { fn is_member(&self) -> bool { - // NOTE: we don't care about MembershipStatus at the moment because this is only used - // where an invited, accepted or confirmed user is expected if this ever changes or - // if from_i32 is changed to return Some(Revoked) this check needs to be changed accordingly - self.membership_type >= MembershipType::User + // Only allow not revoked members, we can not use the Confirmed status here + // as some endpoints can be triggered by invited users during joining + self.membership_status != MembershipStatus::Revoked && self.membership_type >= MembershipType::User } fn is_confirmed_and_admin(&self) -> bool { self.membership_status == MembershipStatus::Confirmed && self.membership_type >= MembershipType::Admin @@ -690,6 +719,36 @@ impl OrgHeaders { } } +// org_id is usually the second path param ("/organizations/"), +// but there are cases where it is a query value. +// First check the path, if this is not a valid uuid, try the query values. +fn get_org_id(request: &Request<'_>) -> Option { + if let Some(Ok(org_id)) = request.param::(1) { + Some(org_id) + } else if let Some(Ok(org_id)) = request.query_value::("organizationId") { + Some(org_id) + } else { + None + } +} + +// Special Guard to ensure that there is an organization id present +// If there is no org id trigger the Outcome::Forward. +// This is useful for endpoints which work for both organization and personal vaults, like purge. +pub struct OrgIdGuard; + +#[rocket::async_trait] +impl<'r> FromRequest<'r> for OrgIdGuard { + type Error = &'static str; + + async fn from_request(request: &'r Request<'_>) -> Outcome { + match get_org_id(request) { + Some(_) => Outcome::Success(OrgIdGuard), + None => Outcome::Forward(rocket::http::Status::NotFound), + } + } +} + #[rocket::async_trait] impl<'r> FromRequest<'r> for OrgHeaders { type Error = &'static str; @@ -697,18 +756,8 @@ impl<'r> FromRequest<'r> for OrgHeaders { async fn from_request(request: &'r Request<'_>) -> Outcome { let headers = try_outcome!(Headers::from_request(request).await); - // org_id is usually the second path param ("/organizations/"), - // but there are cases where it is a query value. - // First check the path, if this is not a valid uuid, try the query values. - let url_org_id: Option = { - if let Some(Ok(org_id)) = request.param::(1) { - Some(org_id) - } else if let Some(Ok(org_id)) = request.query_value::("organizationId") { - Some(org_id) - } else { - None - } - }; + // Extract the org_id from the request + let url_org_id = get_org_id(request); match url_org_id { Some(org_id) if uuid::Uuid::parse_str(&org_id).is_ok() => { @@ -826,7 +875,7 @@ impl<'r> FromRequest<'r> for ManagerHeaders { _ => err_handler!("Error getting DB"), }; - if !Collection::can_access_collection(&headers.membership, &col_id, &conn).await { + if !Collection::is_coll_manageable_by_user(&col_id, &headers.membership.user_uuid, &conn).await { err_handler!("The current user isn't a manager for this collection") } } @@ -908,8 +957,8 @@ impl ManagerHeaders { if uuid::Uuid::parse_str(col_id.as_ref()).is_err() { err!("Collection Id is malformed!"); } - if !Collection::can_access_collection(&h.membership, col_id, conn).await { - err!("You don't have access to all collections!"); + if !Collection::is_coll_manageable_by_user(col_id, &h.membership.user_uuid, conn).await { + err!("Collection not found", "The current user isn't a manager for this collection") } } diff --git a/src/config.rs b/src/config.rs index 7d0d89f2..f649fccc 100644 --- a/src/config.rs +++ b/src/config.rs @@ -14,7 +14,10 @@ use serde::de::{self, Deserialize, Deserializer, MapAccess, Visitor}; use crate::{ error::Error, - util::{get_active_web_release, get_env, get_env_bool, is_valid_email, parse_experimental_client_feature_flags}, + util::{ + get_active_web_release, get_env, get_env_bool, is_valid_email, parse_experimental_client_feature_flags, + FeatureFlagFilter, + }, }; static CONFIG_FILE: LazyLock = LazyLock::new(|| { @@ -930,7 +933,7 @@ make_config! { }, } -fn validate_config(cfg: &ConfigItems) -> Result<(), Error> { +fn validate_config(cfg: &ConfigItems, on_update: bool) -> Result<(), Error> { // Validate connection URL is valid and DB feature is enabled #[cfg(sqlite)] { @@ -1036,33 +1039,17 @@ fn validate_config(cfg: &ConfigItems) -> Result<(), Error> { } } - // Server (v2025.6.2): https://github.com/bitwarden/server/blob/d094be3267f2030bd0dc62106bc6871cf82682f5/src/Core/Constants.cs#L103 - // Client (web-v2025.6.1): https://github.com/bitwarden/clients/blob/747c2fd6a1c348a57a76e4a7de8128466ffd3c01/libs/common/src/enums/feature-flag.enum.ts#L12 - // Android (v2025.6.0): https://github.com/bitwarden/android/blob/b5b022caaad33390c31b3021b2c1205925b0e1a2/app/src/main/kotlin/com/x8bit/bitwarden/data/platform/manager/model/FlagKey.kt#L22 - // iOS (v2025.6.0): https://github.com/bitwarden/ios/blob/ff06d9c6cc8da89f78f37f376495800201d7261a/BitwardenShared/Core/Platform/Models/Enum/FeatureFlag.swift#L7 - // - // NOTE: Move deprecated flags to the utils::parse_experimental_client_feature_flags() DEPRECATED_FLAGS const! - const KNOWN_FLAGS: &[&str] = &[ - // Autofill Team - "inline-menu-positioning-improvements", - "inline-menu-totp", - "ssh-agent", - // Key Management Team - "ssh-key-vault-item", - "pm-25373-windows-biometrics-v2", - // Tools - "export-attachments", - // Mobile Team - "anon-addy-self-host-alias", - "simple-login-self-host-alias", - "mutual-tls", - ]; - let configured_flags = parse_experimental_client_feature_flags(&cfg.experimental_client_feature_flags); - let invalid_flags: Vec<_> = configured_flags.keys().filter(|flag| !KNOWN_FLAGS.contains(&flag.as_str())).collect(); + let invalid_flags = + parse_experimental_client_feature_flags(&cfg.experimental_client_feature_flags, FeatureFlagFilter::InvalidOnly); if !invalid_flags.is_empty() { - err!(format!("Unrecognized experimental client feature flags: {invalid_flags:?}.\n\n\ + let feature_flags_error = format!("Unrecognized experimental client feature flags: {:?}.\n\ Please ensure all feature flags are spelled correctly and that they are supported in this version.\n\ - Supported flags: {KNOWN_FLAGS:?}")); + Supported flags: {:?}\n", invalid_flags, SUPPORTED_FEATURE_FLAGS); + if on_update { + err!(feature_flags_error); + } else { + println!("[WARNING] {feature_flags_error}"); + } } const MAX_FILESIZE_KB: i64 = i64::MAX >> 10; @@ -1505,6 +1492,35 @@ pub enum PathType { RsaKey, } +// Official available feature flags can be found here: +// Server (v2026.2.1): https://github.com/bitwarden/server/blob/0e42725d0837bd1c0dabd864ff621a579959744b/src/Core/Constants.cs#L135 +// Client (v2026.2.1): https://github.com/bitwarden/clients/blob/f96380c3138291a028bdd2c7a5fee540d5c98ba5/libs/common/src/enums/feature-flag.enum.ts#L12 +// Android (v2026.2.1): https://github.com/bitwarden/android/blob/6902c19c0093fa476bbf74ccaa70c9f14afbb82f/core/src/main/kotlin/com/bitwarden/core/data/manager/model/FlagKey.kt#L31 +// iOS (v2026.2.1): https://github.com/bitwarden/ios/blob/cdd9ba1770ca2ffc098d02d12cc3208e3a830454/BitwardenShared/Core/Platform/Models/Enum/FeatureFlag.swift#L7 +pub const SUPPORTED_FEATURE_FLAGS: &[&str] = &[ + // Architecture + "desktop-ui-migration-milestone-1", + "desktop-ui-migration-milestone-2", + "desktop-ui-migration-milestone-3", + "desktop-ui-migration-milestone-4", + // Auth Team + "pm-5594-safari-account-switching", + // Autofill Team + "ssh-agent", + "ssh-agent-v2", + // Key Management Team + "ssh-key-vault-item", + "pm-25373-windows-biometrics-v2", + // Mobile Team + "anon-addy-self-host-alias", + "simple-login-self-host-alias", + "mutual-tls", + "cxp-import-mobile", + "cxp-export-mobile", + // Platform Team + "pm-30529-webauthn-related-origins", +]; + impl Config { pub async fn load() -> Result { // Loading from env and file @@ -1518,7 +1534,7 @@ impl Config { // Fill any missing with defaults let config = builder.build(); if !SKIP_CONFIG_VALIDATION.load(Ordering::Relaxed) { - validate_config(&config)?; + validate_config(&config, false)?; } Ok(Config { @@ -1554,7 +1570,7 @@ impl Config { let env = &self.inner.read().unwrap()._env; env.merge(&builder, false, &mut overrides).build() }; - validate_config(&config)?; + validate_config(&config, true)?; // Save both the user and the combined config { diff --git a/src/db/mod.rs b/src/db/mod.rs index ae2b1221..d2ed9479 100644 --- a/src/db/mod.rs +++ b/src/db/mod.rs @@ -387,7 +387,6 @@ pub mod models; #[cfg(sqlite)] pub fn backup_sqlite() -> Result { use diesel::Connection; - use std::{fs::File, io::Write}; let db_url = CONFIG.database_url(); if DbConnType::from_url(&CONFIG.database_url()).map(|t| t == DbConnType::Sqlite).unwrap_or(false) { @@ -401,16 +400,13 @@ pub fn backup_sqlite() -> Result { .to_string_lossy() .into_owned(); - match File::create(backup_file.clone()) { - Ok(mut f) => { - let serialized_db = conn.serialize_database_to_buffer(); - f.write_all(serialized_db.as_slice()).expect("Error writing SQLite backup"); - Ok(backup_file) - } - Err(e) => { - err_silent!(format!("Unable to save SQLite backup: {e:?}")) - } - } + diesel::sql_query("VACUUM INTO ?") + .bind::(&backup_file) + .execute(&mut conn) + .map(|_| ()) + .map_res("VACUUM INTO failed")?; + + Ok(backup_file) } else { err_silent!("The database type is not SQLite. Backups only works for SQLite databases") } diff --git a/src/db/models/cipher.rs b/src/db/models/cipher.rs index 497e8d8b..ff390eb3 100644 --- a/src/db/models/cipher.rs +++ b/src/db/models/cipher.rs @@ -577,7 +577,7 @@ impl Cipher { if let Some(cached_member) = cipher_sync_data.members.get(org_uuid) { return cached_member.has_full_access(); } - } else if let Some(member) = Membership::find_by_user_and_org(user_uuid, org_uuid, conn).await { + } else if let Some(member) = Membership::find_confirmed_by_user_and_org(user_uuid, org_uuid, conn).await { return member.has_full_access(); } } @@ -686,10 +686,12 @@ impl Cipher { ciphers::table .filter(ciphers::uuid.eq(&self.uuid)) .inner_join(ciphers_collections::table.on( - ciphers::uuid.eq(ciphers_collections::cipher_uuid))) + ciphers::uuid.eq(ciphers_collections::cipher_uuid) + )) .inner_join(users_collections::table.on( ciphers_collections::collection_uuid.eq(users_collections::collection_uuid) - .and(users_collections::user_uuid.eq(user_uuid)))) + .and(users_collections::user_uuid.eq(user_uuid)) + )) .select((users_collections::read_only, users_collections::hide_passwords, users_collections::manage)) .load::<(bool, bool, bool)>(conn) .expect("Error getting user access restrictions") @@ -715,6 +717,9 @@ impl Cipher { .inner_join(users_organizations::table.on( users_organizations::uuid.eq(groups_users::users_organizations_uuid) )) + .inner_join(groups::table.on(groups::uuid.eq(collections_groups::groups_uuid) + .and(groups::organizations_uuid.eq(users_organizations::org_uuid)) + )) .filter(users_organizations::user_uuid.eq(user_uuid)) .select((collections_groups::read_only, collections_groups::hide_passwords, collections_groups::manage)) .load::<(bool, bool, bool)>(conn) @@ -813,28 +818,28 @@ impl Cipher { let mut query = ciphers::table .left_join(ciphers_collections::table.on( ciphers::uuid.eq(ciphers_collections::cipher_uuid) - )) + )) .left_join(users_organizations::table.on( - ciphers::organization_uuid.eq(users_organizations::org_uuid.nullable()) + ciphers::organization_uuid.eq(users_organizations::org_uuid.nullable()) .and(users_organizations::user_uuid.eq(user_uuid)) .and(users_organizations::status.eq(MembershipStatus::Confirmed as i32)) - )) + )) .left_join(users_collections::table.on( - ciphers_collections::collection_uuid.eq(users_collections::collection_uuid) + ciphers_collections::collection_uuid.eq(users_collections::collection_uuid) // Ensure that users_collections::user_uuid is NULL for unconfirmed users. .and(users_organizations::user_uuid.eq(users_collections::user_uuid)) - )) + )) .left_join(groups_users::table.on( - groups_users::users_organizations_uuid.eq(users_organizations::uuid) - )) - .left_join(groups::table.on( - groups::uuid.eq(groups_users::groups_uuid) - )) + groups_users::users_organizations_uuid.eq(users_organizations::uuid) + )) + .left_join(groups::table.on(groups::uuid.eq(groups_users::groups_uuid) + // Ensure that group and membership belong to the same org + .and(groups::organizations_uuid.eq(users_organizations::org_uuid)) + )) .left_join(collections_groups::table.on( - collections_groups::collections_uuid.eq(ciphers_collections::collection_uuid).and( - collections_groups::groups_uuid.eq(groups::uuid) - ) - )) + collections_groups::collections_uuid.eq(ciphers_collections::collection_uuid) + .and(collections_groups::groups_uuid.eq(groups::uuid)) + )) .filter(ciphers::user_uuid.eq(user_uuid)) // Cipher owner .or_filter(users_organizations::access_all.eq(true)) // access_all in org .or_filter(users_collections::user_uuid.eq(user_uuid)) // Access to collection @@ -1054,7 +1059,9 @@ impl Cipher { .left_join(groups_users::table.on( groups_users::users_organizations_uuid.eq(users_organizations::uuid) )) - .left_join(groups::table.on(groups::uuid.eq(groups_users::groups_uuid))) + .left_join(groups::table.on(groups::uuid.eq(groups_users::groups_uuid) + .and(groups::organizations_uuid.eq(users_organizations::org_uuid)) + )) .left_join(collections_groups::table.on( collections_groups::collections_uuid.eq(ciphers_collections::collection_uuid) .and(collections_groups::groups_uuid.eq(groups::uuid)) @@ -1115,7 +1122,9 @@ impl Cipher { .left_join(groups_users::table.on( groups_users::users_organizations_uuid.eq(users_organizations::uuid) )) - .left_join(groups::table.on(groups::uuid.eq(groups_users::groups_uuid))) + .left_join(groups::table.on(groups::uuid.eq(groups_users::groups_uuid) + .and(groups::organizations_uuid.eq(users_organizations::org_uuid)) + )) .left_join(collections_groups::table.on( collections_groups::collections_uuid.eq(ciphers_collections::collection_uuid) .and(collections_groups::groups_uuid.eq(groups::uuid)) @@ -1183,8 +1192,8 @@ impl Cipher { .left_join(groups_users::table.on( groups_users::users_organizations_uuid.eq(users_organizations::uuid) )) - .left_join(groups::table.on( - groups::uuid.eq(groups_users::groups_uuid) + .left_join(groups::table.on(groups::uuid.eq(groups_users::groups_uuid) + .and(groups::organizations_uuid.eq(users_organizations::org_uuid)) )) .left_join(collections_groups::table.on( collections_groups::collections_uuid.eq(ciphers_collections::collection_uuid).and( diff --git a/src/db/models/collection.rs b/src/db/models/collection.rs index 52ded966..b1f82335 100644 --- a/src/db/models/collection.rs +++ b/src/db/models/collection.rs @@ -191,7 +191,7 @@ impl Collection { self.update_users_revision(conn).await; CollectionCipher::delete_all_by_collection(&self.uuid, conn).await?; CollectionUser::delete_all_by_collection(&self.uuid, conn).await?; - CollectionGroup::delete_all_by_collection(&self.uuid, conn).await?; + CollectionGroup::delete_all_by_collection(&self.uuid, &self.org_uuid, conn).await?; db_run! { conn: { diesel::delete(collections::table.filter(collections::uuid.eq(self.uuid))) @@ -239,8 +239,8 @@ impl Collection { .left_join(groups_users::table.on( groups_users::users_organizations_uuid.eq(users_organizations::uuid) )) - .left_join(groups::table.on( - groups::uuid.eq(groups_users::groups_uuid) + .left_join(groups::table.on(groups::uuid.eq(groups_users::groups_uuid) + .and(groups::organizations_uuid.eq(users_organizations::org_uuid)) )) .left_join(collections_groups::table.on( collections_groups::groups_uuid.eq(groups_users::groups_uuid).and( @@ -355,8 +355,8 @@ impl Collection { .left_join(groups_users::table.on( groups_users::users_organizations_uuid.eq(users_organizations::uuid) )) - .left_join(groups::table.on( - groups::uuid.eq(groups_users::groups_uuid) + .left_join(groups::table.on(groups::uuid.eq(groups_users::groups_uuid) + .and(groups::organizations_uuid.eq(users_organizations::org_uuid)) )) .left_join(collections_groups::table.on( collections_groups::groups_uuid.eq(groups_users::groups_uuid).and( @@ -422,8 +422,8 @@ impl Collection { .left_join(groups_users::table.on( groups_users::users_organizations_uuid.eq(users_organizations::uuid) )) - .left_join(groups::table.on( - groups::uuid.eq(groups_users::groups_uuid) + .left_join(groups::table.on(groups::uuid.eq(groups_users::groups_uuid) + .and(groups::organizations_uuid.eq(users_organizations::org_uuid)) )) .left_join(collections_groups::table.on( collections_groups::groups_uuid.eq(groups_users::groups_uuid) @@ -484,8 +484,8 @@ impl Collection { .left_join(groups_users::table.on( groups_users::users_organizations_uuid.eq(users_organizations::uuid) )) - .left_join(groups::table.on( - groups::uuid.eq(groups_users::groups_uuid) + .left_join(groups::table.on(groups::uuid.eq(groups_users::groups_uuid) + .and(groups::organizations_uuid.eq(users_organizations::org_uuid)) )) .left_join(collections_groups::table.on( collections_groups::groups_uuid.eq(groups_users::groups_uuid).and( @@ -513,7 +513,8 @@ impl Collection { }} } - pub async fn is_manageable_by_user(&self, user_uuid: &UserId, conn: &DbConn) -> bool { + pub async fn is_coll_manageable_by_user(uuid: &CollectionId, user_uuid: &UserId, conn: &DbConn) -> bool { + let uuid = uuid.to_string(); let user_uuid = user_uuid.to_string(); db_run! { conn: { collections::table @@ -530,17 +531,17 @@ impl Collection { .left_join(groups_users::table.on( groups_users::users_organizations_uuid.eq(users_organizations::uuid) )) - .left_join(groups::table.on( - groups::uuid.eq(groups_users::groups_uuid) + .left_join(groups::table.on(groups::uuid.eq(groups_users::groups_uuid) + .and(groups::organizations_uuid.eq(users_organizations::org_uuid)) )) .left_join(collections_groups::table.on( collections_groups::groups_uuid.eq(groups_users::groups_uuid).and( collections_groups::collections_uuid.eq(collections::uuid) ) )) - .filter(collections::uuid.eq(&self.uuid)) + .filter(collections::uuid.eq(&uuid)) .filter( - users_collections::collection_uuid.eq(&self.uuid).and(users_collections::manage.eq(true)).or(// Directly accessed collection + users_collections::collection_uuid.eq(&uuid).and(users_collections::manage.eq(true)).or(// Directly accessed collection users_organizations::access_all.eq(true).or( // access_all in Organization users_organizations::atype.le(MembershipType::Admin as i32) // Org admin or owner )).or( @@ -558,6 +559,10 @@ impl Collection { .unwrap_or(0) != 0 }} } + + pub async fn is_manageable_by_user(&self, user_uuid: &UserId, conn: &DbConn) -> bool { + Self::is_coll_manageable_by_user(&self.uuid, user_uuid, conn).await + } } /// Database methods diff --git a/src/db/models/device.rs b/src/db/models/device.rs index 4e3d0197..1026574c 100644 --- a/src/db/models/device.rs +++ b/src/db/models/device.rs @@ -1,6 +1,6 @@ use chrono::{NaiveDateTime, Utc}; -use data_encoding::{BASE64, BASE64URL}; +use data_encoding::BASE64URL; use derive_more::{Display, From}; use serde_json::Value; @@ -49,11 +49,16 @@ impl Device { push_uuid: Some(PushId(get_uuid())), push_token: None, - refresh_token: crypto::encode_random_bytes::<64>(&BASE64URL), + refresh_token: Device::generate_refresh_token(), twofactor_remember: None, } } + #[inline(always)] + pub fn generate_refresh_token() -> String { + crypto::encode_random_bytes::<64>(&BASE64URL) + } + pub fn to_json(&self) -> Value { json!({ "id": self.uuid, @@ -67,10 +72,13 @@ impl Device { } pub fn refresh_twofactor_remember(&mut self) -> String { - let twofactor_remember = crypto::encode_random_bytes::<180>(&BASE64); - self.twofactor_remember = Some(twofactor_remember.clone()); + use crate::auth::{encode_jwt, generate_2fa_remember_claims}; - twofactor_remember + let two_factor_remember_claim = generate_2fa_remember_claims(self.uuid.clone(), self.user_uuid.clone()); + let two_factor_remember_string = encode_jwt(&two_factor_remember_claim); + self.twofactor_remember = Some(two_factor_remember_string.clone()); + + two_factor_remember_string } pub fn delete_twofactor_remember(&mut self) { @@ -257,6 +265,17 @@ impl Device { .unwrap_or(0) != 0 }} } + + pub async fn rotate_refresh_tokens_by_user(user_uuid: &UserId, conn: &DbConn) -> EmptyResult { + // Generate a new token per device. + // We cannot do a single UPDATE with one value because each device needs a unique token. + let devices = Self::find_by_user(user_uuid, conn).await; + for mut device in devices { + device.refresh_token = Device::generate_refresh_token(); + device.save(false, conn).await?; + } + Ok(()) + } } #[derive(Display)] diff --git a/src/db/models/group.rs b/src/db/models/group.rs index a24b5325..f41ad9ca 100644 --- a/src/db/models/group.rs +++ b/src/db/models/group.rs @@ -1,6 +1,6 @@ use super::{CollectionId, Membership, MembershipId, OrganizationId, User, UserId}; use crate::api::EmptyResult; -use crate::db::schema::{collections_groups, groups, groups_users, users_organizations}; +use crate::db::schema::{collections, collections_groups, groups, groups_users, users_organizations}; use crate::db::DbConn; use crate::error::MapResult; use chrono::{NaiveDateTime, Utc}; @@ -81,7 +81,7 @@ impl Group { // If both read_only and hide_passwords are false, then manage should be true // You can't have an entry with read_only and manage, or hide_passwords and manage // Or an entry with everything to false - let collections_groups: Vec = CollectionGroup::find_by_group(&self.uuid, conn) + let collections_groups: Vec = CollectionGroup::find_by_group(&self.uuid, &self.organizations_uuid, conn) .await .iter() .map(|entry| { @@ -191,7 +191,7 @@ impl Group { pub async fn delete_all_by_organization(org_uuid: &OrganizationId, conn: &DbConn) -> EmptyResult { for group in Self::find_by_organization(org_uuid, conn).await { - group.delete(conn).await?; + group.delete(org_uuid, conn).await?; } Ok(()) } @@ -246,8 +246,8 @@ impl Group { .inner_join(users_organizations::table.on( users_organizations::uuid.eq(groups_users::users_organizations_uuid) )) - .inner_join(groups::table.on( - groups::uuid.eq(groups_users::groups_uuid) + .inner_join(groups::table.on(groups::uuid.eq(groups_users::groups_uuid) + .and(groups::organizations_uuid.eq(users_organizations::org_uuid)) )) .filter(users_organizations::user_uuid.eq(user_uuid)) .filter(groups::access_all.eq(true)) @@ -276,9 +276,9 @@ impl Group { }} } - pub async fn delete(&self, conn: &DbConn) -> EmptyResult { - CollectionGroup::delete_all_by_group(&self.uuid, conn).await?; - GroupUser::delete_all_by_group(&self.uuid, conn).await?; + pub async fn delete(&self, org_uuid: &OrganizationId, conn: &DbConn) -> EmptyResult { + CollectionGroup::delete_all_by_group(&self.uuid, org_uuid, conn).await?; + GroupUser::delete_all_by_group(&self.uuid, org_uuid, conn).await?; db_run! { conn: { diesel::delete(groups::table.filter(groups::uuid.eq(&self.uuid))) @@ -306,8 +306,8 @@ impl Group { } impl CollectionGroup { - pub async fn save(&mut self, conn: &DbConn) -> EmptyResult { - let group_users = GroupUser::find_by_group(&self.groups_uuid, conn).await; + pub async fn save(&mut self, org_uuid: &OrganizationId, conn: &DbConn) -> EmptyResult { + let group_users = GroupUser::find_by_group(&self.groups_uuid, org_uuid, conn).await; for group_user in group_users { group_user.update_user_revision(conn).await; } @@ -365,10 +365,19 @@ impl CollectionGroup { } } - pub async fn find_by_group(group_uuid: &GroupId, conn: &DbConn) -> Vec { + pub async fn find_by_group(group_uuid: &GroupId, org_uuid: &OrganizationId, conn: &DbConn) -> Vec { db_run! { conn: { collections_groups::table + .inner_join(groups::table.on( + groups::uuid.eq(collections_groups::groups_uuid) + )) + .inner_join(collections::table.on( + collections::uuid.eq(collections_groups::collections_uuid) + .and(collections::org_uuid.eq(groups::organizations_uuid)) + )) .filter(collections_groups::groups_uuid.eq(group_uuid)) + .filter(collections::org_uuid.eq(org_uuid)) + .select(collections_groups::all_columns) .load::(conn) .expect("Error loading collection groups") }} @@ -383,6 +392,13 @@ impl CollectionGroup { .inner_join(users_organizations::table.on( users_organizations::uuid.eq(groups_users::users_organizations_uuid) )) + .inner_join(groups::table.on(groups::uuid.eq(collections_groups::groups_uuid) + .and(groups::organizations_uuid.eq(users_organizations::org_uuid)) + )) + .inner_join(collections::table.on( + collections::uuid.eq(collections_groups::collections_uuid) + .and(collections::org_uuid.eq(groups::organizations_uuid)) + )) .filter(users_organizations::user_uuid.eq(user_uuid)) .select(collections_groups::all_columns) .load::(conn) @@ -394,14 +410,20 @@ impl CollectionGroup { db_run! { conn: { collections_groups::table .filter(collections_groups::collections_uuid.eq(collection_uuid)) + .inner_join(collections::table.on( + collections::uuid.eq(collections_groups::collections_uuid) + )) + .inner_join(groups::table.on(groups::uuid.eq(collections_groups::groups_uuid) + .and(groups::organizations_uuid.eq(collections::org_uuid)) + )) .select(collections_groups::all_columns) .load::(conn) .expect("Error loading collection groups") }} } - pub async fn delete(&self, conn: &DbConn) -> EmptyResult { - let group_users = GroupUser::find_by_group(&self.groups_uuid, conn).await; + pub async fn delete(&self, org_uuid: &OrganizationId, conn: &DbConn) -> EmptyResult { + let group_users = GroupUser::find_by_group(&self.groups_uuid, org_uuid, conn).await; for group_user in group_users { group_user.update_user_revision(conn).await; } @@ -415,8 +437,8 @@ impl CollectionGroup { }} } - pub async fn delete_all_by_group(group_uuid: &GroupId, conn: &DbConn) -> EmptyResult { - let group_users = GroupUser::find_by_group(group_uuid, conn).await; + pub async fn delete_all_by_group(group_uuid: &GroupId, org_uuid: &OrganizationId, conn: &DbConn) -> EmptyResult { + let group_users = GroupUser::find_by_group(group_uuid, org_uuid, conn).await; for group_user in group_users { group_user.update_user_revision(conn).await; } @@ -429,10 +451,14 @@ impl CollectionGroup { }} } - pub async fn delete_all_by_collection(collection_uuid: &CollectionId, conn: &DbConn) -> EmptyResult { + pub async fn delete_all_by_collection( + collection_uuid: &CollectionId, + org_uuid: &OrganizationId, + conn: &DbConn, + ) -> EmptyResult { let collection_assigned_to_groups = CollectionGroup::find_by_collection(collection_uuid, conn).await; for collection_assigned_to_group in collection_assigned_to_groups { - let group_users = GroupUser::find_by_group(&collection_assigned_to_group.groups_uuid, conn).await; + let group_users = GroupUser::find_by_group(&collection_assigned_to_group.groups_uuid, org_uuid, conn).await; for group_user in group_users { group_user.update_user_revision(conn).await; } @@ -494,10 +520,19 @@ impl GroupUser { } } - pub async fn find_by_group(group_uuid: &GroupId, conn: &DbConn) -> Vec { + pub async fn find_by_group(group_uuid: &GroupId, org_uuid: &OrganizationId, conn: &DbConn) -> Vec { db_run! { conn: { groups_users::table + .inner_join(groups::table.on( + groups::uuid.eq(groups_users::groups_uuid) + )) + .inner_join(users_organizations::table.on( + users_organizations::uuid.eq(groups_users::users_organizations_uuid) + .and(users_organizations::org_uuid.eq(groups::organizations_uuid)) + )) .filter(groups_users::groups_uuid.eq(group_uuid)) + .filter(groups::organizations_uuid.eq(org_uuid)) + .select(groups_users::all_columns) .load::(conn) .expect("Error loading group users") }} @@ -522,6 +557,13 @@ impl GroupUser { .inner_join(collections_groups::table.on( collections_groups::groups_uuid.eq(groups_users::groups_uuid) )) + .inner_join(groups::table.on( + groups::uuid.eq(groups_users::groups_uuid) + )) + .inner_join(collections::table.on( + collections::uuid.eq(collections_groups::collections_uuid) + .and(collections::org_uuid.eq(groups::organizations_uuid)) + )) .filter(collections_groups::collections_uuid.eq(collection_uuid)) .filter(groups_users::users_organizations_uuid.eq(member_uuid)) .count() @@ -575,8 +617,8 @@ impl GroupUser { }} } - pub async fn delete_all_by_group(group_uuid: &GroupId, conn: &DbConn) -> EmptyResult { - let group_users = GroupUser::find_by_group(group_uuid, conn).await; + pub async fn delete_all_by_group(group_uuid: &GroupId, org_uuid: &OrganizationId, conn: &DbConn) -> EmptyResult { + let group_users = GroupUser::find_by_group(group_uuid, org_uuid, conn).await; for group_user in group_users { group_user.update_user_revision(conn).await; } diff --git a/src/db/models/org_policy.rs b/src/db/models/org_policy.rs index 0607f146..7e922f35 100644 --- a/src/db/models/org_policy.rs +++ b/src/db/models/org_policy.rs @@ -269,7 +269,7 @@ impl OrgPolicy { continue; } - if let Some(user) = Membership::find_by_user_and_org(user_uuid, &policy.org_uuid, conn).await { + if let Some(user) = Membership::find_confirmed_by_user_and_org(user_uuid, &policy.org_uuid, conn).await { if user.atype < MembershipType::Admin { return true; } @@ -332,7 +332,7 @@ impl OrgPolicy { for policy in OrgPolicy::find_confirmed_by_user_and_active_policy(user_uuid, OrgPolicyType::SendOptions, conn).await { - if let Some(user) = Membership::find_by_user_and_org(user_uuid, &policy.org_uuid, conn).await { + if let Some(user) = Membership::find_confirmed_by_user_and_org(user_uuid, &policy.org_uuid, conn).await { if user.atype < MembershipType::Admin { match serde_json::from_str::(&policy.data) { Ok(opts) => { diff --git a/src/db/models/organization.rs b/src/db/models/organization.rs index 035402a7..e05e7227 100644 --- a/src/db/models/organization.rs +++ b/src/db/models/organization.rs @@ -524,7 +524,8 @@ impl Membership { "familySponsorshipValidUntil": null, "familySponsorshipToDelete": null, "accessSecretsManager": false, - "limitCollectionCreation": self.atype < MembershipType::Manager, // If less then a manager return true, to limit collection creations + // limit collection creation to managers with access_all permission to prevent issues + "limitCollectionCreation": self.atype < MembershipType::Manager || !self.access_all, "limitCollectionDeletion": true, "limitItemDeletion": false, "allowAdminAccessToAllCollectionItems": true, @@ -1083,7 +1084,9 @@ impl Membership { .left_join(collections_groups::table.on( collections_groups::groups_uuid.eq(groups_users::groups_uuid) )) - .left_join(groups::table.on(groups::uuid.eq(groups_users::groups_uuid))) + .left_join(groups::table.on(groups::uuid.eq(groups_users::groups_uuid) + .and(groups::organizations_uuid.eq(users_organizations::org_uuid)) + )) .left_join(ciphers_collections::table.on( ciphers_collections::collection_uuid.eq(collections_groups::collections_uuid).and(ciphers_collections::cipher_uuid.eq(&cipher_uuid)) diff --git a/src/db/models/send.rs b/src/db/models/send.rs index 8180f843..84802c54 100644 --- a/src/db/models/send.rs +++ b/src/db/models/send.rs @@ -46,6 +46,16 @@ pub enum SendType { File = 1, } +enum SendAuthType { + #[allow(dead_code)] + // Send requires email OTP verification + Email = 0, // Not yet supported by Vaultwarden + // Send requires a password + Password = 1, + // Send requires no auth + None = 2, +} + impl Send { pub fn new(atype: i32, name: String, data: String, akey: String, deletion_date: NaiveDateTime) -> Self { let now = Utc::now().naive_utc(); @@ -145,6 +155,7 @@ impl Send { "maxAccessCount": self.max_access_count, "accessCount": self.access_count, "password": self.password_hash.as_deref().map(|h| BASE64URL_NOPAD.encode(h)), + "authType": if self.password_hash.is_some() { SendAuthType::Password as i32 } else { SendAuthType::None as i32 }, "disabled": self.disabled, "hideEmail": self.hide_email, diff --git a/src/db/models/two_factor.rs b/src/db/models/two_factor.rs index f0a1e663..0dc08e3e 100644 --- a/src/db/models/two_factor.rs +++ b/src/db/models/two_factor.rs @@ -20,7 +20,6 @@ pub struct TwoFactor { pub last_used: i64, } -#[allow(dead_code)] #[derive(num_derive::FromPrimitive)] pub enum TwoFactorType { Authenticator = 0, diff --git a/src/db/models/user.rs b/src/db/models/user.rs index bcafd548..6b30f1cc 100644 --- a/src/db/models/user.rs +++ b/src/db/models/user.rs @@ -185,13 +185,14 @@ impl User { /// These routes are able to use the previous stamp id for the next 2 minutes. /// After these 2 minutes this stamp will expire. /// - pub fn set_password( + pub async fn set_password( &mut self, password: &str, new_key: Option, reset_security_stamp: bool, allow_next_route: Option>, - ) { + conn: &DbConn, + ) -> EmptyResult { self.password_hash = crypto::hash_password(password.as_bytes(), &self.salt, self.password_iterations as u32); if let Some(route) = allow_next_route { @@ -203,12 +204,15 @@ impl User { } if reset_security_stamp { - self.reset_security_stamp() + self.reset_security_stamp(conn).await?; } + Ok(()) } - pub fn reset_security_stamp(&mut self) { + pub async fn reset_security_stamp(&mut self, conn: &DbConn) -> EmptyResult { self.security_stamp = get_uuid(); + Device::rotate_refresh_tokens_by_user(&self.uuid, conn).await?; + Ok(()) } /// Set the stamp_exception to only allow a subsequent request matching a specific route using the current security-stamp. diff --git a/src/mail.rs b/src/mail.rs index 270a839e..cdbd269a 100644 --- a/src/mail.rs +++ b/src/mail.rs @@ -302,10 +302,10 @@ pub async fn send_invite( .append_pair("organizationUserId", &member_id) .append_pair("token", &invite_token); - if CONFIG.sso_enabled() { - query_params.append_pair("orgUserHasExistingUser", "false"); + if CONFIG.sso_enabled() && CONFIG.sso_only() { query_params.append_pair("orgSsoIdentifier", &org_id); - } else if user.private_key.is_some() { + } + if user.private_key.is_some() { query_params.append_pair("orgUserHasExistingUser", "true"); } } diff --git a/src/main.rs b/src/main.rs index d74ae5cc..1eca532c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -570,6 +570,12 @@ async fn launch_rocket(pool: db::DbPool, extra_debug: bool) -> Result<(), Error> let basepath = &CONFIG.domain_path(); let mut config = rocket::Config::from(rocket::Config::figment()); + + // We install our own signal handlers below; disable Rocket's built-in handlers + config.shutdown.ctrlc = false; + #[cfg(unix)] + config.shutdown.signals.clear(); + config.temp_dir = canonicalize(CONFIG.tmp_folder()).unwrap().into(); config.cli_colors = false; // Make sure Rocket does not color any values for logging. config.limits = Limits::new() @@ -613,11 +619,7 @@ async fn launch_rocket(pool: db::DbPool, extra_debug: bool) -> Result<(), Error> CONFIG.set_rocket_shutdown_handle(instance.shutdown()); - tokio::spawn(async move { - tokio::signal::ctrl_c().await.expect("Error setting Ctrl-C handler"); - info!("Exiting Vaultwarden!"); - CONFIG.shutdown(); - }); + spawn_shutdown_signal_handler(); #[cfg(all(unix, sqlite))] { @@ -645,6 +647,35 @@ async fn launch_rocket(pool: db::DbPool, extra_debug: bool) -> Result<(), Error> Ok(()) } +#[cfg(unix)] +fn spawn_shutdown_signal_handler() { + tokio::spawn(async move { + use tokio::signal::unix::signal; + + let mut sigint = signal(SignalKind::interrupt()).expect("Error setting SIGINT handler"); + let mut sigterm = signal(SignalKind::terminate()).expect("Error setting SIGTERM handler"); + let mut sigquit = signal(SignalKind::quit()).expect("Error setting SIGQUIT handler"); + + let signal_name = tokio::select! { + _ = sigint.recv() => "SIGINT", + _ = sigterm.recv() => "SIGTERM", + _ = sigquit.recv() => "SIGQUIT", + }; + + info!("Received {signal_name}, initiating graceful shutdown"); + CONFIG.shutdown(); + }); +} + +#[cfg(not(unix))] +fn spawn_shutdown_signal_handler() { + tokio::spawn(async move { + tokio::signal::ctrl_c().await.expect("Error setting Ctrl-C handler"); + info!("Received Ctrl-C, initiating graceful shutdown"); + CONFIG.shutdown(); + }); +} + fn schedule_jobs(pool: db::DbPool) { if CONFIG.job_poll_interval_ms() == 0 { info!("Job scheduler disabled."); diff --git a/src/metrics.rs b/src/metrics.rs index cdaf07e0..4901e670 100644 --- a/src/metrics.rs +++ b/src/metrics.rs @@ -299,6 +299,7 @@ pub fn increment_auth_attempts(_method: &str, _status: &str) {} pub fn update_user_sessions(_user_type: &str, _count: i64) {} #[cfg(not(feature = "enable_metrics"))] +#[allow(clippy::unused_async)] pub async fn update_business_metrics(_conn: &mut DbConn) -> Result<(), Error> { Ok(()) } diff --git a/src/sso_client.rs b/src/sso_client.rs index 0d73d906..6204ab48 100644 --- a/src/sso_client.rs +++ b/src/sso_client.rs @@ -1,6 +1,5 @@ use std::{borrow::Cow, sync::LazyLock, time::Duration}; -use mini_moka::sync::Cache; use openidconnect::{core::*, reqwest, *}; use regex::Regex; use url::Url; @@ -13,9 +12,14 @@ use crate::{ }; static CLIENT_CACHE_KEY: LazyLock = LazyLock::new(|| "sso-client".to_string()); -static CLIENT_CACHE: LazyLock> = LazyLock::new(|| { - Cache::builder().max_capacity(1).time_to_live(Duration::from_secs(CONFIG.sso_client_cache_expiration())).build() +static CLIENT_CACHE: LazyLock> = LazyLock::new(|| { + moka::sync::Cache::builder() + .max_capacity(1) + .time_to_live(Duration::from_secs(CONFIG.sso_client_cache_expiration())) + .build() }); +static REFRESH_CACHE: LazyLock>> = + LazyLock::new(|| moka::future::Cache::builder().max_capacity(1000).time_to_live(Duration::from_secs(30)).build()); /// OpenID Connect Core client. pub type CustomClient = openidconnect::Client< @@ -38,6 +42,8 @@ pub type CustomClient = openidconnect::Client< EndpointSet, >; +pub type RefreshTokenResponse = (Option, String, Option); + #[derive(Clone)] pub struct Client { pub http_client: reqwest::Client, @@ -231,23 +237,29 @@ impl Client { verifier } - pub async fn exchange_refresh_token( - refresh_token: String, - ) -> ApiResult<(Option, String, Option)> { + pub async fn exchange_refresh_token(refresh_token: String) -> ApiResult { + let client = Client::cached().await?; + + REFRESH_CACHE + .get_with(refresh_token.clone(), async move { client._exchange_refresh_token(refresh_token).await }) + .await + .map_err(Into::into) + } + + async fn _exchange_refresh_token(&self, refresh_token: String) -> Result { let rt = RefreshToken::new(refresh_token); - let client = Client::cached().await?; - let token_response = - match client.core_client.exchange_refresh_token(&rt).request_async(&client.http_client).await { - Err(err) => err!(format!("Request to exchange_refresh_token endpoint failed: {:?}", err)), - Ok(token_response) => token_response, - }; - - Ok(( - token_response.refresh_token().map(|token| token.secret().clone()), - token_response.access_token().secret().clone(), - token_response.expires_in(), - )) + match self.core_client.exchange_refresh_token(&rt).request_async(&self.http_client).await { + Err(err) => { + error!("Request to exchange_refresh_token endpoint failed: {err}"); + Err(format!("Request to exchange_refresh_token endpoint failed: {err}")) + } + Ok(token_response) => Ok(( + token_response.refresh_token().map(|token| token.secret().clone()), + token_response.access_token().secret().clone(), + token_response.expires_in(), + )), + } } } diff --git a/src/static/global_domains.json b/src/static/global_domains.json index e3f08813..3b13a3e9 100644 --- a/src/static/global_domains.json +++ b/src/static/global_domains.json @@ -111,7 +111,8 @@ "microsoftstore.com", "xbox.com", "azure.com", - "windowsazure.com" + "windowsazure.com", + "cloud.microsoft" ], "excluded": false }, @@ -971,5 +972,13 @@ "pinterest.se" ], "excluded": false + }, + { + "type": 91, + "domains": [ + "twitter.com", + "x.com" + ], + "excluded": false } -] \ No newline at end of file +] diff --git a/src/static/scripts/admin_diagnostics.js b/src/static/scripts/admin_diagnostics.js index 5594b439..2cff4410 100644 --- a/src/static/scripts/admin_diagnostics.js +++ b/src/static/scripts/admin_diagnostics.js @@ -109,6 +109,9 @@ async function generateSupportString(event, dj) { supportString += "* Websocket Check: disabled\n"; } supportString += `* HTTP Response Checks: ${httpResponseCheck}\n`; + if (dj.invalid_feature_flags != "") { + supportString += `* Invalid feature flags: true\n`; + } const jsonResponse = await fetch(`${BASE_URL}/admin/diagnostics/config`, { "headers": { "Accept": "application/json" } @@ -128,6 +131,10 @@ async function generateSupportString(event, dj) { supportString += `\n**Environment settings which are overridden:** ${dj.overrides}\n`; } + if (dj.invalid_feature_flags != "") { + supportString += `\n**Invalid feature flags:** ${dj.invalid_feature_flags}\n`; + } + // Add http response check messages if they exists if (httpResponseCheck === false) { supportString += "\n**Failed HTTP Checks:**\n"; diff --git a/src/static/templates/admin/diagnostics.hbs b/src/static/templates/admin/diagnostics.hbs index 77f2c95b..d27bce4c 100644 --- a/src/static/templates/admin/diagnostics.hbs +++ b/src/static/templates/admin/diagnostics.hbs @@ -194,6 +194,14 @@
+ {{#if page_data.invalid_feature_flags}} +
Invalid Feature Flags + Warning +
+
+ Flags: {{page_data.invalid_feature_flags}} +
+ {{/if}} diff --git a/src/static/templates/scss/vaultwarden.scss.hbs b/src/static/templates/scss/vaultwarden.scss.hbs index 1859c1ea..230ac2e7 100644 --- a/src/static/templates/scss/vaultwarden.scss.hbs +++ b/src/static/templates/scss/vaultwarden.scss.hbs @@ -158,6 +158,13 @@ app-root a[routerlink="/signup"] { {{/if}} {{/if}} +{{#if remember_2fa_disabled}} +/* Hide checkbox to remember 2FA token for 30 days */ +app-two-factor-auth > form > bit-form-control { + @extend %vw-hide; +} +{{/if}} + {{#unless mail_2fa_enabled}} /* Hide `Email` 2FA if mail is not enabled */ .providers-2fa-1 { diff --git a/src/util.rs b/src/util.rs index aa4e7914..182b7b3b 100644 --- a/src/util.rs +++ b/src/util.rs @@ -16,7 +16,10 @@ use tokio::{ time::{sleep, Duration}, }; -use crate::{config::PathType, CONFIG}; +use crate::{ + config::{PathType, SUPPORTED_FEATURE_FLAGS}, + CONFIG, +}; pub struct AppHeaders(); @@ -153,9 +156,11 @@ impl Cors { fn get_allowed_origin(headers: &HeaderMap<'_>) -> Option { let origin = Cors::get_header(headers, "Origin"); let safari_extension_origin = "file://"; + let desktop_custom_file_origin = "bw-desktop-file://bundle"; if origin == CONFIG.domain_origin() || origin == safari_extension_origin + || origin == desktop_custom_file_origin || (CONFIG.sso_enabled() && origin == CONFIG.sso_authority()) { Some(origin) @@ -629,6 +634,21 @@ fn _process_key(key: &str) -> String { } } +pub fn deser_opt_nonempty_str<'de, D, T>(deserializer: D) -> Result, D::Error> +where + D: Deserializer<'de>, + T: From, +{ + use serde::Deserialize; + Ok(Option::::deserialize(deserializer)?.and_then(|s| { + if s.is_empty() { + None + } else { + Some(T::from(s)) + } + })) +} + #[derive(Clone, Debug, Deserialize)] #[serde(untagged)] pub enum NumberOrString { @@ -763,21 +783,28 @@ pub fn convert_json_key_lcase_first(src_json: Value) -> Value { } } +pub enum FeatureFlagFilter { + #[allow(dead_code)] + Unfiltered, + ValidOnly, + InvalidOnly, +} + /// Parses the experimental client feature flags string into a HashMap. -pub fn parse_experimental_client_feature_flags(experimental_client_feature_flags: &str) -> HashMap { - // These flags could still be configured, but are deprecated and not used anymore - // To prevent old installations from starting filter these out and not error out - const DEPRECATED_FLAGS: &[&str] = - &["autofill-overlay", "autofill-v2", "browser-fileless-import", "extension-refresh", "fido2-vault-credentials"]; +pub fn parse_experimental_client_feature_flags( + experimental_client_feature_flags: &str, + filter_mode: FeatureFlagFilter, +) -> HashMap { experimental_client_feature_flags .split(',') - .filter_map(|f| { - let flag = f.trim(); - if !flag.is_empty() && !DEPRECATED_FLAGS.contains(&flag) { - return Some((flag.to_owned(), true)); - } - None + .map(str::trim) + .filter(|flag| !flag.is_empty()) + .filter(|flag| match filter_mode { + FeatureFlagFilter::Unfiltered => true, + FeatureFlagFilter::ValidOnly => SUPPORTED_FEATURE_FLAGS.contains(flag), + FeatureFlagFilter::InvalidOnly => !SUPPORTED_FEATURE_FLAGS.contains(flag), }) + .map(|flag| (flag.to_owned(), true)) .collect() } diff --git a/tools/global_domains.py b/tools/global_domains.py index 66edca31..78a31701 100755 --- a/tools/global_domains.py +++ b/tools/global_domains.py @@ -79,3 +79,4 @@ for name, domain_list in domain_lists.items(): # Write out the global domains JSON file. with open(file=OUTPUT_FILE, mode='w', encoding='utf-8') as f: json.dump(global_domains, f, indent=2) + f.write("\n")