Compare commits

..
1 Commits
Author SHA1 Message Date
Daniel García d518f6f01e Implement V2 registration support 2026-08-01 04:06:39 +02:00
75 changed files with 1687 additions and 1611 deletions
-8
View File
@@ -316,14 +316,6 @@
## unauthenticated access to potentially sensitive data. ## unauthenticated access to potentially sensitive data.
# SHOW_PASSWORD_HINT=false # SHOW_PASSWORD_HINT=false
#########################
### Client settings ###
#########################
## Control whether clients onboarding interstitials are suppressed
## (post-login welcome dialogs, extension install prompts, setup extension redirects, and premium upsell modals)
# CLIENT_SUPPRESS_ONBOARDING=false
######################### #########################
### Advanced settings ### ### Advanced settings ###
######################### #########################
+2 -2
View File
@@ -41,12 +41,12 @@ jobs:
# Uses the Docker-based action (hadolint pre-bundled in ghcr.io/hadolint/hadolint:v2.14.0-debian) # Uses the Docker-based action (hadolint pre-bundled in ghcr.io/hadolint/hadolint:v2.14.0-debian)
# so no binary is downloaded at runtime. Pinned by commit SHA for supply-chain safety. # so no binary is downloaded at runtime. Pinned by commit SHA for supply-chain safety.
- name: Run hadolint on Dockerfile.debian - name: Run hadolint on Dockerfile.debian
uses: hadolint/hadolint-action@2a66e89f53d0771bb131a7fa31f3136336094aa6 # v3.4.0 uses: hadolint/hadolint-action@2332a7b74a6de0dda2e2221d575162eba76ba5e5 # v3.3.0
with: with:
dockerfile: docker/Dockerfile.debian dockerfile: docker/Dockerfile.debian
- name: Run hadolint on Dockerfile.alpine - name: Run hadolint on Dockerfile.alpine
uses: hadolint/hadolint-action@2a66e89f53d0771bb131a7fa31f3136336094aa6 # v3.4.0 uses: hadolint/hadolint-action@2332a7b74a6de0dda2e2221d575162eba76ba5e5 # v3.3.0
with: with:
dockerfile: docker/Dockerfile.alpine dockerfile: docker/Dockerfile.alpine
# End Test Dockerfiles with hadolint # End Test Dockerfiles with hadolint
+10 -10
View File
@@ -106,7 +106,7 @@ jobs:
# Login to Docker Hub # Login to Docker Hub
- name: Login to Docker Hub - name: Login to Docker Hub
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0 uses: docker/login-action@abd2ef45e78c5afb21d64d4ca52ee8550d9572c7 # v4.5.1
with: with:
username: ${{ secrets.DOCKERHUB_USERNAME }} username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }} password: ${{ secrets.DOCKERHUB_TOKEN }}
@@ -121,7 +121,7 @@ jobs:
# Login to GitHub Container Registry # Login to GitHub Container Registry
- name: Login to GitHub Container Registry - name: Login to GitHub Container Registry
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0 uses: docker/login-action@abd2ef45e78c5afb21d64d4ca52ee8550d9572c7 # v4.5.1
with: with:
registry: ghcr.io registry: ghcr.io
username: ${{ github.repository_owner }} username: ${{ github.repository_owner }}
@@ -137,7 +137,7 @@ jobs:
# Login to Quay.io # Login to Quay.io
- name: Login to Quay.io - name: Login to Quay.io
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0 uses: docker/login-action@abd2ef45e78c5afb21d64d4ca52ee8550d9572c7 # v4.5.1
with: with:
registry: quay.io registry: quay.io
username: ${{ secrets.QUAY_USERNAME }} username: ${{ secrets.QUAY_USERNAME }}
@@ -237,7 +237,7 @@ jobs:
# Upload artifacts to Github Actions and Attest the binaries # Upload artifacts to Github Actions and Attest the binaries
- name: Attest binaries - name: Attest binaries
uses: actions/attest@1e69f48acb82d1966a394da916b4c1698aa569d6 # v4.2.2 uses: actions/attest@f7c74d28b9d84cb8768d0b8ca14a4bac6ef463e6 # v4.2.0
with: with:
subject-path: vaultwarden-${{ env.NORMALIZED_ARCH }} subject-path: vaultwarden-${{ env.NORMALIZED_ARCH }}
@@ -272,7 +272,7 @@ jobs:
# Login to Docker Hub # Login to Docker Hub
- name: Login to Docker Hub - name: Login to Docker Hub
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0 uses: docker/login-action@abd2ef45e78c5afb21d64d4ca52ee8550d9572c7 # v4.5.1
with: with:
username: ${{ secrets.DOCKERHUB_USERNAME }} username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }} password: ${{ secrets.DOCKERHUB_TOKEN }}
@@ -287,7 +287,7 @@ jobs:
# Login to GitHub Container Registry # Login to GitHub Container Registry
- name: Login to GitHub Container Registry - name: Login to GitHub Container Registry
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0 uses: docker/login-action@abd2ef45e78c5afb21d64d4ca52ee8550d9572c7 # v4.5.1
with: with:
registry: ghcr.io registry: ghcr.io
username: ${{ github.repository_owner }} username: ${{ github.repository_owner }}
@@ -303,7 +303,7 @@ jobs:
# Login to Quay.io # Login to Quay.io
- name: Login to Quay.io - name: Login to Quay.io
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0 uses: docker/login-action@abd2ef45e78c5afb21d64d4ca52ee8550d9572c7 # v4.5.1
with: with:
registry: quay.io registry: quay.io
username: ${{ secrets.QUAY_USERNAME }} username: ${{ secrets.QUAY_USERNAME }}
@@ -365,7 +365,7 @@ jobs:
# Attest container images # Attest container images
- name: Attest - docker.io - ${{ matrix.base_image }} - name: Attest - docker.io - ${{ matrix.base_image }}
if: ${{ vars.DOCKERHUB_REPO != '' && env.DIGEST_SHA != ''}} if: ${{ vars.DOCKERHUB_REPO != '' && env.DIGEST_SHA != ''}}
uses: actions/attest@1e69f48acb82d1966a394da916b4c1698aa569d6 # v4.2.2 uses: actions/attest@f7c74d28b9d84cb8768d0b8ca14a4bac6ef463e6 # v4.2.0
with: with:
subject-name: ${{ vars.DOCKERHUB_REPO }} subject-name: ${{ vars.DOCKERHUB_REPO }}
subject-digest: ${{ env.DIGEST_SHA }} subject-digest: ${{ env.DIGEST_SHA }}
@@ -373,7 +373,7 @@ jobs:
- name: Attest - ghcr.io - ${{ matrix.base_image }} - name: Attest - ghcr.io - ${{ matrix.base_image }}
if: ${{ vars.GHCR_REPO != '' && env.DIGEST_SHA != ''}} if: ${{ vars.GHCR_REPO != '' && env.DIGEST_SHA != ''}}
uses: actions/attest@1e69f48acb82d1966a394da916b4c1698aa569d6 # v4.2.2 uses: actions/attest@f7c74d28b9d84cb8768d0b8ca14a4bac6ef463e6 # v4.2.0
with: with:
subject-name: ${{ vars.GHCR_REPO }} subject-name: ${{ vars.GHCR_REPO }}
subject-digest: ${{ env.DIGEST_SHA }} subject-digest: ${{ env.DIGEST_SHA }}
@@ -381,7 +381,7 @@ jobs:
- name: Attest - quay.io - ${{ matrix.base_image }} - name: Attest - quay.io - ${{ matrix.base_image }}
if: ${{ vars.QUAY_REPO != '' && env.DIGEST_SHA != ''}} if: ${{ vars.QUAY_REPO != '' && env.DIGEST_SHA != ''}}
uses: actions/attest@1e69f48acb82d1966a394da916b4c1698aa569d6 # v4.2.2 uses: actions/attest@f7c74d28b9d84cb8768d0b8ca14a4bac6ef463e6 # v4.2.0
with: with:
subject-name: ${{ vars.QUAY_REPO }} subject-name: ${{ vars.QUAY_REPO }}
subject-digest: ${{ env.DIGEST_SHA }} subject-digest: ${{ env.DIGEST_SHA }}
+1 -1
View File
@@ -50,6 +50,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@5595ccaf912efad79be6eef63a5619ff05969be3 # v4.37.6 uses: github/codeql-action/upload-sarif@e4fba868fa4b1b91e1fdab776edc8cfbe6e9fb81 # v4.37.3
with: with:
sarif_file: 'trivy-results.sarif' sarif_file: 'trivy-results.sarif'
+1 -1
View File
@@ -23,4 +23,4 @@ jobs:
# When this version is updated, do not forget to update this in `.pre-commit-config.yaml` too # When this version is updated, do not forget to update this in `.pre-commit-config.yaml` too
- name: Spell Check Repo - name: Spell Check Repo
uses: crate-ci/typos@8a48f81b6c64dcfea44b3633223084c4be58ac5f # v1.49.0 uses: crate-ci/typos@bee27e3a4fd1ea2111cf90ab89cd076c870fce14 # v1.48.0
+1 -1
View File
@@ -24,7 +24,7 @@ jobs:
persist-credentials: false persist-credentials: false
- name: Run zizmor - name: Run zizmor
uses: zizmorcore/zizmor-action@3dc1ecc9bcb9e94e9b2c709687979e1298497054 # v0.6.2 uses: zizmorcore/zizmor-action@6fc4b006235f201fdab3722e17240ab420d580e5 # v0.6.1
with: with:
# intentionally not scanning the entire repository, # intentionally not scanning the entire repository,
# since it contains integration tests. # since it contains integration tests.
+5 -4
View File
@@ -18,10 +18,9 @@ repos:
# When this version is updated, do not forget to update this in `.github/workflows/typos.yaml` too # When this version is updated, do not forget to update this in `.github/workflows/typos.yaml` too
- repo: https://github.com/crate-ci/typos - repo: https://github.com/crate-ci/typos
rev: 8a48f81b6c64dcfea44b3633223084c4be58ac5f # v1.49.0 rev: bee27e3a4fd1ea2111cf90ab89cd076c870fce14 # v1.48.0
hooks: hooks:
- id: typos - id: typos
always_run: true
- repo: local - repo: local
hooks: hooks:
@@ -39,7 +38,8 @@ repos:
entry: cargo test entry: cargo test
language: system language: system
args: [ "--features", "sqlite,mysql,postgresql", "--" ] args: [ "--features", "sqlite,mysql,postgresql", "--" ]
types_or: [ rust, toml ] # Cargo.lock matches toml type which is intended types_or: [ rust, file ]
files: (Cargo.toml|Cargo.lock|rust-toolchain.toml|rustfmt.toml|.*\.rs$)
pass_filenames: false pass_filenames: false
- id: cargo-clippy - id: cargo-clippy
name: cargo clippy name: cargo clippy
@@ -47,7 +47,8 @@ repos:
entry: cargo clippy entry: cargo clippy
language: system language: system
args: [ "--features", "sqlite,mysql,postgresql", "--", "-D", "warnings" ] args: [ "--features", "sqlite,mysql,postgresql", "--", "-D", "warnings" ]
types_or: [ rust, toml ] # Cargo.lock matches toml type which is intended types_or: [ rust, file ]
files: (Cargo.toml|Cargo.lock|rust-toolchain.toml|rustfmt.toml|.*\.rs$)
pass_filenames: false pass_filenames: false
- id: check-docker-templates - id: check-docker-templates
name: check docker templates name: check docker templates
+1
View File
@@ -15,6 +15,7 @@ extend-ignore-re = [
"(?i)helo_name", "(?i)helo_name",
"Server name sent during.+HELO", "Server name sent during.+HELO",
# COSE Is short for CBOR Object Signing and Encryption, ignore these specific items # COSE Is short for CBOR Object Signing and Encryption, ignore these specific items
"COSE",
"COSEKey", "COSEKey",
"COSEAlgorithm", "COSEAlgorithm",
# Ignore this specific string as it's valid # Ignore this specific string as it's valid
Generated
+174 -185
View File
@@ -22,9 +22,9 @@ dependencies = [
[[package]] [[package]]
name = "aho-corasick" name = "aho-corasick"
version = "1.1.5" version = "1.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c982642fa9e8606056828ee9a8505737230110bb1099153c79efe865c59d12ba" checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301"
dependencies = [ dependencies = [
"memchr", "memchr",
] ]
@@ -52,9 +52,9 @@ checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"
[[package]] [[package]]
name = "android_system_properties" name = "android_system_properties"
version = "0.1.6" version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ae221649c9976a6f6c56ae1facf410f3ddb33cc661c4b7b61020a912d4237fbc" checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
dependencies = [ dependencies = [
"libc", "libc",
] ]
@@ -150,9 +150,9 @@ dependencies = [
[[package]] [[package]]
name = "async-compression" name = "async-compression"
version = "0.4.43" version = "0.4.42"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3976abdc8fe7d1133d43d304afd42abdf5bc3e1319d263d223bde07b5efc4be8" checksum = "e79b3f8a79cccc2898f31920fc69f304859b3bd567490f75ebf51ae1c792a9ac"
dependencies = [ dependencies = [
"compression-codecs", "compression-codecs",
"compression-core", "compression-core",
@@ -213,7 +213,7 @@ version = "3.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "290f7f2596bd5b78a9fec8088ccd89180d7f9f55b94b0576823bbbdc72ee8311" checksum = "290f7f2596bd5b78a9fec8088ccd89180d7f9f55b94b0576823bbbdc72ee8311"
dependencies = [ dependencies = [
"event-listener 5.4.2", "event-listener 5.4.1",
"event-listener-strategy", "event-listener-strategy",
"pin-project-lite", "pin-project-lite",
] ]
@@ -231,7 +231,7 @@ dependencies = [
"async-task", "async-task",
"blocking", "blocking",
"cfg-if", "cfg-if",
"event-listener 5.4.2", "event-listener 5.4.1",
"futures-lite", "futures-lite",
"rustix", "rustix",
] ]
@@ -349,9 +349,9 @@ checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53"
[[package]] [[package]]
name = "aws-config" name = "aws-config"
version = "1.10.1" 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 = "1b180a3c8b55960db3426d8964b8745e652466a1a49fe1a2eda828046d30b5e4" checksum = "701418aa459dac33e50a0f8e818e5662a16bc018a6ac7423659b70f3799d67a8"
dependencies = [ dependencies = [
"aws-credential-types", "aws-credential-types",
"aws-runtime", "aws-runtime",
@@ -369,7 +369,7 @@ dependencies = [
"bytes", "bytes",
"fastrand", "fastrand",
"hex", "hex",
"http 1.5.0", "http 1.4.2",
"sha1 0.10.7", "sha1 0.10.7",
"time", "time",
"tokio", "tokio",
@@ -392,9 +392,9 @@ dependencies = [
[[package]] [[package]]
name = "aws-runtime" name = "aws-runtime"
version = "1.9.1" 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 = "c9007227e10b5fed2f3e0a2beff489211e2b5604c400b7a9d5d81ca9d64c24bb" checksum = "a6b50a43f3ccdf331521c6d6c68b7cc9668b6e09d439ebda9569df5722324d76"
dependencies = [ dependencies = [
"aws-credential-types", "aws-credential-types",
"aws-sigv4", "aws-sigv4",
@@ -407,7 +407,7 @@ dependencies = [
"bytes", "bytes",
"bytes-utils", "bytes-utils",
"fastrand", "fastrand",
"http 1.5.0", "http 1.4.2",
"http-body 1.1.0", "http-body 1.1.0",
"percent-encoding", "percent-encoding",
"pin-project-lite", "pin-project-lite",
@@ -417,9 +417,9 @@ dependencies = [
[[package]] [[package]]
name = "aws-sdk-sso" name = "aws-sdk-sso"
version = "1.105.0" version = "1.104.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6ffd0fbe7873cb548a7aa60f9573c268fff94155397fd4f14dc9f1ecaaab8516" checksum = "b53416d16c278234845392e38d93bd4481d2f09daa0f005a2277f0aa91f59c22"
dependencies = [ dependencies = [
"arc-swap", "arc-swap",
"aws-credential-types", "aws-credential-types",
@@ -436,16 +436,16 @@ dependencies = [
"bytes", "bytes",
"fastrand", "fastrand",
"http 0.2.12", "http 0.2.12",
"http 1.5.0", "http 1.4.2",
"regex-lite", "regex-lite",
"tracing", "tracing",
] ]
[[package]] [[package]]
name = "aws-sdk-ssooidc" name = "aws-sdk-ssooidc"
version = "1.107.0" version = "1.106.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "175763eb222a46377df7aa257a3bca980ab3e96703fefc8f4d0b8da6ad2e254c" checksum = "cc9b706c3305ed0285d5b1b696c747aa34950f830fb03e3e6c76890f99b9f188"
dependencies = [ dependencies = [
"arc-swap", "arc-swap",
"aws-credential-types", "aws-credential-types",
@@ -462,16 +462,16 @@ dependencies = [
"bytes", "bytes",
"fastrand", "fastrand",
"http 0.2.12", "http 0.2.12",
"http 1.5.0", "http 1.4.2",
"regex-lite", "regex-lite",
"tracing", "tracing",
] ]
[[package]] [[package]]
name = "aws-sdk-sts" name = "aws-sdk-sts"
version = "1.110.0" version = "1.109.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dd8b14781dfbff48984017d57167b6ea0b6471c6920ec52b44a2677c7feb3c13" checksum = "32d214cdfa5bbe17f117e76a7643fadf32a5234fb597322ef8b1fb4b2f17dbbd"
dependencies = [ dependencies = [
"arc-swap", "arc-swap",
"aws-credential-types", "aws-credential-types",
@@ -489,7 +489,7 @@ dependencies = [
"aws-types", "aws-types",
"fastrand", "fastrand",
"http 0.2.12", "http 0.2.12",
"http 1.5.0", "http 1.4.2",
"regex-lite", "regex-lite",
"tracing", "tracing",
] ]
@@ -509,7 +509,7 @@ dependencies = [
"hex", "hex",
"hmac 0.13.0", "hmac 0.13.0",
"http 0.2.12", "http 0.2.12",
"http 1.5.0", "http 1.4.2",
"percent-encoding", "percent-encoding",
"sha2 0.11.0", "sha2 0.11.0",
"time", "time",
@@ -539,7 +539,7 @@ dependencies = [
"bytes-utils", "bytes-utils",
"futures-core", "futures-core",
"futures-util", "futures-util",
"http 1.5.0", "http 1.4.2",
"http-body 1.1.0", "http-body 1.1.0",
"http-body-util", "http-body-util",
"percent-encoding", "percent-encoding",
@@ -596,7 +596,7 @@ dependencies = [
"bytes", "bytes",
"fastrand", "fastrand",
"http 0.2.12", "http 0.2.12",
"http 1.5.0", "http 1.4.2",
"http-body 0.4.6", "http-body 0.4.6",
"http-body 1.1.0", "http-body 1.1.0",
"http-body-util", "http-body-util",
@@ -617,7 +617,7 @@ dependencies = [
"aws-smithy-types", "aws-smithy-types",
"bytes", "bytes",
"http 0.2.12", "http 0.2.12",
"http 1.5.0", "http 1.4.2",
"pin-project-lite", "pin-project-lite",
"tokio", "tokio",
"tracing", "tracing",
@@ -643,7 +643,7 @@ checksum = "7d56e0a4e53127a632224e43633b0fe045fa9e1e3cfc68b9830f1115e103f910"
dependencies = [ dependencies = [
"aws-smithy-runtime-api", "aws-smithy-runtime-api",
"aws-smithy-types", "aws-smithy-types",
"http 1.5.0", "http 1.4.2",
] ]
[[package]] [[package]]
@@ -656,7 +656,7 @@ dependencies = [
"bytes", "bytes",
"bytes-utils", "bytes-utils",
"http 0.2.12", "http 0.2.12",
"http 1.5.0", "http 1.4.2",
"http-body 0.4.6", "http-body 0.4.6",
"http-body 1.1.0", "http-body 1.1.0",
"http-body-util", "http-body-util",
@@ -714,12 +714,6 @@ version = "0.22.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
[[package]]
name = "base64"
version = "0.23.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac07cdecf99051d9a5238b80f35af32cdeba5b336e55d957b318b50137e18da5"
[[package]] [[package]]
name = "base64-simd" name = "base64-simd"
version = "0.8.0" version = "0.8.0"
@@ -934,12 +928,6 @@ version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
[[package]]
name = "cfg_aliases"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f079e83a288787bcd14a6aea84cee5c87a67c5a3e660c30f557a3d24761b3527"
[[package]] [[package]]
name = "chacha20" name = "chacha20"
version = "0.10.1" version = "0.10.1"
@@ -1141,13 +1129,12 @@ dependencies = [
] ]
[[package]] [[package]]
name = "crc-fast" name = "crc32c"
version = "1.10.0" version = "0.6.8"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e75b2483e97a5a7da73ac68a05b629f9c53cff58d8ed1c77866079e18b00dba5" checksum = "3a47af21622d091a8f0fb295b88bc886ac74efcc613efc19f5d0b21de5c89e47"
dependencies = [ dependencies = [
"digest 0.10.7", "rustc_version",
"spin 0.10.1",
] ]
[[package]] [[package]]
@@ -1167,14 +1154,13 @@ checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b"
[[package]] [[package]]
name = "cron" name = "cron"
version = "0.17.0" version = "0.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a5dcd6f69605c2956916ce24e8af637b754964c9a83f4662d3a2361654cdba09" checksum = "5877d3fbf742507b66bc2a1945106bd30dd8504019d596901ddd012a4dd01740"
dependencies = [ dependencies = [
"chrono", "chrono",
"once_cell", "once_cell",
"phf 0.11.3", "winnow 0.6.26",
"winnow 0.7.15",
] ]
[[package]] [[package]]
@@ -1394,9 +1380,9 @@ dependencies = [
[[package]] [[package]]
name = "data-encoding" name = "data-encoding"
version = "2.11.1" 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 = "4583a4551df46e2792f82ceeac45e850d2e2d5debba0b91f102385cda5b11f06" checksum = "a4ae5f15dda3c708c0ade84bfee31ccab44a3da4f88015ed22f63732abe300c8"
[[package]] [[package]]
name = "data-url" name = "data-url"
@@ -1652,13 +1638,13 @@ dependencies = [
[[package]] [[package]]
name = "displaydoc" name = "displaydoc"
version = "0.2.7" version = "0.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c6232dd377dcc64799954cbd3a9bb882e9cdc1308ccd87b1c098f1fb2eaf82a8" checksum = "1ac70aa55017e108007fbaf5aa0f54b021c98f92ff8af59d42eda9da96e3dd4f"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 3.0.3", "syn 2.0.119",
] ]
[[package]] [[package]]
@@ -1751,9 +1737,9 @@ dependencies = [
[[package]] [[package]]
name = "either" name = "either"
version = "1.17.0" version = "1.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9e5e8f6c15a24b9a3ee5efec809ccd006d3b30e8b3bb63c39af737c7f87daa1d" checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e"
[[package]] [[package]]
name = "elliptic-curve" name = "elliptic-curve"
@@ -1778,11 +1764,11 @@ dependencies = [
[[package]] [[package]]
name = "email-encoding" name = "email-encoding"
version = "0.4.2" version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "420b9da095f052ea597503e39073b5b3c522f7db933fbac202d91d24492693fd" checksum = "9298e6504d9b9e780ed3f7dfd43a61be8cd0e09eb07f7706a945b0072b6670b6"
dependencies = [ dependencies = [
"base64 0.23.1", "base64 0.22.1",
"memchr", "memchr",
] ]
@@ -1828,10 +1814,11 @@ checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0"
[[package]] [[package]]
name = "event-listener" name = "event-listener"
version = "5.4.2" version = "5.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a23add41df1562121a9393cb065eab5146a1242410f23a644851e90cfd669d2" checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab"
dependencies = [ dependencies = [
"concurrent-queue",
"parking", "parking",
"pin-project-lite", "pin-project-lite",
] ]
@@ -1842,7 +1829,7 @@ version = "0.5.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93" checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93"
dependencies = [ dependencies = [
"event-listener 5.4.2", "event-listener 5.4.1",
"pin-project-lite", "pin-project-lite",
] ]
@@ -2192,7 +2179,7 @@ dependencies = [
"fnv", "fnv",
"futures-core", "futures-core",
"futures-sink", "futures-sink",
"http 1.5.0", "http 1.4.2",
"indexmap 2.14.0", "indexmap 2.14.0",
"slab", "slab",
"tokio", "tokio",
@@ -2409,9 +2396,9 @@ dependencies = [
[[package]] [[package]]
name = "http" name = "http"
version = "1.5.0" version = "1.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "918d3568bebf352712bc2ef3d46a8bcf1a75b373be6539de198e9105cbbf9ce0" checksum = "6970f50e31d6fc17d3fa27329444bfa74e196cf62e95052a3f6fee181dba6425"
dependencies = [ dependencies = [
"bytes", "bytes",
"itoa", "itoa",
@@ -2435,7 +2422,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ca2a8f2913ee65f60facd6a5905613afaa448497a0230cc41ce022d93290bc2c" checksum = "ca2a8f2913ee65f60facd6a5905613afaa448497a0230cc41ce022d93290bc2c"
dependencies = [ dependencies = [
"bytes", "bytes",
"http 1.5.0", "http 1.4.2",
] ]
[[package]] [[package]]
@@ -2446,7 +2433,7 @@ checksum = "e9f41fd6a08e4d4ec69df65976da761afd5ad5e58a9d4acb46bd1c953a9e3ff2"
dependencies = [ dependencies = [
"bytes", "bytes",
"futures-core", "futures-core",
"http 1.5.0", "http 1.4.2",
"http-body 1.1.0", "http-body 1.1.0",
"pin-project-lite", "pin-project-lite",
] ]
@@ -2465,9 +2452,9 @@ checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
[[package]] [[package]]
name = "hybrid-array" name = "hybrid-array"
version = "0.4.14" version = "0.4.13"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "707114b52a152fa7bdb290cd7cd5912d9467273b6d74e21b8d81aca1f8533f6b" checksum = "818356c5132c1fede50f837ca96afbe78ff42413047f4abb886217845e1b6c8c"
dependencies = [ dependencies = [
"typenum", "typenum",
] ]
@@ -2506,7 +2493,7 @@ dependencies = [
"futures-channel", "futures-channel",
"futures-core", "futures-core",
"h2", "h2",
"http 1.5.0", "http 1.4.2",
"http-body 1.1.0", "http-body 1.1.0",
"httparse", "httparse",
"itoa", "itoa",
@@ -2522,10 +2509,10 @@ version = "0.27.9"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f" checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f"
dependencies = [ dependencies = [
"http 1.5.0", "http 1.4.2",
"hyper 1.11.0", "hyper 1.11.0",
"hyper-util", "hyper-util",
"rustls 0.23.43", "rustls 0.23.42",
"tokio", "tokio",
"tokio-rustls 0.26.4", "tokio-rustls 0.26.4",
"tower-service", "tower-service",
@@ -2541,7 +2528,7 @@ dependencies = [
"bytes", "bytes",
"futures-channel", "futures-channel",
"futures-util", "futures-util",
"http 1.5.0", "http 1.4.2",
"http-body 1.1.0", "http-body 1.1.0",
"hyper 1.11.0", "hyper 1.11.0",
"ipnet", "ipnet",
@@ -2733,9 +2720,9 @@ dependencies = [
[[package]] [[package]]
name = "ipnet" name = "ipnet"
version = "2.12.1" version = "2.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6a756c3fac73139e83f14c2d742155dd2b78d3ee56597b419a0579b7bdd6dd78" checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2"
dependencies = [ dependencies = [
"serde", "serde",
] ]
@@ -2774,9 +2761,9 @@ checksum = "47f142fe24a9c9944451e8349de0a56af5f3e7226dc46f3ed4d4ecc0b85af75e"
[[package]] [[package]]
name = "jiff" name = "jiff"
version = "0.2.35" version = "0.2.34"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "668b7183bd07af9a4885f5c35b0cc5c83c4607a913c16b7e17291832910d2dcc" checksum = "e184d09547b80eb7e20d141ba2fb1fbac843ca53f4cf1b31210adc4c1adc6e16"
dependencies = [ dependencies = [
"defmt", "defmt",
"jiff-core", "jiff-core",
@@ -2802,9 +2789,9 @@ dependencies = [
[[package]] [[package]]
name = "jiff-static" name = "jiff-static"
version = "0.2.35" version = "0.2.34"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3a69dcb3a21cfb32ce1cd056169337ca284af0766dd766e7878819b251a49204" checksum = "323da076b7a6faf914dc677cb05a4b907742ff7375c8322c9e7f5061e5e0e9de"
dependencies = [ dependencies = [
"jiff-core", "jiff-core",
"proc-macro2", "proc-macro2",
@@ -2878,9 +2865,9 @@ dependencies = [
[[package]] [[package]]
name = "job_scheduler_ng" name = "job_scheduler_ng"
version = "2.5.0" version = "2.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "576b4255ab9de8ce7b81060ec54b1b7f8499dfd6c16a66c4cd4cb1ad4eba27e3" checksum = "217723d58ee473953675d15f11e56898a611aca8ea044d5a34eabeade99ef613"
dependencies = [ dependencies = [
"chrono", "chrono",
"cron", "cron",
@@ -2956,18 +2943,18 @@ version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
dependencies = [ dependencies = [
"spin 0.9.9", "spin",
] ]
[[package]] [[package]]
name = "lettre" name = "lettre"
version = "0.11.23" version = "0.11.22"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f2c646bd5cc763b1087b15493e29a64be6147ba8f19342004fa52048ee596eae" checksum = "0da65617f6cb926332d039cb578aad56178da86e128db6a1b09f4c94fa5b3349"
dependencies = [ dependencies = [
"async-std", "async-std",
"async-trait", "async-trait",
"base64 0.23.1", "base64 0.22.1",
"email-encoding", "email-encoding",
"email_address", "email_address",
"fastrand", "fastrand",
@@ -2980,7 +2967,7 @@ dependencies = [
"nom 8.0.0", "nom 8.0.0",
"percent-encoding", "percent-encoding",
"quoted_printable", "quoted_printable",
"rustls 0.23.43", "rustls 0.23.42",
"rustls-native-certs", "rustls-native-certs",
"serde", "serde",
"socket2 0.6.5", "socket2 0.6.5",
@@ -3102,9 +3089,9 @@ dependencies = [
[[package]] [[package]]
name = "mea" name = "mea"
version = "0.6.5" version = "0.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "31fc7d159de0085ab6dd7ff145a9819442cfd3d098f783263120503c3f3e58b0" checksum = "2640d335e7273dacdcf51044026139b2e269c3bb0dfc3f8cb3496b85e3f6a42c"
dependencies = [ dependencies = [
"slab", "slab",
] ]
@@ -3189,7 +3176,7 @@ dependencies = [
"crossbeam-epoch", "crossbeam-epoch",
"crossbeam-utils", "crossbeam-utils",
"equivalent", "equivalent",
"event-listener 5.4.2", "event-listener 5.4.1",
"futures-util", "futures-util",
"parking_lot", "parking_lot",
"portable-atomic", "portable-atomic",
@@ -3207,11 +3194,11 @@ dependencies = [
"bytes", "bytes",
"encoding_rs", "encoding_rs",
"futures-util", "futures-util",
"http 1.5.0", "http 1.4.2",
"httparse", "httparse",
"memchr", "memchr",
"mime", "mime",
"spin 0.9.9", "spin",
"tokio", "tokio",
"tokio-util", "tokio-util",
"version_check", "version_check",
@@ -3234,18 +3221,6 @@ version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b" checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b"
[[package]]
name = "nix"
version = "0.31.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf20d2fde8ff38632c426f1165ed7436270b44f199fc55284c38276f9db47c3d"
dependencies = [
"bitflags 2.13.1",
"cfg-if",
"cfg_aliases",
"libc",
]
[[package]] [[package]]
name = "nom" name = "nom"
version = "7.1.3" version = "7.1.3"
@@ -3395,7 +3370,7 @@ dependencies = [
"base64 0.22.1", "base64 0.22.1",
"chrono", "chrono",
"getrandom 0.2.17", "getrandom 0.2.17",
"http 1.5.0", "http 1.4.2",
"rand 0.8.7", "rand 0.8.7",
"serde", "serde",
"serde_json", "serde_json",
@@ -3426,9 +3401,9 @@ dependencies = [
[[package]] [[package]]
name = "opendal" name = "opendal"
version = "0.58.1" version = "0.57.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4f20562cc7447fcc915fc5c23df305a412ea80a733c9f2fd9e2d267e2815be6d" checksum = "96c9c85ce253ff87225e7669979d877a20c98a06604ec9d6dd5f4473e08f1ae1"
dependencies = [ dependencies = [
"opendal-core", "opendal-core",
"opendal-service-fs", "opendal-service-fs",
@@ -3437,22 +3412,24 @@ dependencies = [
[[package]] [[package]]
name = "opendal-core" name = "opendal-core"
version = "0.58.1" version = "0.57.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec75551ff4cf3e57da98979f6a937aaa9ddb3915bf68cc17d03df733be6646ed" checksum = "c4f8607c90e2c963a91467f50fb49fbc7fb3d573f88cea219ca59ccd3740b309"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"base64 0.23.1", "base64 0.22.1",
"bytes", "bytes",
"futures", "futures",
"http 1.5.0", "http 1.4.2",
"http-body 1.1.0",
"jiff", "jiff",
"log", "log",
"md-5", "md-5",
"mea", "mea",
"percent-encoding", "percent-encoding",
"quick-xml", "quick-xml 0.39.4",
"reqsign-core", "reqsign-core",
"reqwest",
"serde", "serde",
"serde_json", "serde_json",
"tokio", "tokio",
@@ -3463,9 +3440,9 @@ dependencies = [
[[package]] [[package]]
name = "opendal-service-fs" name = "opendal-service-fs"
version = "0.58.1" version = "0.57.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "826c4e17a30643b888fe983897f9a4b23b07066e1d069727a923cc8fb419a702" checksum = "22e89a665fef0e6bd249cf5ea47fc174b7ba892159bee4b9382528b1ca873a2c"
dependencies = [ dependencies = [
"bytes", "bytes",
"log", "log",
@@ -3477,18 +3454,18 @@ dependencies = [
[[package]] [[package]]
name = "opendal-service-s3" name = "opendal-service-s3"
version = "0.58.1" version = "0.57.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "58e80cdf192d7eff05feed747894d64f81905ac4eaf132edf7ea270abdd2d663" checksum = "313d46c9f5ae70bca26b7c3e3fbb9b639292625f28af73aa016f47e788af9deb"
dependencies = [ dependencies = [
"base64 0.23.1", "base64 0.22.1",
"bytes", "bytes",
"crc-fast", "crc32c",
"http 1.5.0", "http 1.4.2",
"log", "log",
"md-5", "md-5",
"opendal-core", "opendal-core",
"quick-xml", "quick-xml 0.39.4",
"reqsign-aws-v4", "reqsign-aws-v4",
"reqsign-core", "reqsign-core",
"reqsign-file-read-tokio", "reqsign-file-read-tokio",
@@ -3507,7 +3484,7 @@ dependencies = [
"dyn-clone", "dyn-clone",
"ed25519-dalek", "ed25519-dalek",
"hmac 0.12.1", "hmac 0.12.1",
"http 1.5.0", "http 1.4.2",
"itertools", "itertools",
"log", "log",
"oauth2", "oauth2",
@@ -4030,6 +4007,16 @@ version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3"
[[package]]
name = "quick-xml"
version = "0.39.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cdcc8dd4e2f670d309a5f0e83fe36dfdc05af317008fea29144da1a2ac858e5e"
dependencies = [
"memchr",
"serde",
]
[[package]] [[package]]
name = "quick-xml" name = "quick-xml"
version = "0.41.0" version = "0.41.0"
@@ -4206,9 +4193,9 @@ dependencies = [
[[package]] [[package]]
name = "regex-automata" name = "regex-automata"
version = "0.4.18" version = "0.4.16"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ad8553b9b26413251cbf30e620595c7a41b3887f03da04579c0e6b0d6a06b4b2" checksum = "8fcfdb36bda0c880c5931cdc7a2bcdc8ba4556847b9d912bca70bc94708711ad"
dependencies = [ dependencies = [
"aho-corasick", "aho-corasick",
"memchr", "memchr",
@@ -4239,18 +4226,19 @@ dependencies = [
] ]
[[package]] [[package]]
name = "reqsign-aws-core" name = "reqsign-aws-v4"
version = "3.0.3" version = "3.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e4af084e1f3cbf3e67e0c972765399bce54ecec804cceba46b39a8331f3c1bff" checksum = "4e9e1168fab3883ec6afed1c2e20c25b2a09f366cdb662ac3e0878ae0332d63e"
dependencies = [ dependencies = [
"anyhow",
"bytes", "bytes",
"form_urlencoded", "form_urlencoded",
"hex", "hex",
"http 1.5.0", "http 1.4.2",
"log", "log",
"percent-encoding", "percent-encoding",
"quick-xml", "quick-xml 0.41.0",
"reqsign-core", "reqsign-core",
"rust-ini", "rust-ini",
"serde", "serde",
@@ -4260,33 +4248,19 @@ dependencies = [
] ]
[[package]] [[package]]
name = "reqsign-aws-v4" name = "reqsign-core"
version = "3.1.0" version = "3.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4ac5b3b7cefa28933792b439186459f77f19f9b6edbeab41b8b187150361a206" checksum = "514a1e0b4aa288652a3fdbda4f0a610f379cdf5374e55a37c9edd03d57ed856b"
dependencies = [
"bytes",
"http 1.5.0",
"log",
"quick-xml",
"reqsign-aws-core",
"reqsign-core",
"serde",
]
[[package]]
name = "reqsign-core"
version = "3.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c07dd510b1e1b9b241883e483358147fb2ed2d497a7b39b065ba61eb93deceb0"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"base64 0.23.1", "base64 0.22.1",
"bytes", "bytes",
"form_urlencoded",
"futures", "futures",
"hex", "hex",
"hmac 0.13.0", "hmac 0.13.0",
"http 1.5.0", "http 1.4.2",
"jiff", "jiff",
"log", "log",
"percent-encoding", "percent-encoding",
@@ -4297,9 +4271,9 @@ dependencies = [
[[package]] [[package]]
name = "reqsign-file-read-tokio" name = "reqsign-file-read-tokio"
version = "3.0.4" version = "3.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "663d9d55abd0df0830ef0ae43708297cc1371cf4e8ca91f3ac813c309cca8c98" checksum = "2b472a8d1f2e5a4be8ce13bb7bdf4b59e9bee613ce124aca23959ddb42176b39"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"reqsign-core", "reqsign-core",
@@ -4317,10 +4291,11 @@ dependencies = [
"cookie", "cookie",
"cookie_store", "cookie_store",
"encoding_rs", "encoding_rs",
"futures-channel",
"futures-core", "futures-core",
"futures-util", "futures-util",
"h2", "h2",
"http 1.5.0", "http 1.4.2",
"http-body 1.1.0", "http-body 1.1.0",
"http-body-util", "http-body-util",
"hyper 1.11.0", "hyper 1.11.0",
@@ -4331,7 +4306,7 @@ dependencies = [
"mime", "mime",
"percent-encoding", "percent-encoding",
"pin-project-lite", "pin-project-lite",
"rustls 0.23.43", "rustls 0.23.42",
"rustls-pki-types", "rustls-pki-types",
"rustls-platform-verifier", "rustls-platform-verifier",
"serde", "serde",
@@ -4600,9 +4575,9 @@ dependencies = [
[[package]] [[package]]
name = "rustls" name = "rustls"
version = "0.23.43" version = "0.23.42"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0283386ce02abc0151e1761d08802dfe86c173b0b494af5cbc086574e453da06" checksum = "3c54fcab019b409d04215d3a17cb438fd7fbf192ee61461f20f4fe18704bc138"
dependencies = [ dependencies = [
"log", "log",
"once_cell", "once_cell",
@@ -4654,7 +4629,7 @@ dependencies = [
"jni", "jni",
"log", "log",
"once_cell", "once_cell",
"rustls 0.23.43", "rustls 0.23.42",
"rustls-native-certs", "rustls-native-certs",
"rustls-platform-verifier-android", "rustls-platform-verifier-android",
"rustls-webpki 0.103.13", "rustls-webpki 0.103.13",
@@ -4744,9 +4719,9 @@ dependencies = [
[[package]] [[package]]
name = "schemars" name = "schemars"
version = "1.2.2" version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "687274d293b6cdc6e73e0fee520bf2049650090d7164f87672d212a3c530cf4a" checksum = "a2b42f36aa1cd011945615b92222f6bf73c599a102a300334cd7f8dbeec726cc"
dependencies = [ dependencies = [
"dyn-clone", "dyn-clone",
"ref-cast", "ref-cast",
@@ -4946,7 +4921,7 @@ dependencies = [
"indexmap 1.9.3", "indexmap 1.9.3",
"indexmap 2.14.0", "indexmap 2.14.0",
"schemars 0.9.0", "schemars 0.9.0",
"schemars 1.2.2", "schemars 1.2.1",
"serde_core", "serde_core",
"serde_json", "serde_json",
"serde_with_macros", "serde_with_macros",
@@ -5132,12 +5107,6 @@ version = "0.9.9"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3763264f6b73151db08c50ff20d7d8a0b8796e021cdea7ceedad07b80155fa0e" checksum = "3763264f6b73151db08c50ff20d7d8a0b8796e021cdea7ceedad07b80155fa0e"
[[package]]
name = "spin"
version = "0.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "023a211cb3138dbc438680b32560ad89f699977624c9f8dbb95a47d5b4c07dd3"
[[package]] [[package]]
name = "spinning_top" name = "spinning_top"
version = "0.3.0" version = "0.3.0"
@@ -5362,10 +5331,19 @@ dependencies = [
] ]
[[package]] [[package]]
name = "time" name = "threadpool"
version = "0.3.55" version = "1.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cdb87b95ec50ddfa440816d227a17b2ccbdda963a316a727fda0fc4334f7d134" checksum = "d050e60b33d41c19108b32cea32164033a9013fe3b46cbd4457559bfbf77afaa"
dependencies = [
"num_cpus",
]
[[package]]
name = "time"
version = "0.3.54"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3e1d5e639ff6bab73cb6885cc7e7b1de96c3f32c68ec55f3952614bec1092244"
dependencies = [ dependencies = [
"deranged", "deranged",
"libc", "libc",
@@ -5446,13 +5424,13 @@ dependencies = [
[[package]] [[package]]
name = "tokio-macros" name = "tokio-macros"
version = "2.7.2" version = "2.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78773a2a397f451582ce068015985c33193cf6dea8b74d2a639fe457b2f07b0e" checksum = "6328af13490e73a9b4694030fafd93f8c8c6a9dede33e821c3fc63eddf8042ba"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 3.0.3", "syn 2.0.119",
] ]
[[package]] [[package]]
@@ -5471,7 +5449,7 @@ version = "0.26.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61"
dependencies = [ dependencies = [
"rustls 0.23.43", "rustls 0.23.42",
"tokio", "tokio",
] ]
@@ -5572,9 +5550,9 @@ dependencies = [
[[package]] [[package]]
name = "toml_parser" name = "toml_parser"
version = "1.1.3+spec-1.1.0" version = "1.1.2+spec-1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d38ac1cf9b95face32296c0a3ede1fdc270627c9d9c02a7274dd6d960dc4d56" checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526"
dependencies = [ dependencies = [
"winnow 1.0.4", "winnow 1.0.4",
] ]
@@ -5623,7 +5601,7 @@ dependencies = [
"bytes", "bytes",
"futures-core", "futures-core",
"futures-util", "futures-util",
"http 1.5.0", "http 1.4.2",
"http-body 1.1.0", "http-body 1.1.0",
"http-body-util", "http-body-util",
"pin-project-lite", "pin-project-lite",
@@ -5724,7 +5702,7 @@ dependencies = [
"byteorder", "byteorder",
"bytes", "bytes",
"data-encoding", "data-encoding",
"http 1.5.0", "http 1.4.2",
"httparse", "httparse",
"log", "log",
"rand 0.8.7", "rand 0.8.7",
@@ -5840,9 +5818,9 @@ checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65"
[[package]] [[package]]
name = "value-bag" name = "value-bag"
version = "1.13.2" version = "1.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "068e763e8279de7ab94b6afebded2cb701678af094feb1c12ccb061b4783c1be" checksum = "ef73bfbaf3216cb59c205d7176bee1194e0d84348979da31f4a71fefe3c2054e"
[[package]] [[package]]
name = "vaultwarden" name = "vaultwarden"
@@ -5875,7 +5853,7 @@ dependencies = [
"handlebars", "handlebars",
"hickory-resolver", "hickory-resolver",
"html5gum", "html5gum",
"http 1.5.0", "http 1.4.2",
"ipnet", "ipnet",
"job_scheduler_ng", "job_scheduler_ng",
"jsonwebtoken", "jsonwebtoken",
@@ -5885,7 +5863,6 @@ dependencies = [
"macros", "macros",
"mimalloc", "mimalloc",
"moka", "moka",
"nix",
"num-derive", "num-derive",
"num-traits", "num-traits",
"opendal", "opendal",
@@ -5904,7 +5881,7 @@ dependencies = [
"rocket", "rocket",
"rocket_ws", "rocket_ws",
"rpassword", "rpassword",
"rustls 0.23.43", "rustls 0.23.42",
"semver", "semver",
"serde", "serde",
"serde_json", "serde_json",
@@ -6415,6 +6392,15 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
[[package]]
name = "winnow"
version = "0.6.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e90edd2ac1aa278a5c4599b1d89cf03074b610800f866d4026dc199d7929a28"
dependencies = [
"memchr",
]
[[package]] [[package]]
name = "winnow" name = "winnow"
version = "0.7.15" version = "0.7.15"
@@ -6515,15 +6501,18 @@ dependencies = [
[[package]] [[package]]
name = "yubico_ng" name = "yubico_ng"
version = "1.0.0" version = "0.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "563eb0ab41031e758446e3737231541bb4556d6af1e893e94faa09c989f794af" checksum = "228e2862e3c66f3224102d9a00d9d3646b271a05cc6c4819fea195fa8b5c00e0"
dependencies = [ dependencies = [
"base64 0.23.1", "base64 0.22.1",
"form_urlencoded", "form_urlencoded",
"getrandom 0.4.3", "futures",
"hmac 0.13.0", "hmac 0.12.1",
"sha1 0.11.0", "rand 0.9.5",
"reqwest",
"sha1 0.10.7",
"threadpool",
] ]
[[package]] [[package]]
+13 -14
View File
@@ -58,7 +58,6 @@ oidc-accept-string-booleans = ["openidconnect/accept-string-booleans"]
unstable = [] unstable = []
[target."cfg(unix)".dependencies] [target."cfg(unix)".dependencies]
nix = { version = "0.31.3", features = ["fs"] }
# Logging # Logging
syslog = "7.0.0" syslog = "7.0.0"
@@ -107,7 +106,7 @@ serde = { version = "1.0.229", features = ["derive"] }
serde_json = "1.0.151" serde_json = "1.0.151"
# A safe, extensible ORM and Query builder # A safe, extensible ORM and Query builder
diesel = { version = "2.3.11", features = ["chrono", "r2d2", "numeric"] } diesel = { version = "2.3.11", features = ["chrono", "r2d2", "numeric", "64-column-tables"] }
diesel_migrations = "2.3.2" diesel_migrations = "2.3.2"
derive_more = { version = "2.1.1", features = [ derive_more = { version = "2.1.1", features = [
@@ -125,7 +124,7 @@ libsqlite3-sys = { version = "0.37.0", optional = true }
# Crypto-related libraries # Crypto-related libraries
rand = "0.10.2" rand = "0.10.2"
ring = "0.17.14" ring = "0.17.14"
rustls = { version = "0.23.43", features = ["ring", "std"], default-features = false } rustls = { version = "0.23.42", features = ["ring", "std"], default-features = false }
subtle = "2.6.1" subtle = "2.6.1"
# UUID generation # UUID generation
@@ -134,13 +133,13 @@ uuid = { version = "1.24.0", features = ["v4"] }
# Date and time libraries # Date and time libraries
chrono = { version = "0.4.45", default-features = false, features = ["clock", "serde"] } chrono = { version = "0.4.45", default-features = false, features = ["clock", "serde"] }
chrono-tz = "0.10.4" chrono-tz = "0.10.4"
time = "0.3.55" time = "0.3.54"
# Job scheduler # Job scheduler
job_scheduler_ng = "2.5.0" job_scheduler_ng = "2.4.0"
# Data encoding library Hex/Base32/Base64 # Data encoding library Hex/Base32/Base64
data-encoding = "2.11.1" data-encoding = "2.11.0"
# JWT library # JWT library
jsonwebtoken = { version = "11.0.0", default-features = false, features = ["rust_crypto", "use_pem"] } jsonwebtoken = { version = "11.0.0", default-features = false, features = ["rust_crypto", "use_pem"] }
@@ -149,7 +148,7 @@ jsonwebtoken = { version = "11.0.0", default-features = false, features = ["rust
totp-lite = "2.0.1" totp-lite = "2.0.1"
# Yubico Library # Yubico Library
yubico_ng = { version = "1.0.0", default-features = false } yubico = { package = "yubico_ng", version = "0.15.0", default-features = false, 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
@@ -162,7 +161,7 @@ webauthn-rs-core = "0.5.5"
url = "2.5.8" url = "2.5.8"
# Email libraries # Email libraries
lettre = { version = "0.11.23", default-features = false, features = [ lettre = { version = "0.11.22", default-features = false, features = [
# Misc # Misc
"tracing", "tracing",
"serde", "serde",
@@ -232,7 +231,7 @@ pastey = "0.2.3"
governor = "0.10.4" governor = "0.10.4"
# CIDR parsing for the trusted proxies of the client IP header # CIDR parsing for the trusted proxies of the client IP header
ipnet = "2.12.1" ipnet = "2.12.0"
# OIDC for SSO # OIDC for SSO
openidconnect = { version = "4.0.1", default-features = false } openidconnect = { version = "4.0.1", default-features = false }
@@ -257,10 +256,10 @@ rpassword = "7.5.4"
grass_compiler = { version = "0.13.4", default-features = false } grass_compiler = { version = "0.13.4", default-features = false }
# File are accessed through Apache OpenDAL # File are accessed through Apache OpenDAL
opendal = { version = "0.58.1", default-features = false, features = ["services-fs"] } opendal = { version = "0.57.0", default-features = false, features = ["services-fs"] }
# For retrieving AWS credentials, including temporary SSO credentials # For retrieving AWS credentials, including temporary SSO credentials
aws-config = { version = "1.10.1", optional = true, default-features = false, features = [ aws-config = { version = "1.10.0", optional = true, default-features = false, features = [
"behavior-version-latest", "behavior-version-latest",
"credentials-process", "credentials-process",
"rt-tokio", "rt-tokio",
@@ -268,9 +267,9 @@ aws-config = { version = "1.10.1", optional = true, default-features = false, fe
] } ] }
aws-credential-types = { version = "1.3.0", optional = true } aws-credential-types = { version = "1.3.0", optional = true }
aws-smithy-runtime-api = { version = "1.14.0", optional = true } aws-smithy-runtime-api = { version = "1.14.0", optional = true }
http = { version = "1.5.0", optional = true } http = { version = "1.4.2", optional = true }
reqsign-aws-v4 = { version = "3.1.0", optional = true } reqsign-aws-v4 = { version = "3.0.2", optional = true }
reqsign-core = { version = "3.2.1", optional = true } reqsign-core = { version = "3.1.0", optional = true }
# Strip debuginfo from the release builds # Strip debuginfo from the release builds
# The debug symbols are to provide better panic traces # The debug symbols are to provide better panic traces
+2 -2
View File
@@ -1,6 +1,6 @@
--- ---
vault_version: "v2026.7.0" vault_version: "v2026.6.4"
vault_image_digest: "sha256:ba8bab66d4330ab9dbafa8f245bcbe99cf6ee3f2c8ce9b5fbb10e9c49658451c" vault_image_digest: "sha256:e7d3b31ec6a991a6bf447721ea341b4192ce5d3b920929211672fd4f3f891427"
# Cross Compile Docker Helper Scripts v1.9.0 # Cross Compile Docker Helper Scripts v1.9.0
# We use the linux/amd64 platform shell scripts since there is no difference between the different platform scripts # 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
+10 -11
View File
@@ -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:v2026.7.0 # $ docker pull docker.io/vaultwarden/web-vault:v2026.6.4
# $ docker image inspect --format "{{.RepoDigests}}" docker.io/vaultwarden/web-vault:v2026.7.0 # $ docker image inspect --format "{{.RepoDigests}}" docker.io/vaultwarden/web-vault:v2026.6.4
# [docker.io/vaultwarden/web-vault@sha256:ba8bab66d4330ab9dbafa8f245bcbe99cf6ee3f2c8ce9b5fbb10e9c49658451c] # [docker.io/vaultwarden/web-vault@sha256:e7d3b31ec6a991a6bf447721ea341b4192ce5d3b920929211672fd4f3f891427]
# #
# - 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:ba8bab66d4330ab9dbafa8f245bcbe99cf6ee3f2c8ce9b5fbb10e9c49658451c # $ docker image inspect --format "{{.RepoTags}}" docker.io/vaultwarden/web-vault@sha256:e7d3b31ec6a991a6bf447721ea341b4192ce5d3b920929211672fd4f3f891427
# [docker.io/vaultwarden/web-vault:v2026.7.0] # [docker.io/vaultwarden/web-vault:v2026.6.4]
# #
FROM --platform=linux/amd64 docker.io/vaultwarden/web-vault@sha256:ba8bab66d4330ab9dbafa8f245bcbe99cf6ee3f2c8ce9b5fbb10e9c49658451c AS vault FROM --platform=linux/amd64 docker.io/vaultwarden/web-vault@sha256:e7d3b31ec6a991a6bf447721ea341b4192ce5d3b920929211672fd4f3f891427 AS vault
########################## ALPINE BUILD IMAGES ########################## ########################## ALPINE BUILD IMAGES ##########################
## NOTE: The Alpine Base Images do not support other platforms then linux/amd64 and linux/arm64 ## NOTE: The Alpine Base Images do not support other platforms then linux/amd64 and linux/arm64
@@ -70,7 +70,7 @@ RUN echo "export CARGO_TARGET=${CARGO_BUILD_TARGET}" >> /env-cargo && \
# Output the current contents of the file # Output the current contents of the file
cat /env-cargo cat /env-cargo
RUN . /env-cargo && \ RUN source /env-cargo && \
rustup target add "${CARGO_TARGET}" rustup target add "${CARGO_TARGET}"
# Copies over *only* your manifests and build files # Copies over *only* your manifests and build files
@@ -86,7 +86,7 @@ ARG DB=sqlite,mysql,postgresql,enable_mimalloc
# Builds your dependencies and removes the # Builds your dependencies and removes the
# dummy project, except the target folder # dummy project, except the target folder
# This folder contains the compiled dependencies # This folder contains the compiled dependencies
RUN . /env-cargo && \ RUN source /env-cargo && \
cargo build --features ${DB} --profile "${CARGO_PROFILE}" --target="${CARGO_TARGET}" && \ cargo build --features ${DB} --profile "${CARGO_PROFILE}" --target="${CARGO_TARGET}" && \
find . -not -path "./target*" -delete find . -not -path "./target*" -delete
@@ -97,13 +97,13 @@ COPY . .
ARG VW_VERSION ARG VW_VERSION
# Builds again, this time it will be the actual source files being build # Builds again, this time it will be the actual source files being build
RUN . /env-cargo && \ RUN source /env-cargo && \
# Make sure that we actually build the project by updating the src/main.rs timestamp # Make sure that we actually build the project by updating the src/main.rs timestamp
# Also do this for build.rs to ensure the version is rechecked # Also do this for build.rs to ensure the version is rechecked
touch build.rs src/main.rs && \ touch build.rs src/main.rs && \
# Create a symlink to the binary target folder to easy copy the binary in the final stage # Create a symlink to the binary target folder to easy copy the binary in the final stage
cargo build --features ${DB} --profile "${CARGO_PROFILE}" --target="${CARGO_TARGET}" && \ cargo build --features ${DB} --profile "${CARGO_PROFILE}" --target="${CARGO_TARGET}" && \
if [ "${CARGO_PROFILE}" = "dev" ] ; then \ if [[ "${CARGO_PROFILE}" == "dev" ]] ; then \
ln -vfsr "/app/target/${CARGO_TARGET}/debug" /app/target/final ; \ ln -vfsr "/app/target/${CARGO_TARGET}/debug" /app/target/final ; \
else \ else \
ln -vfsr "/app/target/${CARGO_TARGET}/${CARGO_PROFILE}" /app/target/final ; \ ln -vfsr "/app/target/${CARGO_TARGET}/${CARGO_PROFILE}" /app/target/final ; \
@@ -126,7 +126,6 @@ RUN . /env-cargo && \
# To uninstall: docker run --privileged --rm tonistiigi/binfmt --uninstall 'qemu-*' # To uninstall: docker run --privileged --rm tonistiigi/binfmt --uninstall 'qemu-*'
# #
# We need to add `--platform` here, because of a podman bug: https://github.com/containers/buildah/issues/4742 # We need to add `--platform` here, because of a podman bug: https://github.com/containers/buildah/issues/4742
# hadolint ignore=DL3065
FROM --platform=$TARGETPLATFORM docker.io/library/alpine:3.24 FROM --platform=$TARGETPLATFORM docker.io/library/alpine:3.24
ENV ROCKET_PROFILE="release" \ ENV ROCKET_PROFILE="release" \
+12 -24
View File
@@ -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:v2026.7.0 # $ docker pull docker.io/vaultwarden/web-vault:v2026.6.4
# $ docker image inspect --format "{{.RepoDigests}}" docker.io/vaultwarden/web-vault:v2026.7.0 # $ docker image inspect --format "{{.RepoDigests}}" docker.io/vaultwarden/web-vault:v2026.6.4
# [docker.io/vaultwarden/web-vault@sha256:ba8bab66d4330ab9dbafa8f245bcbe99cf6ee3f2c8ce9b5fbb10e9c49658451c] # [docker.io/vaultwarden/web-vault@sha256:e7d3b31ec6a991a6bf447721ea341b4192ce5d3b920929211672fd4f3f891427]
# #
# - 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:ba8bab66d4330ab9dbafa8f245bcbe99cf6ee3f2c8ce9b5fbb10e9c49658451c # $ docker image inspect --format "{{.RepoTags}}" docker.io/vaultwarden/web-vault@sha256:e7d3b31ec6a991a6bf447721ea341b4192ce5d3b920929211672fd4f3f891427
# [docker.io/vaultwarden/web-vault:v2026.7.0] # [docker.io/vaultwarden/web-vault:v2026.6.4]
# #
FROM --platform=linux/amd64 docker.io/vaultwarden/web-vault@sha256:ba8bab66d4330ab9dbafa8f245bcbe99cf6ee3f2c8ce9b5fbb10e9c49658451c AS vault FROM --platform=linux/amd64 docker.io/vaultwarden/web-vault@sha256:e7d3b31ec6a991a6bf447721ea341b4192ce5d3b920929211672fd4f3f891427 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
@@ -37,7 +37,6 @@ FROM --platform=linux/amd64 docker.io/tonistiigi/xx@sha256:c64defb9ed5a91eacb37f
########################## BUILD IMAGE ########################## ########################## BUILD IMAGE ##########################
# hadolint ignore=DL3006 # hadolint ignore=DL3006
FROM --platform=$BUILDPLATFORM docker.io/library/rust:1.97.1-slim-trixie AS build FROM --platform=$BUILDPLATFORM docker.io/library/rust:1.97.1-slim-trixie AS build
# hadolint ignore=DL3067
COPY --from=xx / / COPY --from=xx / /
ARG TARGETARCH ARG TARGETARCH
ARG TARGETVARIANT ARG TARGETVARIANT
@@ -81,7 +80,7 @@ RUN mkdir -pv "${CARGO_HOME}" && \
RUN USER=root cargo new --bin /app RUN USER=root cargo new --bin /app
WORKDIR /app WORKDIR /app
RUN . /env-cargo && \ RUN source /env-cargo && \
rustup target add "${CARGO_TARGET}" rustup target add "${CARGO_TARGET}"
# Copies over *only* your manifests and build files # Copies over *only* your manifests and build files
@@ -96,14 +95,9 @@ ARG DB=sqlite,mysql,postgresql
# Builds your dependencies and removes the # Builds your dependencies and removes the
# dummy project, except the target folder # dummy project, except the target folder
# This folder contains the compiled dependencies # This folder contains the compiled dependencies
RUN . /env-cargo && \ RUN source /env-cargo && \
# Configure xx-cargo for target pkg-config and Debian transitive library lookup # Workaround for xx related build issues
# https://github.com/tonistiigi/xx/pull/108#issuecomment-3700635977 # https://github.com/tonistiigi/xx/pull/108#issuecomment-3700635977
# https://github.com/dani-garcia/vaultwarden/discussions/7522
if xx-info is-cross; then \
XX_RUSTFLAGS="-C link-arg=-Wl,-rpath-link,/usr/lib/$(xx-info triple)"; \
export XX_RUSTFLAGS; \
fi && \
PKG_CONFIG="$(command -v "$(xx-info)-pkg-config")" xx-cargo build --features ${DB} --profile "${CARGO_PROFILE}" && \ PKG_CONFIG="$(command -v "$(xx-info)-pkg-config")" xx-cargo build --features ${DB} --profile "${CARGO_PROFILE}" && \
find . -not -path "./target*" -delete find . -not -path "./target*" -delete
@@ -114,20 +108,15 @@ COPY . .
ARG VW_VERSION ARG VW_VERSION
# Builds again, this time it will be the actual source files being build # Builds again, this time it will be the actual source files being build
RUN . /env-cargo && \ RUN source /env-cargo && \
# Make sure that we actually build the project by updating the src/main.rs timestamp # Make sure that we actually build the project by updating the src/main.rs timestamp
# Also do this for build.rs to ensure the version is rechecked # Also do this for build.rs to ensure the version is rechecked
touch build.rs src/main.rs && \ touch build.rs src/main.rs && \
# Create a symlink to the binary target folder to easy copy the binary in the final stage # Create a symlink to the binary target folder to easy copy the binary in the final stage
# Configure xx-cargo for target pkg-config and Debian transitive library lookup # Workaround for xx related build issues
# https://github.com/tonistiigi/xx/pull/108#issuecomment-3700635977 # https://github.com/tonistiigi/xx/pull/108#issuecomment-3700635977
# https://github.com/dani-garcia/vaultwarden/discussions/7522
if xx-info is-cross; then \
XX_RUSTFLAGS="-C link-arg=-Wl,-rpath-link,/usr/lib/$(xx-info triple)"; \
export XX_RUSTFLAGS; \
fi && \
PKG_CONFIG="$(command -v "$(xx-info)-pkg-config")" xx-cargo build --features ${DB} --profile "${CARGO_PROFILE}" && \ PKG_CONFIG="$(command -v "$(xx-info)-pkg-config")" xx-cargo build --features ${DB} --profile "${CARGO_PROFILE}" && \
if [ "${CARGO_PROFILE}" = "dev" ] ; then \ if [[ "${CARGO_PROFILE}" == "dev" ]] ; then \
ln -vfsr "/app/target/${CARGO_TARGET}/debug" /app/target/final ; \ ln -vfsr "/app/target/${CARGO_TARGET}/debug" /app/target/final ; \
else \ else \
ln -vfsr "/app/target/${CARGO_TARGET}/${CARGO_PROFILE}" /app/target/final ; \ ln -vfsr "/app/target/${CARGO_TARGET}/${CARGO_PROFILE}" /app/target/final ; \
@@ -150,7 +139,6 @@ RUN . /env-cargo && \
# To uninstall: docker run --privileged --rm tonistiigi/binfmt --uninstall 'qemu-*' # To uninstall: docker run --privileged --rm tonistiigi/binfmt --uninstall 'qemu-*'
# #
# We need to add `--platform` here, because of a podman bug: https://github.com/containers/buildah/issues/4742 # We need to add `--platform` here, because of a podman bug: https://github.com/containers/buildah/issues/4742
# hadolint ignore=DL3065
FROM --platform=$TARGETPLATFORM docker.io/library/debian:trixie-slim FROM --platform=$TARGETPLATFORM docker.io/library/debian:trixie-slim
ENV ROCKET_PROFILE="release" \ ENV ROCKET_PROFILE="release" \
+5 -12
View File
@@ -28,13 +28,8 @@
# [docker.io/vaultwarden/web-vault:{{ vault_version | replace('+', '_') }}] # [docker.io/vaultwarden/web-vault:{{ vault_version | replace('+', '_') }}]
# #
{% macro xx_cargo_config() -%} {% macro xx_cargo_config() -%}
# Configure xx-cargo for target pkg-config and Debian transitive library lookup # Workaround for xx related build issues
# https://github.com/tonistiigi/xx/pull/108#issuecomment-3700635977 # https://github.com/tonistiigi/xx/pull/108#issuecomment-3700635977
# https://github.com/dani-garcia/vaultwarden/discussions/7522
if xx-info is-cross; then \
XX_RUSTFLAGS="-C link-arg=-Wl,-rpath-link,/usr/lib/$(xx-info triple)"; \
export XX_RUSTFLAGS; \
fi && \
PKG_CONFIG="$(command -v "$(xx-info)-pkg-config")" xx-cargo build --features ${DB} --profile "${CARGO_PROFILE}" PKG_CONFIG="$(command -v "$(xx-info)-pkg-config")" xx-cargo build --features ${DB} --profile "${CARGO_PROFILE}"
{%- endmacro %} {%- endmacro %}
FROM --platform=linux/amd64 docker.io/vaultwarden/web-vault@{{ vault_image_digest }} AS vault FROM --platform=linux/amd64 docker.io/vaultwarden/web-vault@{{ vault_image_digest }} AS vault
@@ -57,7 +52,6 @@ FROM --platform=$BUILDPLATFORM {{ build_stage_image[base].arch_image[arch] }} AS
# hadolint ignore=DL3006 # hadolint ignore=DL3006
FROM --platform=$BUILDPLATFORM {{ build_stage_image[base].image }} AS build FROM --platform=$BUILDPLATFORM {{ build_stage_image[base].image }} AS build
{% if base == "debian" %} {% if base == "debian" %}
# hadolint ignore=DL3067
COPY --from=xx / / COPY --from=xx / /
{% endif %} {% endif %}
ARG TARGETARCH ARG TARGETARCH
@@ -117,7 +111,7 @@ RUN echo "export CARGO_TARGET=${CARGO_BUILD_TARGET}" >> /env-cargo && \
cat /env-cargo cat /env-cargo
{% endif %} {% endif %}
RUN . /env-cargo && \ RUN source /env-cargo && \
rustup target add "${CARGO_TARGET}" rustup target add "${CARGO_TARGET}"
# Copies over *only* your manifests and build files # Copies over *only* your manifests and build files
@@ -137,7 +131,7 @@ ARG DB=sqlite,mysql,postgresql,enable_mimalloc
# Builds your dependencies and removes the # Builds your dependencies and removes the
# dummy project, except the target folder # dummy project, except the target folder
# This folder contains the compiled dependencies # This folder contains the compiled dependencies
RUN . /env-cargo && \ RUN source /env-cargo && \
{% if base == "debian" %} {% if base == "debian" %}
{{ xx_cargo_config() }} && \ {{ xx_cargo_config() }} && \
{% elif base == "alpine" %} {% elif base == "alpine" %}
@@ -152,7 +146,7 @@ COPY . .
ARG VW_VERSION ARG VW_VERSION
# Builds again, this time it will be the actual source files being build # Builds again, this time it will be the actual source files being build
RUN . /env-cargo && \ RUN source /env-cargo && \
# Make sure that we actually build the project by updating the src/main.rs timestamp # Make sure that we actually build the project by updating the src/main.rs timestamp
# Also do this for build.rs to ensure the version is rechecked # Also do this for build.rs to ensure the version is rechecked
touch build.rs src/main.rs && \ touch build.rs src/main.rs && \
@@ -162,7 +156,7 @@ RUN . /env-cargo && \
{% elif base == "alpine" %} {% elif base == "alpine" %}
cargo build --features ${DB} --profile "${CARGO_PROFILE}" --target="${CARGO_TARGET}" && \ cargo build --features ${DB} --profile "${CARGO_PROFILE}" --target="${CARGO_TARGET}" && \
{% endif %} {% endif %}
if [ "${CARGO_PROFILE}" = "dev" ] ; then \ if [[ "${CARGO_PROFILE}" == "dev" ]] ; then \
ln -vfsr "/app/target/${CARGO_TARGET}/debug" /app/target/final ; \ ln -vfsr "/app/target/${CARGO_TARGET}/debug" /app/target/final ; \
else \ else \
ln -vfsr "/app/target/${CARGO_TARGET}/${CARGO_PROFILE}" /app/target/final ; \ ln -vfsr "/app/target/${CARGO_TARGET}/${CARGO_PROFILE}" /app/target/final ; \
@@ -185,7 +179,6 @@ RUN . /env-cargo && \
# To uninstall: docker run --privileged --rm tonistiigi/binfmt --uninstall 'qemu-*' # To uninstall: docker run --privileged --rm tonistiigi/binfmt --uninstall 'qemu-*'
# #
# We need to add `--platform` here, because of a podman bug: https://github.com/containers/buildah/issues/4742 # We need to add `--platform` here, because of a podman bug: https://github.com/containers/buildah/issues/4742
# hadolint ignore=DL3065
FROM --platform=$TARGETPLATFORM {{ runtime_stage_image[base] }} FROM --platform=$TARGETPLATFORM {{ runtime_stage_image[base] }}
ENV ROCKET_PROFILE="release" \ ENV ROCKET_PROFILE="release" \
@@ -0,0 +1,6 @@
DROP TABLE IF EXISTS user_signature_key_pairs;
ALTER TABLE users DROP COLUMN signed_public_key;
ALTER TABLE users DROP COLUMN security_state;
ALTER TABLE users DROP COLUMN security_version;
ALTER TABLE users DROP COLUMN v2_upgrade_token;
@@ -0,0 +1,17 @@
ALTER TABLE users ADD COLUMN signed_public_key TEXT;
ALTER TABLE users ADD COLUMN security_state TEXT;
ALTER TABLE users ADD COLUMN security_version INTEGER;
ALTER TABLE users ADD COLUMN v2_upgrade_token TEXT;
DROP TABLE IF EXISTS user_signature_key_pairs;
CREATE TABLE user_signature_key_pairs (
uuid CHAR(36) NOT NULL PRIMARY KEY,
user_uuid CHAR(36) NOT NULL UNIQUE,
signature_algorithm INTEGER NOT NULL, -- 0 = ed25519, 1 = mldsa44
signing_key TEXT NOT NULL,
verifying_key TEXT NOT NULL,
created_at DATETIME NOT NULL,
updated_at DATETIME NOT NULL,
FOREIGN KEY (user_uuid) REFERENCES users (uuid) ON DELETE CASCADE
);
@@ -0,0 +1,6 @@
DROP TABLE IF EXISTS user_signature_key_pairs;
ALTER TABLE users DROP COLUMN signed_public_key;
ALTER TABLE users DROP COLUMN security_state;
ALTER TABLE users DROP COLUMN security_version;
ALTER TABLE users DROP COLUMN v2_upgrade_token;
@@ -0,0 +1,16 @@
ALTER TABLE users ADD COLUMN signed_public_key TEXT;
ALTER TABLE users ADD COLUMN security_state TEXT;
ALTER TABLE users ADD COLUMN security_version INTEGER;
ALTER TABLE users ADD COLUMN v2_upgrade_token TEXT;
DROP TABLE IF EXISTS user_signature_key_pairs;
CREATE TABLE user_signature_key_pairs (
uuid CHAR(36) NOT NULL PRIMARY KEY,
user_uuid CHAR(36) NOT NULL UNIQUE REFERENCES users (uuid) ON DELETE CASCADE,
signature_algorithm INTEGER NOT NULL, -- 0 = ed25519, 1 = mldsa44
signing_key TEXT NOT NULL,
verifying_key TEXT NOT NULL,
created_at TIMESTAMP NOT NULL,
updated_at TIMESTAMP NOT NULL
);
@@ -0,0 +1,6 @@
DROP TABLE IF EXISTS user_signature_key_pairs;
ALTER TABLE users DROP COLUMN signed_public_key;
ALTER TABLE users DROP COLUMN security_state;
ALTER TABLE users DROP COLUMN security_version;
ALTER TABLE users DROP COLUMN v2_upgrade_token;
@@ -0,0 +1,16 @@
ALTER TABLE users ADD COLUMN signed_public_key TEXT;
ALTER TABLE users ADD COLUMN security_state TEXT;
ALTER TABLE users ADD COLUMN security_version INTEGER;
ALTER TABLE users ADD COLUMN v2_upgrade_token TEXT;
DROP TABLE IF EXISTS user_signature_key_pairs;
CREATE TABLE user_signature_key_pairs (
uuid TEXT NOT NULL PRIMARY KEY,
user_uuid TEXT NOT NULL UNIQUE REFERENCES users (uuid) ON DELETE CASCADE,
signature_algorithm INTEGER NOT NULL, -- 0 = ed25519, 1 = mldsa44
signing_key TEXT NOT NULL,
verifying_key TEXT NOT NULL,
created_at DATETIME NOT NULL,
updated_at DATETIME NOT NULL
);
+3 -13
View File
@@ -21,19 +21,11 @@ TEST_USER3=test3
TEST_USER3_PASSWORD=${TEST_USER3} TEST_USER3_PASSWORD=${TEST_USER3}
TEST_USER3_MAIL=${TEST_USER3}@yopmail.com TEST_USER3_MAIL=${TEST_USER3}@yopmail.com
TEST_USER4=test4
TEST_USER4_PASSWORD=${TEST_USER4}
TEST_USER4_MAIL=${TEST_USER4}@yopmail.com
TEST_USER5=test5
TEST_USER5_PASSWORD=${TEST_USER5}
TEST_USER5_MAIL=${TEST_USER5}@yopmail.com
################### ###################
# Keycloak Config # # Keycloak Config #
################### ###################
KC_BOOTSTRAP_ADMIN_USERNAME=admin KEYCLOAK_ADMIN=admin
KC_BOOTSTRAP_ADMIN_PASSWORD=${KC_BOOTSTRAP_ADMIN_USERNAME} KEYCLOAK_ADMIN_PASSWORD=${KEYCLOAK_ADMIN}
KC_HTTP_HOST=127.0.0.1 KC_HTTP_HOST=127.0.0.1
KC_HTTP_PORT=8080 KC_HTTP_PORT=8080
@@ -47,10 +39,8 @@ DUMMY_AUTHORITY=http://${KC_HTTP_HOST}:${KC_HTTP_PORT}/realms/${DUMMY_REALM}
###################### ######################
ROCKET_ADDRESS=0.0.0.0 ROCKET_ADDRESS=0.0.0.0
ROCKET_PORT=8000 ROCKET_PORT=8000
ROCKET_TLS={certs="/data/ssl/localhost.crt",key="/data/ssl/localhost.key"} DOMAIN=http://localhost:${ROCKET_PORT}
DOMAIN=https://127.0.0.1:${ROCKET_PORT}
LOG_LEVEL=info,oidcwarden::sso=debug LOG_LEVEL=info,oidcwarden::sso=debug
SSO_DEBUG_TOKENS=true
I_REALLY_WANT_VOLATILE_STORAGE=true I_REALLY_WANT_VOLATILE_STORAGE=true
SSO_ENABLED=true SSO_ENABLED=true
+16 -14
View File
@@ -1,8 +1,8 @@
# Integration tests # Integration tests
This allows running integration tests using [Playwright](https://playwright.dev/). This allows running integration tests using [Playwright](https://playwright.dev/).
\
It usse its own [test.env](/test/scenarios/test.env) with different ports to not collide with a running dev instance. It uses its own `test.env` with different ports to not collide with a running dev instance.
## Install ## Install
@@ -11,11 +11,11 @@ Databases (`Mariadb`, `Mysql` and `Postgres`) and `Playwright` will run in conta
### Running Playwright outside docker ### Running Playwright outside docker
It's possible to run `Playwright` outside of the container, this remove the need to rebuild the image for each change. It is possible to run `Playwright` outside of the container, this removes the need to rebuild the image for each change.
You'll additionally need `nodejs` then run: You will additionally need `nodejs` then run:
```bash ```bash
npm ci --ignore-scripts --allow-git=none --allow-remote=none npm ci --ignore-scripts
npx playwright install-deps npx playwright install-deps
npx playwright install firefox npx playwright install firefox
``` ```
@@ -65,7 +65,7 @@ DOCKER_BUILDKIT=1 docker compose --profile playwright --env-file test.env run Pl
If you want you can keep the DB and Keycloak runnning (states are not impacted by the tests): If you want you can keep the DB and Keycloak runnning (states are not impacted by the tests):
```bash ```bash
PW_KEEP_SERVICE_RUNNING=true npx playwright test PW_KEEP_SERVICE_RUNNNING=true npx playwright test
``` ```
### Running specific tests ### Running specific tests
@@ -77,7 +77,7 @@ DOCKER_BUILDKIT=1 docker compose --profile playwright --env-file test.env run Pl
DOCKER_BUILDKIT=1 docker compose --profile playwright --env-file test.env run Playwright test --project=sqlite login DOCKER_BUILDKIT=1 docker compose --profile playwright --env-file test.env run Playwright test --project=sqlite login
``` ```
To run only a specific test (It might fail if it has dependency): To run only a specifc test (It might fail if it has dependency):
```bash ```bash
DOCKER_BUILDKIT=1 docker compose --profile playwright --env-file test.env run Playwright test --project=sqlite -g "Account creation" DOCKER_BUILDKIT=1 docker compose --profile playwright --env-file test.env run Playwright test --project=sqlite -g "Account creation"
@@ -92,7 +92,7 @@ This does not start the server, you will need to start it manually.
```bash ```bash
DOCKER_BUILDKIT=1 docker compose --profile playwright --env-file test.env up Vaultwarden DOCKER_BUILDKIT=1 docker compose --profile playwright --env-file test.env up Vaultwarden
npx playwright codegen "https://127.0.0.1:8000" --ignore-https-errors npx playwright codegen "http://127.0.0.1:8003"
``` ```
## Override web-vault ## Override web-vault
@@ -112,11 +112,12 @@ You can check the result running:
DOCKER_BUILDKIT=1 docker compose --profile playwright --env-file test.env up Vaultwarden DOCKER_BUILDKIT=1 docker compose --profile playwright --env-file test.env up Vaultwarden
``` ```
Then check `https://127.0.0.1:8003/admin/diagnostics` with `admin`. Then check `http://127.0.0.1:8003/admin/diagnostics` with `admin`.
# OpenID Connect test setup # OpenID Connect test setup
Additionally this `docker-compose` template allow to run locally `Vaultwarden`, [Keycloak](https://www.keycloak.org/) and [Maildev](https://github.com/timshel/maildev) to test OIDC. Additionally this `docker-compose` template allows to run locally Vaultwarden,
[Keycloak](https://www.keycloak.org/) and [Maildev](https://github.com/timshel/maildev) to test OIDC.
## Setup ## Setup
@@ -130,17 +131,18 @@ Then start the stack (the `profile` is required to run `Vaultwarden`) :
```bash ```bash
> docker compose --profile vaultwarden --env-file .env up > docker compose --profile vaultwarden --env-file .env up
.... ....
keycloakSetup_1 | Logging into https://127.0.0.1:8080 as user admin of realm master keycloakSetup_1 | Logging into http://127.0.0.1:8080 as user admin of realm master
keycloakSetup_1 | Created new realm with id 'test' keycloakSetup_1 | Created new realm with id 'test'
keycloakSetup_1 | 74af4933-e386-4e64-ba15-a7b61212c45e keycloakSetup_1 | 74af4933-e386-4e64-ba15-a7b61212c45e
oidc_keycloakSetup_1 exited with code 0 oidc_keycloakSetup_1 exited with code 0
``` ```
Wait until `oidc_keycloakSetup_1 exited with code 0` which indicate the correct setup of the Keycloak realm, client and user (It's normal for this container to stop once the configuration is done). Wait until `oidc_keycloakSetup_1 exited with code 0` which indicates the correct setup of the Keycloak realm, client and user
(It is normal for this container to stop once the configuration is done).
Then you can access : Then you can access :
- `Vaultwarden` on https://0.0.0.0:8000 with the default user `test@yopmail.com/test`. - `Vaultwarden` on http://0.0.0.0:8000 with the default user `test@yopmail.com/test`.
- `Keycloak` on http://0.0.0.0:8080/admin/master/console/ with the default user `admin/admin` - `Keycloak` on http://0.0.0.0:8080/admin/master/console/ with the default user `admin/admin`
- `Maildev` on http://0.0.0.0:1080 - `Maildev` on http://0.0.0.0:1080
@@ -169,7 +171,7 @@ docker compose --profile vaultwarden --env-file .env build VaultwardenPrebuild V
All configuration for `keycloak` / `Vaultwarden` / `keycloak_setup.sh` can be found in [.env](.env.template). All configuration for `keycloak` / `Vaultwarden` / `keycloak_setup.sh` can be found in [.env](.env.template).
The content of the file will be loaded as environment variables in all containers. The content of the file will be loaded as environment variables in all containers.
- `keycloak` [configuration](https://www.keycloak.org/server/all-config) include `KC_BOOTSTRAP_ADMIN_USERNAME` / `KC_BOOTSTRAP_ADMIN_PASSWORD` and any variable prefixed `KC_` ([more information](https://www.keycloak.org/server/configuration#_example_configuring_the_db_url_host_parameter)). - `keycloak` [configuration](https://www.keycloak.org/server/all-config) includes `KEYCLOAK_ADMIN` / `KEYCLOAK_ADMIN_PASSWORD` and any variable prefixed `KC_` ([more information](https://www.keycloak.org/server/configuration#_example_configuring_the_db_url_host_parameter)).
- All `Vaultwarden` configuration can be set (EX: `SMTP_*`) - All `Vaultwarden` configuration can be set (EX: `SMTP_*`)
## Cleanup ## Cleanup
+2 -2
View File
@@ -17,7 +17,7 @@ done
set -e set -e
kcadm.sh config credentials --server "http://${KC_HTTP_HOST}:${KC_HTTP_PORT}" --realm master --user "$KC_BOOTSTRAP_ADMIN_USERNAME" --password "$KC_BOOTSTRAP_ADMIN_PASSWORD" --client admin-cli kcadm.sh config credentials --server "http://${KC_HTTP_HOST}:${KC_HTTP_PORT}" --realm master --user "$KEYCLOAK_ADMIN" --password "$KEYCLOAK_ADMIN_PASSWORD" --client admin-cli
kcadm.sh create realms -s realm="$TEST_REALM" -s enabled=true -s "accessTokenLifespan=600" kcadm.sh create realms -s realm="$TEST_REALM" -s enabled=true -s "accessTokenLifespan=600"
kcadm.sh create clients -r test -s "clientId=$SSO_CLIENT_ID" -s "secret=$SSO_CLIENT_SECRET" -s "redirectUris=[\"$DOMAIN/*\"]" -i kcadm.sh create clients -r test -s "clientId=$SSO_CLIENT_ID" -s "secret=$SSO_CLIENT_SECRET" -s "redirectUris=[\"$DOMAIN/*\"]" -i
@@ -39,6 +39,6 @@ kcadm.sh create realms -s realm="$DUMMY_REALM" -s enabled=true -s "accessTokenLi
# THEN in another terminal: # THEN in another terminal:
# docker exec -it keycloakSetup-dev /bin/bash # docker exec -it keycloakSetup-dev /bin/bash
# export PATH=$PATH:/opt/keycloak/bin # export PATH=$PATH:/opt/keycloak/bin
# kcadm.sh config credentials --server "http://${KC_HTTP_HOST}:${KC_HTTP_PORT}" --realm master --user "$KC_BOOTSTRAP_ADMIN_USERNAME" --password "$KC_BOOTSTRAP_ADMIN_PASSWORD" --client admin-cli # kcadm.sh config credentials --server "http://${KC_HTTP_HOST}:${KC_HTTP_PORT}" --realm master --user "$KEYCLOAK_ADMIN" --password "$KEYCLOAK_ADMIN_PASSWORD" --client admin-cli
# ENJOY # ENJOY
# Doc: https://wjw465150.gitbooks.io/keycloak-documentation/content/server_admin/topics/admin-cli.html # Doc: https://wjw465150.gitbooks.io/keycloak-documentation/content/server_admin/topics/admin-cli.html
+1 -1
View File
@@ -28,7 +28,7 @@ RUN mkdir /playwright
WORKDIR /playwright WORKDIR /playwright
COPY package.json package-lock.json . COPY package.json package-lock.json .
RUN npm ci --ignore-scripts --allow-git=none --allow-remote=none && npx playwright install-deps && npx playwright install firefox RUN npm ci --ignore-scripts && npx playwright install-deps && npx playwright install firefox
COPY docker-compose.yml test.env ./ COPY docker-compose.yml test.env ./
COPY compose ./compose COPY compose ./compose
-1
View File
@@ -35,7 +35,6 @@ WORKDIR /
COPY --from=prebuilt /start.sh . COPY --from=prebuilt /start.sh .
COPY --from=prebuilt /vaultwarden . COPY --from=prebuilt /vaultwarden .
COPY --from=build /data ./data
COPY --from=build /web-vault ./web-vault COPY --from=build /web-vault ./web-vault
ENTRYPOINT ["/start.sh"] ENTRYPOINT ["/start.sh"]
-11
View File
@@ -22,14 +22,3 @@ if [[ ! -z "$REPO_URL" ]] && [[ ! -z "$COMMIT_HASH" ]] ; then
mv build /web-vault mv build /web-vault
fi fi
# Lower the KDF iterations default for faster tests.
sed -i 's/(6e5,2e6,6e5)/(1e5,2e6,1e5)/' /web-vault/app/main.*.js
# Generate a self signed cert
mkdir -p /data/ssl; cd /data/ssl
openssl req -x509 -out localhost.crt -keyout localhost.key \
-newkey rsa:2048 -nodes -sha256 \
-subj '/CN=localhost' -extensions EXT -config <( \
printf "[dn]\nCN=localhost\n[req]\ndistinguished_name = dn\n[EXT]\nsubjectAltName=DNS:localhost\nkeyUsage=digitalSignature\nextendedKeyUsage=serverAuth")
+6 -9
View File
@@ -24,15 +24,12 @@ services:
environment: environment:
- ADMIN_TOKEN - ADMIN_TOKEN
- DATABASE_URL - DATABASE_URL
- CLIENT_SUPPRESS_ONBOARDING
- EMAIL_2FA_AUTO_FALLBACK
- I_REALLY_WANT_VOLATILE_STORAGE - I_REALLY_WANT_VOLATILE_STORAGE
- LOG_LEVEL - LOG_LEVEL
- LOGIN_RATELIMIT_MAX_BURST - LOGIN_RATELIMIT_MAX_BURST
- SMTP_HOST - SMTP_HOST
- SMTP_FROM - SMTP_FROM
- SMTP_DEBUG - SMTP_DEBUG
- SSO_AUTH_ONLY_NOT_SESSION
- SSO_DEBUG_TOKENS - SSO_DEBUG_TOKENS
- SSO_ENABLED - SSO_ENABLED
- SSO_FRONTEND - SSO_FRONTEND
@@ -73,7 +70,7 @@ services:
Mysql: Mysql:
profiles: ["playwright"] profiles: ["playwright"]
container_name: playwright_mysql container_name: playwright_mysql
image: mysql:9.7.0 image: mysql:8.4.1
env_file: test.env env_file: test.env
healthcheck: healthcheck:
test: ["CMD", "mysqladmin" ,"ping", "-h", "localhost"] test: ["CMD", "mysqladmin" ,"ping", "-h", "localhost"]
@@ -85,7 +82,7 @@ services:
Postgres: Postgres:
profiles: ["playwright"] profiles: ["playwright"]
container_name: playwright_postgres container_name: playwright_postgres
image: postgres:18.4 image: postgres:16.3
env_file: test.env env_file: test.env
healthcheck: healthcheck:
test: ["CMD-SHELL", "pg_isready -d $${POSTGRES_DB} -U $${POSTGRES_USER}"] test: ["CMD-SHELL", "pg_isready -d $${POSTGRES_DB} -U $${POSTGRES_USER}"]
@@ -97,7 +94,7 @@ services:
Maildev: Maildev:
profiles: ["vaultwarden", "maildev"] profiles: ["vaultwarden", "maildev"]
container_name: maildev container_name: maildev
image: timshel/maildev:3.2.19 image: timshel/maildev:3.0.4
ports: ports:
- ${SMTP_PORT}:1025 - ${SMTP_PORT}:1025
- 1080:1080 - 1080:1080
@@ -105,7 +102,7 @@ services:
Keycloak: Keycloak:
profiles: ["keycloak", "vaultwarden"] profiles: ["keycloak", "vaultwarden"]
container_name: keycloak-${ENV:-dev} container_name: keycloak-${ENV:-dev}
image: quay.io/keycloak/keycloak:26.6.2 image: quay.io/keycloak/keycloak:26.3.4
network_mode: "host" network_mode: "host"
command: command:
- start-dev - start-dev
@@ -115,12 +112,12 @@ services:
profiles: ["keycloak", "vaultwarden"] profiles: ["keycloak", "vaultwarden"]
container_name: keycloakSetup-${ENV:-dev} container_name: keycloakSetup-${ENV:-dev}
image: keycloak_setup-${ENV:-dev} image: keycloak_setup-${ENV:-dev}
network_mode: "host"
build: build:
context: compose/keycloak context: compose/keycloak
dockerfile: Dockerfile dockerfile: Dockerfile
args: args:
KEYCLOAK_VERSION: 26.6.2 KEYCLOAK_VERSION: 26.3.4
network_mode: "host"
depends_on: depends_on:
- Keycloak - Keycloak
restart: "no" restart: "no"
+1 -1
View File
@@ -1,4 +1,4 @@
import { type FullConfig } from '@playwright/test'; import { firefox, type FullConfig } from '@playwright/test';
import { execSync } from 'node:child_process'; import { execSync } from 'node:child_process';
import fs from 'fs'; import fs from 'fs';
+13 -2
View File
@@ -207,7 +207,7 @@ export async function startVault(browser: Browser, testInfo: TestInfo, env = {},
} }
export async function stopVault(force: boolean = false) { export async function stopVault(force: boolean = false) {
if( force === false && process.env.PW_KEEP_SERVICE_RUNNING === "true" ) { if( force === false && process.env.PW_KEEP_SERVICE_RUNNNING === "true" ) {
console.log(`Keep vaultwarden running on: ${process.env.DOMAIN}`); console.log(`Keep vaultwarden running on: ${process.env.DOMAIN}`);
} else { } else {
console.log(`Vaultwarden stopping`); console.log(`Vaultwarden stopping`);
@@ -231,7 +231,6 @@ export async function checkNotification(page: Page, hasText: string) {
} }
export async function cleanLanding(page: Page) { export async function cleanLanding(page: Page) {
await page.context().clearCookies();
await page.goto('/', { waitUntil: 'domcontentloaded' }); await page.goto('/', { waitUntil: 'domcontentloaded' });
await expect(page.getByRole('button').nth(0)).toBeVisible(); await expect(page.getByRole('button').nth(0)).toBeVisible();
@@ -249,3 +248,15 @@ export async function logout(test: Test, page: Page, user: { name: string }) {
await expect(page.getByRole('heading', { name: 'Log in' })).toBeVisible(); await expect(page.getByRole('heading', { name: 'Log in' })).toBeVisible();
}); });
} }
export async function ignoreExtension(page: Page) {
await page.waitForLoadState('domcontentloaded');
try {
await page.getByRole('button', { name: 'Add it later' }).click({timeout: 5_000});
await page.getByRole('link', { name: 'Skip to web app' }).click();
} catch (error) {
console.log('Extension setup not visible. Continuing');
}
}
+580 -582
View File
File diff suppressed because it is too large Load Diff
+7 -7
View File
@@ -8,14 +8,14 @@
"author": "", "author": "",
"license": "ISC", "license": "ISC",
"devDependencies": { "devDependencies": {
"@playwright/test": "1.60.0", "@playwright/test": "1.56.1",
"dotenv": "17.4.2", "dotenv": "17.2.3",
"dotenv-expand": "13.0.0", "dotenv-expand": "12.0.3",
"maildev": "npm:@timshel_npm/maildev@3.2.19" "maildev": "npm:@timshel_npm/maildev@3.2.5"
}, },
"dependencies": { "dependencies": {
"mysql2": "3.22.3", "mysql2": "3.15.3",
"otpauth": "9.5.1", "otpauth": "9.4.1",
"pg": "8.21.0" "pg": "8.16.3"
} }
} }
+4 -10
View File
@@ -25,12 +25,10 @@ export default defineConfig({
/* Long global timeout for complex tests /* Long global timeout for complex tests
* 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: 240 * 1000, timeout: 120 * 1000,
actionTimeout: 40 * 1000, actionTimeout: 20 * 1000,
navigationTimeout: 40 * 1000, navigationTimeout: 20 * 1000,
expect: { timeout: 40 * 1000 }, expect: { timeout: 20 * 1000 },
"permissions": ["clipboard-read"],
/* 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: {
@@ -39,10 +37,6 @@ export default defineConfig({
browserName: 'firefox', browserName: 'firefox',
locale: 'en-GB', locale: 'en-GB',
timezoneId: 'Europe/London', timezoneId: 'Europe/London',
ignoreHTTPSErrors: true,
launchOptions: {
args: ['--ignore-certificate-errors']
},
/* Always collect trace (other values add random test failures) See https://playwright.dev/docs/trace-viewer */ /* Always collect trace (other values add random test failures) See https://playwright.dev/docs/trace-viewer */
trace: 'on', trace: 'on',
+4 -6
View File
@@ -10,7 +10,7 @@ DOCKER_BUILDKIT=1
##################### #####################
# Playwright Config # # Playwright Config #
##################### #####################
PW_KEEP_SERVICE_RUNNING=${PW_KEEP_SERVICE_RUNNING:-false} PW_KEEP_SERVICE_RUNNNING=${PW_KEEP_SERVICE_RUNNNING:-false}
PW_SMTP_FROM=vaultwarden@playwright.test PW_SMTP_FROM=vaultwarden@playwright.test
##################### #####################
@@ -38,8 +38,8 @@ TEST_USER3_MAIL=${TEST_USER3}@example.com
################### ###################
# Keycloak Config # # Keycloak Config #
################### ###################
KC_BOOTSTRAP_ADMIN_USERNAME=admin KEYCLOAK_ADMIN=admin
KC_BOOTSTRAP_ADMIN_PASSWORD=${KC_BOOTSTRAP_ADMIN_USERNAME} KEYCLOAK_ADMIN_PASSWORD=${KEYCLOAK_ADMIN}
KC_HTTP_HOST=127.0.0.1 KC_HTTP_HOST=127.0.0.1
KC_HTTP_PORT=8081 KC_HTTP_PORT=8081
@@ -52,12 +52,10 @@ DUMMY_AUTHORITY=http://${KC_HTTP_HOST}:${KC_HTTP_PORT}/realms/${DUMMY_REALM}
# Vaultwarden Config # # Vaultwarden Config #
###################### ######################
ROCKET_PORT=8003 ROCKET_PORT=8003
ROCKET_TLS={certs="/data/ssl/localhost.crt",key="/data/ssl/localhost.key"} DOMAIN=http://localhost:${ROCKET_PORT}
DOMAIN=https://127.0.0.1:${ROCKET_PORT}
LOG_LEVEL=info,oidcwarden::sso=debug LOG_LEVEL=info,oidcwarden::sso=debug
LOGIN_RATELIMIT_MAX_BURST=100 LOGIN_RATELIMIT_MAX_BURST=100
ADMIN_TOKEN=admin ADMIN_TOKEN=admin
CLIENT_SUPPRESS_ONBOARDING=true
SMTP_SECURITY=off SMTP_SECURITY=off
SMTP_PORT=${MAILDEV_SMTP_PORT} SMTP_PORT=${MAILDEV_SMTP_PORT}
+11 -5
View File
@@ -1,8 +1,6 @@
import { test, expect, type TestInfo } from '@playwright/test'; import { test, expect, type TestInfo } from '@playwright/test';
import * as utils from "../global-utils"; import * as utils from "../global-utils";
import * as orgs from './setups/orgs';
import { createAccount } from './setups/user'; import { createAccount } from './setups/user';
let users = utils.loadEnv(); let users = utils.loadEnv();
@@ -18,12 +16,20 @@ test.afterAll('Teardown', async ({}) => {
test('Create', async ({ page }) => { test('Create', async ({ page }) => {
await createAccount(test, page, users.user1); await createAccount(test, page, users.user1);
await orgs.create(test, page, 'New organisation'); await test.step('Create Org', async () => {
await page.getByRole('link', { name: 'New organisation' }).click();
await page.getByLabel('Organisation name (required)').fill('Test');
await page.getByRole('button', { name: 'Submit' }).click();
await page.locator('div').filter({ hasText: 'Members' }).nth(2).click();
await utils.checkNotification(page, 'Organisation created');
});
await test.step('Create Collection', async () => { await test.step('Create Collection', async () => {
await page.getByRole('button', { name: 'New', exact: true }).click(); await page.getByRole('link', { name: 'Collections' }).click();
await page.getByRole('button', { name: 'New' }).click();
await page.getByRole('menuitem', { name: 'Collection' }).click(); await page.getByRole('menuitem', { name: 'Collection' }).click();
await page.getByRole('textbox', { name: 'Name * (required)', exact: true }).fill('RandomCollec'); await page.getByLabel('Name (required)').fill('RandomCollec');
await page.getByRole('button', { name: 'Save' }).click(); await page.getByRole('button', { name: 'Save' }).click();
await utils.checkNotification(page, 'Created collection RandomCollec'); await utils.checkNotification(page, 'Created collection RandomCollec');
await expect(page.getByRole('button', { name: 'RandomCollec' })).toBeVisible(); await expect(page.getByRole('button', { name: 'RandomCollec' })).toBeVisible();
-56
View File
@@ -1,56 +0,0 @@
import { test, expect, type Page, type TestInfo } from '@playwright/test';
import * as OTPAuth from "otpauth";
import * as utils from "../global-utils";
import { createAccount, logUser } from './setups/user';
import { activateTOTP, disableTOTP } from './setups/2fa';
let users = utils.loadEnv();
let totp;
test.beforeAll('Setup', async ({ browser }, testInfo: TestInfo) => {
await utils.startVault(browser, testInfo, {});
});
test.afterAll('Teardown', async ({}) => {
utils.stopVault();
});
test('Change Key settings', async ({ page }) => {
await createAccount(test, page, users.user1);
await test.step('Change SHA-256 Iterations', async () => {
await page.getByRole('button', { name: 'Toggle collapse Settings' }).click();
await page.getByRole('link', { name: 'Security' }).click();
await page.getByRole('link', { name: 'Keys' }).click();
await page.getByRole('spinbutton', { name: 'KDF iterations * (required)'}).fill('700000');
await page.getByRole('button', { name: 'Update encryption settings' }).click();
await page.getByRole('textbox', { name: 'Master password * (required)' }).fill(users.user1.password);
await page.getByRole('button', { name: 'Update settings' }).click();
await page.getByRole('heading', { name: 'Log in' }).click();
});
await logUser(test, page, users.user1);
await test.step('Switch to Argon2', async () => {
await page.getByRole('button', { name: 'Toggle collapse Settings' }).click();
await page.getByRole('link', { name: 'Security' }).click();
await page.getByRole('link', { name: 'Keys' }).click();
await page.locator('.ng-arrow-wrapper').click();
await page.getByText('Argon2id').click();
await page.getByRole('spinbutton', { name: 'KDF memory (MB) * (required)'}).fill('16');
await page.getByRole('spinbutton', { name: 'KDF iterations * (required)'}).fill('2');
await page.getByRole('spinbutton', { name: 'KDF parallelism * (required)'}).fill('1');
await page.getByRole('button', { name: 'Update encryption settings' }).click();
await page.getByRole('textbox', { name: 'Master password * (required)' }).fill(users.user1.password);
await page.getByRole('button', { name: 'Update settings' }).click();
await page.getByRole('heading', { name: 'Log in' }).click();
});
await logUser(test, page, users.user1);
});
+25 -6
View File
@@ -41,10 +41,13 @@ test('Account creation', async ({ page }) => {
test('Login', async ({ context, page }) => { test('Login', async ({ context, page }) => {
const mailBuffer = mailserver.buffer(users.user1.email); const mailBuffer = mailserver.buffer(users.user1.email);
await logUser(test, page, users.user1, { mailBuffer }); await logUser(test, page, users.user1, mailBuffer);
await test.step('verify email', async () => { await test.step('verify email', async () => {
await page.getByRole('button', { name: "Send email" }).click(); await page.getByText('Verify your account\'s email').click();
await expect(page.getByText('Verify your account\'s email')).toBeVisible();
await page.getByRole('button', { name: 'Send email' }).click();
await utils.checkNotification(page, 'Check your email inbox for a verification link'); await utils.checkNotification(page, 'Check your email inbox for a verification link');
const verify = await mailBuffer.expect((m) => m.subject === "Verify Your Email"); const verify = await mailBuffer.expect((m) => m.subject === "Verify Your Email");
@@ -75,10 +78,26 @@ test('Activate 2fa', async ({ page }) => {
test('2fa', async ({ page }) => { test('2fa', async ({ page }) => {
const emails = mailserver.buffer(users.user1.email); const emails = mailserver.buffer(users.user1.email);
await logUser(test, page, users.user1, { await test.step('login', async () => {
mailBuffer: emails, await page.goto('/');
mail2fa: true,
}); await page.getByLabel(/Email address/).fill(users.user1.email);
await page.getByRole('button', { name: 'Continue' }).click();
await page.getByLabel('Master password').fill(users.user1.password);
await page.getByRole('button', { name: 'Log in with master password' }).click();
await expect(page.getByRole('heading', { name: 'Verify your Identity' })).toBeVisible();
const code = await retrieveEmailCode(test, page, emails);
await page.getByLabel(/Verification code/).fill(code);
await page.getByRole('button', { name: 'Continue' }).click();
await page.getByRole('button', { name: 'Add it later' }).click();
await page.getByRole('link', { name: 'Skip to web app' }).click();
await expect(page).toHaveTitle(/Vaults/);
})
await disableEmail(test, page, users.user1);
emails.close(); emails.close();
}); });
+2 -2
View File
@@ -37,8 +37,8 @@ test('Authenticator 2fa', async ({ page }) => {
await page.getByLabel(/Email address/).fill(users.user1.email); await page.getByLabel(/Email address/).fill(users.user1.email);
await page.getByRole('button', { name: 'Continue' }).click(); await page.getByRole('button', { name: 'Continue' }).click();
await page.getByRole('textbox', { name: 'Master password * (required)', exact: true }).fill(users.user1.password); await page.getByLabel('Master password').fill(users.user1.password);
await page.getByRole('button', { name: 'Log in', exact: true }).click(); await page.getByRole('button', { name: 'Log in with master password' }).click();
await expect(page.getByRole('heading', { name: 'Verify your Identity' })).toBeVisible(); await expect(page.getByRole('heading', { name: 'Verify your Identity' })).toBeVisible();
await page.getByLabel(/Verification code/).fill(totp.generate({timestamp})); await page.getByLabel(/Verification code/).fill(totp.generate({timestamp}));
+21 -23
View File
@@ -4,7 +4,6 @@ import { MailDev } from 'maildev';
import * as utils from '../global-utils'; import * as utils from '../global-utils';
import * as orgs from './setups/orgs'; import * as orgs from './setups/orgs';
import { createAccount, logUser } from './setups/user'; import { createAccount, logUser } from './setups/user';
import { activateTOTP } from './setups/2fa';
let users = utils.loadEnv(); let users = utils.loadEnv();
@@ -21,7 +20,6 @@ test.beforeAll('Setup', async ({ browser }, testInfo: TestInfo) => {
await utils.startVault(browser, testInfo, { await utils.startVault(browser, testInfo, {
SMTP_HOST: process.env.MAILDEV_HOST, SMTP_HOST: process.env.MAILDEV_HOST,
SMTP_FROM: process.env.PW_SMTP_FROM, SMTP_FROM: process.env.PW_SMTP_FROM,
EMAIL_2FA_AUTO_FALLBACK: "true",
}); });
mail1Buffer = mailServer.buffer(users.user1.email); mail1Buffer = mailServer.buffer(users.user1.email);
@@ -47,7 +45,7 @@ test('Invite users', async ({ page }) => {
await orgs.policies(test, page, 'Test'); await orgs.policies(test, page, 'Test');
await page.getByRole('button', { name: 'Account recovery' }).click(); await page.getByRole('button', { name: 'Account recovery' }).click();
await page.getByRole('checkbox', { name: 'Turn on' }).check(); await page.getByRole('checkbox', { name: 'Turn on' }).check();
await page.getByRole('checkbox', { name: 'Automatically enroll new' }).check(); await page.getByRole('checkbox', { name: 'Require new members' }).check();
await page.getByRole('button', { name: 'Save' }).click(); await page.getByRole('button', { name: 'Save' }).click();
await utils.checkNotification(page, 'Edited policy Account recovery'); await utils.checkNotification(page, 'Edited policy Account recovery');
}); });
@@ -68,16 +66,18 @@ test('invited with new account', async ({ page }) => {
await page.goto(link); await page.goto(link);
await expect(page).toHaveTitle(/Create account | Vaultwarden Web/); await expect(page).toHaveTitle(/Create account | Vaultwarden Web/);
// await page.getByLabel('Name').fill(users.user2.name); //await page.getByLabel('Name').fill(users.user2.name);
await page.getByRole('textbox', { name: 'Master password * (required)', exact: true }).fill(users.user2.password); await page.getByLabel('Master password (required)', { exact: true }).fill(users.user2.password);
await page.getByRole('textbox', { name: 'Confirm master password * (' }).fill(users.user2.password); await page.getByLabel('Confirm master password (').fill(users.user2.password);
await page.getByRole('button', { name: 'Create account' }).click(); await page.getByRole('button', { name: 'Create account' }).click();
await utils.checkNotification(page, 'Your new account has been created'); await utils.checkNotification(page, 'Your new account has been created');
await utils.checkNotification(page, 'Invitation accepted');
await utils.ignoreExtension(page);
// Redirected to the vault // Redirected to the vault
await expect(page).toHaveTitle('Vaults | Vaultwarden Web'); await expect(page).toHaveTitle('Vaults | Vaultwarden Web');
// await utils.checkNotification(page, 'You have been logged in!'); // await utils.checkNotification(page, 'You have been logged in!');
await utils.checkNotification(page, 'Successfully accepted your invitation');
}); });
await test.step('Check mails', async () => { await test.step('Check mails', async () => {
@@ -100,19 +100,21 @@ test('invited with existing account', async ({ page }) => {
await page.getByRole('button', { name: 'Continue' }).click(); await page.getByRole('button', { name: 'Continue' }).click();
// Unlock page // Unlock page
await page.getByRole('textbox', { name: 'Master password * (required)', exact: true }).fill(users.user3.password); await page.getByLabel('Master password').fill(users.user3.password);
await page.getByRole('button', { name: 'Log in', exact: true }).click(); await page.getByRole('button', { name: 'Log in with master password' }).click();
await utils.checkNotification(page, 'Invitation accepted');
await utils.ignoreExtension(page);
// We are now in the default vault page // We are now in the default vault page
await expect(page).toHaveTitle(/Vaultwarden Web/); await expect(page).toHaveTitle(/Vaultwarden Web/);
await utils.checkNotification(page, 'Successfully accepted your invitation');
await mail3Buffer.expect((m) => m.subject === 'New Device Logged In From Firefox'); await mail3Buffer.expect((m) => m.subject === 'New Device Logged In From Firefox');
await mail1Buffer.expect((m) => m.subject.includes('Invitation to Test accepted')); await mail1Buffer.expect((m) => m.subject.includes('Invitation to Test accepted'));
}); });
test('Confirm invited user', async ({ page }) => { test('Confirm invited user', async ({ page }) => {
await logUser(test, page, users.user1, { mailBuffer: mail1Buffer }); await logUser(test, page, users.user1, mail1Buffer);
await orgs.members(test, page, 'Test'); await orgs.members(test, page, 'Test');
await orgs.confirm(test, page, 'Test', users.user2.email); await orgs.confirm(test, page, 'Test', users.user2.email);
@@ -121,26 +123,25 @@ test('Confirm invited user', async ({ page }) => {
}); });
test('Organization is visible', async ({ page }) => { test('Organization is visible', async ({ page }) => {
await logUser(test, page, users.user2, { mailBuffer: mail2Buffer }); await logUser(test, page, users.user2, mail2Buffer);
await page.getByRole('button', { name: 'vault: Test', exact: true }).click(); await page.getByRole('button', { name: 'vault: Test', exact: true }).click();
await expect(page.getByLabel('Filter: Default collection')).toBeVisible(); await expect(page.getByLabel('Filter: Default collection')).toBeVisible();
}); });
test('Recover user password', async ({ page }) => { test('Recover user password', async ({ page }) => {
await logUser(test, page, users.user1, { mailBuffer: mail1Buffer }); await logUser(test, page, users.user1, mail1Buffer);
let newPassword = "TotoNewPassword"; let newPassword = "TotoNewPassword";
await orgs.members(test, page, 'Test'); await orgs.members(test, page, 'Test');
await test.step(`Recover ${users.user2.email}`, async () => { await test.step(`Rrcover ${users.user2.email}`, async () => {
await expect(page.getByRole('heading', { name: 'Members' })).toBeVisible(); await expect(page.getByRole('heading', { name: 'Members' })).toBeVisible();
await page.getByRole('row').filter({hasText: users.user2.email}).getByLabel('Options').click(); await page.getByRole('row').filter({hasText: users.user2.email}).getByLabel('Options').click();
await page.getByRole('menuitem', { name: 'Recover account' }).click(); await page.getByRole('menuitem', { name: 'Recover account' }).click();
await page.getByRole('textbox', { name: 'New master password * (required)', exact: true }).fill(newPassword); await page.getByRole('textbox', { name: 'New master password (required)', exact: true }).fill(newPassword);
await page.getByRole('textbox', { name: 'Confirm new master password * (' }).fill(newPassword); await page.getByRole('textbox', { name: 'Confirm new master password (' }).fill(newPassword);
await page.getByRole('button', { name: 'Save' }).click(); await page.getByRole('button', { name: 'Save' }).click();
await utils.checkNotification(page, 'Account recovery success'); await utils.checkNotification(page, 'Password reset success');
await mail2Buffer.expect((m) => m.subject.includes('Master Password Has Been Changed'));
}); });
let user2 = { let user2 = {
@@ -148,8 +149,5 @@ test('Recover user password', async ({ page }) => {
name: users.user2.name, name: users.user2.name,
password: newPassword, password: newPassword,
}; };
await logUser(test, page, user2, { await logUser(test, page, user2, mail2Buffer);
mailBuffer: mail2Buffer,
notNewDevice: true,
});
}); });
-110
View File
@@ -1,110 +0,0 @@
import { test, expect, type Page, type TestInfo } from '@playwright/test';
import * as OTPAuth from "otpauth";
import * as utils from "../global-utils";
import { createAccount, logUser } from './setups/user';
let users = utils.loadEnv();
let totp;
test.beforeAll('Setup', async ({ browser }, testInfo: TestInfo) => {
await utils.startVault(browser, testInfo, {});
const context = await browser.newContext();
const page = await context.newPage();
await createAccount(test, page, users.user1);
await context.close();
});
test.afterAll('Teardown', async ({}) => {
utils.stopVault();
});
test('Password', async ({ context, page }, testInfo: TestInfo) => {
const label = 'Test Password';
await logUser(test, page, users.user1);
await test.step('Create password entry', async () => {
await page.getByRole('button', { name: 'New item' }).click();
await page.getByRole('textbox', { name: 'Item name * (required)' }).fill(label);
await page.getByRole('textbox', { name: 'Username' }).fill(users.user1.name);
await page.getByRole('textbox', { name: 'Password' }).fill(users.user1.password);
await page.getByRole('button', { name: 'Save' }).click();
await utils.checkNotification(page, 'Item added');
await page.getByRole('button', { name: 'Close' }).click();
});
// Log again
await logUser(test, page, users.user1);
await test.step('Check', async () => {
await page.getByRole('row').filter({ hasText: label }).getByRole('button', { name: label }).click();
await page.getByTestId('copy-username').click();
await utils.checkNotification(page, 'Username copied');
expect(await page.evaluate(() => navigator.clipboard.readText())).toBe(users.user1.name)
await page.getByTestId('copy-password').click();
await utils.checkNotification(page, 'Password copied');
expect(await page.evaluate(() => navigator.clipboard.readText())).toBe(users.user1.password)
await page.getByRole('button', { name: 'Close' }).click();
});
await test.step('Delete', async () => {
await page.getByRole('row').filter({ hasText: label }).getByLabel('Options').click();
await page.getByRole('menuitem', { name: 'Delete' }).click();
await page.getByRole('button', { name: 'Yes' }).click();
await utils.checkNotification(page, 'Item sent to bin');
});
// Log again
await logUser(test, page, users.user1);
await test.step('Deleted', async () => {
await expect(page.getByRole('row').filter({ hasText: label })).toHaveCount(0)
});
});
test('SSH Key', async ({ context, page }, testInfo: TestInfo) => {
const label = 'Test SSH key';
await logUser(test, page, users.user1);
const privateKey = await test.step('Create key entry', async () => {
await page.getByRole('button', { name: 'New', exact: true }).click();
await page.getByRole('menuitem', { name: 'SSH key' }).click();
await page.getByRole('textbox', { name: 'Item name * (required)' }).fill('Test SSH key');
await page.getByRole('button', { name: 'Save' }).click();
await utils.checkNotification(page, 'Item added');
await page.getByRole('button', { name: 'Copy private key' }).click();
await utils.checkNotification(page, 'Private key copied');
return await page.evaluate(() => navigator.clipboard.readText());
});
// Log again
await logUser(test, page, users.user1);
await test.step('Check', async () => {
await page.getByRole('row').filter({ hasText: label }).getByRole('button', { name: label }).click();
await page.getByRole('button', { name: 'Copy private key' }).click();
await utils.checkNotification(page, 'Private key copied');
expect(await page.evaluate(() => navigator.clipboard.readText())).toBe(privateKey)
await page.getByRole('button', { name: 'Close' }).click();
});
await test.step('Delete', async () => {
await page.getByRole('row').filter({ hasText: label }).getByLabel('Options').click();
await page.getByRole('menuitem', { name: 'Delete' }).click();
await page.getByRole('button', { name: 'Yes' }).click();
await utils.checkNotification(page, 'Item sent to bin');
});
// Log again
await logUser(test, page, users.user1);
await test.step('Deleted', async () => {
await expect(page.getByRole('row').filter({ hasText: label })).toHaveCount(0)
})
});
+8 -8
View File
@@ -21,11 +21,11 @@ test('Send', async ({ browser, page }) => {
await page.getByRole('link', { name: 'Send' }).click(); await page.getByRole('link', { name: 'Send' }).click();
await expect(page.locator('#main-content').getByText('Send', { exact: true })).toBeVisible(); await expect(page.locator('#main-content').getByText('Send', { exact: true })).toBeVisible();
await page.getByRole('button', { name: 'New Send', exact: true }).click(); await page.getByRole('button', { name: 'New', exact: true }).click();
await page.getByRole('menuitem', { name: 'Text' }).click(); await page.getByRole('menuitem', { name: 'Text' }).click();
await page.getByRole('textbox', { name: 'Send name * (required)' }).fill('Test'); await page.getByRole('textbox', { name: 'Send name (required)' }).fill('Test');
await page.getByRole('textbox', { name: 'Text to share * (required)' }).fill('test'); await page.getByRole('textbox', { name: 'Text to share (required)' }).fill('test');
await page.getByRole('button', { name: 'Save' }).click(); await page.getByRole('button', { name: 'Save' }).click();
await page.locator('footer').getByRole('button', { name: 'Copy link' }).click(); await page.locator('footer').getByRole('button', { name: 'Copy link' }).click();
@@ -46,14 +46,14 @@ test('Send', async ({ browser, page }) => {
await page.getByRole('link', { name: 'Send' }).click(); await page.getByRole('link', { name: 'Send' }).click();
await expect(page.locator('#main-content').getByText('Send', { exact: true })).toBeVisible(); await expect(page.locator('#main-content').getByText('Send', { exact: true })).toBeVisible();
await page.getByRole('button', { name: 'New' }).click(); await page.getByRole('button', { name: 'New', exact: true }).click();
await page.getByRole('menuitem', { name: 'Text' }).click(); await page.getByRole('menuitem', { name: 'Text' }).click();
await page.getByRole('textbox', { name: 'Send name * (required)' }).fill('Password'); await page.getByRole('textbox', { name: 'Send name (required)' }).fill('Password');
await page.getByRole('textbox', { name: 'Text to share * (required)' }).fill('password'); await page.getByRole('textbox', { name: 'Text to share (required)' }).fill('password');
await page.getByRole('combobox', { name: 'Who can view' }).click(); await page.getByRole('combobox', { name: 'Who can view' }).click();
await page.getByText('Anyone with a password set by you').click(); await page.getByText('Anyone with a password set by you').click();
await page.getByRole('textbox', { name: 'Password * (required)', exact: true }).fill('password'); await page.getByRole('textbox', { name: 'Password (required)' }).fill('password');
await page.getByRole('button', { name: 'Save' }).click(); await page.getByRole('button', { name: 'Save' }).click();
await page.locator('footer').getByRole('button', { name: 'Copy link' }).click(); await page.locator('footer').getByRole('button', { name: 'Copy link' }).click();
@@ -64,7 +64,7 @@ test('Send', async ({ browser, page }) => {
await test.step('View with password', async () => { await test.step('View with password', async () => {
await page2.goto(pwd_url, { waitUntil: 'domcontentloaded' }); await page2.goto(pwd_url, { waitUntil: 'domcontentloaded' });
await expect(page2.getByRole('heading', { name: 'Enter the password to view' })).toBeVisible(); await expect(page2.getByRole('heading', { name: 'Enter the password to view' })).toBeVisible();
await page2.getByRole('textbox', { name: 'Password * (required)' }).fill('password'); await page2.getByRole('textbox', { name: 'Password (required)' }).fill('password');
await page2.getByRole('button', { name: 'Continue' }).click(); await page2.getByRole('button', { name: 'Continue' }).click();
await expect(page2.getByRole('heading', { name: 'View Send' })).toBeVisible(); await expect(page2.getByRole('heading', { name: 'View Send' })).toBeVisible();
await expect(await page2.getByRole('paragraph').filter({ hasText: 'Password' })).toBeVisible(); await expect(await page2.getByRole('paragraph').filter({ hasText: 'Password' })).toBeVisible();
+7 -8
View File
@@ -11,11 +11,10 @@ export async function activateTOTP(test: Test, page: Page, user: { name: string,
await page.getByRole('link', { name: 'Security' }).click(); await page.getByRole('link', { name: 'Security' }).click();
await page.getByRole('link', { name: 'Two-step login' }).click(); await page.getByRole('link', { name: 'Two-step login' }).click();
await page.locator('bit-item').filter({ hasText: /Authenticator app/ }).getByRole('button').click(); await page.locator('bit-item').filter({ hasText: /Authenticator app/ }).getByRole('button').click();
await page.getByRole('textbox', { name: 'Master password * (required)', exact: true }).fill(user.password); await page.getByLabel('Master password (required)').fill(user.password);
await page.getByRole('button', { name: 'Continue' }).click(); await page.getByRole('button', { name: 'Continue' }).click();
const secret = await page.getByLabel('Key', { exact: true }).innerText(); const secret = await page.getByLabel('Key').innerText();
let totp = new OTPAuth.TOTP({ secret, period: 30 }); let totp = new OTPAuth.TOTP({ secret, period: 30 });
await page.getByLabel(/Verification code/).fill(totp.generate()); await page.getByLabel(/Verification code/).fill(totp.generate());
@@ -34,8 +33,8 @@ export async function disableTOTP(test: Test, page: Page, user: { password: stri
await page.getByRole('link', { name: 'Security' }).click(); await page.getByRole('link', { name: 'Security' }).click();
await page.getByRole('link', { name: 'Two-step login' }).click(); await page.getByRole('link', { name: 'Two-step login' }).click();
await page.locator('bit-item').filter({ hasText: /Authenticator app/ }).getByRole('button').click(); await page.locator('bit-item').filter({ hasText: /Authenticator app/ }).getByRole('button').click();
await page.getByRole('textbox', { name: 'Master password * (required)', exact: true }).click() await page.getByLabel('Master password (required)').click();
await page.getByRole('textbox', { name: 'Master password * (required)', exact: true }).fill(user.password); await page.getByLabel('Master password (required)').fill(user.password);
await page.getByRole('button', { name: 'Continue' }).click(); await page.getByRole('button', { name: 'Continue' }).click();
await page.getByRole('button', { name: 'Turn off' }).click(); await page.getByRole('button', { name: 'Turn off' }).click();
await page.getByRole('button', { name: 'Yes' }).click(); await page.getByRole('button', { name: 'Yes' }).click();
@@ -50,7 +49,7 @@ export async function activateEmail(test: Test, page: Page, user: { name: string
await page.getByRole('link', { name: 'Security' }).click(); await page.getByRole('link', { name: 'Security' }).click();
await page.getByRole('link', { name: 'Two-step login' }).click(); await page.getByRole('link', { name: 'Two-step login' }).click();
await page.locator('bit-item').filter({ hasText: 'Enter a code sent to your email' }).getByRole('button').click(); await page.locator('bit-item').filter({ hasText: 'Enter a code sent to your email' }).getByRole('button').click();
await page.getByRole('textbox', { name: 'Master password * (required)', exact: true }).fill(user.password); await page.getByLabel('Master password (required)').fill(user.password);
await page.getByRole('button', { name: 'Continue' }).click(); await page.getByRole('button', { name: 'Continue' }).click();
await page.getByRole('button', { name: 'Send email' }).click(); await page.getByRole('button', { name: 'Send email' }).click();
}); });
@@ -82,8 +81,8 @@ export async function disableEmail(test: Test, page: Page, user: { password: str
await page.getByRole('link', { name: 'Security' }).click(); await page.getByRole('link', { name: 'Security' }).click();
await page.getByRole('link', { name: 'Two-step login' }).click(); await page.getByRole('link', { name: 'Two-step login' }).click();
await page.locator('bit-item').filter({ hasText: 'Email' }).getByRole('button').click(); await page.locator('bit-item').filter({ hasText: 'Email' }).getByRole('button').click();
await page.getByRole('textbox', { name: 'Master password * (required)', exact: true }).click() await page.getByLabel('Master password (required)').click();
await page.getByRole('textbox', { name: 'Master password * (required)', exact: true }).fill(user.password); await page.getByLabel('Master password (required)').fill(user.password);
await page.getByRole('button', { name: 'Continue' }).click(); await page.getByRole('button', { name: 'Continue' }).click();
await page.getByRole('button', { name: 'Turn off' }).click(); await page.getByRole('button', { name: 'Turn off' }).click();
await page.getByRole('button', { name: 'Yes' }).click(); await page.getByRole('button', { name: 'Yes' }).click();
-21
View File
@@ -1,21 +0,0 @@
import { expect, type Browser, Page } from '@playwright/test';
import * as utils from '../../global-utils';
utils.loadEnv();
export async function login(test, page: Page) {
await test.step(`Admin login`, async () => {
await page.goto('/admin');
await page.getByRole('textbox', { name: 'Enter admin token' }).fill(process.env.ADMIN_TOKEN);
await page.getByRole('button', { name: 'Enter' }).click();
});
}
export async function invite(test, page: Page, email: string) {
await test.step(`Invite user with ${email}`, async () => {
await page.getByRole('link', { name: 'Users' }).click();
await page.getByRole('textbox', { name: 'Enter email' }).fill(email);
await page.getByRole('button', { name: 'Invite' }).click();
await expect(page.getByRole('row', { name: email })).toHaveText(/Invited/);
});
}
+1 -1
View File
@@ -5,7 +5,7 @@ const utils = require('../../global-utils');
utils.loadEnv(); utils.loadEnv();
test('DB teardown ?', async ({ serviceName }) => { test('DB teardown ?', async ({ serviceName }) => {
if( process.env.PW_KEEP_SERVICE_RUNNING !== "true" ) { if( process.env.PW_KEEP_SERVICE_RUNNNING !== "true" ) {
utils.stopComposeService(serviceName); utils.stopComposeService(serviceName);
} }
}); });
+11 -14
View File
@@ -3,14 +3,11 @@ import { expect, type Browser,Page } from '@playwright/test';
import * as utils from '../../global-utils'; import * as utils from '../../global-utils';
export async function create(test, page: Page, name: string) { export async function create(test, page: Page, name: string) {
await test.step(`Create Org ${name}`, async () => { await test.step('Create Org', async () => {
let pm_locator = page.locator('a').filter({ hasText: 'Password Manager' }); await page.locator('a').filter({ hasText: 'Password Manager' }).first().click();
if( await pm_locator.count() > 0 ){
pm_locator.first().click();
}
await expect(page.getByTitle('All vaults', { exact: true })).toBeVisible(); await expect(page.getByTitle('All vaults', { exact: true })).toBeVisible();
await page.getByRole('link', { name: 'New organisation' }).click(); await page.getByRole('link', { name: 'New organisation' }).click();
await page.getByRole('textbox', { name: 'Organisation name * (required)', exact: true }).fill(name); await page.getByLabel('Organisation name (required)').fill(name);
await page.getByRole('button', { name: 'Submit' }).click(); await page.getByRole('button', { name: 'Submit' }).click();
await utils.checkNotification(page, 'Organisation created'); await utils.checkNotification(page, 'Organisation created');
@@ -21,7 +18,7 @@ export async function policies(test, page: Page, name: string) {
await test.step(`Navigate to ${name} policies`, async () => { await test.step(`Navigate to ${name} policies`, async () => {
await page.locator('a').filter({ hasText: 'Admin Console' }).first().click(); await page.locator('a').filter({ hasText: 'Admin Console' }).first().click();
await page.locator('org-switcher').getByLabel(/Toggle collapse/).click(); await page.locator('org-switcher').getByLabel(/Toggle collapse/).click();
await page.locator('org-switcher > bit-nav-group > div > bit-nav-item').filter({ hasText: `${name}` }).first().click(); await page.locator('org-switcher').getByRole('link', { name: `${name}` }).first().click();
await expect(page.getByRole('heading', { name: `${name} collections` })).toBeVisible(); await expect(page.getByRole('heading', { name: `${name} collections` })).toBeVisible();
await page.getByRole('button', { name: 'Toggle collapse Settings' }).click(); await page.getByRole('button', { name: 'Toggle collapse Settings' }).click();
await page.getByRole('link', { name: 'Policies' }).click(); await page.getByRole('link', { name: 'Policies' }).click();
@@ -33,11 +30,11 @@ export async function members(test, page: Page, name: string) {
await test.step(`Navigate to ${name} members`, async () => { await test.step(`Navigate to ${name} members`, async () => {
await page.locator('a').filter({ hasText: 'Admin Console' }).first().click(); await page.locator('a').filter({ hasText: 'Admin Console' }).first().click();
await page.locator('org-switcher').getByLabel(/Toggle collapse/).click(); await page.locator('org-switcher').getByLabel(/Toggle collapse/).click();
await page.locator('org-switcher > bit-nav-group > div > bit-nav-item').filter({ hasText: `${name}` }).first().click(); await page.locator('org-switcher').getByRole('link', { name: `${name}` }).first().click();
await expect(page.getByRole('heading', { name: `${name} collections` })).toBeVisible(); await expect(page.getByRole('heading', { name: `${name} collections` })).toBeVisible();
await page.getByRole('link', { name: 'Members' }).click(); await page.locator('div').filter({ hasText: 'Members' }).nth(2).click();
await expect(page.getByRole('heading', { name: 'Members' })).toBeVisible(); await expect(page.getByRole('heading', { name: 'Members' })).toBeVisible();
await expect(page.getByRole('columnheader', { name: 'Select all' })).toBeVisible(); await expect(page.getByRole('cell', { name: 'All' })).toBeVisible();
}); });
} }
@@ -45,13 +42,13 @@ export async function invite(test, page: Page, name: string, email: string) {
await test.step(`Invite ${email}`, async () => { await test.step(`Invite ${email}`, async () => {
await expect(page.getByRole('heading', { name: 'Members' })).toBeVisible(); await expect(page.getByRole('heading', { name: 'Members' })).toBeVisible();
await page.getByRole('button', { name: 'Invite member' }).click(); await page.getByRole('button', { name: 'Invite member' }).click();
await page.getByRole('textbox', { name: 'Email * (required)', exact: true }).fill(email); await page.getByLabel('Email (required)').fill(email);
await page.getByRole('tab', { name: 'Collections' }).click(); await page.getByRole('tab', { name: 'Collections' }).click();
await page.getByRole('combobox', { name: 'Permission' }).click(); await page.getByRole('combobox', { name: 'Permission' }).click();
await page.getByText('Edit items', { exact: true }).click(); await page.getByText('Edit items', { exact: true }).click();
await page.getByRole('combobox', { name: 'Select collections' }).click(); await page.getByLabel('Select collections').click();
await page.getByLabel('Options List').getByText('Default collection').click(); await page.getByText('Default collection').click();
await page.getByRole('columnheader', { name: 'Collection', exact: true }).click(); await page.getByRole('cell', { name: 'Collection', exact: true }).click();
await page.getByRole('button', { name: 'Save' }).click(); await page.getByRole('button', { name: 'Save' }).click();
await utils.checkNotification(page, 'User(s) invited'); await utils.checkNotification(page, 'User(s) invited');
}); });
+1 -1
View File
@@ -6,7 +6,7 @@ const utils = require('../../global-utils');
utils.loadEnv(); utils.loadEnv();
test('Keycloak teardown', async () => { test('Keycloak teardown', async () => {
if( process.env.PW_KEEP_SERVICE_RUNNING === "true" ) { if( process.env.PW_KEEP_SERVICE_RUNNNING === "true" ) {
console.log("Keep Keycloak running"); console.log("Keep Keycloak running");
} else { } else {
console.log("Keycloak stopping"); console.log("Keycloak stopping");
+17 -8
View File
@@ -15,8 +15,11 @@ export async function logNewUser(
options: { mailBuffer?: MailBuffer } = {} options: { mailBuffer?: MailBuffer } = {}
) { ) {
await test.step(`Create user ${user.name}`, async () => { await test.step(`Create user ${user.name}`, async () => {
await page.context().clearCookies();
await test.step('Landing page', async () => { await test.step('Landing page', async () => {
await utils.cleanLanding(page); await utils.cleanLanding(page);
await page.locator("input[type=email].vw-email-sso").fill(user.email); await page.locator("input[type=email].vw-email-sso").fill(user.email);
await page.getByRole('button', { name: /Use single sign-on/ }).click(); await page.getByRole('button', { name: /Use single sign-on/ }).click();
}); });
@@ -30,24 +33,26 @@ export async function logNewUser(
await test.step('Create Vault account', async () => { await test.step('Create Vault account', async () => {
await expect(page.getByRole('heading', { name: 'Join organisation' })).toBeVisible(); await expect(page.getByRole('heading', { name: 'Join organisation' })).toBeVisible();
await page.getByRole('textbox', { name: 'Master password * (required)', exact: true }).fill(user.password); await page.getByLabel('Master password (required)', { exact: true }).fill(user.password);
await page.getByRole('textbox', { name: 'Confirm master password * (' }).fill(user.password); await page.getByLabel('Confirm master password (').fill(user.password);
await page.getByRole('button', { name: 'Create account' }).click(); await page.getByRole('button', { name: 'Create account' }).click();
}); });
await utils.checkNotification(page, 'Account successfully created!');
await utils.checkNotification(page, 'Invitation accepted');
await utils.ignoreExtension(page);
await test.step('Default vault page', async () => { await test.step('Default vault page', async () => {
await expect(page).toHaveTitle(/Vaultwarden Web/); await expect(page).toHaveTitle(/Vaultwarden Web/);
await expect(page.getByTitle('All vaults', { exact: true })).toBeVisible(); await expect(page.getByTitle('All vaults', { exact: true })).toBeVisible();
}); });
await utils.checkNotification(page, 'Account successfully created!');
await utils.checkNotification(page, 'Invitation accepted');
if( options.mailBuffer ){ if( options.mailBuffer ){
let mailBuffer = options.mailBuffer; let mailBuffer = options.mailBuffer;
await test.step('Check emails', async () => { await test.step('Check emails', async () => {
await mailBuffer.expect((m) => m.subject.includes("New Device Logged"));
await mailBuffer.expect((m) => m.subject === "Welcome"); await mailBuffer.expect((m) => m.subject === "Welcome");
await mailBuffer.expect((m) => m.subject.includes("New Device Logged"));
}); });
} }
}); });
@@ -64,14 +69,16 @@ export async function logUser(
mailBuffer ?: MailBuffer, mailBuffer ?: MailBuffer,
totp?: OTPAuth.TOTP, totp?: OTPAuth.TOTP,
mail2fa?: boolean, mail2fa?: boolean,
notNewDevice?: boolean,
} = {} } = {}
) { ) {
let mailBuffer = options.mailBuffer; let mailBuffer = options.mailBuffer;
await test.step(`Log user ${user.email}`, async () => { await test.step(`Log user ${user.email}`, async () => {
await page.context().clearCookies();
await test.step('Landing page', async () => { await test.step('Landing page', async () => {
await utils.cleanLanding(page); await utils.cleanLanding(page);
await page.locator("input[type=email].vw-email-sso").fill(user.email); await page.locator("input[type=email].vw-email-sso").fill(user.email);
await page.getByRole('button', { name: /Use single sign-on/ }).click(); await page.getByRole('button', { name: /Use single sign-on/ }).click();
}); });
@@ -110,12 +117,14 @@ export async function logUser(
await page.getByRole('button', { name: 'Unlock' }).click(); await page.getByRole('button', { name: 'Unlock' }).click();
}); });
await utils.ignoreExtension(page);
await test.step('Default vault page', async () => { await test.step('Default vault page', async () => {
await expect(page).toHaveTitle(/Vaultwarden Web/); await expect(page).toHaveTitle(/Vaultwarden Web/);
await expect(page.getByTitle('All vaults', { exact: true })).toBeVisible(); await expect(page.getByTitle('All vaults', { exact: true })).toBeVisible();
}); });
if( mailBuffer && !options.notNewDevice ){ if( mailBuffer ){
await test.step('Check email', async () => { await test.step('Check email', async () => {
await mailBuffer.expect((m) => m.subject.includes("New Device Logged")); await mailBuffer.expect((m) => m.subject.includes("New Device Logged"));
}); });
+9 -25
View File
@@ -3,7 +3,6 @@ import { expect, type Browser, Page } from '@playwright/test';
import { type MailBuffer } from 'maildev'; import { type MailBuffer } from 'maildev';
import * as utils from '../../global-utils'; import * as utils from '../../global-utils';
import { retrieveEmailCode } from './2fa';
export async function createAccount(test, page: Page, user: { email: string, name: string, password: string }, mailBuffer?: MailBuffer) { export async function createAccount(test, page: Page, user: { email: string, name: string, password: string }, mailBuffer?: MailBuffer) {
await test.step(`Create user ${user.name}`, async () => { await test.step(`Create user ${user.name}`, async () => {
@@ -18,11 +17,12 @@ export async function createAccount(test, page: Page, user: { email: string, nam
await page.getByRole('button', { name: 'Continue' }).click(); await page.getByRole('button', { name: 'Continue' }).click();
// Vault finish Creation // Vault finish Creation
await page.getByRole('textbox', { name: 'Master password * (required)', exact: true }).fill(user.password); await page.getByLabel('Master password (required)', { exact: true }).fill(user.password);
await page.getByRole('textbox', { name: 'Confirm master password * (' }).fill(user.password); await page.getByLabel('Confirm master password (').fill(user.password);
await page.getByRole('button', { name: 'Create account' }).click(); await page.getByRole('button', { name: 'Create account' }).click();
await utils.checkNotification(page, 'Your new account has been created') await utils.checkNotification(page, 'Your new account has been created')
await utils.ignoreExtension(page);
// We are now in the default vault page // We are now in the default vault page
await expect(page).toHaveTitle('Vaults | Vaultwarden Web'); await expect(page).toHaveTitle('Vaults | Vaultwarden Web');
@@ -35,16 +35,7 @@ export async function createAccount(test, page: Page, user: { email: string, nam
}); });
} }
export async function logUser( export async function logUser(test, page: Page, user: { email: string, password: string }, mailBuffer?: MailBuffer) {
test,
page: Page,
user: { email: string, password: string },
options: {
mailBuffer ?: MailBuffer,
mail2fa?: boolean,
notNewDevice?: boolean,
} = {}
) {
await test.step(`Log user ${user.email}`, async () => { await test.step(`Log user ${user.email}`, async () => {
await utils.cleanLanding(page); await utils.cleanLanding(page);
@@ -52,23 +43,16 @@ export async function logUser(
await page.getByRole('button', { name: 'Continue' }).click(); await page.getByRole('button', { name: 'Continue' }).click();
// Unlock page // Unlock page
await page.getByRole('textbox', { name: 'Master password * (required)', exact: true }).fill(user.password); await page.getByLabel('Master password').fill(user.password);
await page.getByRole('button', { name: 'Log in', exact: true }).click(); await page.getByRole('button', { name: 'Log in with master password' }).click();
if( options.mail2fa ){ await utils.ignoreExtension(page);
await test.step('2FA check', async () => {
await expect(page.getByRole('heading', { name: 'Verify your Identity' })).toBeVisible();
let code = await retrieveEmailCode(test, page, options.mailBuffer);
await page.getByLabel(/Verification code/).fill(code);
await page.getByRole('button', { name: 'Continue' }).click();
});
}
// We are now in the default vault page // We are now in the default vault page
await expect(page).toHaveTitle(/Vaultwarden Web/); await expect(page).toHaveTitle(/Vaultwarden Web/);
if( options.mailBuffer && !options.notNewDevice ){ if( mailBuffer ){
await options.mailBuffer.expect((m) => m.subject === "New Device Logged In From Firefox"); await mailBuffer.expect((m) => m.subject === "New Device Logged In From Firefox");
} }
}); });
} }
+9 -52
View File
@@ -1,7 +1,6 @@
import { test, expect, type TestInfo } from '@playwright/test'; import { test, expect, type TestInfo } from '@playwright/test';
import { MailDev } from 'maildev'; import { MailDev } from 'maildev';
import * as admin from "./setups/admin";
import { logNewUser, logUser } from './setups/sso'; import { logNewUser, logUser } from './setups/sso';
import { activateEmail, disableEmail } from './setups/2fa'; import { activateEmail, disableEmail } from './setups/2fa';
import * as utils from "../global-utils"; import * as utils from "../global-utils";
@@ -20,7 +19,7 @@ test.beforeAll('Setup', async ({ browser }, testInfo: TestInfo) => {
await utils.startVault(browser, testInfo, { await utils.startVault(browser, testInfo, {
SSO_ENABLED: true, SSO_ENABLED: true,
SSO_ONLY: true, SSO_ONLY: false,
SMTP_HOST: process.env.MAILDEV_HOST, SMTP_HOST: process.env.MAILDEV_HOST,
SMTP_FROM: process.env.PW_SMTP_FROM, SMTP_FROM: process.env.PW_SMTP_FROM,
}); });
@@ -33,64 +32,22 @@ test.afterAll('Teardown', async ({}) => {
} }
}); });
test('2FA email', async ({ page }) => { test('Create and activate 2FA', async ({ page }) => {
const mailBuffer = mailserver.buffer(users.user1.email); const mailBuffer = mailserver.buffer(users.user1.email);
await logNewUser(test, page, users.user1, {mailBuffer: mailBuffer}); await logNewUser(test, page, users.user1, {mailBuffer: mailBuffer});
await activateEmail(test, page, users.user1, mailBuffer); await activateEmail(test, page, users.user1, mailBuffer);
await logUser(test, page, users.user1, {mailBuffer: mailBuffer, mail2fa: true, notNewDevice: true}); mailBuffer.close();
});
test('Log and disable', async ({ page }) => {
const mailBuffer = mailserver.buffer(users.user1.email);
await logUser(test, page, users.user1, {mailBuffer: mailBuffer, mail2fa: true});
await disableEmail(test, page, users.user1); await disableEmail(test, page, users.user1);
mailBuffer.close(); mailBuffer.close();
}); });
test('Admin invite', async ({ page }) => {
const mailBuffer = mailserver.buffer(users.user2.email);
await admin.login(test, page);
await admin.invite(test, page, users.user2.email);
const link = await test.step('Extract email link', async () => {
const invited = await mailBuffer.expect((m) => m.subject === "Join Vaultwarden");
await page.setContent(invited.html);
return await page.getByTestId("invite").getAttribute("href");
});
await test.step('Redirect to Keycloak', async () => {
await page.goto(link);
});
await test.step('Keycloak login', async () => {
await expect(page.getByRole('heading', { name: 'Sign in to your account' })).toBeVisible();
await page.getByLabel(/Username/).fill(users.user2.name);
await page.getByLabel('Password', { exact: true }).fill(users.user2.password);
await page.getByRole('button', { name: 'Sign In' }).click();
});
await test.step('Create Vault account', async () => {
await expect(page.getByRole('heading', { name: 'Join organisation' })).toBeVisible();
await page.getByRole('textbox', { name: 'Master password * (required)', exact: true }).fill(users.user2.password);
await page.getByRole('textbox', { name: 'Confirm master password * (' }).fill(users.user2.password);
await page.getByRole('button', { name: 'Create account' }).click();
});
await test.step('Default vault page', async () => {
await expect(page).toHaveTitle('Vaults | Vaultwarden Web');
await utils.checkNotification(page, 'Account successfully created!');
await utils.checkNotification(page, 'Invitation accepted');
});
await test.step('Check mails', async () => {
await mailBuffer.expect((m) => m.subject.includes("New Device Logged"));
await mailBuffer.expect((m) => m.subject === "Welcome");
});
mailBuffer.close();
});
+4 -6
View File
@@ -33,8 +33,8 @@ test('Non SSO login', async ({ page }) => {
await page.getByRole('button', { name: 'Other' }).click(); await page.getByRole('button', { name: 'Other' }).click();
// Unlock page // Unlock page
await page.getByRole('textbox', { name: 'Master password * (required)', exact: true }).fill(users.user1.password); await page.getByLabel('Master password').fill(users.user1.password);
await page.getByRole('button', { name: 'Log in', exact: true }).click(); await page.getByRole('button', { name: 'Log in with master password' }).click();
// We are now in the default vault page // We are now in the default vault page
await expect(page).toHaveTitle(/Vaultwarden Web/); await expect(page).toHaveTitle(/Vaultwarden Web/);
@@ -58,7 +58,6 @@ test('Non SSO login impossible', async ({ page, browser }, testInfo: TestInfo) =
// Landing page // Landing page
await page.goto('/'); await page.goto('/');
await page.locator("input[type=email].vw-email-sso").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);
@@ -67,6 +66,7 @@ test('Non SSO login impossible', async ({ page, browser }, testInfo: TestInfo) =
await expect(page.getByRole('button', { name: 'Other' })).toHaveCount(0); await expect(page.getByRole('button', { name: 'Other' })).toHaveCount(0);
}); });
test('No SSO login', async ({ page }, testInfo: TestInfo) => { test('No SSO login', async ({ page }, testInfo: TestInfo) => {
await utils.restartVault(page, testInfo, { await utils.restartVault(page, testInfo, {
SSO_ENABLED: false SSO_ENABLED: false
@@ -74,14 +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.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' })).toHaveCount(1); await expect(page.getByRole('button', { name: 'Log in with master password' })).toHaveCount(1);
}); });
@@ -67,16 +67,17 @@ test('invited with new account', async ({ page }) => {
await test.step('Create Vault account', async () => { await test.step('Create Vault account', async () => {
await expect(page.getByRole('heading', { name: 'Join organisation' })).toBeVisible(); await expect(page.getByRole('heading', { name: 'Join organisation' })).toBeVisible();
await page.getByRole('textbox', { name: 'Master password * (required)', exact: true }).fill(users.user2.password); await page.getByLabel('Master password (required)', { exact: true }).fill(users.user2.password);
await page.getByRole('textbox', { name: 'Confirm master password * (' }).fill(users.user2.password); await page.getByLabel('Confirm master password (').fill(users.user2.password);
await page.getByRole('button', { name: 'Create account' }).click(); await page.getByRole('button', { name: 'Create account' }).click();
await utils.checkNotification(page, 'Account successfully created!');
await utils.checkNotification(page, 'Invitation accepted');
await utils.ignoreExtension(page);
}); });
await test.step('Default vault page', async () => { await test.step('Default vault page', async () => {
await expect(page).toHaveTitle(/Vaultwarden Web/); await expect(page).toHaveTitle(/Vaultwarden Web/);
await utils.checkNotification(page, 'Account successfully created!');
await utils.checkNotification(page, 'Invitation accepted');
}); });
await test.step('Check mails', async () => { await test.step('Check mails', async () => {
@@ -94,7 +95,6 @@ test('invited with existing account', async ({ page }) => {
await test.step('Redirect to Keycloak', async () => { await test.step('Redirect to Keycloak', async () => {
await page.goto(link); await page.goto(link);
await page.getByRole('button', { name: /Use single sign-on/ }).click();
}); });
await test.step('Keycloak login', async () => { await test.step('Keycloak login', async () => {
@@ -108,11 +108,13 @@ test('invited with existing account', async ({ page }) => {
await expect(page).toHaveTitle('Vaultwarden Web'); await expect(page).toHaveTitle('Vaultwarden Web');
await page.getByLabel('Master password').fill(users.user3.password); await page.getByLabel('Master password').fill(users.user3.password);
await page.getByRole('button', { name: 'Unlock' }).click(); await page.getByRole('button', { name: 'Unlock' }).click();
await utils.checkNotification(page, 'Invitation accepted');
await utils.ignoreExtension(page);
}); });
await test.step('Default vault page', async () => { await test.step('Default vault page', async () => {
await expect(page).toHaveTitle(/Vaultwarden Web/); await expect(page).toHaveTitle(/Vaultwarden Web/);
await utils.checkNotification(page, 'Successfully accepted your invitation');
}); });
await test.step('Check mails', async () => { await test.step('Check mails', async () => {
+8 -19
View File
@@ -49,7 +49,7 @@ test('Organization is visible', async ({ page }) => {
await expect(page.getByLabel('Filter: Default collection')).toBeVisible(); await expect(page.getByLabel('Filter: Default collection')).toBeVisible();
}); });
test('Activate password policy', async ({ page }) => { test('Enforce password policy', async ({ page }) => {
await logUser(test, page, users.user1); await logUser(test, page, users.user1);
await orgs.policies(test, page, '/Test'); await orgs.policies(test, page, '/Test');
@@ -61,27 +61,16 @@ test('Activate password policy', async ({ page }) => {
await page.getByRole('button', { name: 'Save' }).click(); await page.getByRole('button', { name: 'Save' }).click();
await utils.checkNotification(page, 'Edited policy Master password requirements.'); await utils.checkNotification(page, 'Edited policy Master password requirements.');
}); });
});
test('Unlock trigger policyy', async ({ page }) => { await utils.logout(test, page, users.user1);
await page.goto('/', { waitUntil: 'domcontentloaded' });
await page.locator("input[type=email].vw-email-sso").fill(users.user2.email); await test.step(`Unlock trigger policy`, async () => {
await page.getByRole('button', { name: /Use single sign-on/ }).click(); await page.locator("input[type=email].vw-email-sso").fill(users.user1.email);
await page.getByRole('button', { name: 'Use single sign-on' }).click();
await test.step('Keycloak login', async () => { await page.getByRole('textbox', { name: 'Master password (required)' }).fill(users.user1.password);
await expect(page.getByRole('heading', { name: 'Sign in to your account' })).toBeVisible();
await page.getByLabel(/Username/).fill(users.user2.name);
await page.getByLabel('Password', { exact: true }).fill(users.user2.password);
await page.getByRole('button', { name: 'Sign In' }).click();
});
await test.step('Unlock vault', async () => {
await expect(page).toHaveTitle('Vaultwarden Web');
await expect(page.getByRole('heading', { name: 'Your vault is locked' })).toBeVisible();
await page.getByLabel('Master password').fill(users.user2.password);
await page.getByRole('button', { name: 'Unlock' }).click(); await page.getByRole('button', { name: 'Unlock' }).click();
});
await expect(page.getByRole('heading', { name: 'Update master password' })).toBeVisible(); await expect(page.getByRole('heading', { name: 'Update master password' })).toBeVisible();
});
}); });
-31
View File
@@ -716,36 +716,6 @@ fn web_vault_compare(active: &str, latest: &str) -> i8 {
} }
} }
fn check_template_overrides() -> Vec<&'static str> {
let template_folder = std::path::PathBuf::from(CONFIG.templates_folder());
let mut overrides = Vec::new();
for folder in ["admin", "email", "scss"] {
if folder_has_hbs_files(&template_folder.join(folder)) {
overrides.push(folder);
}
}
if folder_has_hbs_files(&template_folder) {
overrides.push("other");
}
overrides
}
fn folder_has_hbs_files(dir: &std::path::Path) -> bool {
let Ok(files) = std::fs::read_dir(dir) else {
// No files in this directory at all, so we can return false
return false;
};
files.flatten().any(|f| {
// Validate if it is a file and if it has the `.hbs` extension and starts with a-z or 0-9
f.file_type().is_ok_and(|t| t.is_file())
&& f.path().extension().is_some_and(|e| e.eq_ignore_ascii_case("hbs"))
&& f.file_name().to_str().is_some_and(|n| n.starts_with(|c: char| c.is_ascii_alphanumeric()))
})
}
#[get("/diagnostics")] #[get("/diagnostics")]
async fn diagnostics(_token: AdminToken, ip_header: IpHeader, conn: DbConn) -> ApiResult<Html<String>> { async fn diagnostics(_token: AdminToken, ip_header: IpHeader, conn: DbConn) -> ApiResult<Html<String>> {
use chrono::prelude::*; use chrono::prelude::*;
@@ -800,7 +770,6 @@ async fn diagnostics(_token: AdminToken, ip_header: IpHeader, conn: DbConn) -> A
"db_version": get_sql_server_version(&conn).await, "db_version": get_sql_server_version(&conn).await,
"admin_url": format!("{}/diagnostics", admin_url()), "admin_url": format!("{}/diagnostics", admin_url()),
"overrides": &CONFIG.get_overrides().join(", "), "overrides": &CONFIG.get_overrides().join(", "),
"template_overrides": check_template_overrides().join(", "),
"invalid_feature_flags": invalid_feature_flags, "invalid_feature_flags": invalid_feature_flags,
"host_arch": env::consts::ARCH, "host_arch": env::consts::ARCH,
"host_os": env::consts::OS, "host_os": env::consts::OS,
+277 -50
View File
@@ -22,7 +22,8 @@ use crate::{
models::{ models::{
AuthRequest, AuthRequestId, Cipher, CipherId, Device, DeviceId, DeviceType, DeviceWithAuthRequest, AuthRequest, AuthRequestId, Cipher, CipherId, Device, DeviceId, DeviceType, DeviceWithAuthRequest,
EmergencyAccess, EmergencyAccessId, EventType, Folder, FolderId, Invitation, Membership, MembershipId, EmergencyAccess, EmergencyAccessId, EventType, Folder, FolderId, Invitation, Membership, MembershipId,
OrgPolicy, OrgPolicyType, Organization, OrganizationId, Send, SendId, User, UserId, UserKdfType, OrgPolicy, OrgPolicyType, Organization, OrganizationId, Send, SendId, SignatureAlgorithm, User, UserId,
UserKdfType, UserSignatureKeyPair,
}, },
}, },
mail, mail,
@@ -41,6 +42,8 @@ pub fn routes() -> Vec<rocket::Route> {
post_profile, post_profile,
put_avatar, put_avatar,
get_public_keys, get_public_keys,
get_account_public_keys,
get_keys,
post_keys, post_keys,
post_password, post_password,
post_set_password, post_set_password,
@@ -102,6 +105,9 @@ pub struct RegisterData {
#[serde(alias = "userAsymmetricKeys")] #[serde(alias = "userAsymmetricKeys")]
keys: Option<KeysData>, keys: Option<KeysData>,
// Supersedes `keys`, and the only way a v2 account can be registered.
account_keys: Option<AccountKeysData>,
master_password_hint: Option<String>, master_password_hint: Option<String>,
name: Option<String>, name: Option<String>,
@@ -116,36 +122,6 @@ pub struct RegisterData {
org_invite_token: Option<String>, org_invite_token: Option<String>,
} }
impl RegisterData {
fn hash(&self) -> String {
self.compat.fold(|rdc| &rdc.master_password_hash, |rdcu| &rdcu.master_password_authentication.hash).to_owned()
}
fn kdf(&self) -> &KDFData {
self.compat.fold(|rdc| &rdc.kdf, |rdcu| &rdcu.master_password_authentication.kdf)
}
fn key(&self) -> String {
self.compat.fold(|rdc| &rdc.key, |rdcu| &rdcu.master_password_unlock.key).to_owned()
}
// When comparing with salt, email need to be normalized:
// - https://github.com/bitwarden/clients/blob/web-v2026.5.0/libs/common/src/key-management/master-password/services/master-password.service.ts#L171
fn unprocessable(&self) -> bool {
let mut unprocessable = false;
*self.compat.fold(
|_| &false,
|rdcu| {
let email = self.email.trim().to_lowercase();
unprocessable = rdcu.master_password_authentication.kdf != rdcu.master_password_unlock.kdf
|| rdcu.master_password_authentication.salt != email
|| rdcu.master_password_unlock.salt != email;
&unprocessable
},
)
}
}
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
struct RegisterDataOld { struct RegisterDataOld {
#[serde(flatten)] #[serde(flatten)]
@@ -183,6 +159,34 @@ impl RegisterDataCompat {
RegisterDataCompat::RegisterDataCur(rdcu) => fcu(rdcu), RegisterDataCompat::RegisterDataCur(rdcu) => fcu(rdcu),
} }
} }
fn hash(&self) -> String {
self.fold(|rdc| &rdc.master_password_hash, |rdcu| &rdcu.master_password_authentication.hash).to_owned()
}
fn kdf(&self) -> &KDFData {
self.fold(|rdc| &rdc.kdf, |rdcu| &rdcu.master_password_authentication.kdf)
}
fn key(&self) -> String {
self.fold(|rdc| &rdc.key, |rdcu| &rdcu.master_password_unlock.key).to_owned()
}
// When comparing with salt, email need to be normalized:
// - https://github.com/bitwarden/clients/blob/web-v2026.5.0/libs/common/src/key-management/master-password/services/master-password.service.ts#L171
fn unprocessable(&self, email: &str) -> bool {
let mut unprocessable = false;
*self.fold(
|_| &false,
|rdcu| {
let email = email.trim().to_lowercase();
unprocessable = rdcu.master_password_authentication.kdf != rdcu.master_password_unlock.kdf
|| rdcu.master_password_authentication.salt != email
|| rdcu.master_password_unlock.salt != email;
&unprocessable
},
)
}
} }
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
@@ -192,6 +196,167 @@ struct KeysData {
public_key: String, public_key: String,
} }
/// The `accountKeys` payload, which replaces the flat `keys`/`userAsymmetricKeys` object.
///
/// It carries either a "v1" state (just the encryption key pair) or a "v2" one, which adds a
/// signature key pair, a signed public key, and a signed security state. The two deprecated
/// top-level fields are still sent by the SDK alongside the nested ones and are only used as a
/// fallback for clients that don't send `publicKeyEncryptionKeyPair` yet.
///
/// Ref: <https://github.com/bitwarden/server/blob/main/src/Core/KeyManagement/Models/Api/Request/AccountKeysRequestModel.cs>
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct AccountKeysData {
user_key_encrypted_account_private_key: Option<String>,
account_public_key: Option<String>,
public_key_encryption_key_pair: Option<PublicKeyEncryptionKeyPairData>,
signature_key_pair: Option<SignatureKeyPairData>,
security_state: Option<SecurityStateData>,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
struct PublicKeyEncryptionKeyPairData {
wrapped_private_key: String,
public_key: String,
signed_public_key: Option<String>,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
struct SignatureKeyPairData {
signature_algorithm: String,
wrapped_signing_key: String,
verifying_key: String,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
struct SecurityStateData {
security_state: String,
security_version: i32,
}
pub struct ValidatedAccountKeys {
private_key: String,
public_key: String,
v2: Option<ValidatedV2AccountKeys>,
}
struct ValidatedV2AccountKeys {
signed_public_key: String,
signing_key: String,
verifying_key: String,
signature_algorithm: SignatureAlgorithm,
security_state: String,
security_version: i32,
}
impl AccountKeysData {
/// Checks that the payload describes a complete account cryptographic state.
///
/// The v2 fields have to be all present or all absent: a client that receives a COSE-wrapped
/// private key without the matching signature key pair and security state refuses to unlock the
/// vault, so storing half a state would produce an account nobody can log into.
pub fn validate(self) -> ApiResult<ValidatedAccountKeys> {
let (private_key, public_key, signed_public_key) = if let Some(key_pair) = self.public_key_encryption_key_pair {
(key_pair.wrapped_private_key, key_pair.public_key, key_pair.signed_public_key)
// Older clients only send the deprecated top-level fields, which are always v1.
} else if let (Some(private_key), Some(public_key)) =
(self.user_key_encrypted_account_private_key, self.account_public_key)
{
(private_key, public_key, None)
} else {
err!("The account keys are missing an encryption key pair")
};
let v2 = match (signed_public_key, self.signature_key_pair, self.security_state) {
(Some(signed_public_key), Some(signature_key_pair), Some(security_state)) => {
let Some(signature_algorithm) = SignatureAlgorithm::from_str(&signature_key_pair.signature_algorithm)
else {
err!(format!("Unsupported signature algorithm: {}", signature_key_pair.signature_algorithm))
};
Some(ValidatedV2AccountKeys {
signed_public_key,
signing_key: signature_key_pair.wrapped_signing_key,
verifying_key: signature_key_pair.verifying_key,
signature_algorithm,
security_state: security_state.security_state,
security_version: security_state.security_version,
})
}
(None, None, None) => None,
_ => err!(
"Invalid account keys: the signed public key, signature key pair and security state must either all be present or all be absent"
),
};
Ok(ValidatedAccountKeys {
private_key,
public_key,
v2,
})
}
}
impl From<KeysData> for ValidatedAccountKeys {
fn from(keys: KeysData) -> Self {
Self {
private_key: keys.encrypted_private_key,
public_key: keys.public_key,
v2: None,
}
}
}
impl ValidatedAccountKeys {
/// Writes the parts of the state that live on the user itself. The user still needs saving, and
/// [`Self::save_signature_key_pair`] still needs calling once it has been.
///
/// Rejects downgrading an account from v2 back to v1.
pub fn apply(&self, user: &mut User) -> EmptyResult {
if user.is_v2() && self.v2.is_none() {
err!("Cannot downgrade an account from v2 to v1 encryption")
}
user.private_key = Some(self.private_key.clone());
user.public_key = Some(self.public_key.clone());
user.signed_public_key = self.v2.as_ref().map(|v2| v2.signed_public_key.clone());
user.security_state = self.v2.as_ref().map(|v2| v2.security_state.clone());
user.security_version = self.v2.as_ref().map(|v2| v2.security_version);
Ok(())
}
/// Persists the signature key pair. Separate from [`Self::apply`] because the row has a foreign
/// key to the user, so it can only be written once the user exists.
pub async fn save_signature_key_pair(&self, user_id: &UserId, conn: &DbConn) -> EmptyResult {
// Skip if the account is v1, since v1 accounts don't have a signature key pair.
let Some(v2) = &self.v2 else {
return Ok(());
};
let mut key_pair = match UserSignatureKeyPair::find_active_by_user(user_id, conn).await {
Some(mut key_pair) => {
key_pair.signature_algorithm = v2.signature_algorithm as i32;
key_pair.signing_key.clone_from(&v2.signing_key);
key_pair.verifying_key.clone_from(&v2.verifying_key);
key_pair
}
None => UserSignatureKeyPair::new(
user_id.clone(),
v2.signature_algorithm,
v2.signing_key.clone(),
v2.verifying_key.clone(),
),
};
key_pair.save(conn).await
}
}
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct MasterPasswordAuthentication { pub struct MasterPasswordAuthentication {
@@ -216,11 +381,12 @@ pub struct MasterPasswordUnlock {
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct SetPasswordData { pub struct SetPasswordData {
#[serde(flatten)] #[serde(flatten)]
kdf: KDFData, compat: RegisterDataCompat,
key: String,
keys: Option<KeysData>, keys: Option<KeysData>,
master_password_hash: String, // Supersedes `keys`, and the only way a v2 account can be initialized here.
account_keys: Option<AccountKeysData>,
master_password_hint: Option<String>, master_password_hint: Option<String>,
org_identifier: Option<String>, org_identifier: Option<String>,
} }
@@ -263,7 +429,7 @@ pub async fn register(data: Json<RegisterData>, email_verification: bool, conn:
let mut pending_emergency_access = None; let mut pending_emergency_access = None;
if data.unprocessable() { if data.compat.unprocessable(&data.email) {
err_code!("Unexpected RegisterData format", Status::UnprocessableEntity.code); err_code!("Unexpected RegisterData format", Status::UnprocessableEntity.code);
} }
@@ -386,9 +552,9 @@ pub async fn register(data: Json<RegisterData>, email_verification: bool, conn:
// Make sure we don't leave a lingering invitation. // Make sure we don't leave a lingering invitation.
Invitation::take(&email, &conn).await; Invitation::take(&email, &conn).await;
set_kdf_data(&mut user, data.kdf())?; set_kdf_data(&mut user, data.compat.kdf())?;
user.set_password(&data.hash(), Some(data.key()), true, None, &conn).await?; user.set_password(&data.compat.hash(), Some(data.compat.key()), true, None, &conn).await?;
user.password_hint = password_hint; user.password_hint = password_hint;
// Add extra fields if present // Add extra fields if present
@@ -396,9 +562,13 @@ pub async fn register(data: Json<RegisterData>, email_verification: bool, conn:
user.name = name; user.name = name;
} }
if let Some(keys) = data.keys { let account_keys = match (data.account_keys, data.keys) {
user.private_key = Some(keys.encrypted_private_key); (Some(account_keys), _) => Some(account_keys.validate()?),
user.public_key = Some(keys.public_key); (None, Some(keys)) => Some(keys.into()),
(None, None) => None,
};
if let Some(ref account_keys) = account_keys {
account_keys.apply(&mut user)?;
} }
if email_verified { if email_verified {
@@ -422,6 +592,10 @@ pub async fn register(data: Json<RegisterData>, email_verification: bool, conn:
user.save(&conn).await?; user.save(&conn).await?;
if let Some(account_keys) = account_keys {
account_keys.save_signature_key_pair(&user.uuid, &conn).await?;
}
// accept any open emergency access invitations // accept any open emergency access invitations
if !CONFIG.mail_enabled() && CONFIG.emergency_access_allowed() { if !CONFIG.mail_enabled() && CONFIG.emergency_access_allowed() {
for mut emergency_invite in EmergencyAccess::find_all_invited_by_grantee_email(&user.email, &conn).await { for mut emergency_invite in EmergencyAccess::find_all_invited_by_grantee_email(&user.email, &conn).await {
@@ -444,16 +618,26 @@ async fn post_set_password(data: Json<SetPasswordData>, headers: Headers, conn:
err!("Account already initialized, cannot set password") err!("Account already initialized, cannot set password")
} }
if data.compat.unprocessable(&user.email) {
err_code!("Unexpected SetPasswordData format", Status::UnprocessableEntity.code);
}
// Check against the password hint setting here so if it fails, // Check against the password hint setting here so if it fails,
// the user can retry without losing their invitation below. // the user can retry without losing their invitation below.
let password_hint = clean_password_hint(data.master_password_hint.as_ref()); let password_hint = clean_password_hint(data.master_password_hint.as_ref());
enforce_password_hint_setting(password_hint.as_ref())?; enforce_password_hint_setting(password_hint.as_ref())?;
set_kdf_data(&mut user, &data.kdf)?; let account_keys = match (data.account_keys, data.keys) {
(Some(account_keys), _) => Some(account_keys.validate()?),
(None, Some(keys)) => Some(keys.into()),
(None, None) => None,
};
set_kdf_data(&mut user, data.compat.kdf())?;
user.set_password( user.set_password(
&data.master_password_hash, &data.compat.hash(),
Some(data.key), Some(data.compat.key()),
false, false,
Some(vec![String::from("revision_date")]), // We need to allow revision-date to use the old security_timestamp Some(vec![String::from("revision_date")]), // We need to allow revision-date to use the old security_timestamp
&conn, &conn,
@@ -461,9 +645,8 @@ async fn post_set_password(data: Json<SetPasswordData>, headers: Headers, conn:
.await?; .await?;
user.password_hint = password_hint; user.password_hint = password_hint;
if let Some(keys) = data.keys { if let Some(ref account_keys) = account_keys {
user.private_key = Some(keys.encrypted_private_key); account_keys.apply(&mut user)?;
user.public_key = Some(keys.public_key);
} }
if let Some(identifier) = data.org_identifier if let Some(identifier) = data.org_identifier
@@ -492,6 +675,10 @@ async fn post_set_password(data: Json<SetPasswordData>, headers: Headers, conn:
user.save(&conn).await?; user.save(&conn).await?;
if let Some(account_keys) = account_keys {
account_keys.save_signature_key_pair(&user.uuid, &conn).await?;
}
Ok(Json(json!({ Ok(Json(json!({
"object": "set-password", "object": "set-password",
"captchaBypassToken": "", "captchaBypassToken": "",
@@ -573,20 +760,60 @@ async fn get_public_keys(user_id: UserId, _headers: Headers, conn: DbConn) -> Js
}))) })))
} }
#[get("/users/<user_id>/keys")]
async fn get_account_public_keys(user_id: UserId, _headers: Headers, conn: DbConn) -> JsonResult {
let user = match User::find_by_uuid(&user_id, &conn).await {
Some(user) if user.public_key.is_some() => user,
Some(_) => err_code!("User has no public_key", Status::NotFound.code),
None => err_code!("User doesn't exist", Status::NotFound.code),
};
Ok(Json(user.public_keys_json(&conn).await))
}
#[get("/accounts/keys")]
async fn get_keys(headers: Headers, conn: DbConn) -> JsonResult {
let user = headers.user;
Ok(Json(json!({
"key": user.akey,
"privateKey": user.private_key,
"publicKey": user.public_key,
"accountKeys": user.account_keys_json(&conn).await,
"object": "keys"
})))
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
struct PostKeysData {
#[serde(flatten)]
keys: Option<KeysData>,
account_keys: Option<AccountKeysData>,
}
#[post("/accounts/keys", data = "<data>")] #[post("/accounts/keys", data = "<data>")]
async fn post_keys(data: Json<KeysData>, headers: Headers, conn: DbConn) -> JsonResult { async fn post_keys(data: Json<PostKeysData>, headers: Headers, conn: DbConn) -> JsonResult {
let data: KeysData = data.into_inner(); let data: PostKeysData = data.into_inner();
let mut user = headers.user; let mut user = headers.user;
user.private_key = Some(data.encrypted_private_key); // `accountKeys` supersedes the flat `keys` object when both are sent.
user.public_key = Some(data.public_key); let account_keys = match (data.account_keys, data.keys) {
(Some(account_keys), _) => account_keys.validate()?,
(None, Some(keys)) => keys.into(),
(None, None) => err!("No account keys provided"),
};
account_keys.apply(&mut user)?;
user.save(&conn).await?; user.save(&conn).await?;
account_keys.save_signature_key_pair(&user.uuid, &conn).await?;
Ok(Json(json!({ Ok(Json(json!({
"key": user.akey,
"privateKey": user.private_key, "privateKey": user.private_key,
"publicKey": user.public_key, "publicKey": user.public_key,
"accountKeys": user.account_keys_json(&conn).await,
"object":"keys" "object":"keys"
}))) })))
} }
+4 -3
View File
@@ -198,6 +198,7 @@ async fn sync(data: SyncData, headers: Headers, client_version: Option<ClientVer
"sends": sends_json, "sends": sends_json,
"userDecryption": { "userDecryption": {
"masterPasswordUnlock": master_password_unlock, "masterPasswordUnlock": master_password_unlock,
"v2UpgradeToken": headers.user.v2_upgrade_token_json(),
}, },
"object": "sync" "object": "sync"
}))) })))
@@ -870,7 +871,7 @@ async fn put_collections_admin(
headers: Headers, headers: Headers,
conn: DbConn, conn: DbConn,
nt: Notify<'_>, nt: Notify<'_>,
) -> JsonResult { ) -> EmptyResult {
post_collections_admin(cipher_id, data, headers, conn, nt).await post_collections_admin(cipher_id, data, headers, conn, nt).await
} }
@@ -881,7 +882,7 @@ async fn post_collections_admin(
headers: Headers, headers: Headers,
conn: DbConn, conn: DbConn,
nt: Notify<'_>, nt: Notify<'_>,
) -> JsonResult { ) -> EmptyResult {
let data: CollectionsAdminData = data.into_inner(); let data: CollectionsAdminData = data.into_inner();
let Some(cipher) = Cipher::find_by_uuid(&cipher_id, &conn).await else { let Some(cipher) = Cipher::find_by_uuid(&cipher_id, &conn).await else {
@@ -940,7 +941,7 @@ async fn post_collections_admin(
) )
.await; .await;
Ok(Json(cipher.to_json(&headers.host, &headers.user.uuid, None, CipherSyncType::Organization, &conn).await?)) Ok(())
} }
#[derive(Deserialize)] #[derive(Deserialize)]
+1 -1
View File
@@ -238,7 +238,7 @@ fn config() -> Json<Value> {
"disableUserRegistration": CONFIG.is_signup_disabled(), "disableUserRegistration": CONFIG.is_signup_disabled(),
// When enabled, this setting signals to clients that onboarding interstitials // When enabled, this setting signals to clients that onboarding interstitials
// (post-login welcome dialogs, extension install prompts, setup extension redirects, and premium upsell modals) should be suppressed // (post-login welcome dialogs, extension install prompts, setup extension redirects, and premium upsell modals) should be suppressed
"suppressOnboardingInterstitials": CONFIG.client_suppress_onboarding(), "suppressOnboardingInterstitials": false
}, },
"environment": { "environment": {
"vault": domain, "vault": domain,
+13 -44
View File
@@ -1,10 +1,6 @@
use rocket::{Route, serde::json::Json}; use rocket::{Route, serde::json::Json};
use serde_json::Value; use serde_json::Value;
use yubico_ng::{ use yubico::{config::Config, verify_async};
Verifier, YubicoError,
config::Config,
transport::{AsyncTransport, Response},
};
use crate::{ use crate::{
CONFIG, CONFIG,
@@ -18,39 +14,12 @@ use crate::{
models::{EventType, TwoFactor, TwoFactorType}, models::{EventType, TwoFactor, TwoFactorType},
}, },
error::{Error, MapResult}, error::{Error, MapResult},
http_client,
}; };
pub fn routes() -> Vec<Route> { pub fn routes() -> Vec<Route> {
routes![generate_yubikey, activate_yubikey, activate_yubikey_put,] routes![generate_yubikey, activate_yubikey, activate_yubikey_put,]
} }
struct HttpClientTransport {
client: reqwest::Client,
}
impl HttpClientTransport {
fn new() -> Result<Self, reqwest::Error> {
http_client::get_reqwest_client_builder(false).redirect(reqwest::redirect::Policy::none()).build().map(
|client| Self {
client,
},
)
}
}
impl AsyncTransport for HttpClientTransport {
type Error = YubicoError;
async fn yubico_get(&self, url: &str) -> Result<Response, Self::Error> {
let response = self.client.get(url).send().await.map_err(YubicoError::transport)?;
Ok(Response {
status: response.status().as_u16(),
body: response.text().await.map_err(YubicoError::transport)?,
})
}
}
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
struct EnableYubikeyData { struct EnableYubikeyData {
@@ -75,7 +44,8 @@ pub struct YubikeyMetadata {
fn parse_yubikeys(data: &EnableYubikeyData) -> Vec<String> { fn parse_yubikeys(data: &EnableYubikeyData) -> Vec<String> {
let data_keys = [&data.key1, &data.key2, &data.key3, &data.key4, &data.key5]; let data_keys = [&data.key1, &data.key2, &data.key3, &data.key4, &data.key5];
data_keys.into_iter().flatten().filter(|e| !e.is_empty()).cloned().collect()
data_keys.into_iter().flatten().cloned().collect()
} }
fn jsonify_yubikeys(yubikeys: Vec<String>) -> Value { fn jsonify_yubikeys(yubikeys: Vec<String>) -> Value {
@@ -103,15 +73,13 @@ fn get_yubico_credentials() -> Result<(String, String), Error> {
async fn verify_yubikey_otp(otp: String) -> EmptyResult { async fn verify_yubikey_otp(otp: String) -> EmptyResult {
let (yubico_id, yubico_secret) = get_yubico_credentials()?; let (yubico_id, yubico_secret) = get_yubico_credentials()?;
let mut config = Config::default().set_client_id(yubico_id).set_key(yubico_secret)?; let config = Config::default().set_client_id(yubico_id).set_key(yubico_secret);
if let Some(yubico_server) = CONFIG.yubico_server() {
config = config.set_api_host(yubico_server); match CONFIG.yubico_server() {
Some(server) => verify_async(otp, config.set_api_hosts(vec![server])).await,
None => verify_async(otp, config).await,
} }
.map_res("Failed to verify OTP")
let client = HttpClientTransport::new()?;
let verifier = Verifier::with_client(config, client)?;
verifier.verify(otp).await.map_res("Failed to verify OTP")
} }
#[post("/two-factor/get-yubikey", data = "<data>")] #[post("/two-factor/get-yubikey", data = "<data>")]
@@ -169,9 +137,10 @@ async fn activate_yubikey(data: Json<EnableYubikeyData>, headers: Headers, conn:
let yubikeys = parse_yubikeys(&data); let yubikeys = parse_yubikeys(&data);
if yubikeys.is_empty() { if yubikeys.is_empty() {
// Return an error to prevent saving empty keys which would cause users not being able to login anymore. return Ok(Json(json!({
// To remove all keys users should click the `Deactivate all keys` button "enabled": false,
err!("A key is required."); "object": "twoFactorU2f",
})));
} }
// Ensure they are valid OTPs // Ensure they are valid OTPs
+4 -26
View File
@@ -318,7 +318,7 @@ async fn sso_login(
Some((user, _)) if !user.enabled => { Some((user, _)) if !user.enabled => {
err!( err!(
"This user has been disabled", "This user has been disabled",
format!("IP: {}. Username: {}.", ip.ip, user.email), format!("IP: {}. Username: {}.", ip.ip, user.display_name()),
ErrorEvent { ErrorEvent {
event: EventType::UserFailedLogIn event: EventType::UserFailedLogIn
} }
@@ -534,18 +534,7 @@ async fn authenticated_response(
Value::Null Value::Null
}; };
let account_keys = if user.private_key.is_some() { let account_keys = user.account_keys_json(conn).await;
json!({
"publicKeyEncryptionKeyPair": {
"wrappedPrivateKey": user.private_key,
"publicKey": user.public_key,
"Object": "publicKeyEncryptionKeyPair"
},
"Object": "privateKeys"
})
} else {
Value::Null
};
let mut result = json!({ let mut result = json!({
"access_token": auth_tokens.access_token(), "access_token": auth_tokens.access_token(),
@@ -577,7 +566,7 @@ async fn authenticated_response(
result["TwoFactorToken"] = Value::String(token); result["TwoFactorToken"] = Value::String(token);
} }
info!("User {} logged in successfully. IP: {}", user.email, ip.ip); info!("User {} logged in successfully. IP: {}", user.display_name(), ip.ip);
Ok(Json(result)) Ok(Json(result))
} }
@@ -685,18 +674,7 @@ async fn user_api_key_login(
Value::Null Value::Null
}; };
let account_keys = if user.private_key.is_some() { let account_keys = user.account_keys_json(conn).await;
json!({
"publicKeyEncryptionKeyPair": {
"wrappedPrivateKey": user.private_key,
"publicKey": user.public_key,
"Object": "publicKeyEncryptionKeyPair"
},
"Object": "privateKeys"
})
} else {
Value::Null
};
// Note: No refresh_token is returned. The CLI just repeats the // Note: No refresh_token is returned. The CLI just repeats the
// client_credentials login flow when the existing token expires. // client_credentials login flow when the existing token expires.
+1 -1
View File
@@ -30,7 +30,7 @@ pub use crate::api::{
}, },
web::catchers as web_catchers, web::catchers as web_catchers,
web::routes as web_routes, web::routes as web_routes,
web::{invalidate_css_cache, static_files}, web::static_files,
}; };
use crate::{ use crate::{
CONFIG, CONFIG,
+5 -38
View File
@@ -1,7 +1,4 @@
use std::{ use std::path::{Path, PathBuf};
path::{Path, PathBuf},
sync::{Arc, RwLock},
};
use rocket::{ use rocket::{
Catcher, Route, Catcher, Route,
@@ -16,13 +13,12 @@ use crate::{
CONFIG, CONFIG,
api::{ApiResult, EmptyResult, core::now}, api::{ApiResult, EmptyResult, core::now},
auth::decode_file_download, auth::decode_file_download,
crypto::sha256_hex,
db::{ db::{
DbConn, DbConn,
models::{AttachmentId, CipherId}, models::{AttachmentId, CipherId},
}, },
error::Error, error::Error,
util::{Cached, EtagCached}, util::Cached,
}; };
pub fn routes() -> Vec<Route> { pub fn routes() -> Vec<Route> {
@@ -67,27 +63,8 @@ fn not_found() -> ApiResult<Html<String>> {
Ok(Html(text)) Ok(Html(text))
} }
struct CssCache {
css: String,
etag: String,
}
static CSS_CACHE: RwLock<Option<Arc<CssCache>>> = RwLock::new(None);
pub fn invalidate_css_cache() {
*CSS_CACHE.write().unwrap() = None;
}
#[get("/css/vaultwarden.css")] #[get("/css/vaultwarden.css")]
fn vaultwarden_css() -> EtagCached<Css<String>> { fn vaultwarden_css() -> Cached<Css<String>> {
// If reload_templates is false, and we already have the CSS Cached, return this
if !CONFIG.reload_templates()
&& let Some(cached) = CSS_CACHE.read().unwrap().as_ref()
{
return EtagCached::new(Css(cached.css.clone()), &cached.etag);
}
// Else, there is either no cache, or reload_templates is true and we need to rebuild the CSS
let css_options = json!({ let css_options = json!({
"emergency_access_allowed": CONFIG.emergency_access_allowed(), "emergency_access_allowed": CONFIG.emergency_access_allowed(),
"load_user_scss": true, "load_user_scss": true,
@@ -135,18 +112,8 @@ fn vaultwarden_css() -> EtagCached<Css<String>> {
} }
}; };
let etag = sha256_hex(css.as_bytes()); // Cache for one day should be enough and not too much
let cached = Arc::new(CssCache { Cached::ttl(Css(css), 86_400, false)
css,
etag,
});
if !CONFIG.reload_templates() {
*CSS_CACHE.write().unwrap() = Some(Arc::clone(&cached));
}
// Etag Caching will let the browser send us an etag to verify and send new content if needed
EtagCached::new(Css(cached.css.clone()), &cached.etag)
} }
#[get("/")] #[get("/")]
+5 -13
View File
@@ -659,11 +659,6 @@ make_config! {
events_days_retain: i64, false, option; events_days_retain: i64, false, option;
}, },
client {
/// Control whether clients onboarding interstitials are suppressed |> post-login welcome dialogs, extension install prompts, setup extension redirects, and premium upsell modals
client_suppress_onboarding: bool, true, def, false;
},
/// Advanced settings /// Advanced settings
advanced { advanced {
/// Client IP header |> If not present, the remote IP is used. /// Client IP header |> If not present, the remote IP is used.
@@ -1162,8 +1157,11 @@ fn validate_config(cfg: &ConfigItems, on_update: bool) -> Result<(), Error> {
} }
#[cfg(unix)] #[cfg(unix)]
if nix::unistd::access(&path, nix::unistd::AccessFlags::X_OK).is_err() { {
err!(format!("sendmail command at `{path:?}` isn't executable")); use std::os::unix::fs::PermissionsExt;
if !metadata.permissions().mode() & 0o111 != 0 {
err!(format!("sendmail command at `{path:?}` isn't executable"));
}
} }
} }
} }
@@ -1503,9 +1501,6 @@ impl Config {
let operator = storage::operator_for_path(&CONFIG_FILE_PARENT_DIR)?; let operator = storage::operator_for_path(&CONFIG_FILE_PARENT_DIR)?;
operator.write(&CONFIG_FILENAME, config_str).await?; operator.write(&CONFIG_FILENAME, config_str).await?;
// Invalidate CSS Cache because several config items might have impact on the rendered CSS
crate::api::invalidate_css_cache();
Ok(()) Ok(())
} }
@@ -1588,9 +1583,6 @@ impl Config {
writer._overrides = Vec::new(); writer._overrides = Vec::new();
} }
// Invalidate CSS Cache because several config items might have impact on the rendered CSS
crate::api::invalidate_css_cache();
Ok(()) Ok(())
} }
+3 -7
View File
@@ -298,16 +298,12 @@ impl Event {
) -> Vec<Self> { ) -> Vec<Self> {
conn.run(move |conn| { conn.run(move |conn| {
event::table event::table
.inner_join( .inner_join(users_organizations::table.on(users_organizations::uuid.eq(member_uuid)))
users_organizations::table
.on(users_organizations::uuid.eq(member_uuid).and(users_organizations::org_uuid.eq(org_uuid))),
)
.filter(event::org_uuid.eq(org_uuid)) .filter(event::org_uuid.eq(org_uuid))
.filter(event::event_date.between(start, end)) .filter(event::event_date.between(start, end))
.filter( .filter(
event::org_user_uuid event::user_uuid
.eq(member_uuid) .eq(users_organizations::user_uuid.nullable())
.or(event::user_uuid.eq(users_organizations::user_uuid.nullable()))
.or(event::act_user_uuid.eq(users_organizations::user_uuid.nullable())), .or(event::act_user_uuid.eq(users_organizations::user_uuid.nullable())),
) )
.select(event::all_columns) .select(event::all_columns)
+2
View File
@@ -17,6 +17,7 @@ mod two_factor;
mod two_factor_duo_context; mod two_factor_duo_context;
mod two_factor_incomplete; mod two_factor_incomplete;
mod user; mod user;
mod user_signature_key_pair;
pub use self::archive::Archive; pub use self::archive::Archive;
pub use self::attachment::{Attachment, AttachmentId}; pub use self::attachment::{Attachment, AttachmentId};
@@ -40,3 +41,4 @@ pub use self::two_factor::{TwoFactor, TwoFactorType};
pub use self::two_factor_duo_context::TwoFactorDuoContext; pub use self::two_factor_duo_context::TwoFactorDuoContext;
pub use self::two_factor_incomplete::TwoFactorIncomplete; pub use self::two_factor_incomplete::TwoFactorIncomplete;
pub use self::user::{Invitation, SsoUser, User, UserId, UserKdfType, UserStampException}; pub use self::user::{Invitation, SsoUser, User, UserId, UserKdfType, UserStampException};
pub use self::user_signature_key_pair::{SignatureAlgorithm, UserSignatureKeyPair};
-1
View File
@@ -91,7 +91,6 @@ impl OrgPolicy {
"type": self.atype, "type": self.atype,
"data": data_json, "data": data_json,
"enabled": self.enabled, "enabled": self.enabled,
"revisionDate": null,
"object": "policy", "object": "policy",
}); });
+76 -15
View File
@@ -20,6 +20,7 @@ use macros::UuidFromParam;
use super::{ use super::{
Cipher, Device, EmergencyAccess, Favorite, Folder, Membership, MembershipType, TwoFactor, TwoFactorIncomplete, Cipher, Device, EmergencyAccess, Favorite, Folder, Membership, MembershipType, TwoFactor, TwoFactorIncomplete,
UserSignatureKeyPair,
}; };
#[derive(Identifiable, Queryable, Insertable, AsChangeset, Selectable)] #[derive(Identifiable, Queryable, Insertable, AsChangeset, Selectable)]
@@ -69,6 +70,15 @@ pub struct User {
pub avatar_color: Option<String>, pub avatar_color: Option<String>,
pub external_id: Option<String>, // Todo: Needs to be removed in the future, this is not used anymore. pub external_id: Option<String>, // Todo: Needs to be removed in the future, this is not used anymore.
// "v2" account cryptographic state. Either all of these are set (together with a row in
// `user_signature_key_pairs`) or none of them are; see `User::is_v2`.
pub signed_public_key: Option<String>,
pub security_state: Option<String>,
pub security_version: Option<i32>,
/// JSON `{"wrappedUserKey1": ..., "wrappedUserKey2": ...}`, letting clients that still hold the
/// v1 user key obtain the v2 one after another client performed the upgrade. Opaque to us.
pub v2_upgrade_token: Option<String>,
} }
#[derive(Identifiable, Queryable, Insertable)] #[derive(Identifiable, Queryable, Insertable)]
@@ -154,9 +164,18 @@ impl User {
avatar_color: None, avatar_color: None,
external_id: None, // Todo: Needs to be removed in the future, this is not used anymore. external_id: None, // Todo: Needs to be removed in the future, this is not used anymore.
signed_public_key: None,
security_state: None,
security_version: None,
v2_upgrade_token: None,
} }
} }
pub fn is_v2(&self) -> bool {
self.signed_public_key.is_some() && self.security_state.is_some() && self.security_version.is_some()
}
pub fn check_valid_password(&self, password: &str) -> bool { pub fn check_valid_password(&self, password: &str) -> bool {
crypto::verify_password_hash( crypto::verify_password_hash(
password.as_bytes(), password.as_bytes(),
@@ -253,6 +272,61 @@ impl User {
/// Database methods /// Database methods
impl User { impl User {
async fn v2_signature_key_pair(&self, conn: &DbConn) -> Option<UserSignatureKeyPair> {
if !self.is_v2() {
return None;
}
UserSignatureKeyPair::find_active_by_user(&self.uuid, conn).await
}
pub async fn account_keys_json(&self, conn: &DbConn) -> Value {
if self.private_key.is_none() {
return Value::Null;
}
let (signed_public_key, signature_key_pair, security_state) = match self.v2_signature_key_pair(conn).await {
Some(key_pair) => (
json!(self.signed_public_key),
key_pair.to_json(),
json!({
"securityState": self.security_state,
"securityVersion": self.security_version,
}),
),
None => (Value::Null, Value::Null, Value::Null),
};
json!({
"publicKeyEncryptionKeyPair": {
"wrappedPrivateKey": self.private_key,
"publicKey": self.public_key,
"signedPublicKey": signed_public_key,
"object": "publicKeyEncryptionKeyPair",
},
"signatureKeyPair": signature_key_pair,
"securityState": security_state,
"object": "privateKeys"
})
}
pub async fn public_keys_json(&self, conn: &DbConn) -> Value {
let (signed_public_key, verifying_key) = match self.v2_signature_key_pair(conn).await {
Some(key_pair) => (json!(self.signed_public_key), json!(key_pair.verifying_key)),
None => (Value::Null, Value::Null),
};
json!({
"publicKey": self.public_key,
"signedPublicKey": signed_public_key,
"verifyingKey": verifying_key,
"object": "publicKeys"
})
}
pub fn v2_upgrade_token_json(&self) -> Value {
self.v2_upgrade_token.as_ref().and_then(|token| serde_json::from_str(token).ok()).unwrap_or(Value::Null)
}
pub async fn to_json(&self, conn: &DbConn) -> Value { pub async fn to_json(&self, conn: &DbConn) -> Value {
let mut orgs_json = Vec::new(); let mut orgs_json = Vec::new();
for c in Membership::find_confirmed_by_user(&self.uuid, conn).await { for c in Membership::find_confirmed_by_user(&self.uuid, conn).await {
@@ -268,21 +342,7 @@ impl User {
UserStatus::Enabled UserStatus::Enabled
}; };
let account_keys = if self.private_key.is_some() { let account_keys = self.account_keys_json(conn).await;
json!({
"publicKeyEncryptionKeyPair": {
"wrappedPrivateKey": self.private_key,
"publicKey": self.public_key,
"signedPublicKey": null,
"object": "publicKeyEncryptionKeyPair",
},
"securityState": null,
"signatureKeyPair": null,
"object": "privateKeys"
})
} else {
Value::Null
};
json!({ json!({
"_status": status as i32, "_status": status as i32,
@@ -357,6 +417,7 @@ impl User {
Device::delete_all_by_user(&self.uuid, conn).await?; Device::delete_all_by_user(&self.uuid, conn).await?;
TwoFactor::delete_all_by_user(&self.uuid, conn).await?; TwoFactor::delete_all_by_user(&self.uuid, conn).await?;
TwoFactorIncomplete::delete_all_by_user(&self.uuid, conn).await?; TwoFactorIncomplete::delete_all_by_user(&self.uuid, conn).await?;
UserSignatureKeyPair::delete_all_by_user(&self.uuid, conn).await?;
Invitation::take(&self.email, conn).await; // Delete invitation if any Invitation::take(&self.email, conn).await; // Delete invitation if any
conn.run(move |conn| { conn.run(move |conn| {
+169
View File
@@ -0,0 +1,169 @@
use chrono::{NaiveDateTime, Utc};
use derive_more::{AsRef, Deref, Display, From};
use diesel::prelude::*;
use serde_json::Value;
use crate::{
api::EmptyResult,
db::{DbConn, schema::user_signature_key_pairs},
error::MapResult,
util::get_uuid,
};
use macros::UuidFromParam;
use super::UserId;
/// A user's signature key pair, part of the "v2" account cryptographic state.
///
/// Upstream keeps this in its own table rather than as columns on the user, with a unique index on
/// the user id. The stated intent is to eventually keep superseded key pairs around (an `active`
/// flag was sketched but not shipped), which is why this is modelled as a row with its own identity
/// instead of a set of user attributes.
///
/// Ref: <https://github.com/bitwarden/server/blob/main/src/Sql/dbo/KeyManagement/Tables/UserSignatureKeyPair.sql>
#[derive(Identifiable, Queryable, Insertable, AsChangeset, Selectable)]
#[diesel(table_name = user_signature_key_pairs)]
#[diesel(treat_none_as_null = true)]
#[diesel(primary_key(uuid))]
pub struct UserSignatureKeyPair {
pub uuid: UserSignatureKeyPairId,
pub user_uuid: UserId,
pub signature_algorithm: i32,
/// The signing (private) key, wrapped by the user key.
pub signing_key: String,
/// The COSE-encoded public verifying key.
pub verifying_key: String,
pub created_at: NaiveDateTime,
pub updated_at: NaiveDateTime,
}
/// https://github.com/bitwarden/server/blob/main/src/Core/KeyManagement/Enums/SignatureAlgorithm.cs
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum SignatureAlgorithm {
Ed25519 = 0,
MlDsa44 = 1,
}
impl SignatureAlgorithm {
pub fn from_str(algorithm: &str) -> Option<Self> {
match algorithm {
"ed25519" => Some(Self::Ed25519),
"mldsa44" => Some(Self::MlDsa44),
_ => None,
}
}
pub fn from_i32(algorithm: i32) -> Option<Self> {
match algorithm {
0 => Some(Self::Ed25519),
1 => Some(Self::MlDsa44),
_ => None,
}
}
pub fn as_str(self) -> &'static str {
match self {
Self::Ed25519 => "ed25519",
Self::MlDsa44 => "mldsa44",
}
}
}
/// Local methods
impl UserSignatureKeyPair {
pub fn new(
user_uuid: UserId,
signature_algorithm: SignatureAlgorithm,
signing_key: String,
verifying_key: String,
) -> Self {
let now = Utc::now().naive_utc();
Self {
uuid: UserSignatureKeyPairId(get_uuid()),
user_uuid,
signature_algorithm: signature_algorithm as i32,
signing_key,
verifying_key,
created_at: now,
updated_at: now,
}
}
pub fn to_json(&self) -> Value {
json!({
"wrappedSigningKey": self.signing_key,
"verifyingKey": self.verifying_key,
"object": "signatureKeyPair",
})
}
}
/// Database methods
impl UserSignatureKeyPair {
pub async fn save(&mut self, conn: &DbConn) -> EmptyResult {
self.updated_at = Utc::now().naive_utc();
db_run! { conn:
mysql {
diesel::insert_into(user_signature_key_pairs::table)
.values(&*self)
.on_conflict(diesel::dsl::DuplicatedKeys)
.do_update()
.set(&*self)
.execute(conn)
.map_res("Error saving user signature key pair")
}
postgresql, sqlite {
diesel::insert_into(user_signature_key_pairs::table)
.values(&*self)
.on_conflict(user_signature_key_pairs::user_uuid)
.do_update()
.set(&*self)
.execute(conn)
.map_res("Error saving user signature key pair")
}
}
}
/// The key pair currently in use by the user. There is at most one today, enforced by a unique
/// index on `user_uuid`.
pub async fn find_active_by_user(user_uuid: &UserId, conn: &DbConn) -> Option<Self> {
conn.run(move |conn| {
user_signature_key_pairs::table
.filter(user_signature_key_pairs::user_uuid.eq(user_uuid))
.first::<Self>(conn)
.ok()
})
.await
}
pub async fn delete_all_by_user(user_uuid: &UserId, conn: &DbConn) -> EmptyResult {
conn.run(move |conn| {
diesel::delete(user_signature_key_pairs::table.filter(user_signature_key_pairs::user_uuid.eq(user_uuid)))
.execute(conn)
.map_res("Error deleting user signature key pairs")
})
.await
}
}
#[derive(
Clone,
Debug,
AsRef,
Deref,
DieselNewType,
Display,
From,
FromForm,
Hash,
PartialEq,
Eq,
Serialize,
Deserialize,
UuidFromParam,
)]
pub struct UserSignatureKeyPairId(String);
+18
View File
@@ -217,6 +217,22 @@ table! {
api_key -> Nullable<Text>, api_key -> Nullable<Text>,
avatar_color -> Nullable<Text>, avatar_color -> Nullable<Text>,
external_id -> Nullable<Text>, external_id -> Nullable<Text>,
signed_public_key -> Nullable<Text>,
security_state -> Nullable<Text>,
security_version -> Nullable<Integer>,
v2_upgrade_token -> Nullable<Text>,
}
}
table! {
user_signature_key_pairs (uuid) {
uuid -> Text,
user_uuid -> Text,
signature_algorithm -> Integer,
signing_key -> Text,
verifying_key -> Text,
created_at -> Timestamp,
updated_at -> Timestamp,
} }
} }
@@ -382,6 +398,7 @@ joinable!(collections_groups -> groups (groups_uuid));
joinable!(event -> users_organizations (uuid)); joinable!(event -> users_organizations (uuid));
joinable!(auth_requests -> users (user_uuid)); joinable!(auth_requests -> users (user_uuid));
joinable!(sso_users -> users (user_uuid)); joinable!(sso_users -> users (user_uuid));
joinable!(user_signature_key_pairs -> users (user_uuid));
allow_tables_to_appear_in_same_query!( allow_tables_to_appear_in_same_query!(
archives, archives,
@@ -408,4 +425,5 @@ allow_tables_to_appear_in_same_query!(
collections_groups, collections_groups,
event, event,
auth_requests, auth_requests,
user_signature_key_pairs,
); );
+1 -1
View File
@@ -58,7 +58,7 @@ use serde_json::{Error as SerdeErr, Value};
use std::io::Error as IoErr; use std::io::Error as IoErr;
use std::time::SystemTimeError as TimeErr; use std::time::SystemTimeError as TimeErr;
use webauthn_rs::prelude::WebauthnError as WebauthnErr; use webauthn_rs::prelude::WebauthnError as WebauthnErr;
use yubico_ng::error::YubicoError as YubiErr; use yubico::yubicoerror::YubicoError as YubiErr;
#[derive(Serialize)] #[derive(Serialize)]
pub struct Empty {} pub struct Empty {}
+2 -1
View File
@@ -1,5 +1,6 @@
"use strict"; "use strict";
/* exported BASE_URL, _post, _delete */ /* eslint-env es2017, browser */
/* exported BASE_URL, _post _delete */
function getBaseUrl() { function getBaseUrl() {
// If the base URL is `https://vaultwarden.example.com/base/path/admin/`, // If the base URL is `https://vaultwarden.example.com/base/path/admin/`,
+15 -21
View File
@@ -1,4 +1,5 @@
"use strict"; "use strict";
/* eslint-env es2017, browser */
/* global BASE_URL:readable, bootstrap:readable */ /* global BASE_URL:readable, bootstrap:readable */
var dnsCheck = false; var dnsCheck = false;
@@ -79,44 +80,37 @@ async function generateSupportString(event, dj) {
event.preventDefault(); event.preventDefault();
event.stopPropagation(); event.stopPropagation();
// Health check Markdown emoji, if something is a failure or not
const chk = v => v ? "true :white_check_mark:" : "false :x:";
// Yes/No Markdown emoji, if something is not a failure, but just yes or no
const yn = v => v ? "yes :heavy_plus_sign:" : "no :heavy_minus_sign:";
const template_overrides = dj.template_overrides !== "" ? ` (${dj.template_overrides})` : "";
let supportString = "### Your environment (Generated via diagnostics page)\n\n"; let supportString = "### Your environment (Generated via diagnostics page)\n\n";
supportString += `* Vaultwarden version: v${dj.current_release}\n`; supportString += `* Vaultwarden version: v${dj.current_release}\n`;
supportString += `* Web-vault version: v${dj.active_web_release}\n`; supportString += `* Web-vault version: v${dj.active_web_release}\n`;
supportString += `* OS/Arch: ${dj.host_os}/${dj.host_arch}\n`; supportString += `* OS/Arch: ${dj.host_os}/${dj.host_arch}\n`;
supportString += `* Running within a container: ${yn(dj.running_within_container)} (Base: ${dj.container_base_image})\n`; supportString += `* Running within a container: ${dj.running_within_container} (Base: ${dj.container_base_image})\n`;
supportString += `* Database type: ${dj.db_type}\n`; supportString += `* Database type: ${dj.db_type}\n`;
supportString += `* Database version: ${dj.db_version}\n`; supportString += `* Database version: ${dj.db_version}\n`;
supportString += `* Uses config.json: ${yn(dj.overrides !== "")}\n`; supportString += `* Uses config.json: ${dj.overrides !== ""}\n`;
supportString += `* Uses custom templates: ${yn(dj.template_overrides !== "")}${template_overrides}\n`; supportString += `* Uses a reverse proxy: ${dj.ip_header_exists}\n`;
supportString += `* Uses a reverse proxy: ${yn(dj.ip_header_exists)}\n`;
if (dj.ip_header_exists) { if (dj.ip_header_exists) {
supportString += `* IP Header check: ${chk(dj.ip_header_match)} (${dj.ip_header_name})\n`; supportString += `* IP Header check: ${dj.ip_header_match} (${dj.ip_header_name})\n`;
} }
supportString += `* Internet access: ${chk(dj.has_http_access)}\n`; supportString += `* Internet access: ${dj.has_http_access}\n`;
supportString += `* Internet access via a proxy: ${yn(dj.uses_proxy)}\n`; supportString += `* Internet access via a proxy: ${dj.uses_proxy}\n`;
supportString += `* DNS Check: ${chk(dnsCheck)}\n`; supportString += `* DNS Check: ${dnsCheck}\n`;
if (dj.tz_env !== "") { if (dj.tz_env !== "") {
supportString += `* TZ environment: ${dj.tz_env}\n`; supportString += `* TZ environment: ${dj.tz_env}\n`;
} }
supportString += `* Browser/Server Time Check: ${chk(timeCheck)}\n`; supportString += `* Browser/Server Time Check: ${timeCheck}\n`;
supportString += `* Server/NTP Time Check: ${chk(ntpTimeCheck)}\n`; supportString += `* Server/NTP Time Check: ${ntpTimeCheck}\n`;
supportString += `* Domain Configuration Check: ${chk(domainCheck)}\n`; supportString += `* Domain Configuration Check: ${domainCheck}\n`;
supportString += `* HTTPS Check: ${chk(httpsCheck)}\n`; supportString += `* HTTPS Check: ${httpsCheck}\n`;
if (dj.enable_websocket) { if (dj.enable_websocket) {
supportString += `* Websocket Check: ${chk(websocketCheck)}\n`; supportString += `* Websocket Check: ${websocketCheck}\n`;
} else { } else {
supportString += "* Websocket Check: disabled\n"; supportString += "* Websocket Check: disabled\n";
} }
supportString += `* HTTP Response Checks: ${chk(httpResponseCheck)}\n`; supportString += `* HTTP Response Checks: ${httpResponseCheck}\n`;
if (dj.invalid_feature_flags != "") { if (dj.invalid_feature_flags != "") {
supportString += "* Invalid feature flags: true\n"; supportString += `* Invalid feature flags: true\n`;
} }
const jsonResponse = await fetch(`${BASE_URL}/admin/diagnostics/config`, { const jsonResponse = await fetch(`${BASE_URL}/admin/diagnostics/config`, {
+2 -1
View File
@@ -1,5 +1,6 @@
"use strict"; "use strict";
/* global jQuery, _post:readable, BASE_URL:readable, reload:readable, jdenticon:readable */ /* eslint-env es2017, browser, jquery */
/* global _post:readable, BASE_URL:readable, reload:readable, jdenticon:readable */
function deleteOrganization(event) { function deleteOrganization(event) {
event.preventDefault(); event.preventDefault();
+1
View File
@@ -1,4 +1,5 @@
"use strict"; "use strict";
/* eslint-env es2017, browser */
/* global _post:readable, BASE_URL:readable */ /* global _post:readable, BASE_URL:readable */
function smtpTest(event) { function smtpTest(event) {
+2 -1
View File
@@ -1,5 +1,6 @@
"use strict"; "use strict";
/* global jQuery, _post:readable, _delete:readable, BASE_URL:readable, reload:readable, jdenticon:readable */ /* eslint-env es2017, browser, jquery */
/* global _post:readable, _delete:readable BASE_URL:readable, reload:readable, jdenticon:readable */
function deleteUser(event) { function deleteUser(event) {
event.preventDefault(); event.preventDefault();
@@ -77,16 +77,6 @@
<span class="d-block"><b>No</b></span> <span class="d-block"><b>No</b></span>
{{/unless}} {{/unless}}
</dd> </dd>
<dt class="col-sm-5">Uses custom templates</dt>
<dd class="col-sm-7">
{{#if page_data.template_overrides}}
<span class="d-inline"><b>Yes</b></span>
<span class="badge bg-info text-dark abbr-badge" title="Custom template files are used.&#013;&#010;{{page_data.template_overrides}}">Details</span>
{{/if}}
{{#unless page_data.template_overrides}}
<span class="d-block"><b>No</b></span>
{{/unless}}
</dd>
<dt class="col-sm-5">Uses a reverse proxy</dt> <dt class="col-sm-5">Uses a reverse proxy</dt>
<dd class="col-sm-7"> <dd class="col-sm-7">
{{#if page_data.ip_header_exists}} {{#if page_data.ip_header_exists}}
+2 -2
View File
@@ -67,7 +67,7 @@ pub(crate) fn operator_for_path(path: &str) -> Result<opendal::Operator, crate::
s3::operator_for_path(path)? s3::operator_for_path(path)?
} else { } else {
let builder = opendal::services::Fs::default().root(path); let builder = opendal::services::Fs::default().root(path);
opendal::Operator::new(builder)? opendal::Operator::new(builder)?.finish()
}; };
OPERATORS_BY_PATH.insert(path.to_owned(), operator.clone()); OPERATORS_BY_PATH.insert(path.to_owned(), operator.clone());
@@ -236,7 +236,7 @@ mod s3 {
builder.credential_provider_chain(ProvideCredentialChain::new().push(OpenDALS3CredentialProvider)); builder.credential_provider_chain(ProvideCredentialChain::new().push(OpenDALS3CredentialProvider));
} }
Ok(opendal::Operator::new(builder)?) Ok(opendal::Operator::new(builder)?.finish())
} }
fn uri_has_option(uri: &opendal::OperatorUri, names: &[&str]) -> bool { fn uri_has_option(uri: &opendal::OperatorUri, names: &[&str]) -> bool {
-38
View File
@@ -257,44 +257,6 @@ impl<'r, R: 'r + Responder<'r, 'static> + Send> Responder<'r, 'static> for Cache
} }
} }
pub struct EtagCached<R> {
response: R,
etag: String,
}
impl<R> EtagCached<R> {
/// An `etag` response should always be quoted
pub fn new(response: R, etag: &str) -> Self {
Self {
response,
etag: format!("\"{etag}\""),
}
}
}
impl<'r, R: 'r + Responder<'r, 'static> + Send> Responder<'r, 'static> for EtagCached<R> {
fn respond_to(self, request: &'r Request<'_>) -> response::Result<'static> {
// Check and validate a `If-None-Match` ETag header
// Multiple tags could be returned for the same URI if the browser has multiple versions cached
// Also, weak tags are prefixed with `W/`, but ETags are always weak, so just strip it too before comparing
let etag_matches = request
.headers()
.get_one("If-None-Match")
.is_some_and(|v| v.split(',').any(|t| t.trim().trim_start_matches("W/") == self.etag));
let mut res = if etag_matches {
Response::build().status(Status::NotModified).ok()?
} else {
self.response.respond_to(request)?
};
// Both 200 (OK) and 304 (Not Modified) need to return the etag and cache-control
res.set_raw_header("Etag", self.etag);
res.set_raw_header("Cache-Control", "public, no-cache");
Ok(res)
}
}
// Log all the routes from the main paths list, and the attachments endpoint // Log all the routes from the main paths list, and the attachments endpoint
// Effectively ignores, any static file route, and the alive endpoint // Effectively ignores, any static file route, and the alive endpoint
const LOGGED_ROUTES: [&str; 7] = ["/api", "/admin", "/identity", "/icons", "/attachments", "/events", "/notifications"]; const LOGGED_ROUTES: [&str; 7] = ["/api", "/admin", "/identity", "/icons", "/attachments", "/events", "/notifications"];