Browse Source

Merge branch 'main' into feature/prometheus-metrics

pull/6202/head
Ross Golder 3 days ago
committed by GitHub
parent
commit
0568f00c21
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 10
      .env.template
  2. 9
      .github/workflows/build.yml
  3. 2
      .github/workflows/check-templates.yml
  4. 2
      .github/workflows/hadolint.yml
  5. 35
      .github/workflows/release.yml
  6. 6
      .github/workflows/trivy.yml
  7. 2
      .github/workflows/zizmor.yml
  8. 251
      Cargo.lock
  9. 19
      Cargo.toml
  10. 4
      docker/DockerSettings.yaml
  11. 12
      docker/Dockerfile.alpine
  12. 12
      docker/Dockerfile.debian
  13. 16
      playwright/README.md
  14. 6
      playwright/compose/warden/Dockerfile
  15. 1
      playwright/compose/warden/build.sh
  16. 6
      playwright/playwright.config.ts
  17. 4
      playwright/test.env
  18. 19
      playwright/tests/setups/sso.ts
  19. 21
      playwright/tests/sso_login.spec.ts
  20. 2
      playwright/tests/sso_organization.spec.ts
  21. 8
      src/api/core/organizations.rs
  22. 172
      src/api/core/two_factor/webauthn.rs
  23. 44
      src/api/identity.rs
  24. 4
      src/api/mod.rs
  25. 1
      src/api/web.rs
  26. 43
      src/config.rs
  27. 2
      src/db/mod.rs
  28. 1
      src/db/models/two_factor.rs
  29. 2
      src/main.rs
  30. 13
      src/static/templates/scss/vaultwarden.scss.hbs

10
.env.template

@ -80,8 +80,16 @@
## Timeout when acquiring database connection ## Timeout when acquiring database connection
# DATABASE_TIMEOUT=30 # DATABASE_TIMEOUT=30
## Database idle timeout
## Timeout in seconds before idle connections to the database are closed.
# DATABASE_IDLE_TIMEOUT=600
## Database min connections
## Define the minimum size of the connection pool used for connecting to the database.
# DATABASE_MIN_CONNS=2
## Database max connections ## Database max connections
## Define the size of the connection pool used for connecting to the database. ## Define the maximum size of the connection pool used for connecting to the database.
# DATABASE_MAX_CONNS=10 # DATABASE_MAX_CONNS=10
## Database connection initialization ## Database connection initialization

9
.github/workflows/build.yml

@ -34,8 +34,7 @@ jobs:
permissions: permissions:
actions: write actions: write
contents: read contents: read
# We use Ubuntu 22.04 here because this matches the library versions used within the Debian docker containers runs-on: ubuntu-24.04
runs-on: ubuntu-22.04
timeout-minutes: 120 timeout-minutes: 120
# Make warnings errors, this is to prevent warnings slipping through. # Make warnings errors, this is to prevent warnings slipping through.
# This is done globally to prevent rebuilds when the RUSTFLAGS env variable changes. # This is done globally to prevent rebuilds when the RUSTFLAGS env variable changes.
@ -56,7 +55,7 @@ jobs:
# Checkout the repo # Checkout the repo
- name: "Checkout" - name: "Checkout"
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 #v4.2.2 uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 #v5.0.0
with: with:
persist-credentials: false persist-credentials: false
fetch-depth: 0 fetch-depth: 0
@ -82,7 +81,7 @@ jobs:
# Only install the clippy and rustfmt components on the default rust-toolchain # Only install the clippy and rustfmt components on the default rust-toolchain
- name: "Install rust-toolchain version" - name: "Install rust-toolchain version"
uses: dtolnay/rust-toolchain@b3b07ba8b418998c39fb20f53e8b695cdcc8de1b # master @ Apr 29, 2025, 9:22 PM GMT+2 uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9 # master @ Aug 23, 2025, 3:20 AM GMT+2
if: ${{ matrix.channel == 'rust-toolchain' }} if: ${{ matrix.channel == 'rust-toolchain' }}
with: with:
toolchain: "${{steps.toolchain.outputs.RUST_TOOLCHAIN}}" toolchain: "${{steps.toolchain.outputs.RUST_TOOLCHAIN}}"
@ -92,7 +91,7 @@ jobs:
# Install the any other channel to be used for which we do not execute clippy and rustfmt # Install the any other channel to be used for which we do not execute clippy and rustfmt
- name: "Install MSRV version" - name: "Install MSRV version"
uses: dtolnay/rust-toolchain@b3b07ba8b418998c39fb20f53e8b695cdcc8de1b # master @ Apr 29, 2025, 9:22 PM GMT+2 uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9 # master @ Aug 23, 2025, 3:20 AM GMT+2
if: ${{ matrix.channel != 'rust-toolchain' }} if: ${{ matrix.channel != 'rust-toolchain' }}
with: with:
toolchain: "${{steps.toolchain.outputs.RUST_TOOLCHAIN}}" toolchain: "${{steps.toolchain.outputs.RUST_TOOLCHAIN}}"

2
.github/workflows/check-templates.yml

@ -14,7 +14,7 @@ jobs:
steps: steps:
# Checkout the repo # Checkout the repo
- name: "Checkout" - name: "Checkout"
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 #v4.2.2 uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 #v5.0.0
with: with:
persist-credentials: false persist-credentials: false
# End Checkout the repo # End Checkout the repo

2
.github/workflows/hadolint.yml

@ -35,7 +35,7 @@ jobs:
# End Download hadolint # End Download hadolint
# Checkout the repo # Checkout the repo
- name: Checkout - name: Checkout
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 #v4.2.2 uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 #v5.0.0
with: with:
persist-credentials: false persist-credentials: false
# End Checkout the repo # End Checkout the repo

35
.github/workflows/release.yml

@ -10,33 +10,16 @@ on:
# https://docs.github.com/en/actions/writing-workflows/workflow-syntax-for-github-actions#filter-pattern-cheat-sheet # https://docs.github.com/en/actions/writing-workflows/workflow-syntax-for-github-actions#filter-pattern-cheat-sheet
- '[1-2].[0-9]+.[0-9]+' - '[1-2].[0-9]+.[0-9]+'
jobs: concurrency:
# https://github.com/marketplace/actions/skip-duplicate-actions # Apply concurrency control only on the upstream repo
# Some checks to determine if we need to continue with building a new docker. group: ${{ github.repository == 'dani-garcia/vaultwarden' && format('{0}-{1}', github.workflow, github.ref) || github.run_id }}
# We will skip this check if we are creating a tag, because that has the same hash as a previous run already. # Don't cancel other runs when creating a tag
skip_check: cancel-in-progress: ${{ github.ref_type == 'branch' }}
# Only run this in the upstream repo and not on forks
if: ${{ github.repository == 'dani-garcia/vaultwarden' }}
name: Cancel older jobs when running
permissions:
actions: write
runs-on: ubuntu-24.04
outputs:
should_skip: ${{ steps.skip_check.outputs.should_skip }}
steps:
- name: Skip Duplicates Actions
id: skip_check
uses: fkirc/skip-duplicate-actions@f75f66ce1886f00957d99748a42c724f4330bdcf # v5.3.1
with:
cancel_others: 'true'
# Only run this when not creating a tag
if: ${{ github.ref_type == 'branch' }}
jobs:
docker-build: docker-build:
needs: skip_check
if: ${{ needs.skip_check.outputs.should_skip != 'true' && github.repository == 'dani-garcia/vaultwarden' }}
name: Build Vaultwarden containers name: Build Vaultwarden containers
if: ${{ github.repository == 'dani-garcia/vaultwarden' }}
permissions: permissions:
packages: write packages: write
contents: read contents: read
@ -89,7 +72,7 @@ jobs:
# Checkout the repo # Checkout the repo
- name: Checkout - name: Checkout
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 #v4.2.2 uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 #v5.0.0
# We need fetch-depth of 0 so we also get all the tag metadata # We need fetch-depth of 0 so we also get all the tag metadata
with: with:
persist-credentials: false persist-credentials: false
@ -192,7 +175,7 @@ jobs:
- name: Bake ${{ matrix.base_image }} containers - name: Bake ${{ matrix.base_image }} containers
id: bake_vw id: bake_vw
uses: docker/bake-action@37816e747588cb137173af99ab33873600c46ea8 # v6.8.0 uses: docker/bake-action@3acf805d94d93a86cce4ca44798a76464a75b88c # v6.9.0
env: env:
BASE_TAGS: "${{ env.BASE_TAGS }}" BASE_TAGS: "${{ env.BASE_TAGS }}"
SOURCE_COMMIT: "${{ env.SOURCE_COMMIT }}" SOURCE_COMMIT: "${{ env.SOURCE_COMMIT }}"

6
.github/workflows/trivy.yml

@ -31,12 +31,12 @@ jobs:
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 #v4.2.2 uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 #v5.0.0
with: with:
persist-credentials: false persist-credentials: false
- name: Run Trivy vulnerability scanner - name: Run Trivy vulnerability scanner
uses: aquasecurity/trivy-action@dc5a429b52fcf669ce959baa2c2dd26090d2a6c4 # v0.32.0 uses: aquasecurity/trivy-action@b6643a29fecd7f34b3597bc6acb0a98b03d33ff8 # v0.33.0 + b6643a2
env: env:
TRIVY_DB_REPOSITORY: docker.io/aquasec/trivy-db:2,public.ecr.aws/aquasecurity/trivy-db:2,ghcr.io/aquasecurity/trivy-db:2 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 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
@ -48,6 +48,6 @@ jobs:
severity: CRITICAL,HIGH severity: CRITICAL,HIGH
- name: Upload Trivy scan results to GitHub Security tab - name: Upload Trivy scan results to GitHub Security tab
uses: github/codeql-action/upload-sarif@df559355d593797519d70b90fc8edd5db049e7a2 # v3.29.9 uses: github/codeql-action/upload-sarif@3c3833e0f8c1c83d449a7478aa59c036a9165498 # v3.29.11
with: with:
sarif_file: 'trivy-results.sarif' sarif_file: 'trivy-results.sarif'

2
.github/workflows/zizmor.yml

@ -16,7 +16,7 @@ jobs:
security-events: write security-events: write
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 #v5.0.0
with: with:
persist-credentials: false persist-credentials: false

251
Cargo.lock

@ -167,11 +167,13 @@ dependencies = [
[[package]] [[package]]
name = "async-compression" name = "async-compression"
version = "0.4.27" version = "0.4.28"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ddb939d66e4ae03cee6091612804ba446b12878410cfa17f785f4dd67d4014e8" checksum = "6448dfb3960f0b038e88c781ead1e7eb7929dfc3a71a1336ec9086c00f6d1e75"
dependencies = [ dependencies = [
"brotli", "brotli",
"compression-codecs",
"compression-core",
"flate2", "flate2",
"futures-core", "futures-core",
"memchr", "memchr",
@ -277,9 +279,9 @@ dependencies = [
[[package]] [[package]]
name = "async-std" name = "async-std"
version = "1.13.1" version = "1.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "730294c1c08c2e0f85759590518f6333f0d5a0a766a27d519c1b244c3dfd8a24" checksum = "2c8e079a4ab67ae52b7403632e4618815d6db36d2a010cfe41b02c1b1578f93b"
dependencies = [ dependencies = [
"async-channel 1.9.0", "async-channel 1.9.0",
"async-global-executor", "async-global-executor",
@ -332,9 +334,9 @@ checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de"
[[package]] [[package]]
name = "async-trait" name = "async-trait"
version = "0.1.88" version = "0.1.89"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e539d3fca749fcee5236ab05e93a52867dd549cc157c8cb7f99595f3cedffdb5" checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@ -436,9 +438,9 @@ dependencies = [
[[package]] [[package]]
name = "aws-sdk-sso" name = "aws-sdk-sso"
version = "1.80.0" version = "1.81.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e822be5d4ed48fa7adc983de1b814dea33a5460c7e0e81b053b8d2ca3b14c354" checksum = "79ede098271e3471036c46957cba2ba30888f53bda2515bf04b560614a30a36e"
dependencies = [ dependencies = [
"aws-credential-types", "aws-credential-types",
"aws-runtime", "aws-runtime",
@ -458,9 +460,9 @@ dependencies = [
[[package]] [[package]]
name = "aws-sdk-ssooidc" name = "aws-sdk-ssooidc"
version = "1.81.0" version = "1.82.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "66aa7b30f1fac6e02ca26e3839fa78db3b94f6298a6e7a6208fb59071d93a87e" checksum = "43326f724ba2cc957e6f3deac0ca1621a3e5d4146f5970c24c8a108dac33070f"
dependencies = [ dependencies = [
"aws-credential-types", "aws-credential-types",
"aws-runtime", "aws-runtime",
@ -480,9 +482,9 @@ dependencies = [
[[package]] [[package]]
name = "aws-sdk-sts" name = "aws-sdk-sts"
version = "1.82.0" version = "1.84.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2194426df72592f91df0cda790cb1e571aa87d66cecfea59a64031b58145abe3" checksum = "91abcdbfb48c38a0419eb75e0eac772a4783a96750392680e4f3c25a8a0535b9"
dependencies = [ dependencies = [
"aws-credential-types", "aws-credential-types",
"aws-runtime", "aws-runtime",
@ -584,9 +586,9 @@ dependencies = [
[[package]] [[package]]
name = "aws-smithy-runtime" name = "aws-smithy-runtime"
version = "1.8.6" version = "1.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9e107ce0783019dbff59b3a244aa0c114e4a8c9d93498af9162608cd5474e796" checksum = "a3d57c8b53a72d15c8e190475743acf34e4996685e346a3448dd54ef696fc6e0"
dependencies = [ dependencies = [
"aws-smithy-async", "aws-smithy-async",
"aws-smithy-http", "aws-smithy-http",
@ -607,9 +609,9 @@ dependencies = [
[[package]] [[package]]
name = "aws-smithy-runtime-api" name = "aws-smithy-runtime-api"
version = "1.8.7" version = "1.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "75d52251ed4b9776a3e8487b2a01ac915f73b2da3af8fc1e77e0fce697a550d4" checksum = "07f5e0fc8a6b3f2303f331b94504bbf754d85488f402d6f1dd7a6080f99afe56"
dependencies = [ dependencies = [
"aws-smithy-async", "aws-smithy-async",
"aws-smithy-types", "aws-smithy-types",
@ -760,9 +762,9 @@ checksum = "383d29d513d8764dcdc42ea295d979eb99c3c9f00607b3692cf68a431f7dca72"
[[package]] [[package]]
name = "bitflags" name = "bitflags"
version = "2.9.1" version = "2.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967" checksum = "34efbcccd345379ca2868b2b2c9d3782e9cc58ba87bc7d79d5b53d9c9ae6f25d"
[[package]] [[package]]
name = "blake2" name = "blake2"
@ -806,9 +808,9 @@ dependencies = [
[[package]] [[package]]
name = "brotli" name = "brotli"
version = "8.0.1" version = "8.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9991eea70ea4f293524138648e41ee89b0b2b12ddef3b255effa43c8056e0e0d" checksum = "4bd8b9603c7aa97359dbd97ecf258968c95f3adddd6db2f7e7a5bef101c84560"
dependencies = [ dependencies = [
"alloc-no-stdlib", "alloc-no-stdlib",
"alloc-stdlib", "alloc-stdlib",
@ -878,7 +880,7 @@ dependencies = [
"futures", "futures",
"hashbrown 0.15.5", "hashbrown 0.15.5",
"once_cell", "once_cell",
"thiserror 2.0.14", "thiserror 2.0.16",
"tokio", "tokio",
"web-time", "web-time",
] ]
@ -943,9 +945,9 @@ dependencies = [
[[package]] [[package]]
name = "cc" name = "cc"
version = "1.2.32" version = "1.2.34"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2352e5597e9c544d5e6d9c95190d5d27738ade584fa8db0a16e130e5c2b5296e" checksum = "42bc4aea80032b7bf409b0bc7ccad88853858911b7713a8062fdc0623867bedc"
dependencies = [ dependencies = [
"jobserver", "jobserver",
"libc", "libc",
@ -954,9 +956,9 @@ dependencies = [
[[package]] [[package]]
name = "cfg-if" name = "cfg-if"
version = "1.0.1" version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268" checksum = "2fd1289c04a9ea8cb22300a459a72a385d7c73d3259e2ed7dcb2af674838cfa9"
[[package]] [[package]]
name = "cfg_aliases" name = "cfg_aliases"
@ -1015,6 +1017,28 @@ version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b9e769b5c8c8283982a987c6e948e540254f1058d5a74b8794914d4ef5fc2a24" checksum = "b9e769b5c8c8283982a987c6e948e540254f1058d5a74b8794914d4ef5fc2a24"
[[package]]
name = "compression-codecs"
version = "0.4.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "46cc6539bf1c592cff488b9f253b30bc0ec50d15407c2cf45e27bd8f308d5905"
dependencies = [
"brotli",
"compression-core",
"flate2",
"futures-core",
"memchr",
"pin-project-lite",
"zstd",
"zstd-safe",
]
[[package]]
name = "compression-core"
version = "0.4.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2957e823c15bde7ecf1e8b64e537aa03a6be5fda0e2334e99887669e75b12e01"
[[package]] [[package]]
name = "concurrent-queue" name = "concurrent-queue"
version = "2.5.0" version = "2.5.0"
@ -1298,9 +1322,9 @@ checksum = "2a2330da5de22e8a3cb63252ce2abb30116bf5265e89c0e01bc17015ce30a476"
[[package]] [[package]]
name = "data-url" name = "data-url"
version = "0.3.1" version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c297a1c74b71ae29df00c3e22dd9534821d60eb9af5a0192823fa2acea70c2a" checksum = "be1e0bca6c3637f992fc1cc7cbc52a78c1ef6db076dbf1059c4323d6a2048376"
[[package]] [[package]]
name = "der" name = "der"
@ -1817,9 +1841,9 @@ checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b"
[[package]] [[package]]
name = "form_urlencoded" name = "form_urlencoded"
version = "1.2.1" version = "1.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf"
dependencies = [ dependencies = [
"percent-encoding", "percent-encoding",
] ]
@ -1947,9 +1971,9 @@ dependencies = [
[[package]] [[package]]
name = "generator" name = "generator"
version = "0.8.5" version = "0.8.7"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d18470a76cb7f8ff746cf1f7470914f900252ec36bbc40b569d74b1258446827" checksum = "605183a538e3e2a9c1038635cc5c2d194e2ee8fd0d1b66b8349fad7dbacce5a2"
dependencies = [ dependencies = [
"cc", "cc",
"cfg-if", "cfg-if",
@ -2051,7 +2075,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2d9e3df7f0222ce5184154973d247c591d9aadc28ce7a73c6cd31100c9facff6" checksum = "2d9e3df7f0222ce5184154973d247c591d9aadc28ce7a73c6cd31100c9facff6"
dependencies = [ dependencies = [
"codemap", "codemap",
"indexmap 2.10.0", "indexmap 2.11.0",
"lasso", "lasso",
"once_cell", "once_cell",
"phf 0.11.3", "phf 0.11.3",
@ -2080,7 +2104,7 @@ dependencies = [
"futures-core", "futures-core",
"futures-sink", "futures-sink",
"http 1.3.1", "http 1.3.1",
"indexmap 2.10.0", "indexmap 2.11.0",
"slab", "slab",
"tokio", "tokio",
"tokio-util", "tokio-util",
@ -2106,7 +2130,7 @@ dependencies = [
"pest_derive", "pest_derive",
"serde", "serde",
"serde_json", "serde_json",
"thiserror 2.0.14", "thiserror 2.0.16",
"walkdir", "walkdir",
] ]
@ -2173,7 +2197,7 @@ dependencies = [
"once_cell", "once_cell",
"rand 0.9.2", "rand 0.9.2",
"ring", "ring",
"thiserror 2.0.14", "thiserror 2.0.16",
"tinyvec", "tinyvec",
"tokio", "tokio",
"tracing", "tracing",
@ -2196,7 +2220,7 @@ dependencies = [
"rand 0.9.2", "rand 0.9.2",
"resolv-conf", "resolv-conf",
"smallvec", "smallvec",
"thiserror 2.0.14", "thiserror 2.0.16",
"tokio", "tokio",
"tracing", "tracing",
] ]
@ -2241,9 +2265,9 @@ dependencies = [
[[package]] [[package]]
name = "html5gum" name = "html5gum"
version = "0.7.0" version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b3918b5f36d61861b757261da986b51be562c7a87ac4e531d4158e67e08bff72" checksum = "ba6fbe46e93059ce8ee19fbefdb0c7699cc7197fcaac048f2c3593f3e5da845f"
dependencies = [ dependencies = [
"jetscii", "jetscii",
] ]
@ -2341,19 +2365,21 @@ dependencies = [
[[package]] [[package]]
name = "hyper" name = "hyper"
version = "1.6.0" version = "1.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cc2b571658e38e0c01b1fdca3bbbe93c00d3d71693ff2770043f8c29bc7d6f80" checksum = "eb3aa54a13a0dfe7fbe3a59e0c76093041720fdc77b110cc0fc260fafb4dc51e"
dependencies = [ dependencies = [
"atomic-waker",
"bytes", "bytes",
"futures-channel", "futures-channel",
"futures-util", "futures-core",
"h2", "h2",
"http 1.3.1", "http 1.3.1",
"http-body 1.0.1", "http-body 1.0.1",
"httparse", "httparse",
"itoa", "itoa",
"pin-project-lite", "pin-project-lite",
"pin-utils",
"smallvec", "smallvec",
"tokio", "tokio",
"want", "want",
@ -2366,7 +2392,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58"
dependencies = [ dependencies = [
"http 1.3.1", "http 1.3.1",
"hyper 1.6.0", "hyper 1.7.0",
"hyper-util", "hyper-util",
"rustls 0.23.31", "rustls 0.23.31",
"rustls-native-certs", "rustls-native-certs",
@ -2385,7 +2411,7 @@ checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0"
dependencies = [ dependencies = [
"bytes", "bytes",
"http-body-util", "http-body-util",
"hyper 1.6.0", "hyper 1.7.0",
"hyper-util", "hyper-util",
"native-tls", "native-tls",
"tokio", "tokio",
@ -2406,7 +2432,7 @@ dependencies = [
"futures-util", "futures-util",
"http 1.3.1", "http 1.3.1",
"http-body 1.0.1", "http-body 1.0.1",
"hyper 1.6.0", "hyper 1.7.0",
"ipnet", "ipnet",
"libc", "libc",
"percent-encoding", "percent-encoding",
@ -2537,9 +2563,9 @@ checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
[[package]] [[package]]
name = "idna" name = "idna"
version = "1.0.3" version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e" checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de"
dependencies = [ dependencies = [
"idna_adapter", "idna_adapter",
"smallvec", "smallvec",
@ -2569,9 +2595,9 @@ dependencies = [
[[package]] [[package]]
name = "indexmap" name = "indexmap"
version = "2.10.0" version = "2.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fe4cd85333e22411419a0bcae1297d25e58c9443848b11dc6a86fefe8c78a661" checksum = "f2481980430f9f78649238835720ddccc57e52df14ffce1c6f37391d61b563e9"
dependencies = [ dependencies = [
"equivalent", "equivalent",
"hashbrown 0.15.5", "hashbrown 0.15.5",
@ -2596,9 +2622,9 @@ dependencies = [
[[package]] [[package]]
name = "io-uring" name = "io-uring"
version = "0.7.9" version = "0.7.10"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d93587f37623a1a17d94ef2bc9ada592f5465fe7732084ab7beefabe5c77c0c4" checksum = "046fa2d4d00aea763528b4950358d0ead425372445dc8ff86312b3c69ff7727b"
dependencies = [ dependencies = [
"bitflags", "bitflags",
"cfg-if", "cfg-if",
@ -2678,9 +2704,9 @@ dependencies = [
[[package]] [[package]]
name = "jobserver" name = "jobserver"
version = "0.1.33" version = "0.1.34"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "38f262f097c174adebe41eb73d66ae9c06b2844fb0da69969647bbddd9b0538a" checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33"
dependencies = [ dependencies = [
"getrandom 0.3.3", "getrandom 0.3.3",
"libc", "libc",
@ -2784,9 +2810,9 @@ checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de"
[[package]] [[package]]
name = "libmimalloc-sys" name = "libmimalloc-sys"
version = "0.1.43" version = "0.1.44"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bf88cd67e9de251c1781dbe2f641a1a3ad66eaae831b8a2c38fbdc5ddae16d4d" checksum = "667f4fec20f29dfc6bc7357c582d91796c169ad7e2fce709468aefeb2c099870"
dependencies = [ dependencies = [
"cc", "cc",
"libc", "libc",
@ -2862,7 +2888,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "419e0dc8046cb947daa77eb95ae174acfbddb7673b4151f56d1eed8e93fbfaca" checksum = "419e0dc8046cb947daa77eb95ae174acfbddb7673b4151f56d1eed8e93fbfaca"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"generator 0.8.5", "generator 0.8.7",
"scoped-tls", "scoped-tls",
"tracing", "tracing",
"tracing-subscriber", "tracing-subscriber",
@ -2930,9 +2956,9 @@ dependencies = [
[[package]] [[package]]
name = "mimalloc" name = "mimalloc"
version = "0.1.47" version = "0.1.48"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b1791cbe101e95af5764f06f20f6760521f7158f69dbf9d6baf941ee1bf6bc40" checksum = "e1ee66a4b64c74f4ef288bcbb9192ad9c3feaad75193129ac8509af543894fd8"
dependencies = [ dependencies = [
"libmimalloc-sys", "libmimalloc-sys",
] ]
@ -3515,9 +3541,9 @@ dependencies = [
[[package]] [[package]]
name = "percent-encoding" name = "percent-encoding"
version = "2.3.1" version = "2.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
[[package]] [[package]]
name = "pest" name = "pest"
@ -3526,7 +3552,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1db05f56d34358a8b1066f67cbb203ee3e7ed2ba674a6263a1d5ec6db2204323" checksum = "1db05f56d34358a8b1066f67cbb203ee3e7ed2ba674a6263a1d5ec6db2204323"
dependencies = [ dependencies = [
"memchr", "memchr",
"thiserror 2.0.14", "thiserror 2.0.16",
"ucd-trie", "ucd-trie",
] ]
@ -3761,9 +3787,9 @@ dependencies = [
[[package]] [[package]]
name = "proc-macro2" name = "proc-macro2"
version = "1.0.97" version = "1.0.101"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d61789d7719defeb74ea5fe81f2fdfdbd28a803847077cecce2ff14e1472f6f1" checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de"
dependencies = [ dependencies = [
"unicode-ident", "unicode-ident",
] ]
@ -3876,7 +3902,7 @@ dependencies = [
"rustc-hash", "rustc-hash",
"rustls 0.23.31", "rustls 0.23.31",
"socket2 0.5.10", "socket2 0.5.10",
"thiserror 2.0.14", "thiserror 2.0.16",
"tokio", "tokio",
"tracing", "tracing",
"web-time", "web-time",
@ -3897,7 +3923,7 @@ dependencies = [
"rustls 0.23.31", "rustls 0.23.31",
"rustls-pki-types", "rustls-pki-types",
"slab", "slab",
"thiserror 2.0.14", "thiserror 2.0.16",
"tinyvec", "tinyvec",
"tracing", "tracing",
"web-time", "web-time",
@ -4048,14 +4074,14 @@ dependencies = [
[[package]] [[package]]
name = "regex" name = "regex"
version = "1.11.1" version = "1.11.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" checksum = "23d7fd106d8c02486a8d64e778353d1cffe08ce79ac2e82f540c86d0facf6912"
dependencies = [ dependencies = [
"aho-corasick", "aho-corasick",
"memchr", "memchr",
"regex-automata 0.4.9", "regex-automata 0.4.10",
"regex-syntax 0.8.5", "regex-syntax 0.8.6",
] ]
[[package]] [[package]]
@ -4069,20 +4095,20 @@ dependencies = [
[[package]] [[package]]
name = "regex-automata" name = "regex-automata"
version = "0.4.9" version = "0.4.10"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" checksum = "6b9458fa0bfeeac22b5ca447c63aaf45f28439a709ccd244698632f9aa6394d6"
dependencies = [ dependencies = [
"aho-corasick", "aho-corasick",
"memchr", "memchr",
"regex-syntax 0.8.5", "regex-syntax 0.8.6",
] ]
[[package]] [[package]]
name = "regex-lite" name = "regex-lite"
version = "0.1.6" version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "53a49587ad06b26609c52e423de037e7f57f20d53535d66e08c695f347df952a" checksum = "943f41321c63ef1c92fd763bfe054d2668f7f225a5c29f0105903dc2fc04ba30"
[[package]] [[package]]
name = "regex-syntax" name = "regex-syntax"
@ -4092,9 +4118,9 @@ checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1"
[[package]] [[package]]
name = "regex-syntax" name = "regex-syntax"
version = "0.8.5" version = "0.8.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" checksum = "caf4aa5b0f434c91fe5c7f1ecb6a5ece2130b02ad2a590589dda5146df959001"
[[package]] [[package]]
name = "reopen" name = "reopen"
@ -4159,7 +4185,7 @@ dependencies = [
"http 1.3.1", "http 1.3.1",
"http-body 1.0.1", "http-body 1.0.1",
"http-body-util", "http-body-util",
"hyper 1.6.0", "hyper 1.7.0",
"hyper-rustls", "hyper-rustls",
"hyper-tls", "hyper-tls",
"hyper-util", "hyper-util",
@ -4257,7 +4283,7 @@ dependencies = [
"either", "either",
"figment", "figment",
"futures", "futures",
"indexmap 2.10.0", "indexmap 2.11.0",
"log", "log",
"memchr", "memchr",
"multer", "multer",
@ -4289,7 +4315,7 @@ checksum = "575d32d7ec1a9770108c879fc7c47815a80073f96ca07ff9525a94fcede1dd46"
dependencies = [ dependencies = [
"devise", "devise",
"glob", "glob",
"indexmap 2.10.0", "indexmap 2.11.0",
"proc-macro2", "proc-macro2",
"quote", "quote",
"rocket_http", "rocket_http",
@ -4309,7 +4335,7 @@ dependencies = [
"futures", "futures",
"http 0.2.12", "http 0.2.12",
"hyper 0.14.32", "hyper 0.14.32",
"indexmap 2.10.0", "indexmap 2.11.0",
"log", "log",
"memchr", "memchr",
"pear", "pear",
@ -4382,12 +4408,13 @@ dependencies = [
[[package]] [[package]]
name = "rust-ini" name = "rust-ini"
version = "0.21.2" version = "0.21.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e7295b7ce3bf4806b419dc3420745998b447178b7005e2011947b38fc5aa6791" checksum = "4e310ef0e1b6eeb79169a1171daf9abcb87a2e17c03bee2c4bb100b55c75409f"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"ordered-multimap", "ordered-multimap",
"trim-in-place",
] ]
[[package]] [[package]]
@ -4718,9 +4745,9 @@ dependencies = [
[[package]] [[package]]
name = "serde_json" name = "serde_json"
version = "1.0.142" version = "1.0.143"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "030fedb782600dcbd6f02d479bf0d817ac3bb40d644745b769d6a96bc3afc5a7" checksum = "d401abef1d108fbd9cbaebc3e46611f4b1021f714a0597a71f41ee463f5f4a5a"
dependencies = [ dependencies = [
"itoa", "itoa",
"memchr", "memchr",
@ -4787,7 +4814,7 @@ dependencies = [
"chrono", "chrono",
"hex", "hex",
"indexmap 1.9.3", "indexmap 1.9.3",
"indexmap 2.10.0", "indexmap 2.11.0",
"schemars 0.9.0", "schemars 0.9.0",
"schemars 1.0.4", "schemars 1.0.4",
"serde", "serde",
@ -4883,7 +4910,7 @@ checksum = "297f631f50729c8c99b84667867963997ec0b50f32b2a7dbcab828ef0541e8bb"
dependencies = [ dependencies = [
"num-bigint", "num-bigint",
"num-traits", "num-traits",
"thiserror 2.0.14", "thiserror 2.0.16",
"time", "time",
] ]
@ -5030,9 +5057,9 @@ dependencies = [
[[package]] [[package]]
name = "syn" name = "syn"
version = "2.0.105" version = "2.0.106"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7bc3fcb250e53458e712715cf74285c1f889686520d79294a9ef3bd7aa1fc619" checksum = "ede7c438028d4436d71104916910f5bb611972c5cfd7f89b8300a8186e6fada6"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@ -5100,15 +5127,15 @@ checksum = "7b2093cf4c8eb1e67749a6762251bc9cd836b6fc171623bd0a9d324d37af2417"
[[package]] [[package]]
name = "tempfile" name = "tempfile"
version = "3.20.0" version = "3.21.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e8a64e3985349f2441a1a9ef0b853f869006c3855f2cda6862a94d26ebb9d6a1" checksum = "15b61f8f20e3a6f7e0649d825294eaf317edce30f82cf6026e7e4cb9222a7d1e"
dependencies = [ dependencies = [
"fastrand", "fastrand",
"getrandom 0.3.3", "getrandom 0.3.3",
"once_cell", "once_cell",
"rustix", "rustix",
"windows-sys 0.59.0", "windows-sys 0.60.2",
] ]
[[package]] [[package]]
@ -5122,11 +5149,11 @@ dependencies = [
[[package]] [[package]]
name = "thiserror" name = "thiserror"
version = "2.0.14" version = "2.0.16"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b0949c3a6c842cbde3f1686d6eea5a010516deb7085f79db747562d4102f41e" checksum = "3467d614147380f2e4e374161426ff399c91084acd2363eaf549172b3d5e60c0"
dependencies = [ dependencies = [
"thiserror-impl 2.0.14", "thiserror-impl 2.0.16",
] ]
[[package]] [[package]]
@ -5142,9 +5169,9 @@ dependencies = [
[[package]] [[package]]
name = "thiserror-impl" name = "thiserror-impl"
version = "2.0.14" version = "2.0.16"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cc5b44b4ab9c2fdd0e0512e6bece8388e214c0749f5862b114cc5b7a25daf227" checksum = "6c5e1be1c48b9172ee610da68fd9cd2770e7a4056cb3fc98710ee6906f0c7960"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@ -5223,9 +5250,9 @@ dependencies = [
[[package]] [[package]]
name = "tinyvec" name = "tinyvec"
version = "1.9.0" version = "1.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09b3661f17e86524eccd4371ab0429194e0d7c008abb45f7a7495b1719463c71" checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa"
dependencies = [ dependencies = [
"tinyvec_macros", "tinyvec_macros",
] ]
@ -5352,13 +5379,13 @@ version = "0.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "75129e1dc5000bfbaa9fee9d1b21f974f9fbad9daec557a521ee6e080825f6e8" checksum = "75129e1dc5000bfbaa9fee9d1b21f974f9fbad9daec557a521ee6e080825f6e8"
dependencies = [ dependencies = [
"indexmap 2.10.0", "indexmap 2.11.0",
"serde", "serde",
"serde_spanned 1.0.0", "serde_spanned 1.0.0",
"toml_datetime 0.7.0", "toml_datetime 0.7.0",
"toml_parser", "toml_parser",
"toml_writer", "toml_writer",
"winnow 0.7.12", "winnow 0.7.13",
] ]
[[package]] [[package]]
@ -5385,12 +5412,12 @@ version = "0.22.27"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a"
dependencies = [ dependencies = [
"indexmap 2.10.0", "indexmap 2.11.0",
"serde", "serde",
"serde_spanned 0.6.9", "serde_spanned 0.6.9",
"toml_datetime 0.6.11", "toml_datetime 0.6.11",
"toml_write", "toml_write",
"winnow 0.7.12", "winnow 0.7.13",
] ]
[[package]] [[package]]
@ -5399,7 +5426,7 @@ version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b551886f449aa90d4fe2bdaa9f4a2577ad2dde302c61ecf262d80b116db95c10" checksum = "b551886f449aa90d4fe2bdaa9f4a2577ad2dde302c61ecf262d80b116db95c10"
dependencies = [ dependencies = [
"winnow 0.7.12", "winnow 0.7.13",
] ]
[[package]] [[package]]
@ -5533,6 +5560,12 @@ dependencies = [
"tracing-log", "tracing-log",
] ]
[[package]]
name = "trim-in-place"
version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "343e926fc669bc8cde4fa3129ab681c63671bae288b1f1081ceee6d9d37904fc"
[[package]] [[package]]
name = "triomphe" name = "triomphe"
version = "0.1.14" version = "0.1.14"
@ -5621,9 +5654,9 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
[[package]] [[package]]
name = "url" name = "url"
version = "2.5.4" version = "2.5.7"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" checksum = "08bc136a29a3d1758e07a9cca267be308aeebf5cfd5a10f3f67ab2097683ef5b"
dependencies = [ dependencies = [
"form_urlencoded", "form_urlencoded",
"idna", "idna",
@ -6022,11 +6055,11 @@ checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
[[package]] [[package]]
name = "winapi-util" name = "winapi-util"
version = "0.1.9" version = "0.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" checksum = "0978bf7171b3d90bac376700cb56d606feb40f251a475a5d6634613564460b22"
dependencies = [ dependencies = [
"windows-sys 0.59.0", "windows-sys 0.60.2",
] ]
[[package]] [[package]]
@ -6399,9 +6432,9 @@ dependencies = [
[[package]] [[package]]
name = "winnow" name = "winnow"
version = "0.7.12" version = "0.7.13"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f3edebf492c8125044983378ecb5766203ad3b4c2f7a922bd7dd207f6d443e95" checksum = "21a0236b59786fed61e2a80582dd500fe61f18b5dca67a4a067d0bc9039339cf"
dependencies = [ dependencies = [
"memchr", "memchr",
] ]

19
Cargo.toml

@ -84,7 +84,7 @@ tokio-util = { version = "0.7.16", features = ["compat"]}
# A generic serialization/deserialization framework # A generic serialization/deserialization framework
serde = { version = "1.0.219", features = ["derive"] } serde = { version = "1.0.219", features = ["derive"] }
serde_json = "1.0.142" serde_json = "1.0.143"
# A safe, extensible ORM and Query builder # A safe, extensible ORM and Query builder
diesel = { version = "2.2.12", features = ["chrono", "r2d2", "numeric"] } diesel = { version = "2.2.12", features = ["chrono", "r2d2", "numeric"] }
@ -128,17 +128,16 @@ yubico = { package = "yubico_ng", version = "0.14.1", features = ["online-tokio"
# WebAuthn libraries # WebAuthn libraries
# danger-allow-state-serialisation is needed to save the state in the db # danger-allow-state-serialisation is needed to save the state in the db
# danger-credential-internals is needed to support U2F to Webauthn migration # danger-credential-internals is needed to support U2F to Webauthn migration
# danger-user-presence-only-security-keys is needed to disable UV webauthn-rs = { version = "0.5.2", features = ["danger-allow-state-serialisation", "danger-credential-internals"] }
webauthn-rs = { version = "0.5.2", features = ["danger-allow-state-serialisation", "danger-credential-internals", "danger-user-presence-only-security-keys"] }
webauthn-rs-proto = "0.5.2" webauthn-rs-proto = "0.5.2"
webauthn-rs-core = "0.5.2" webauthn-rs-core = "0.5.2"
# Handling of URL's for WebAuthn and favicons # Handling of URL's for WebAuthn and favicons
url = "2.5.4" url = "2.5.7"
# Email libraries # Email libraries
lettre = { version = "0.11.18", features = ["smtp-transport", "sendmail-transport", "builder", "serde", "hostname", "tracing", "tokio1-rustls", "ring", "rustls-native-certs"], default-features = false } lettre = { version = "0.11.18", features = ["smtp-transport", "sendmail-transport", "builder", "serde", "hostname", "tracing", "tokio1-rustls", "ring", "rustls-native-certs"], default-features = false }
percent-encoding = "2.3.1" # URL encoding library used for URL's in the emails percent-encoding = "2.3.2" # URL encoding library used for URL's in the emails
email_address = "0.2.9" email_address = "0.2.9"
# HTML Template library # HTML Template library
@ -149,9 +148,9 @@ reqwest = { version = "0.12.23", features = ["rustls-tls", "rustls-tls-native-ro
hickory-resolver = "0.25.2" hickory-resolver = "0.25.2"
# Favicon extraction libraries # Favicon extraction libraries
html5gum = "0.7.0" html5gum = "0.8.0"
regex = { version = "1.11.1", features = ["std", "perf", "unicode-perl"], default-features = false } regex = { version = "1.11.2", features = ["std", "perf", "unicode-perl"], default-features = false }
data-url = "0.3.1" data-url = "0.3.2"
bytes = "1.10.1" bytes = "1.10.1"
svg-hush = "0.9.5" svg-hush = "0.9.5"
@ -181,7 +180,7 @@ semver = "1.0.26"
# Allow overriding the default memory allocator # Allow overriding the default memory allocator
# Mainly used for the musl builds, since the default musl malloc is very slow # Mainly used for the musl builds, since the default musl malloc is very slow
mimalloc = { version = "0.1.47", features = ["secure"], default-features = false, optional = true } mimalloc = { version = "0.1.48", features = ["secure"], default-features = false, optional = true }
# Prometheus metrics # Prometheus metrics
prometheus = { version = "0.13.1", default-features = false, optional = true } prometheus = { version = "0.13.1", default-features = false, optional = true }
@ -204,7 +203,7 @@ opendal = { version = "0.54.0", features = ["services-fs"], default-features = f
anyhow = { version = "1.0.99", optional = true } anyhow = { version = "1.0.99", optional = true }
aws-config = { version = "1.8.5", features = ["behavior-version-latest", "rt-tokio", "credentials-process", "sso"], default-features = false, optional = true } aws-config = { version = "1.8.5", features = ["behavior-version-latest", "rt-tokio", "credentials-process", "sso"], default-features = false, optional = true }
aws-credential-types = { version = "1.2.5", optional = true } aws-credential-types = { version = "1.2.5", optional = true }
aws-smithy-runtime-api = { version = "1.8.7", optional = true } aws-smithy-runtime-api = { version = "1.9.0", optional = true }
http = { version = "1.3.1", optional = true } http = { version = "1.3.1", optional = true }
reqsign = { version = "0.16.5", optional = true } reqsign = { version = "0.16.5", optional = true }

4
docker/DockerSettings.yaml

@ -1,6 +1,6 @@
--- ---
vault_version: "v2025.7.2" vault_version: "v2025.8.0"
vault_image_digest: "sha256:e40b20eeffbcccb27db6c08c3aaa1cf7d3c92333f634dec26a077590e910e1c9" vault_image_digest: "sha256:41c2b51c87882248f405d5a0ab37210d2672a312ec5d4f3b9afcdbbe8eb9d57d"
# Cross Compile Docker Helper Scripts v1.6.1 # Cross Compile Docker Helper Scripts v1.6.1
# We use the linux/amd64 platform shell scripts since there is no difference between the different platform scripts # 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 # https://github.com/tonistiigi/xx | https://hub.docker.com/r/tonistiigi/xx/tags

12
docker/Dockerfile.alpine

@ -19,15 +19,15 @@
# - From https://hub.docker.com/r/vaultwarden/web-vault/tags, # - 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. # click the tag name to view the digest of the image it currently points to.
# - From the command line: # - From the command line:
# $ docker pull docker.io/vaultwarden/web-vault:v2025.7.2 # $ docker pull docker.io/vaultwarden/web-vault:v2025.8.0
# $ docker image inspect --format "{{.RepoDigests}}" docker.io/vaultwarden/web-vault:v2025.7.2 # $ docker image inspect --format "{{.RepoDigests}}" docker.io/vaultwarden/web-vault:v2025.8.0
# [docker.io/vaultwarden/web-vault@sha256:e40b20eeffbcccb27db6c08c3aaa1cf7d3c92333f634dec26a077590e910e1c9] # [docker.io/vaultwarden/web-vault@sha256:41c2b51c87882248f405d5a0ab37210d2672a312ec5d4f3b9afcdbbe8eb9d57d]
# #
# - Conversely, to get the tag name from the digest: # - Conversely, to get the tag name from the digest:
# $ docker image inspect --format "{{.RepoTags}}" docker.io/vaultwarden/web-vault@sha256:e40b20eeffbcccb27db6c08c3aaa1cf7d3c92333f634dec26a077590e910e1c9 # $ docker image inspect --format "{{.RepoTags}}" docker.io/vaultwarden/web-vault@sha256:41c2b51c87882248f405d5a0ab37210d2672a312ec5d4f3b9afcdbbe8eb9d57d
# [docker.io/vaultwarden/web-vault:v2025.7.2] # [docker.io/vaultwarden/web-vault:v2025.8.0]
# #
FROM --platform=linux/amd64 docker.io/vaultwarden/web-vault@sha256:e40b20eeffbcccb27db6c08c3aaa1cf7d3c92333f634dec26a077590e910e1c9 AS vault FROM --platform=linux/amd64 docker.io/vaultwarden/web-vault@sha256:41c2b51c87882248f405d5a0ab37210d2672a312ec5d4f3b9afcdbbe8eb9d57d AS vault
########################## ALPINE BUILD IMAGES ########################## ########################## ALPINE BUILD IMAGES ##########################
## NOTE: The Alpine Base Images do not support other platforms then linux/amd64 ## NOTE: The Alpine Base Images do not support other platforms then linux/amd64

12
docker/Dockerfile.debian

@ -19,15 +19,15 @@
# - From https://hub.docker.com/r/vaultwarden/web-vault/tags, # - 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. # click the tag name to view the digest of the image it currently points to.
# - From the command line: # - From the command line:
# $ docker pull docker.io/vaultwarden/web-vault:v2025.7.2 # $ docker pull docker.io/vaultwarden/web-vault:v2025.8.0
# $ docker image inspect --format "{{.RepoDigests}}" docker.io/vaultwarden/web-vault:v2025.7.2 # $ docker image inspect --format "{{.RepoDigests}}" docker.io/vaultwarden/web-vault:v2025.8.0
# [docker.io/vaultwarden/web-vault@sha256:e40b20eeffbcccb27db6c08c3aaa1cf7d3c92333f634dec26a077590e910e1c9] # [docker.io/vaultwarden/web-vault@sha256:41c2b51c87882248f405d5a0ab37210d2672a312ec5d4f3b9afcdbbe8eb9d57d]
# #
# - Conversely, to get the tag name from the digest: # - Conversely, to get the tag name from the digest:
# $ docker image inspect --format "{{.RepoTags}}" docker.io/vaultwarden/web-vault@sha256:e40b20eeffbcccb27db6c08c3aaa1cf7d3c92333f634dec26a077590e910e1c9 # $ docker image inspect --format "{{.RepoTags}}" docker.io/vaultwarden/web-vault@sha256:41c2b51c87882248f405d5a0ab37210d2672a312ec5d4f3b9afcdbbe8eb9d57d
# [docker.io/vaultwarden/web-vault:v2025.7.2] # [docker.io/vaultwarden/web-vault:v2025.8.0]
# #
FROM --platform=linux/amd64 docker.io/vaultwarden/web-vault@sha256:e40b20eeffbcccb27db6c08c3aaa1cf7d3c92333f634dec26a077590e910e1c9 AS vault FROM --platform=linux/amd64 docker.io/vaultwarden/web-vault@sha256:41c2b51c87882248f405d5a0ab37210d2672a312ec5d4f3b9afcdbbe8eb9d57d AS vault
########################## Cross Compile Docker Helper Scripts ########################## ########################## Cross Compile Docker Helper Scripts ##########################
## We use the linux/amd64 no matter which Build Platform, since these are all bash scripts ## We use the linux/amd64 no matter which Build Platform, since these are all bash scripts

16
playwright/README.md

@ -91,17 +91,25 @@ When creating new scenario use the recorder to more easily identify elements
This does not start the server, you will need to start it manually. This does not start the server, you will need to start it manually.
```bash ```bash
npx playwright codegen "http://127.0.0.1:8000" DOCKER_BUILDKIT=1 docker compose --profile playwright --env-file test.env up Vaultwarden
npx playwright codegen "http://127.0.0.1:8003"
``` ```
## Override web-vault ## Override web-vault
It is possible to change the `web-vault` used by referencing a different `bw_web_builds` commit. It is possible to change the `web-vault` used by referencing a different `bw_web_builds` commit.
Simplest is to set and uncomment `PW_WV_REPO_URL` and `PW_WV_COMMIT_HASH` in the `test.env`.
Ensure that the image is built with:
```bash
DOCKER_BUILDKIT=1 docker compose --profile playwright --env-file test.env build Vaultwarden
```
You can check the result running:
```bash ```bash
export PW_WV_REPO_URL=https://github.com/Timshel/oidc_web_builds.git DOCKER_BUILDKIT=1 docker compose --profile playwright --env-file test.env up Vaultwarden
export PW_WV_COMMIT_HASH=8707dc76df3f0cceef2be5bfae37bb29bd17fae6
DOCKER_BUILDKIT=1 docker compose --profile playwright --env-file test.env build Playwright
``` ```
# OpenID Connect test setup # OpenID Connect test setup

6
playwright/compose/warden/Dockerfile

@ -1,6 +1,6 @@
FROM playwright_oidc_vaultwarden_prebuilt AS prebuilt FROM playwright_oidc_vaultwarden_prebuilt AS prebuilt
FROM node:22-bookworm AS build FROM node:22-trixie AS build
ARG REPO_URL ARG REPO_URL
ARG COMMIT_HASH ARG COMMIT_HASH
@ -14,7 +14,7 @@ COPY build.sh /build.sh
RUN /build.sh RUN /build.sh
######################## RUNTIME IMAGE ######################## ######################## RUNTIME IMAGE ########################
FROM docker.io/library/debian:bookworm-slim FROM docker.io/library/debian:trixie-slim
ENV DEBIAN_FRONTEND=noninteractive ENV DEBIAN_FRONTEND=noninteractive
@ -24,7 +24,7 @@ RUN mkdir /data && \
--no-install-recommends \ --no-install-recommends \
ca-certificates \ ca-certificates \
curl \ curl \
libmariadb-dev-compat \ libmariadb-dev \
libpq5 \ libpq5 \
openssl && \ openssl && \
rm -rf /var/lib/apt/lists/* rm -rf /var/lib/apt/lists/*

1
playwright/compose/warden/build.sh

@ -16,7 +16,6 @@ if [[ ! -z "$REPO_URL" ]] && [[ ! -z "$COMMIT_HASH" ]] ; then
export VAULT_VERSION=$(cat Dockerfile | grep "ARG VAULT_VERSION" | cut -d "=" -f2) export VAULT_VERSION=$(cat Dockerfile | grep "ARG VAULT_VERSION" | cut -d "=" -f2)
./scripts/checkout_web_vault.sh ./scripts/checkout_web_vault.sh
./scripts/patch_web_vault.sh
./scripts/build_web_vault.sh ./scripts/build_web_vault.sh
printf '{"version":"%s"}' "$COMMIT_HASH" > ./web-vault/apps/web/build/vw-version.json printf '{"version":"%s"}' "$COMMIT_HASH" > ./web-vault/apps/web/build/vw-version.json

6
playwright/playwright.config.ts

@ -26,9 +26,9 @@ export default defineConfig({
* But short action/nav/expect timeouts to fail on specific step (raise locally if not enough). * But short action/nav/expect timeouts to fail on specific step (raise locally if not enough).
*/ */
timeout: 120 * 1000, timeout: 120 * 1000,
actionTimeout: 10 * 1000, actionTimeout: 20 * 1000,
navigationTimeout: 10 * 1000, navigationTimeout: 20 * 1000,
expect: { timeout: 10 * 1000 }, expect: { timeout: 20 * 1000 },
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
use: { use: {

4
playwright/test.env

@ -66,6 +66,10 @@ SSO_CLIENT_SECRET=warden
SSO_AUTHORITY=http://${KC_HTTP_HOST}:${KC_HTTP_PORT}/realms/${TEST_REALM} SSO_AUTHORITY=http://${KC_HTTP_HOST}:${KC_HTTP_PORT}/realms/${TEST_REALM}
SSO_DEBUG_TOKENS=true SSO_DEBUG_TOKENS=true
# Custom web-vault build
# PW_WV_REPO_URL=https://github.com/dani-garcia/bw_web_builds.git
# PW_WV_COMMIT_HASH=a5f5390895516bce2f48b7baadb6dc399e5fe75a
########################### ###########################
# Docker MariaDb container# # Docker MariaDb container#
########################### ###########################

19
playwright/tests/setups/sso.ts

@ -12,7 +12,7 @@ export async function logNewUser(
test: Test, test: Test,
page: Page, page: Page,
user: { email: string, name: string, password: string }, user: { email: string, name: string, password: string },
options: { mailBuffer?: MailBuffer, override?: boolean } = {} options: { mailBuffer?: MailBuffer } = {}
) { ) {
await test.step(`Create user ${user.name}`, async () => { await test.step(`Create user ${user.name}`, async () => {
await page.context().clearCookies(); await page.context().clearCookies();
@ -20,12 +20,8 @@ export async function logNewUser(
await test.step('Landing page', async () => { await test.step('Landing page', async () => {
await utils.cleanLanding(page); await utils.cleanLanding(page);
if( options.override ) { await page.locator("input[type=email].vw-email-sso").fill(user.email);
await page.getByRole('button', { name: 'Continue' }).click(); await page.getByRole('button', { name: /Use single sign-on/ }).click();
} else {
await page.getByLabel(/Email address/).fill(user.email);
await page.getByRole('button', { name: /Use single sign-on/ }).click();
}
}); });
await test.step('Keycloak login', async () => { await test.step('Keycloak login', async () => {
@ -69,7 +65,6 @@ export async function logUser(
user: { email: string, password: string }, user: { email: string, password: string },
options: { options: {
mailBuffer ?: MailBuffer, mailBuffer ?: MailBuffer,
override?: boolean,
totp?: OTPAuth.TOTP, totp?: OTPAuth.TOTP,
mail2fa?: boolean, mail2fa?: boolean,
} = {} } = {}
@ -82,12 +77,8 @@ export async function logUser(
await test.step('Landing page', async () => { await test.step('Landing page', async () => {
await utils.cleanLanding(page); await utils.cleanLanding(page);
if( options.override ) { await page.locator("input[type=email].vw-email-sso").fill(user.email);
await page.getByRole('button', { name: 'Continue' }).click(); await page.getByRole('button', { name: /Use single sign-on/ }).click();
} else {
await page.getByLabel(/Email address/).fill(user.email);
await page.getByRole('button', { name: /Use single sign-on/ }).click();
}
}); });
await test.step('Keycloak login', async () => { await test.step('Keycloak login', async () => {

21
playwright/tests/sso_login.spec.ts

@ -29,8 +29,8 @@ test('SSO login', async ({ page }) => {
test('Non SSO login', async ({ page }) => { test('Non SSO login', async ({ page }) => {
// Landing page // Landing page
await page.goto('/'); await page.goto('/');
await page.getByLabel(/Email address/).fill(users.user1.email); await page.locator("input[type=email].vw-email-sso").fill(users.user1.email);
await page.getByRole('button', { name: 'Continue' }).click(); await page.getByRole('button', { name: 'Other' }).click();
// Unlock page // Unlock page
await page.getByLabel('Master password').fill(users.user1.password); await page.getByLabel('Master password').fill(users.user1.password);
@ -58,20 +58,12 @@ test('Non SSO login impossible', async ({ page, browser }, testInfo: TestInfo) =
// Landing page // Landing page
await page.goto('/'); await page.goto('/');
await page.getByLabel(/Email address/).fill(users.user1.email);
// Check that SSO login is available // Check that SSO login is available
await expect(page.getByRole('button', { name: /Use single sign-on/ })).toHaveCount(1); await expect(page.getByRole('button', { name: /Use single sign-on/ })).toHaveCount(1);
await page.getByLabel(/Email address/).fill(users.user1.email); // No Continue/Other
await page.getByRole('button', { name: 'Continue' }).click(); await expect(page.getByRole('button', { name: 'Other' })).toHaveCount(0);
// Unlock page
await page.getByLabel('Master password').fill(users.user1.password);
await page.getByRole('button', { name: 'Log in with master password' }).click();
// An error should appear
await page.getByLabel('SSO sign-in is required')
}); });
@ -82,13 +74,12 @@ test('No SSO login', async ({ page }, testInfo: TestInfo) => {
// Landing page // Landing page
await page.goto('/'); await page.goto('/');
await page.getByLabel(/Email address/).fill(users.user1.email);
// No SSO button (rely on a correct selector checked in previous test) // No SSO button (rely on a correct selector checked in previous test)
await page.getByLabel('Master password');
await expect(page.getByRole('button', { name: /Use single sign-on/ })).toHaveCount(0); await expect(page.getByRole('button', { name: /Use single sign-on/ })).toHaveCount(0);
// Can continue to Master password // Can continue to Master password
await page.getByLabel(/Email address/).fill(users.user1.email);
await page.getByRole('button', { name: 'Continue' }).click(); await page.getByRole('button', { name: 'Continue' }).click();
await expect(page.getByRole('button', { name: /Log in with master password/ })).toHaveCount(1); await expect(page.getByRole('button', { name: 'Log in with master password' })).toHaveCount(1);
}); });

2
playwright/tests/sso_organization.spec.ts

@ -65,7 +65,7 @@ test('Enforce password policy', async ({ page }) => {
await utils.logout(test, page, users.user1); await utils.logout(test, page, users.user1);
await test.step(`Unlock trigger policy`, async () => { await test.step(`Unlock trigger policy`, async () => {
await page.getByRole('textbox', { name: 'Email address (required)' }).fill(users.user1.email); await page.locator("input[type=email].vw-email-sso").fill(users.user1.email);
await page.getByRole('button', { name: 'Use single sign-on' }).click(); await page.getByRole('button', { name: 'Use single sign-on' }).click();
await page.getByRole('textbox', { name: 'Master password (required)' }).fill(users.user1.password); await page.getByRole('textbox', { name: 'Master password (required)' }).fill(users.user1.password);

8
src/api/core/organizations.rs

@ -2063,12 +2063,12 @@ async fn list_policies_token(org_id: OrganizationId, token: &str, mut conn: DbCo
async fn get_master_password_policy(org_id: OrganizationId, _headers: Headers, mut conn: DbConn) -> JsonResult { async fn get_master_password_policy(org_id: OrganizationId, _headers: Headers, mut conn: DbConn) -> JsonResult {
let policy = let policy =
OrgPolicy::find_by_org_and_type(&org_id, OrgPolicyType::MasterPassword, &mut conn).await.unwrap_or_else(|| { OrgPolicy::find_by_org_and_type(&org_id, OrgPolicyType::MasterPassword, &mut conn).await.unwrap_or_else(|| {
let data = match CONFIG.sso_master_password_policy() { let (enabled, data) = match CONFIG.sso_master_password_policy_value() {
Some(policy) => policy, Some(policy) if CONFIG.sso_enabled() => (true, policy.to_string()),
None => "null".to_string(), _ => (false, "null".to_string()),
}; };
OrgPolicy::new(org_id, OrgPolicyType::MasterPassword, CONFIG.sso_master_password_policy().is_some(), data) OrgPolicy::new(org_id, OrgPolicyType::MasterPassword, enabled, data)
}); });
Ok(Json(policy.to_json())) Ok(Json(policy.to_json()))

172
src/api/core/two_factor/webauthn.rs

@ -17,19 +17,19 @@ use rocket::serde::json::Json;
use rocket::Route; use rocket::Route;
use serde_json::Value; use serde_json::Value;
use std::str::FromStr; use std::str::FromStr;
use std::sync::{Arc, LazyLock}; use std::sync::LazyLock;
use std::time::Duration; use std::time::Duration;
use url::Url; use url::Url;
use uuid::Uuid; use uuid::Uuid;
use webauthn_rs::prelude::{Base64UrlSafeData, SecurityKey, SecurityKeyAuthentication, SecurityKeyRegistration}; use webauthn_rs::prelude::{Base64UrlSafeData, Credential, Passkey, PasskeyAuthentication, PasskeyRegistration};
use webauthn_rs::{Webauthn, WebauthnBuilder}; use webauthn_rs::{Webauthn, WebauthnBuilder};
use webauthn_rs_proto::{ use webauthn_rs_proto::{
AuthenticationExtensionsClientOutputs, AuthenticatorAssertionResponseRaw, AuthenticatorAttestationResponseRaw, AuthenticationExtensionsClientOutputs, AuthenticatorAssertionResponseRaw, AuthenticatorAttestationResponseRaw,
PublicKeyCredential, RegisterPublicKeyCredential, RegistrationExtensionsClientOutputs, PublicKeyCredential, RegisterPublicKeyCredential, RegistrationExtensionsClientOutputs,
RequestAuthenticationExtensions, RequestAuthenticationExtensions, UserVerificationPolicy,
}; };
pub static WEBAUTHN_2FA_CONFIG: LazyLock<Arc<Webauthn>> = LazyLock::new(|| { static WEBAUTHN: LazyLock<Webauthn> = LazyLock::new(|| {
let domain = CONFIG.domain(); let domain = CONFIG.domain();
let domain_origin = CONFIG.domain_origin(); let domain_origin = CONFIG.domain_origin();
let rp_id = Url::parse(&domain).map(|u| u.domain().map(str::to_owned)).ok().flatten().unwrap_or_default(); let rp_id = Url::parse(&domain).map(|u| u.domain().map(str::to_owned)).ok().flatten().unwrap_or_default();
@ -38,14 +38,11 @@ pub static WEBAUTHN_2FA_CONFIG: LazyLock<Arc<Webauthn>> = LazyLock::new(|| {
let webauthn = WebauthnBuilder::new(&rp_id, &rp_origin) let webauthn = WebauthnBuilder::new(&rp_id, &rp_origin)
.expect("Creating WebauthnBuilder failed") .expect("Creating WebauthnBuilder failed")
.rp_name(&domain) .rp_name(&domain)
.timeout(Duration::from_millis(60000)) .timeout(Duration::from_millis(60000));
.danger_set_user_presence_only_security_keys(true);
Arc::new(webauthn.build().expect("Building Webauthn failed")) webauthn.build().expect("Building Webauthn failed")
}); });
pub type Webauthn2FaConfig<'a> = &'a rocket::State<Arc<Webauthn>>;
pub fn routes() -> Vec<Route> { pub fn routes() -> Vec<Route> {
routes![get_webauthn, generate_webauthn_challenge, activate_webauthn, activate_webauthn_put, delete_webauthn,] routes![get_webauthn, generate_webauthn_challenge, activate_webauthn, activate_webauthn_put, delete_webauthn,]
} }
@ -78,7 +75,7 @@ pub struct WebauthnRegistration {
pub name: String, pub name: String,
pub migrated: bool, pub migrated: bool,
pub credential: SecurityKey, pub credential: Passkey,
} }
impl WebauthnRegistration { impl WebauthnRegistration {
@ -89,6 +86,24 @@ impl WebauthnRegistration {
"migrated": self.migrated, "migrated": self.migrated,
}) })
} }
fn set_backup_eligible(&mut self, backup_eligible: bool, backup_state: bool) -> bool {
let mut changed = false;
let mut cred: Credential = self.credential.clone().into();
if cred.backup_state != backup_state {
cred.backup_state = backup_state;
changed = true;
}
if backup_eligible && !cred.backup_eligible {
cred.backup_eligible = true;
changed = true;
}
self.credential = cred.into();
changed
}
} }
#[post("/two-factor/get-webauthn", data = "<data>")] #[post("/two-factor/get-webauthn", data = "<data>")]
@ -113,12 +128,7 @@ async fn get_webauthn(data: Json<PasswordOrOtpData>, headers: Headers, mut conn:
} }
#[post("/two-factor/get-webauthn-challenge", data = "<data>")] #[post("/two-factor/get-webauthn-challenge", data = "<data>")]
async fn generate_webauthn_challenge( async fn generate_webauthn_challenge(data: Json<PasswordOrOtpData>, headers: Headers, mut conn: DbConn) -> JsonResult {
data: Json<PasswordOrOtpData>,
headers: Headers,
webauthn: Webauthn2FaConfig<'_>,
mut conn: DbConn,
) -> JsonResult {
let data: PasswordOrOtpData = data.into_inner(); let data: PasswordOrOtpData = data.into_inner();
let user = headers.user; let user = headers.user;
@ -131,18 +141,27 @@ async fn generate_webauthn_challenge(
.map(|r| r.credential.cred_id().to_owned()) // We return the credentialIds to the clients to avoid double registering .map(|r| r.credential.cred_id().to_owned()) // We return the credentialIds to the clients to avoid double registering
.collect(); .collect();
let (challenge, state) = webauthn.start_securitykey_registration( let (mut challenge, state) = WEBAUTHN.start_passkey_registration(
Uuid::from_str(&user.uuid).expect("Failed to parse UUID"), // Should never fail Uuid::from_str(&user.uuid).expect("Failed to parse UUID"), // Should never fail
&user.email, &user.email,
&user.name, &user.name,
Some(registrations), Some(registrations),
None,
None,
)?; )?;
let mut state = serde_json::to_value(&state)?;
state["rs"]["policy"] = Value::String("discouraged".to_string());
state["rs"]["extensions"].as_object_mut().unwrap().clear();
let type_ = TwoFactorType::WebauthnRegisterChallenge; let type_ = TwoFactorType::WebauthnRegisterChallenge;
TwoFactor::new(user.uuid.clone(), type_, serde_json::to_string(&state)?).save(&mut conn).await?; TwoFactor::new(user.uuid.clone(), type_, serde_json::to_string(&state)?).save(&mut conn).await?;
// Because for this flow we abuse the passkeys as 2FA, and use it more like a securitykey
// we need to modify some of the default settings defined by `start_passkey_registration()`.
challenge.public_key.extensions = None;
if let Some(asc) = challenge.public_key.authenticator_selection.as_mut() {
asc.user_verification = UserVerificationPolicy::Discouraged_DO_NOT_USE;
}
let mut challenge_value = serde_json::to_value(challenge.public_key)?; let mut challenge_value = serde_json::to_value(challenge.public_key)?;
challenge_value["status"] = "ok".into(); challenge_value["status"] = "ok".into();
challenge_value["errorMessage"] = "".into(); challenge_value["errorMessage"] = "".into();
@ -233,12 +252,7 @@ impl From<PublicKeyCredentialCopy> for PublicKeyCredential {
} }
#[post("/two-factor/webauthn", data = "<data>")] #[post("/two-factor/webauthn", data = "<data>")]
async fn activate_webauthn( async fn activate_webauthn(data: Json<EnableWebauthnData>, headers: Headers, mut conn: DbConn) -> JsonResult {
data: Json<EnableWebauthnData>,
headers: Headers,
webauthn: Webauthn2FaConfig<'_>,
mut conn: DbConn,
) -> JsonResult {
let data: EnableWebauthnData = data.into_inner(); let data: EnableWebauthnData = data.into_inner();
let mut user = headers.user; let mut user = headers.user;
@ -253,7 +267,7 @@ async fn activate_webauthn(
let type_ = TwoFactorType::WebauthnRegisterChallenge as i32; let type_ = TwoFactorType::WebauthnRegisterChallenge as i32;
let state = match TwoFactor::find_by_user_and_type(&user.uuid, type_, &mut conn).await { let state = match TwoFactor::find_by_user_and_type(&user.uuid, type_, &mut conn).await {
Some(tf) => { Some(tf) => {
let state: SecurityKeyRegistration = serde_json::from_str(&tf.data)?; let state: PasskeyRegistration = serde_json::from_str(&tf.data)?;
tf.delete(&mut conn).await?; tf.delete(&mut conn).await?;
state state
} }
@ -261,7 +275,7 @@ async fn activate_webauthn(
}; };
// Verify the credentials with the saved state // Verify the credentials with the saved state
let credential = webauthn.finish_securitykey_registration(&data.device_response.into(), &state)?; let credential = WEBAUTHN.finish_passkey_registration(&data.device_response.into(), &state)?;
let mut registrations: Vec<_> = get_webauthn_registrations(&user.uuid, &mut conn).await?.1; let mut registrations: Vec<_> = get_webauthn_registrations(&user.uuid, &mut conn).await?.1;
// TODO: Check for repeated ID's // TODO: Check for repeated ID's
@ -290,13 +304,8 @@ async fn activate_webauthn(
} }
#[put("/two-factor/webauthn", data = "<data>")] #[put("/two-factor/webauthn", data = "<data>")]
async fn activate_webauthn_put( async fn activate_webauthn_put(data: Json<EnableWebauthnData>, headers: Headers, conn: DbConn) -> JsonResult {
data: Json<EnableWebauthnData>, activate_webauthn(data, headers, conn).await
headers: Headers,
webauthn: Webauthn2FaConfig<'_>,
conn: DbConn,
) -> JsonResult {
activate_webauthn(data, headers, webauthn, conn).await
} }
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
@ -366,27 +375,27 @@ pub async fn get_webauthn_registrations(
} }
} }
pub async fn generate_webauthn_login( pub async fn generate_webauthn_login(user_id: &UserId, conn: &mut DbConn) -> JsonResult {
user_id: &UserId,
webauthn: Webauthn2FaConfig<'_>,
conn: &mut DbConn,
) -> JsonResult {
// Load saved credentials // Load saved credentials
let creds: Vec<_> = get_webauthn_registrations(user_id, conn).await?.1.into_iter().map(|r| r.credential).collect(); let creds: Vec<Passkey> =
get_webauthn_registrations(user_id, conn).await?.1.into_iter().map(|r| r.credential).collect();
if creds.is_empty() { if creds.is_empty() {
err!("No Webauthn devices registered") err!("No Webauthn devices registered")
} }
// Generate a challenge based on the credentials // Generate a challenge based on the credentials
let (mut response, state) = webauthn.start_securitykey_authentication(&creds)?; let (mut response, state) = WEBAUTHN.start_passkey_authentication(&creds)?;
// Modify to discourage user verification // Modify to discourage user verification
let mut state = serde_json::to_value(&state)?; let mut state = serde_json::to_value(&state)?;
state["ast"]["policy"] = Value::String("discouraged".to_string());
// Add appid, this is only needed for U2F compatibility, so maybe it can be removed as well // Add appid, this is only needed for U2F compatibility, so maybe it can be removed as well
let app_id = format!("{}/app-id.json", &CONFIG.domain()); let app_id = format!("{}/app-id.json", &CONFIG.domain());
state["ast"]["appid"] = Value::String(app_id.clone()); state["ast"]["appid"] = Value::String(app_id.clone());
response.public_key.user_verification = UserVerificationPolicy::Discouraged_DO_NOT_USE;
response response
.public_key .public_key
.extensions .extensions
@ -406,16 +415,11 @@ pub async fn generate_webauthn_login(
Ok(Json(serde_json::to_value(response.public_key)?)) Ok(Json(serde_json::to_value(response.public_key)?))
} }
pub async fn validate_webauthn_login( pub async fn validate_webauthn_login(user_id: &UserId, response: &str, conn: &mut DbConn) -> EmptyResult {
user_id: &UserId,
response: &str,
webauthn: Webauthn2FaConfig<'_>,
conn: &mut DbConn,
) -> EmptyResult {
let type_ = TwoFactorType::WebauthnLoginChallenge as i32; let type_ = TwoFactorType::WebauthnLoginChallenge as i32;
let state = match TwoFactor::find_by_user_and_type(user_id, type_, conn).await { let mut state = match TwoFactor::find_by_user_and_type(user_id, type_, conn).await {
Some(tf) => { Some(tf) => {
let state: SecurityKeyAuthentication = serde_json::from_str(&tf.data)?; let state: PasskeyAuthentication = serde_json::from_str(&tf.data)?;
tf.delete(conn).await?; tf.delete(conn).await?;
state state
} }
@ -432,7 +436,12 @@ pub async fn validate_webauthn_login(
let mut registrations = get_webauthn_registrations(user_id, conn).await?.1; let mut registrations = get_webauthn_registrations(user_id, conn).await?.1;
let authentication_result = webauthn.finish_securitykey_authentication(&rsp, &state)?; // 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 authentication_result = WEBAUTHN.finish_passkey_authentication(&rsp, &state)?;
for reg in &mut registrations { for reg in &mut registrations {
if ct_eq(reg.credential.cred_id(), authentication_result.cred_id()) { if ct_eq(reg.credential.cred_id(), authentication_result.cred_id()) {
@ -454,3 +463,66 @@ pub async fn validate_webauthn_login(
} }
) )
} }
async fn check_and_update_backup_eligible(
user_id: &UserId,
rsp: &PublicKeyCredential,
registrations: &mut Vec<WebauthnRegistration>,
state: &mut PasskeyAuthentication,
conn: &mut DbConn,
) -> EmptyResult {
// 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;
const FLAG_BACKUP_STATE: u8 = 0b0001_0000;
if let Some(bits) = rsp.response.authenticator_data.get(32) {
let backup_eligible = 0 != (bits & FLAG_BACKUP_ELIGIBLE);
let backup_state = 0 != (bits & FLAG_BACKUP_STATE);
// If the current key is backup eligible, then we probably need to update one of the keys already stored in the database
// This is needed because Vaultwarden didn't store this information when using the previous version of webauthn-rs since it was a new addition to the protocol
// Because we store multiple keys in one json string, we need to fetch the correct key first, and update its information before we let it verify
if 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(&registrations)?,
)
.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)?;
if let Some(credentials) = raw_state
.get_mut("ast")
.and_then(|v| v.get_mut("credentials"))
.and_then(|v| v.as_array_mut())
{
for cred in credentials.iter_mut() {
if cred.get("cred_id").is_some_and(|v| {
// Deserialize to a [u8] so it can be compared using `ct_eq` with the `rsp_id`
let cred_id_slice: Base64UrlSafeData = serde_json::from_value(v.clone()).unwrap();
ct_eq(cred_id_slice, rsp_id)
}) {
cred["backup_eligible"] = Value::Bool(backup_eligible);
cred["backup_state"] = Value::Bool(backup_state);
}
}
}
*state = serde_json::from_value(raw_state)?;
}
break;
}
}
}
}
Ok(())
}

44
src/api/identity.rs

@ -9,7 +9,6 @@ use rocket::{
}; };
use serde_json::Value; use serde_json::Value;
use crate::api::core::two_factor::webauthn::Webauthn2FaConfig;
use crate::{ use crate::{
api::{ api::{
core::{ core::{
@ -49,7 +48,6 @@ async fn login(
data: Form<ConnectData>, data: Form<ConnectData>,
client_header: ClientHeaders, client_header: ClientHeaders,
client_version: Option<ClientVersion>, client_version: Option<ClientVersion>,
webauthn: Webauthn2FaConfig<'_>,
mut conn: DbConn, mut conn: DbConn,
) -> JsonResult { ) -> JsonResult {
let data: ConnectData = data.into_inner(); let data: ConnectData = data.into_inner();
@ -72,7 +70,7 @@ async fn login(
_check_is_some(&data.device_name, "device_name cannot be blank")?; _check_is_some(&data.device_name, "device_name cannot be blank")?;
_check_is_some(&data.device_type, "device_type cannot be blank")?; _check_is_some(&data.device_type, "device_type cannot be blank")?;
_password_login(data, &mut user_id, &mut conn, &client_header.ip, &client_version, webauthn).await _password_login(data, &mut user_id, &mut conn, &client_header.ip, &client_version).await
} }
"client_credentials" => { "client_credentials" => {
_check_is_some(&data.client_id, "client_id cannot be blank")?; _check_is_some(&data.client_id, "client_id cannot be blank")?;
@ -93,7 +91,7 @@ async fn login(
_check_is_some(&data.device_name, "device_name cannot be blank")?; _check_is_some(&data.device_name, "device_name cannot be blank")?;
_check_is_some(&data.device_type, "device_type cannot be blank")?; _check_is_some(&data.device_type, "device_type cannot be blank")?;
_sso_login(data, &mut user_id, &mut conn, &client_header.ip, &client_version, webauthn).await _sso_login(data, &mut user_id, &mut conn, &client_header.ip, &client_version).await
} }
"authorization_code" => err!("SSO sign-in is not available"), "authorization_code" => err!("SSO sign-in is not available"),
t => err!("Invalid type", t), t => err!("Invalid type", t),
@ -171,7 +169,6 @@ async fn _sso_login(
conn: &mut DbConn, conn: &mut DbConn,
ip: &ClientIp, ip: &ClientIp,
client_version: &Option<ClientVersion>, client_version: &Option<ClientVersion>,
webauthn: Webauthn2FaConfig<'_>,
) -> JsonResult { ) -> JsonResult {
AuthMethod::Sso.check_scope(data.scope.as_ref())?; AuthMethod::Sso.check_scope(data.scope.as_ref())?;
@ -270,7 +267,7 @@ async fn _sso_login(
} }
Some((mut user, sso_user)) => { Some((mut user, sso_user)) => {
let mut device = get_device(&data, conn, &user).await?; let mut device = get_device(&data, conn, &user).await?;
let twofactor_token = twofactor_auth(&user, &data, &mut device, ip, client_version, webauthn, conn).await?; let twofactor_token = twofactor_auth(&mut user, &data, &mut device, ip, client_version, conn).await?;
if user.private_key.is_none() { if user.private_key.is_none() {
// User was invited a stub was created // User was invited a stub was created
@ -325,7 +322,6 @@ async fn _password_login(
conn: &mut DbConn, conn: &mut DbConn,
ip: &ClientIp, ip: &ClientIp,
client_version: &Option<ClientVersion>, client_version: &Option<ClientVersion>,
webauthn: Webauthn2FaConfig<'_>,
) -> JsonResult { ) -> JsonResult {
// Validate scope // Validate scope
AuthMethod::Password.check_scope(data.scope.as_ref())?; AuthMethod::Password.check_scope(data.scope.as_ref())?;
@ -435,14 +431,13 @@ async fn _password_login(
let mut device = get_device(&data, conn, &user).await?; let mut device = get_device(&data, conn, &user).await?;
let twofactor_token = twofactor_auth(&user, &data, &mut device, ip, client_version, webauthn, conn).await?; let twofactor_token = twofactor_auth(&mut user, &data, &mut device, ip, client_version, conn).await?;
let auth_tokens = auth::AuthTokens::new(&device, &user, AuthMethod::Password, data.client_id); let auth_tokens = auth::AuthTokens::new(&device, &user, AuthMethod::Password, data.client_id);
authenticated_response(&user, &mut device, auth_tokens, twofactor_token, &now, conn, ip).await authenticated_response(&user, &mut device, auth_tokens, twofactor_token, &now, conn, ip).await
} }
#[allow(clippy::too_many_arguments)]
async fn authenticated_response( async fn authenticated_response(
user: &User, user: &User,
device: &mut Device, device: &mut Device,
@ -663,12 +658,11 @@ async fn get_device(data: &ConnectData, conn: &mut DbConn, user: &User) -> ApiRe
} }
async fn twofactor_auth( async fn twofactor_auth(
user: &User, user: &mut User,
data: &ConnectData, data: &ConnectData,
device: &mut Device, device: &mut Device,
ip: &ClientIp, ip: &ClientIp,
client_version: &Option<ClientVersion>, client_version: &Option<ClientVersion>,
webauthn: Webauthn2FaConfig<'_>,
conn: &mut DbConn, conn: &mut DbConn,
) -> ApiResult<Option<String>> { ) -> ApiResult<Option<String>> {
let twofactors = TwoFactor::find_by_user(&user.uuid, conn).await; let twofactors = TwoFactor::find_by_user(&user.uuid, conn).await;
@ -688,7 +682,7 @@ async fn twofactor_auth(
Some(ref code) => code, Some(ref code) => code,
None => { None => {
err_json!( err_json!(
_json_err_twofactor(&twofactor_ids, &user.uuid, data, client_version, webauthn, conn).await?, _json_err_twofactor(&twofactor_ids, &user.uuid, data, client_version, conn).await?,
"2FA token not provided" "2FA token not provided"
) )
} }
@ -705,9 +699,7 @@ async fn twofactor_auth(
Some(TwoFactorType::Authenticator) => { Some(TwoFactorType::Authenticator) => {
authenticator::validate_totp_code_str(&user.uuid, twofactor_code, &selected_data?, ip, conn).await? authenticator::validate_totp_code_str(&user.uuid, twofactor_code, &selected_data?, ip, conn).await?
} }
Some(TwoFactorType::Webauthn) => { Some(TwoFactorType::Webauthn) => webauthn::validate_webauthn_login(&user.uuid, twofactor_code, conn).await?,
webauthn::validate_webauthn_login(&user.uuid, twofactor_code, webauthn, conn).await?
}
Some(TwoFactorType::YubiKey) => yubikey::validate_yubikey_login(twofactor_code, &selected_data?).await?, Some(TwoFactorType::YubiKey) => yubikey::validate_yubikey_login(twofactor_code, &selected_data?).await?,
Some(TwoFactorType::Duo) => { Some(TwoFactorType::Duo) => {
match CONFIG.duo_use_iframe() { match CONFIG.duo_use_iframe() {
@ -731,7 +723,6 @@ async fn twofactor_auth(
Some(TwoFactorType::Email) => { Some(TwoFactorType::Email) => {
email::validate_email_code_str(&user.uuid, twofactor_code, &selected_data?, &ip.ip, conn).await? email::validate_email_code_str(&user.uuid, twofactor_code, &selected_data?, &ip.ip, conn).await?
} }
Some(TwoFactorType::Remember) => { Some(TwoFactorType::Remember) => {
match device.twofactor_remember { match device.twofactor_remember {
Some(ref code) if !CONFIG.disable_2fa_remember() && ct_eq(code, twofactor_code) => { Some(ref code) if !CONFIG.disable_2fa_remember() && ct_eq(code, twofactor_code) => {
@ -739,12 +730,28 @@ async fn twofactor_auth(
} }
_ => { _ => {
err_json!( err_json!(
_json_err_twofactor(&twofactor_ids, &user.uuid, data, client_version, webauthn, conn).await?, _json_err_twofactor(&twofactor_ids, &user.uuid, data, client_version, conn).await?,
"2FA Remember token not provided" "2FA Remember token not provided"
) )
} }
} }
} }
Some(TwoFactorType::RecoveryCode) => {
// Check if recovery code is correct
if !user.check_valid_recovery_code(twofactor_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, device.atype, &ip.ip, conn).await?;
log_user_event(EventType::UserRecovered2fa as i32, &user.uuid, device.atype, &ip.ip, conn).await;
// Remove the recovery code, not needed without twofactors
user.totp_recover = None;
user.save(conn).await?;
}
_ => err!( _ => err!(
"Invalid two factor provider", "Invalid two factor provider",
ErrorEvent { ErrorEvent {
@ -773,7 +780,6 @@ async fn _json_err_twofactor(
user_id: &UserId, user_id: &UserId,
data: &ConnectData, data: &ConnectData,
client_version: &Option<ClientVersion>, client_version: &Option<ClientVersion>,
webauthn: Webauthn2FaConfig<'_>,
conn: &mut DbConn, conn: &mut DbConn,
) -> ApiResult<Value> { ) -> ApiResult<Value> {
let mut result = json!({ let mut result = json!({
@ -793,7 +799,7 @@ async fn _json_err_twofactor(
Some(TwoFactorType::Authenticator) => { /* Nothing to do for TOTP */ } Some(TwoFactorType::Authenticator) => { /* Nothing to do for TOTP */ }
Some(TwoFactorType::Webauthn) if CONFIG.domain_set() => { Some(TwoFactorType::Webauthn) if CONFIG.domain_set() => {
let request = webauthn::generate_webauthn_login(user_id, webauthn, conn).await?; let request = webauthn::generate_webauthn_login(user_id, conn).await?;
result["TwoFactorProviders2"][provider.to_string()] = request.0; result["TwoFactorProviders2"][provider.to_string()] = request.0;
} }

4
src/api/mod.rs

@ -114,8 +114,8 @@ async fn master_password_policy(user: &User, conn: &DbConn) -> Value {
enforce_on_login: acc.enforce_on_login || policy.enforce_on_login, enforce_on_login: acc.enforce_on_login || policy.enforce_on_login,
} }
})) }))
} else if let Some(policy_str) = CONFIG.sso_master_password_policy().filter(|_| CONFIG.sso_enabled()) { } else if CONFIG.sso_enabled() {
serde_json::from_str(&policy_str).unwrap_or(json!({})) CONFIG.sso_master_password_policy_value().unwrap_or(json!({}))
} else { } else {
json!({}) json!({})
}; };

1
src/api/web.rs

@ -64,6 +64,7 @@ fn vaultwarden_css() -> Cached<Css<String>> {
"sso_enabled": CONFIG.sso_enabled(), "sso_enabled": CONFIG.sso_enabled(),
"sso_only": CONFIG.sso_enabled() && CONFIG.sso_only(), "sso_only": CONFIG.sso_enabled() && CONFIG.sso_only(),
"yubico_enabled": CONFIG._enable_yubico() && CONFIG.yubico_client_id().is_some() && CONFIG.yubico_secret_key().is_some(), "yubico_enabled": CONFIG._enable_yubico() && CONFIG.yubico_client_id().is_some() && CONFIG.yubico_secret_key().is_some(),
"webauthn_2fa_supported": CONFIG.is_webauthn_2fa_supported(),
}); });
let scss = match CONFIG.render_template("scss/vaultwarden.scss", &css_options) { let scss = match CONFIG.render_template("scss/vaultwarden.scss", &css_options) {

43
src/config.rs

@ -639,9 +639,15 @@ make_config! {
/// Timeout when acquiring database connection /// Timeout when acquiring database connection
database_timeout: u64, false, def, 30; database_timeout: u64, false, def, 30;
/// Database connection pool size /// Timeout in seconds before idle connections to the database are closed
database_idle_timeout: u64, false, def, 600;
/// Database connection max pool size
database_max_conns: u32, false, def, 10; database_max_conns: u32, false, def, 10;
/// Database connection min pool size
database_min_conns: u32, false, def, 2;
/// Database connection init |> SQL statements to run when creating a new database connection, mainly useful for connection-scoped pragmas. If empty, a database-specific default is used. /// Database connection init |> SQL statements to run when creating a new database connection, mainly useful for connection-scoped pragmas. If empty, a database-specific default is used.
database_conn_init: String, false, def, String::new(); database_conn_init: String, false, def, String::new();
@ -691,7 +697,7 @@ make_config! {
/// Allow email association |> Associate existing non-SSO user based on email /// Allow email association |> Associate existing non-SSO user based on email
sso_signups_match_email: bool, true, def, true; sso_signups_match_email: bool, true, def, true;
/// Allow unknown email verification status |> Allowing this with `SSO_SIGNUPS_MATCH_EMAIL=true` open potential account takeover. /// Allow unknown email verification status |> Allowing this with `SSO_SIGNUPS_MATCH_EMAIL=true` open potential account takeover.
sso_allow_unknown_email_verification: bool, false, def, false; sso_allow_unknown_email_verification: bool, true, def, false;
/// Client ID /// Client ID
sso_client_id: String, true, def, String::new(); sso_client_id: String, true, def, String::new();
/// Client Key /// Client Key
@ -836,6 +842,14 @@ fn validate_config(cfg: &ConfigItems) -> Result<(), Error> {
err!(format!("`DATABASE_MAX_CONNS` contains an invalid value. Ensure it is between 1 and {limit}.",)); err!(format!("`DATABASE_MAX_CONNS` contains an invalid value. Ensure it is between 1 and {limit}.",));
} }
if cfg.database_min_conns < 1 || cfg.database_min_conns > limit {
err!(format!("`DATABASE_MIN_CONNS` contains an invalid value. Ensure it is between 1 and {limit}.",));
}
if cfg.database_min_conns > cfg.database_max_conns {
err!(format!("`DATABASE_MIN_CONNS` must be smaller than or equal to `DATABASE_MAX_CONNS`.",));
}
if let Some(log_file) = &cfg.log_file { if let Some(log_file) = &cfg.log_file {
if std::fs::OpenOptions::new().append(true).create(true).open(log_file).is_err() { if std::fs::OpenOptions::new().append(true).create(true).open(log_file).is_err() {
err!("Unable to write to log file", log_file); err!("Unable to write to log file", log_file);
@ -968,7 +982,7 @@ fn validate_config(cfg: &ConfigItems) -> Result<(), Error> {
validate_internal_sso_issuer_url(&cfg.sso_authority)?; validate_internal_sso_issuer_url(&cfg.sso_authority)?;
validate_internal_sso_redirect_url(&cfg.sso_callback_path)?; validate_internal_sso_redirect_url(&cfg.sso_callback_path)?;
check_master_password_policy(&cfg.sso_master_password_policy)?; validate_sso_master_password_policy(&cfg.sso_master_password_policy)?;
} }
if cfg._enable_yubico { if cfg._enable_yubico {
@ -1184,12 +1198,19 @@ fn validate_internal_sso_redirect_url(sso_callback_path: &String) -> Result<open
} }
} }
fn check_master_password_policy(sso_master_password_policy: &Option<String>) -> Result<(), Error> { fn validate_sso_master_password_policy(
sso_master_password_policy: &Option<String>,
) -> Result<Option<serde_json::Value>, Error> {
let policy = sso_master_password_policy.as_ref().map(|mpp| serde_json::from_str::<serde_json::Value>(mpp)); let policy = sso_master_password_policy.as_ref().map(|mpp| serde_json::from_str::<serde_json::Value>(mpp));
if let Some(Err(error)) = policy {
err!(format!("Invalid sso_master_password_policy ({error}), Ensure that it's correctly escaped with ''")) match policy {
None => Ok(None),
Some(Ok(jsobject @ serde_json::Value::Object(_))) => Ok(Some(jsobject)),
Some(Ok(_)) => err!("Invalid sso_master_password_policy: parsed value is not a JSON object"),
Some(Err(error)) => {
err!(format!("Invalid sso_master_password_policy ({error}), Ensure that it's correctly escaped with ''"))
}
} }
Ok(())
} }
/// Extracts an RFC 6454 web origin from a URL. /// Extracts an RFC 6454 web origin from a URL.
@ -1534,6 +1555,10 @@ impl Config {
} }
} }
pub fn is_webauthn_2fa_supported(&self) -> bool {
Url::parse(&self.domain()).expect("DOMAIN not a valid URL").domain().is_some()
}
/// Tests whether the admin token is set to a non-empty value. /// Tests whether the admin token is set to a non-empty value.
pub fn is_admin_token_set(&self) -> bool { pub fn is_admin_token_set(&self) -> bool {
let token = self.admin_token(); let token = self.admin_token();
@ -1594,6 +1619,10 @@ impl Config {
validate_internal_sso_redirect_url(&self.sso_callback_path()) validate_internal_sso_redirect_url(&self.sso_callback_path())
} }
pub fn sso_master_password_policy_value(&self) -> Option<serde_json::Value> {
validate_sso_master_password_policy(&self.sso_master_password_policy()).ok().flatten()
}
pub fn sso_scopes_vec(&self) -> Vec<String> { pub fn sso_scopes_vec(&self) -> Vec<String> {
self.sso_scopes().split_whitespace().map(str::to_string).collect() self.sso_scopes().split_whitespace().map(str::to_string).collect()
} }

2
src/db/mod.rs

@ -136,6 +136,8 @@ macro_rules! generate_connections {
let manager = ConnectionManager::new(&url); let manager = ConnectionManager::new(&url);
let pool = Pool::builder() let pool = Pool::builder()
.max_size(CONFIG.database_max_conns()) .max_size(CONFIG.database_max_conns())
.min_idle(Some(CONFIG.database_min_conns()))
.idle_timeout(Some(Duration::from_secs(CONFIG.database_idle_timeout())))
.connection_timeout(Duration::from_secs(CONFIG.database_timeout())) .connection_timeout(Duration::from_secs(CONFIG.database_timeout()))
.connection_customizer(Box::new(DbConnOptions{ .connection_customizer(Box::new(DbConnOptions{
init_stmts: conn_type.get_init_stmts() init_stmts: conn_type.get_init_stmts()

1
src/db/models/two_factor.rs

@ -31,6 +31,7 @@ pub enum TwoFactorType {
Remember = 5, Remember = 5,
OrganizationDuo = 6, OrganizationDuo = 6,
Webauthn = 7, Webauthn = 7,
RecoveryCode = 8,
// These are implementation details // These are implementation details
U2fRegisterChallenge = 1000, U2fRegisterChallenge = 1000,

2
src/main.rs

@ -62,7 +62,6 @@ mod sso_client;
mod util; mod util;
use crate::api::core::two_factor::duo_oidc::purge_duo_contexts; use crate::api::core::two_factor::duo_oidc::purge_duo_contexts;
use crate::api::core::two_factor::webauthn::WEBAUTHN_2FA_CONFIG;
use crate::api::purge_auth_requests; use crate::api::purge_auth_requests;
use crate::api::{WS_ANONYMOUS_SUBSCRIPTIONS, WS_USERS}; use crate::api::{WS_ANONYMOUS_SUBSCRIPTIONS, WS_USERS};
pub use config::{PathType, CONFIG}; pub use config::{PathType, CONFIG};
@ -620,7 +619,6 @@ async fn launch_rocket(pool: db::DbPool, extra_debug: bool) -> Result<(), Error>
.manage(pool) .manage(pool)
.manage(Arc::clone(&WS_USERS)) .manage(Arc::clone(&WS_USERS))
.manage(Arc::clone(&WS_ANONYMOUS_SUBSCRIPTIONS)) .manage(Arc::clone(&WS_ANONYMOUS_SUBSCRIPTIONS))
.manage(Arc::clone(&WEBAUTHN_2FA_CONFIG))
.attach(util::AppHeaders()) .attach(util::AppHeaders())
.attach(util::Cors()) .attach(util::Cors())
.attach(util::BetterLogging(extra_debug)); .attach(util::BetterLogging(extra_debug));

13
src/static/templates/scss/vaultwarden.scss.hbs

@ -55,7 +55,7 @@ app-root ng-component > form > div:nth-child(1) > div > button[buttontype="secon
{{/if}} {{/if}}
/* Hide the `Log in with passkey` settings */ /* Hide the `Log in with passkey` settings */
app-change-password app-webauthn-login-settings { app-user-layout app-password-settings app-webauthn-login-settings {
@extend %vw-hide; @extend %vw-hide;
} }
/* Hide Log in with passkey on the login page */ /* Hide Log in with passkey on the login page */
@ -133,6 +133,10 @@ bit-nav-logo bit-nav-item a:before {
bit-nav-logo bit-nav-item .bwi-shield { bit-nav-logo bit-nav-item .bwi-shield {
@extend %vw-hide; @extend %vw-hide;
} }
/* Hide Device Login Protection button on user settings page */
app-user-layout app-danger-zone button:nth-child(1) {
@extend %vw-hide;
}
/**** END Static Vaultwarden Changes ****/ /**** END Static Vaultwarden Changes ****/
/**** START Dynamic Vaultwarden Changes ****/ /**** START Dynamic Vaultwarden Changes ****/
{{#if signup_disabled}} {{#if signup_disabled}}
@ -168,6 +172,13 @@ app-root a[routerlink="/signup"] {
} }
{{/unless}} {{/unless}}
{{#unless webauthn_2fa_supported}}
/* Hide `Passkey` 2FA if it is not supported */
.providers-2fa-7 {
@extend %vw-hide;
}
{{/unless}}
{{#unless emergency_access_allowed}} {{#unless emergency_access_allowed}}
/* Hide Emergency Access if not allowed */ /* Hide Emergency Access if not allowed */
bit-nav-item[route="settings/emergency-access"] { bit-nav-item[route="settings/emergency-access"] {

Loading…
Cancel
Save