mirror of
https://github.com/dani-garcia/vaultwarden.git
synced 2025-09-10 10:45:57 +03:00
Compare commits
23 Commits
a133d4e90c
...
main
Author | SHA1 | Date | |
---|---|---|---|
|
a2ad1dc7c3 | ||
|
7cc4dfabbf | ||
|
5a8736e116 | ||
|
f76362ff89 | ||
|
6db5b7115d | ||
|
3510351f4d | ||
|
7161f612a1 | ||
|
5ee908517f | ||
|
55577fa4eb | ||
|
843c063649 | ||
|
550b670dba | ||
|
de808c5ad9 | ||
|
1f73630136 | ||
|
77008a91e9 | ||
|
7f386d38ae | ||
|
8e7eeab293 | ||
|
e35c6f8705 | ||
|
ae7b725c0f | ||
|
2a5489a4b2 | ||
|
8fd0ee4211 | ||
|
4a5516e150 | ||
|
7fc94516ce | ||
|
5ea0779d6b |
@@ -80,8 +80,16 @@
|
|||||||
## Timeout when acquiring database connection
|
## Timeout when acquiring database connection
|
||||||
# DATABASE_TIMEOUT=30
|
# DATABASE_TIMEOUT=30
|
||||||
|
|
||||||
|
## Database idle timeout
|
||||||
|
## Timeout in seconds before idle connections to the database are closed.
|
||||||
|
# DATABASE_IDLE_TIMEOUT=600
|
||||||
|
|
||||||
|
## Database min connections
|
||||||
|
## Define the minimum size of the connection pool used for connecting to the database.
|
||||||
|
# DATABASE_MIN_CONNS=2
|
||||||
|
|
||||||
## Database max connections
|
## Database max connections
|
||||||
## Define the size of the connection pool used for connecting to the database.
|
## Define the maximum size of the connection pool used for connecting to the database.
|
||||||
# DATABASE_MAX_CONNS=10
|
# DATABASE_MAX_CONNS=10
|
||||||
|
|
||||||
## Database connection initialization
|
## Database connection initialization
|
||||||
@@ -485,7 +493,7 @@
|
|||||||
# SSO_AUTHORITY=https://auth.example.com
|
# SSO_AUTHORITY=https://auth.example.com
|
||||||
|
|
||||||
## Authorization request scopes. Optional SSO scopes, override if email and profile are not enough (`openid` is implicit).
|
## Authorization request scopes. Optional SSO scopes, override if email and profile are not enough (`openid` is implicit).
|
||||||
#SSO_SCOPES="email profile"
|
# SSO_SCOPES="email profile"
|
||||||
|
|
||||||
## Additional authorization url parameters (ex: to obtain a `refresh_token` with Google Auth).
|
## Additional authorization url parameters (ex: to obtain a `refresh_token` with Google Auth).
|
||||||
# SSO_AUTHORIZE_EXTRA_PARAMS="access_type=offline&prompt=consent"
|
# SSO_AUTHORIZE_EXTRA_PARAMS="access_type=offline&prompt=consent"
|
||||||
@@ -571,7 +579,7 @@
|
|||||||
##
|
##
|
||||||
## According to the RFC6238 (https://tools.ietf.org/html/rfc6238),
|
## According to the RFC6238 (https://tools.ietf.org/html/rfc6238),
|
||||||
## we allow by default the TOTP code which was valid one step back and one in the future.
|
## we allow by default the TOTP code which was valid one step back and one in the future.
|
||||||
## This can however allow attackers to be a bit more lucky with there attempts because there are 3 valid codes.
|
## This can however allow attackers to be a bit more lucky with their attempts because there are 3 valid codes.
|
||||||
## You can disable this, so that only the current TOTP Code is allowed.
|
## You can disable this, so that only the current TOTP Code is allowed.
|
||||||
## Keep in mind that when a sever drifts out of time, valid codes could be marked as invalid.
|
## Keep in mind that when a sever drifts out of time, valid codes could be marked as invalid.
|
||||||
## In any case, if a code has been used it can not be used again, also codes which predates it will be invalid.
|
## In any case, if a code has been used it can not be used again, also codes which predates it will be invalid.
|
||||||
@@ -611,7 +619,7 @@
|
|||||||
# SMTP_AUTH_MECHANISM=
|
# SMTP_AUTH_MECHANISM=
|
||||||
|
|
||||||
## Server name sent during the SMTP HELO
|
## Server name sent during the SMTP HELO
|
||||||
## By default this value should be is on the machine's hostname,
|
## By default this value should be the machine's hostname,
|
||||||
## but might need to be changed in case it trips some anti-spam filters
|
## but might need to be changed in case it trips some anti-spam filters
|
||||||
# HELO_NAME=
|
# HELO_NAME=
|
||||||
|
|
||||||
|
9
.github/workflows/build.yml
vendored
9
.github/workflows/build.yml
vendored
@@ -34,8 +34,7 @@ jobs:
|
|||||||
permissions:
|
permissions:
|
||||||
actions: write
|
actions: write
|
||||||
contents: read
|
contents: read
|
||||||
# We use Ubuntu 22.04 here because this matches the library versions used within the Debian docker containers
|
runs-on: ubuntu-24.04
|
||||||
runs-on: ubuntu-22.04
|
|
||||||
timeout-minutes: 120
|
timeout-minutes: 120
|
||||||
# Make warnings errors, this is to prevent warnings slipping through.
|
# Make warnings errors, this is to prevent warnings slipping through.
|
||||||
# This is done globally to prevent rebuilds when the RUSTFLAGS env variable changes.
|
# This is done globally to prevent rebuilds when the RUSTFLAGS env variable changes.
|
||||||
@@ -56,7 +55,7 @@ jobs:
|
|||||||
|
|
||||||
# Checkout the repo
|
# Checkout the repo
|
||||||
- name: "Checkout"
|
- name: "Checkout"
|
||||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 #v4.2.2
|
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 #v5.0.0
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
@@ -82,7 +81,7 @@ jobs:
|
|||||||
|
|
||||||
# Only install the clippy and rustfmt components on the default rust-toolchain
|
# Only install the clippy and rustfmt components on the default rust-toolchain
|
||||||
- name: "Install rust-toolchain version"
|
- name: "Install rust-toolchain version"
|
||||||
uses: dtolnay/rust-toolchain@b3b07ba8b418998c39fb20f53e8b695cdcc8de1b # master @ Apr 29, 2025, 9:22 PM GMT+2
|
uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9 # master @ Aug 23, 2025, 3:20 AM GMT+2
|
||||||
if: ${{ matrix.channel == 'rust-toolchain' }}
|
if: ${{ matrix.channel == 'rust-toolchain' }}
|
||||||
with:
|
with:
|
||||||
toolchain: "${{steps.toolchain.outputs.RUST_TOOLCHAIN}}"
|
toolchain: "${{steps.toolchain.outputs.RUST_TOOLCHAIN}}"
|
||||||
@@ -92,7 +91,7 @@ jobs:
|
|||||||
|
|
||||||
# Install the any other channel to be used for which we do not execute clippy and rustfmt
|
# Install the any other channel to be used for which we do not execute clippy and rustfmt
|
||||||
- name: "Install MSRV version"
|
- name: "Install MSRV version"
|
||||||
uses: dtolnay/rust-toolchain@b3b07ba8b418998c39fb20f53e8b695cdcc8de1b # master @ Apr 29, 2025, 9:22 PM GMT+2
|
uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9 # master @ Aug 23, 2025, 3:20 AM GMT+2
|
||||||
if: ${{ matrix.channel != 'rust-toolchain' }}
|
if: ${{ matrix.channel != 'rust-toolchain' }}
|
||||||
with:
|
with:
|
||||||
toolchain: "${{steps.toolchain.outputs.RUST_TOOLCHAIN}}"
|
toolchain: "${{steps.toolchain.outputs.RUST_TOOLCHAIN}}"
|
||||||
|
2
.github/workflows/check-templates.yml
vendored
2
.github/workflows/check-templates.yml
vendored
@@ -14,7 +14,7 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
# Checkout the repo
|
# Checkout the repo
|
||||||
- name: "Checkout"
|
- name: "Checkout"
|
||||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 #v4.2.2
|
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 #v5.0.0
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
# End Checkout the repo
|
# End Checkout the repo
|
||||||
|
2
.github/workflows/hadolint.yml
vendored
2
.github/workflows/hadolint.yml
vendored
@@ -35,7 +35,7 @@ jobs:
|
|||||||
# End Download hadolint
|
# End Download hadolint
|
||||||
# Checkout the repo
|
# Checkout the repo
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 #v4.2.2
|
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 #v5.0.0
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
# End Checkout the repo
|
# End Checkout the repo
|
||||||
|
41
.github/workflows/release.yml
vendored
41
.github/workflows/release.yml
vendored
@@ -10,33 +10,16 @@ on:
|
|||||||
# https://docs.github.com/en/actions/writing-workflows/workflow-syntax-for-github-actions#filter-pattern-cheat-sheet
|
# https://docs.github.com/en/actions/writing-workflows/workflow-syntax-for-github-actions#filter-pattern-cheat-sheet
|
||||||
- '[1-2].[0-9]+.[0-9]+'
|
- '[1-2].[0-9]+.[0-9]+'
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
# Apply concurrency control only on the upstream repo
|
||||||
|
group: ${{ github.repository == 'dani-garcia/vaultwarden' && format('{0}-{1}', github.workflow, github.ref) || github.run_id }}
|
||||||
|
# Don't cancel other runs when creating a tag
|
||||||
|
cancel-in-progress: ${{ github.ref_type == 'branch' }}
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
# https://github.com/marketplace/actions/skip-duplicate-actions
|
|
||||||
# Some checks to determine if we need to continue with building a new docker.
|
|
||||||
# We will skip this check if we are creating a tag, because that has the same hash as a previous run already.
|
|
||||||
skip_check:
|
|
||||||
# Only run this in the upstream repo and not on forks
|
|
||||||
if: ${{ github.repository == 'dani-garcia/vaultwarden' }}
|
|
||||||
name: Cancel older jobs when running
|
|
||||||
permissions:
|
|
||||||
actions: write
|
|
||||||
runs-on: ubuntu-24.04
|
|
||||||
outputs:
|
|
||||||
should_skip: ${{ steps.skip_check.outputs.should_skip }}
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Skip Duplicates Actions
|
|
||||||
id: skip_check
|
|
||||||
uses: fkirc/skip-duplicate-actions@f75f66ce1886f00957d99748a42c724f4330bdcf # v5.3.1
|
|
||||||
with:
|
|
||||||
cancel_others: 'true'
|
|
||||||
# Only run this when not creating a tag
|
|
||||||
if: ${{ github.ref_type == 'branch' }}
|
|
||||||
|
|
||||||
docker-build:
|
docker-build:
|
||||||
needs: skip_check
|
|
||||||
if: ${{ needs.skip_check.outputs.should_skip != 'true' && github.repository == 'dani-garcia/vaultwarden' }}
|
|
||||||
name: Build Vaultwarden containers
|
name: Build Vaultwarden containers
|
||||||
|
if: ${{ github.repository == 'dani-garcia/vaultwarden' }}
|
||||||
permissions:
|
permissions:
|
||||||
packages: write
|
packages: write
|
||||||
contents: read
|
contents: read
|
||||||
@@ -89,7 +72,7 @@ jobs:
|
|||||||
|
|
||||||
# Checkout the repo
|
# Checkout the repo
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 #v4.2.2
|
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 #v5.0.0
|
||||||
# We need fetch-depth of 0 so we also get all the tag metadata
|
# We need fetch-depth of 0 so we also get all the tag metadata
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
@@ -120,7 +103,7 @@ jobs:
|
|||||||
|
|
||||||
# Login to Docker Hub
|
# Login to Docker Hub
|
||||||
- name: Login to Docker Hub
|
- name: Login to Docker Hub
|
||||||
uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0
|
uses: docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1 # v3.5.0
|
||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
@@ -136,7 +119,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@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0
|
uses: docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1 # v3.5.0
|
||||||
with:
|
with:
|
||||||
registry: ghcr.io
|
registry: ghcr.io
|
||||||
username: ${{ github.repository_owner }}
|
username: ${{ github.repository_owner }}
|
||||||
@@ -153,7 +136,7 @@ jobs:
|
|||||||
|
|
||||||
# Login to Quay.io
|
# Login to Quay.io
|
||||||
- name: Login to Quay.io
|
- name: Login to Quay.io
|
||||||
uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0
|
uses: docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1 # v3.5.0
|
||||||
with:
|
with:
|
||||||
registry: quay.io
|
registry: quay.io
|
||||||
username: ${{ secrets.QUAY_USERNAME }}
|
username: ${{ secrets.QUAY_USERNAME }}
|
||||||
@@ -192,7 +175,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Bake ${{ matrix.base_image }} containers
|
- name: Bake ${{ matrix.base_image }} containers
|
||||||
id: bake_vw
|
id: bake_vw
|
||||||
uses: docker/bake-action@37816e747588cb137173af99ab33873600c46ea8 # v6.8.0
|
uses: docker/bake-action@3acf805d94d93a86cce4ca44798a76464a75b88c # v6.9.0
|
||||||
env:
|
env:
|
||||||
BASE_TAGS: "${{ env.BASE_TAGS }}"
|
BASE_TAGS: "${{ env.BASE_TAGS }}"
|
||||||
SOURCE_COMMIT: "${{ env.SOURCE_COMMIT }}"
|
SOURCE_COMMIT: "${{ env.SOURCE_COMMIT }}"
|
||||||
|
6
.github/workflows/trivy.yml
vendored
6
.github/workflows/trivy.yml
vendored
@@ -31,12 +31,12 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 #v4.2.2
|
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 #v5.0.0
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
- name: Run Trivy vulnerability scanner
|
- name: Run Trivy vulnerability scanner
|
||||||
uses: aquasecurity/trivy-action@dc5a429b52fcf669ce959baa2c2dd26090d2a6c4 # v0.32.0
|
uses: aquasecurity/trivy-action@b6643a29fecd7f34b3597bc6acb0a98b03d33ff8 # v0.33.0 + b6643a2
|
||||||
env:
|
env:
|
||||||
TRIVY_DB_REPOSITORY: docker.io/aquasec/trivy-db:2,public.ecr.aws/aquasecurity/trivy-db:2,ghcr.io/aquasecurity/trivy-db:2
|
TRIVY_DB_REPOSITORY: docker.io/aquasec/trivy-db:2,public.ecr.aws/aquasecurity/trivy-db:2,ghcr.io/aquasecurity/trivy-db:2
|
||||||
TRIVY_JAVA_DB_REPOSITORY: docker.io/aquasec/trivy-java-db:1,public.ecr.aws/aquasecurity/trivy-java-db:1,ghcr.io/aquasecurity/trivy-java-db:1
|
TRIVY_JAVA_DB_REPOSITORY: docker.io/aquasec/trivy-java-db:1,public.ecr.aws/aquasecurity/trivy-java-db:1,ghcr.io/aquasecurity/trivy-java-db:1
|
||||||
@@ -48,6 +48,6 @@ jobs:
|
|||||||
severity: CRITICAL,HIGH
|
severity: CRITICAL,HIGH
|
||||||
|
|
||||||
- name: Upload Trivy scan results to GitHub Security tab
|
- name: Upload Trivy scan results to GitHub Security tab
|
||||||
uses: github/codeql-action/upload-sarif@4e828ff8d448a8a6e532957b1811f387a63867e8 # v3.29.4
|
uses: github/codeql-action/upload-sarif@3c3833e0f8c1c83d449a7478aa59c036a9165498 # v3.29.11
|
||||||
with:
|
with:
|
||||||
sarif_file: 'trivy-results.sarif'
|
sarif_file: 'trivy-results.sarif'
|
||||||
|
4
.github/workflows/zizmor.yml
vendored
4
.github/workflows/zizmor.yml
vendored
@@ -16,12 +16,12 @@ jobs:
|
|||||||
security-events: write
|
security-events: write
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 #v5.0.0
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
- name: Run zizmor
|
- name: Run zizmor
|
||||||
uses: zizmorcore/zizmor-action@f52a838cfabf134edcbaa7c8b3677dde20045018 # v0.1.1
|
uses: zizmorcore/zizmor-action@5ca5fc7a4779c5263a3ffa0e1f693009994446d1 # v0.1.2
|
||||||
with:
|
with:
|
||||||
# intentionally not scanning the entire repository,
|
# intentionally not scanning the entire repository,
|
||||||
# since it contains integration tests.
|
# since it contains integration tests.
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
---
|
---
|
||||||
repos:
|
repos:
|
||||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||||
rev: v5.0.0
|
rev: v6.0.0
|
||||||
hooks:
|
hooks:
|
||||||
- id: check-yaml
|
- id: check-yaml
|
||||||
- id: check-json
|
- id: check-json
|
||||||
|
427
Cargo.lock
generated
427
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
47
Cargo.toml
47
Cargo.toml
@@ -6,7 +6,7 @@ name = "vaultwarden"
|
|||||||
version = "1.0.0"
|
version = "1.0.0"
|
||||||
authors = ["Daniel García <dani-garcia@users.noreply.github.com>"]
|
authors = ["Daniel García <dani-garcia@users.noreply.github.com>"]
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
rust-version = "1.86.0"
|
rust-version = "1.87.0"
|
||||||
resolver = "2"
|
resolver = "2"
|
||||||
|
|
||||||
repository = "https://github.com/dani-garcia/vaultwarden"
|
repository = "https://github.com/dani-garcia/vaultwarden"
|
||||||
@@ -77,12 +77,12 @@ dashmap = "6.1.0"
|
|||||||
|
|
||||||
# Async futures
|
# Async futures
|
||||||
futures = "0.3.31"
|
futures = "0.3.31"
|
||||||
tokio = { version = "1.47.0", features = ["rt-multi-thread", "fs", "io-util", "parking_lot", "time", "signal", "net"] }
|
tokio = { version = "1.47.1", features = ["rt-multi-thread", "fs", "io-util", "parking_lot", "time", "signal", "net"] }
|
||||||
tokio-util = { version = "0.7.15", features = ["compat"]}
|
tokio-util = { version = "0.7.16", features = ["compat"]}
|
||||||
|
|
||||||
# A generic serialization/deserialization framework
|
# A generic serialization/deserialization framework
|
||||||
serde = { version = "1.0.219", features = ["derive"] }
|
serde = { version = "1.0.219", features = ["derive"] }
|
||||||
serde_json = "1.0.141"
|
serde_json = "1.0.143"
|
||||||
|
|
||||||
# A safe, extensible ORM and Query builder
|
# A safe, extensible ORM and Query builder
|
||||||
diesel = { version = "2.2.12", features = ["chrono", "r2d2", "numeric"] }
|
diesel = { version = "2.2.12", features = ["chrono", "r2d2", "numeric"] }
|
||||||
@@ -101,7 +101,7 @@ ring = "0.17.14"
|
|||||||
subtle = "2.6.1"
|
subtle = "2.6.1"
|
||||||
|
|
||||||
# UUID generation
|
# UUID generation
|
||||||
uuid = { version = "1.17.0", features = ["v4"] }
|
uuid = { version = "1.18.0", features = ["v4"] }
|
||||||
|
|
||||||
# Date and time libraries
|
# Date and time libraries
|
||||||
chrono = { version = "0.4.41", features = ["clock", "serde"], default-features = false }
|
chrono = { version = "0.4.41", features = ["clock", "serde"], default-features = false }
|
||||||
@@ -109,7 +109,7 @@ chrono-tz = "0.10.4"
|
|||||||
time = "0.3.41"
|
time = "0.3.41"
|
||||||
|
|
||||||
# Job scheduler
|
# Job scheduler
|
||||||
job_scheduler_ng = "2.2.0"
|
job_scheduler_ng = "2.3.0"
|
||||||
|
|
||||||
# Data encoding library Hex/Base32/Base64
|
# Data encoding library Hex/Base32/Base64
|
||||||
data-encoding = "2.9.0"
|
data-encoding = "2.9.0"
|
||||||
@@ -121,35 +121,34 @@ jsonwebtoken = "9.3.1"
|
|||||||
totp-lite = "2.0.1"
|
totp-lite = "2.0.1"
|
||||||
|
|
||||||
# Yubico Library
|
# Yubico Library
|
||||||
yubico = { package = "yubico_ng", version = "0.13.0", features = ["online-tokio"], default-features = false }
|
yubico = { package = "yubico_ng", version = "0.14.1", features = ["online-tokio"], default-features = false }
|
||||||
|
|
||||||
# WebAuthn libraries
|
# WebAuthn libraries
|
||||||
# danger-allow-state-serialisation is needed to save the state in the db
|
# danger-allow-state-serialisation is needed to save the state in the db
|
||||||
# danger-credential-internals is needed to support U2F to Webauthn migration
|
# danger-credential-internals is needed to support U2F to Webauthn migration
|
||||||
# danger-user-presence-only-security-keys is needed to disable UV
|
webauthn-rs = { version = "0.5.2", features = ["danger-allow-state-serialisation", "danger-credential-internals"] }
|
||||||
webauthn-rs = { version = "0.5.2", features = ["danger-allow-state-serialisation", "danger-credential-internals", "danger-user-presence-only-security-keys"] }
|
|
||||||
webauthn-rs-proto = "0.5.2"
|
webauthn-rs-proto = "0.5.2"
|
||||||
webauthn-rs-core = "0.5.2"
|
webauthn-rs-core = "0.5.2"
|
||||||
|
|
||||||
# Handling of URL's for WebAuthn and favicons
|
# Handling of URL's for WebAuthn and favicons
|
||||||
url = "2.5.4"
|
url = "2.5.7"
|
||||||
|
|
||||||
# Email libraries
|
# Email libraries
|
||||||
lettre = { version = "0.11.18", features = ["smtp-transport", "sendmail-transport", "builder", "serde", "hostname", "tracing", "tokio1-rustls", "ring", "rustls-native-certs"], default-features = false }
|
lettre = { version = "0.11.18", features = ["smtp-transport", "sendmail-transport", "builder", "serde", "hostname", "tracing", "tokio1-rustls", "ring", "rustls-native-certs"], default-features = false }
|
||||||
percent-encoding = "2.3.1" # URL encoding library used for URL's in the emails
|
percent-encoding = "2.3.2" # URL encoding library used for URL's in the emails
|
||||||
email_address = "0.2.9"
|
email_address = "0.2.9"
|
||||||
|
|
||||||
# HTML Template library
|
# HTML Template library
|
||||||
handlebars = { version = "6.3.2", features = ["dir_source"] }
|
handlebars = { version = "6.3.2", features = ["dir_source"] }
|
||||||
|
|
||||||
# HTTP client (Used for favicons, version check, DUO and HIBP API)
|
# HTTP client (Used for favicons, version check, DUO and HIBP API)
|
||||||
reqwest = { version = "0.12.22", features = ["rustls-tls", "rustls-tls-native-roots", "stream", "json", "deflate", "gzip", "brotli", "zstd", "socks", "cookies", "charset", "http2", "system-proxy"], default-features = false}
|
reqwest = { version = "0.12.23", features = ["rustls-tls", "rustls-tls-native-roots", "stream", "json", "deflate", "gzip", "brotli", "zstd", "socks", "cookies", "charset", "http2", "system-proxy"], default-features = false}
|
||||||
hickory-resolver = "0.25.2"
|
hickory-resolver = "0.25.2"
|
||||||
|
|
||||||
# Favicon extraction libraries
|
# Favicon extraction libraries
|
||||||
html5gum = "0.7.0"
|
html5gum = "0.8.0"
|
||||||
regex = { version = "1.11.1", features = ["std", "perf", "unicode-perl"], default-features = false }
|
regex = { version = "1.11.2", features = ["std", "perf", "unicode-perl"], default-features = false }
|
||||||
data-url = "0.3.1"
|
data-url = "0.3.2"
|
||||||
bytes = "1.10.1"
|
bytes = "1.10.1"
|
||||||
svg-hush = "0.9.5"
|
svg-hush = "0.9.5"
|
||||||
|
|
||||||
@@ -167,19 +166,19 @@ openssl = "0.10.73"
|
|||||||
pico-args = "0.5.0"
|
pico-args = "0.5.0"
|
||||||
|
|
||||||
# Macro ident concatenation
|
# Macro ident concatenation
|
||||||
pastey = "0.1.0"
|
pastey = "0.1.1"
|
||||||
governor = "0.10.0"
|
governor = "0.10.1"
|
||||||
|
|
||||||
# OIDC for SSO
|
# OIDC for SSO
|
||||||
openidconnect = { version = "4.0.1", features = ["reqwest", "native-tls"] }
|
openidconnect = { version = "4.0.1", features = ["reqwest", "native-tls"] }
|
||||||
mini-moka = "0.10.2"
|
mini-moka = "0.10.3"
|
||||||
|
|
||||||
# Check client versions for specific features.
|
# Check client versions for specific features.
|
||||||
semver = "1.0.26"
|
semver = "1.0.26"
|
||||||
|
|
||||||
# Allow overriding the default memory allocator
|
# Allow overriding the default memory allocator
|
||||||
# Mainly used for the musl builds, since the default musl malloc is very slow
|
# Mainly used for the musl builds, since the default musl malloc is very slow
|
||||||
mimalloc = { version = "0.1.47", features = ["secure"], default-features = false, optional = true }
|
mimalloc = { version = "0.1.48", features = ["secure"], default-features = false, optional = true }
|
||||||
|
|
||||||
which = "8.0.0"
|
which = "8.0.0"
|
||||||
|
|
||||||
@@ -196,10 +195,10 @@ grass_compiler = { version = "0.13.4", default-features = false }
|
|||||||
opendal = { version = "0.54.0", features = ["services-fs"], default-features = false }
|
opendal = { version = "0.54.0", features = ["services-fs"], default-features = false }
|
||||||
|
|
||||||
# For retrieving AWS credentials, including temporary SSO credentials
|
# For retrieving AWS credentials, including temporary SSO credentials
|
||||||
anyhow = { version = "1.0.98", optional = true }
|
anyhow = { version = "1.0.99", optional = true }
|
||||||
aws-config = { version = "1.8.3", features = ["behavior-version-latest", "rt-tokio", "credentials-process", "sso"], default-features = false, optional = true }
|
aws-config = { version = "1.8.5", features = ["behavior-version-latest", "rt-tokio", "credentials-process", "sso"], default-features = false, optional = true }
|
||||||
aws-credential-types = { version = "1.2.4", optional = true }
|
aws-credential-types = { version = "1.2.5", optional = true }
|
||||||
aws-smithy-runtime-api = { version = "1.8.5", optional = true }
|
aws-smithy-runtime-api = { version = "1.9.0", optional = true }
|
||||||
http = { version = "1.3.1", optional = true }
|
http = { version = "1.3.1", optional = true }
|
||||||
reqsign = { version = "0.16.5", optional = true }
|
reqsign = { version = "0.16.5", optional = true }
|
||||||
|
|
||||||
@@ -284,6 +283,7 @@ clone_on_ref_ptr = "deny"
|
|||||||
equatable_if_let = "deny"
|
equatable_if_let = "deny"
|
||||||
filter_map_next = "deny"
|
filter_map_next = "deny"
|
||||||
float_cmp_const = "deny"
|
float_cmp_const = "deny"
|
||||||
|
implicit_clone = "deny"
|
||||||
inefficient_to_string = "deny"
|
inefficient_to_string = "deny"
|
||||||
iter_on_empty_collections = "deny"
|
iter_on_empty_collections = "deny"
|
||||||
iter_on_single_items = "deny"
|
iter_on_single_items = "deny"
|
||||||
@@ -298,7 +298,6 @@ needless_continue = "deny"
|
|||||||
needless_lifetimes = "deny"
|
needless_lifetimes = "deny"
|
||||||
option_option = "deny"
|
option_option = "deny"
|
||||||
string_add_assign = "deny"
|
string_add_assign = "deny"
|
||||||
string_to_string = "deny"
|
|
||||||
unnecessary_join = "deny"
|
unnecessary_join = "deny"
|
||||||
unnecessary_self_imports = "deny"
|
unnecessary_self_imports = "deny"
|
||||||
unnested_or_patterns = "deny"
|
unnested_or_patterns = "deny"
|
||||||
|
@@ -1,12 +1,12 @@
|
|||||||
---
|
---
|
||||||
vault_version: "v2025.7.0"
|
vault_version: "v2025.8.0"
|
||||||
vault_image_digest: "sha256:f6ac819a2cd9e226f2cd2ec26196ede94a41e672e9672a11b5f307a19278b15e"
|
vault_image_digest: "sha256:41c2b51c87882248f405d5a0ab37210d2672a312ec5d4f3b9afcdbbe8eb9d57d"
|
||||||
# Cross Compile Docker Helper Scripts v1.6.1
|
# Cross Compile Docker Helper Scripts v1.6.1
|
||||||
# We use the linux/amd64 platform shell scripts since there is no difference between the different platform scripts
|
# We use the linux/amd64 platform shell scripts since there is no difference between the different platform scripts
|
||||||
# https://github.com/tonistiigi/xx | https://hub.docker.com/r/tonistiigi/xx/tags
|
# https://github.com/tonistiigi/xx | https://hub.docker.com/r/tonistiigi/xx/tags
|
||||||
xx_image_digest: "sha256:9c207bead753dda9430bdd15425c6518fc7a03d866103c516a2c6889188f5894"
|
xx_image_digest: "sha256:9c207bead753dda9430bdd15425c6518fc7a03d866103c516a2c6889188f5894"
|
||||||
rust_version: 1.88.0 # Rust version to be used
|
rust_version: 1.89.0 # Rust version to be used
|
||||||
debian_version: bookworm # Debian release name to be used
|
debian_version: trixie # Debian release name to be used
|
||||||
alpine_version: "3.22" # Alpine version to be used
|
alpine_version: "3.22" # Alpine version to be used
|
||||||
# For which platforms/architectures will we try to build images
|
# For which platforms/architectures will we try to build images
|
||||||
platforms: ["linux/amd64", "linux/arm64", "linux/arm/v7", "linux/arm/v6"]
|
platforms: ["linux/amd64", "linux/arm64", "linux/arm/v7", "linux/arm/v6"]
|
||||||
|
@@ -19,23 +19,23 @@
|
|||||||
# - From https://hub.docker.com/r/vaultwarden/web-vault/tags,
|
# - From https://hub.docker.com/r/vaultwarden/web-vault/tags,
|
||||||
# click the tag name to view the digest of the image it currently points to.
|
# click the tag name to view the digest of the image it currently points to.
|
||||||
# - From the command line:
|
# - From the command line:
|
||||||
# $ docker pull docker.io/vaultwarden/web-vault:v2025.7.0
|
# $ docker pull docker.io/vaultwarden/web-vault:v2025.8.0
|
||||||
# $ docker image inspect --format "{{.RepoDigests}}" docker.io/vaultwarden/web-vault:v2025.7.0
|
# $ docker image inspect --format "{{.RepoDigests}}" docker.io/vaultwarden/web-vault:v2025.8.0
|
||||||
# [docker.io/vaultwarden/web-vault@sha256:f6ac819a2cd9e226f2cd2ec26196ede94a41e672e9672a11b5f307a19278b15e]
|
# [docker.io/vaultwarden/web-vault@sha256:41c2b51c87882248f405d5a0ab37210d2672a312ec5d4f3b9afcdbbe8eb9d57d]
|
||||||
#
|
#
|
||||||
# - Conversely, to get the tag name from the digest:
|
# - Conversely, to get the tag name from the digest:
|
||||||
# $ docker image inspect --format "{{.RepoTags}}" docker.io/vaultwarden/web-vault@sha256:f6ac819a2cd9e226f2cd2ec26196ede94a41e672e9672a11b5f307a19278b15e
|
# $ docker image inspect --format "{{.RepoTags}}" docker.io/vaultwarden/web-vault@sha256:41c2b51c87882248f405d5a0ab37210d2672a312ec5d4f3b9afcdbbe8eb9d57d
|
||||||
# [docker.io/vaultwarden/web-vault:v2025.7.0]
|
# [docker.io/vaultwarden/web-vault:v2025.8.0]
|
||||||
#
|
#
|
||||||
FROM --platform=linux/amd64 docker.io/vaultwarden/web-vault@sha256:f6ac819a2cd9e226f2cd2ec26196ede94a41e672e9672a11b5f307a19278b15e AS vault
|
FROM --platform=linux/amd64 docker.io/vaultwarden/web-vault@sha256:41c2b51c87882248f405d5a0ab37210d2672a312ec5d4f3b9afcdbbe8eb9d57d AS vault
|
||||||
|
|
||||||
########################## ALPINE BUILD IMAGES ##########################
|
########################## ALPINE BUILD IMAGES ##########################
|
||||||
## NOTE: The Alpine Base Images do not support other platforms then linux/amd64
|
## NOTE: The Alpine Base Images do not support other platforms then linux/amd64
|
||||||
## And for Alpine we define all build images here, they will only be loaded when actually used
|
## And for Alpine we define all build images here, they will only be loaded when actually used
|
||||||
FROM --platform=linux/amd64 ghcr.io/blackdex/rust-musl:x86_64-musl-stable-1.88.0 AS build_amd64
|
FROM --platform=linux/amd64 ghcr.io/blackdex/rust-musl:x86_64-musl-stable-1.89.0 AS build_amd64
|
||||||
FROM --platform=linux/amd64 ghcr.io/blackdex/rust-musl:aarch64-musl-stable-1.88.0 AS build_arm64
|
FROM --platform=linux/amd64 ghcr.io/blackdex/rust-musl:aarch64-musl-stable-1.89.0 AS build_arm64
|
||||||
FROM --platform=linux/amd64 ghcr.io/blackdex/rust-musl:armv7-musleabihf-stable-1.88.0 AS build_armv7
|
FROM --platform=linux/amd64 ghcr.io/blackdex/rust-musl:armv7-musleabihf-stable-1.89.0 AS build_armv7
|
||||||
FROM --platform=linux/amd64 ghcr.io/blackdex/rust-musl:arm-musleabi-stable-1.88.0 AS build_armv6
|
FROM --platform=linux/amd64 ghcr.io/blackdex/rust-musl:arm-musleabi-stable-1.89.0 AS build_armv6
|
||||||
|
|
||||||
########################## BUILD IMAGE ##########################
|
########################## BUILD IMAGE ##########################
|
||||||
# hadolint ignore=DL3006
|
# hadolint ignore=DL3006
|
||||||
|
@@ -19,15 +19,15 @@
|
|||||||
# - From https://hub.docker.com/r/vaultwarden/web-vault/tags,
|
# - From https://hub.docker.com/r/vaultwarden/web-vault/tags,
|
||||||
# click the tag name to view the digest of the image it currently points to.
|
# click the tag name to view the digest of the image it currently points to.
|
||||||
# - From the command line:
|
# - From the command line:
|
||||||
# $ docker pull docker.io/vaultwarden/web-vault:v2025.7.0
|
# $ docker pull docker.io/vaultwarden/web-vault:v2025.8.0
|
||||||
# $ docker image inspect --format "{{.RepoDigests}}" docker.io/vaultwarden/web-vault:v2025.7.0
|
# $ docker image inspect --format "{{.RepoDigests}}" docker.io/vaultwarden/web-vault:v2025.8.0
|
||||||
# [docker.io/vaultwarden/web-vault@sha256:f6ac819a2cd9e226f2cd2ec26196ede94a41e672e9672a11b5f307a19278b15e]
|
# [docker.io/vaultwarden/web-vault@sha256:41c2b51c87882248f405d5a0ab37210d2672a312ec5d4f3b9afcdbbe8eb9d57d]
|
||||||
#
|
#
|
||||||
# - Conversely, to get the tag name from the digest:
|
# - Conversely, to get the tag name from the digest:
|
||||||
# $ docker image inspect --format "{{.RepoTags}}" docker.io/vaultwarden/web-vault@sha256:f6ac819a2cd9e226f2cd2ec26196ede94a41e672e9672a11b5f307a19278b15e
|
# $ docker image inspect --format "{{.RepoTags}}" docker.io/vaultwarden/web-vault@sha256:41c2b51c87882248f405d5a0ab37210d2672a312ec5d4f3b9afcdbbe8eb9d57d
|
||||||
# [docker.io/vaultwarden/web-vault:v2025.7.0]
|
# [docker.io/vaultwarden/web-vault:v2025.8.0]
|
||||||
#
|
#
|
||||||
FROM --platform=linux/amd64 docker.io/vaultwarden/web-vault@sha256:f6ac819a2cd9e226f2cd2ec26196ede94a41e672e9672a11b5f307a19278b15e AS vault
|
FROM --platform=linux/amd64 docker.io/vaultwarden/web-vault@sha256:41c2b51c87882248f405d5a0ab37210d2672a312ec5d4f3b9afcdbbe8eb9d57d AS vault
|
||||||
|
|
||||||
########################## Cross Compile Docker Helper Scripts ##########################
|
########################## Cross Compile Docker Helper Scripts ##########################
|
||||||
## We use the linux/amd64 no matter which Build Platform, since these are all bash scripts
|
## We use the linux/amd64 no matter which Build Platform, since these are all bash scripts
|
||||||
@@ -36,7 +36,7 @@ FROM --platform=linux/amd64 docker.io/tonistiigi/xx@sha256:9c207bead753dda9430bd
|
|||||||
|
|
||||||
########################## BUILD IMAGE ##########################
|
########################## BUILD IMAGE ##########################
|
||||||
# hadolint ignore=DL3006
|
# hadolint ignore=DL3006
|
||||||
FROM --platform=$BUILDPLATFORM docker.io/library/rust:1.88.0-slim-bookworm AS build
|
FROM --platform=$BUILDPLATFORM docker.io/library/rust:1.89.0-slim-trixie AS build
|
||||||
COPY --from=xx / /
|
COPY --from=xx / /
|
||||||
ARG TARGETARCH
|
ARG TARGETARCH
|
||||||
ARG TARGETVARIANT
|
ARG TARGETVARIANT
|
||||||
@@ -68,15 +68,11 @@ RUN apt-get update && \
|
|||||||
xx-apt-get install -y \
|
xx-apt-get install -y \
|
||||||
--no-install-recommends \
|
--no-install-recommends \
|
||||||
gcc \
|
gcc \
|
||||||
libmariadb3 \
|
|
||||||
libpq-dev \
|
libpq-dev \
|
||||||
libpq5 \
|
libpq5 \
|
||||||
libssl-dev \
|
libssl-dev \
|
||||||
|
libmariadb-dev \
|
||||||
zlib1g-dev && \
|
zlib1g-dev && \
|
||||||
# Force install arch dependend mariadb dev packages
|
|
||||||
# Installing them the normal way breaks several other packages (again)
|
|
||||||
apt-get download "libmariadb-dev-compat:$(xx-info debian-arch)" "libmariadb-dev:$(xx-info debian-arch)" && \
|
|
||||||
dpkg --force-all -i ./libmariadb-dev*.deb && \
|
|
||||||
# Run xx-cargo early, since it sometimes seems to break when run at a later stage
|
# Run xx-cargo early, since it sometimes seems to break when run at a later stage
|
||||||
echo "export CARGO_TARGET=$(xx-cargo --print-target-triple)" >> /env-cargo
|
echo "export CARGO_TARGET=$(xx-cargo --print-target-triple)" >> /env-cargo
|
||||||
|
|
||||||
@@ -166,7 +162,7 @@ RUN source /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
|
||||||
FROM --platform=$TARGETPLATFORM docker.io/library/debian:bookworm-slim
|
FROM --platform=$TARGETPLATFORM docker.io/library/debian:trixie-slim
|
||||||
|
|
||||||
ENV ROCKET_PROFILE="release" \
|
ENV ROCKET_PROFILE="release" \
|
||||||
ROCKET_ADDRESS=0.0.0.0 \
|
ROCKET_ADDRESS=0.0.0.0 \
|
||||||
@@ -179,7 +175,7 @@ RUN mkdir /data && \
|
|||||||
--no-install-recommends \
|
--no-install-recommends \
|
||||||
ca-certificates \
|
ca-certificates \
|
||||||
curl \
|
curl \
|
||||||
libmariadb-dev-compat \
|
libmariadb-dev \
|
||||||
libpq5 \
|
libpq5 \
|
||||||
openssl && \
|
openssl && \
|
||||||
apt-get clean && \
|
apt-get clean && \
|
||||||
|
@@ -86,15 +86,11 @@ RUN apt-get update && \
|
|||||||
xx-apt-get install -y \
|
xx-apt-get install -y \
|
||||||
--no-install-recommends \
|
--no-install-recommends \
|
||||||
gcc \
|
gcc \
|
||||||
libmariadb3 \
|
|
||||||
libpq-dev \
|
libpq-dev \
|
||||||
libpq5 \
|
libpq5 \
|
||||||
libssl-dev \
|
libssl-dev \
|
||||||
|
libmariadb-dev \
|
||||||
zlib1g-dev && \
|
zlib1g-dev && \
|
||||||
# Force install arch dependend mariadb dev packages
|
|
||||||
# Installing them the normal way breaks several other packages (again)
|
|
||||||
apt-get download "libmariadb-dev-compat:$(xx-info debian-arch)" "libmariadb-dev:$(xx-info debian-arch)" && \
|
|
||||||
dpkg --force-all -i ./libmariadb-dev*.deb && \
|
|
||||||
# Run xx-cargo early, since it sometimes seems to break when run at a later stage
|
# Run xx-cargo early, since it sometimes seems to break when run at a later stage
|
||||||
echo "export CARGO_TARGET=$(xx-cargo --print-target-triple)" >> /env-cargo
|
echo "export CARGO_TARGET=$(xx-cargo --print-target-triple)" >> /env-cargo
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@@ -216,7 +212,7 @@ RUN mkdir /data && \
|
|||||||
--no-install-recommends \
|
--no-install-recommends \
|
||||||
ca-certificates \
|
ca-certificates \
|
||||||
curl \
|
curl \
|
||||||
libmariadb-dev-compat \
|
libmariadb-dev \
|
||||||
libpq5 \
|
libpq5 \
|
||||||
openssl && \
|
openssl && \
|
||||||
apt-get clean && \
|
apt-get clean && \
|
||||||
|
@@ -10,7 +10,7 @@ proc-macro = true
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
quote = "1.0.40"
|
quote = "1.0.40"
|
||||||
syn = "2.0.104"
|
syn = "2.0.105"
|
||||||
|
|
||||||
[lints]
|
[lints]
|
||||||
workspace = true
|
workspace = true
|
||||||
|
@@ -39,7 +39,7 @@ 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
|
||||||
DOMAIN=http://127.0.0.1:${ROCKET_PORT}
|
DOMAIN=http://localhost:${ROCKET_PORT}
|
||||||
LOG_LEVEL=info,oidcwarden::sso=debug
|
LOG_LEVEL=info,oidcwarden::sso=debug
|
||||||
I_REALLY_WANT_VOLATILE_STORAGE=true
|
I_REALLY_WANT_VOLATILE_STORAGE=true
|
||||||
|
|
||||||
|
@@ -1,18 +1,18 @@
|
|||||||
# 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
|
||||||
|
|
||||||
This rely on `docker` and the `compose` [plugin](https://docs.docker.com/compose/install/).
|
This relies on `docker` and the `compose` [plugin](https://docs.docker.com/compose/install/).
|
||||||
Databases (`Mariadb`, `Mysql` and `Postgres`) and `Playwright` will run in containers.
|
Databases (`Mariadb`, `Mysql` and `Postgres`) and `Playwright` will run in containers.
|
||||||
|
|
||||||
### 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 install
|
npm install
|
||||||
@@ -33,7 +33,7 @@ To force a rebuild of the Playwright image:
|
|||||||
DOCKER_BUILDKIT=1 docker compose --env-file test.env build Playwright
|
DOCKER_BUILDKIT=1 docker compose --env-file test.env build Playwright
|
||||||
```
|
```
|
||||||
|
|
||||||
To access the ui to easily run test individually and debug if needed (will not work in docker):
|
To access the UI to easily run test individually and debug if needed (this will not work in docker):
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npx playwright test --ui
|
npx playwright test --ui
|
||||||
@@ -42,7 +42,7 @@ npx playwright test --ui
|
|||||||
### DB
|
### DB
|
||||||
|
|
||||||
Projects are configured to allow to run tests only on specific database.
|
Projects are configured to allow to run tests only on specific database.
|
||||||
\
|
|
||||||
You can use:
|
You can use:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@@ -62,7 +62,7 @@ DOCKER_BUILDKIT=1 docker compose --profile playwright --env-file test.env run Pl
|
|||||||
|
|
||||||
### Keep services running
|
### Keep services running
|
||||||
|
|
||||||
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_RUNNNING=true npx playwright test
|
PW_KEEP_SERVICE_RUNNNING=true npx playwright test
|
||||||
@@ -86,31 +86,41 @@ DOCKER_BUILDKIT=1 docker compose --profile playwright --env-file test.env run Pl
|
|||||||
|
|
||||||
## Writing scenario
|
## Writing scenario
|
||||||
|
|
||||||
When creating new scenario use the recorder to more easily identify elements (in general try to rely on visible hint to identify elements and not hidden ids).
|
When creating new scenario use the recorder to more easily identify elements
|
||||||
|
(in general try to rely on visible hint to identify elements and not hidden IDs).
|
||||||
This does not start the server, you will need to start it manually.
|
This does not start the server, you will need to start it manually.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npx playwright codegen "http://127.0.0.1:8000"
|
DOCKER_BUILDKIT=1 docker compose --profile playwright --env-file test.env up Vaultwarden
|
||||||
|
npx playwright codegen "http://127.0.0.1:8003"
|
||||||
```
|
```
|
||||||
|
|
||||||
## Override web-vault
|
## Override web-vault
|
||||||
|
|
||||||
It's possible to change the `web-vault` used by referencing a different `bw_web_builds` commit.
|
It is possible to change the `web-vault` used by referencing a different `bw_web_builds` commit.
|
||||||
|
|
||||||
|
Simplest is to set and uncomment `PW_WV_REPO_URL` and `PW_WV_COMMIT_HASH` in the `test.env`.
|
||||||
|
Ensure that the image is built with:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
export PW_WV_REPO_URL=https://github.com/Timshel/oidc_web_builds.git
|
DOCKER_BUILDKIT=1 docker compose --profile playwright --env-file test.env build Vaultwarden
|
||||||
export PW_WV_COMMIT_HASH=8707dc76df3f0cceef2be5bfae37bb29bd17fae6
|
```
|
||||||
DOCKER_BUILDKIT=1 docker compose --profile playwright --env-file test.env build Playwright
|
|
||||||
|
You can check the result running:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
DOCKER_BUILDKIT=1 docker compose --profile playwright --env-file test.env up Vaultwarden
|
||||||
```
|
```
|
||||||
|
|
||||||
# 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
|
||||||
|
|
||||||
This rely on `docker` and the `compose` [plugin](https://docs.docker.com/compose/install/).
|
This rely on `docker` and the `compose` [plugin](https://docs.docker.com/compose/install/).
|
||||||
First create a copy of `.env.template` as `.env` (This is done to prevent commiting your custom settings, Ex `SMTP_`).
|
First create a copy of `.env.template` as `.env` (This is done to prevent committing your custom settings, Ex `SMTP_`).
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
@@ -125,11 +135,12 @@ 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 http://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
|
||||||
|
|
||||||
@@ -143,7 +154,7 @@ You can run just `Keycloak` with `--profile keycloak`:
|
|||||||
```bash
|
```bash
|
||||||
> docker compose --profile keycloak --env-file .env up
|
> docker compose --profile keycloak --env-file .env up
|
||||||
```
|
```
|
||||||
When running with a local VaultWarden, you can use a front-end build from [dani-garcia/bw_web_builds](https://github.com/dani-garcia/bw_web_builds/releases).
|
When running with a local Vaultwarden, you can use a front-end build from [dani-garcia/bw_web_builds](https://github.com/dani-garcia/bw_web_builds/releases).
|
||||||
|
|
||||||
## Rebuilding the Vaultwarden
|
## Rebuilding the Vaultwarden
|
||||||
|
|
||||||
@@ -155,12 +166,12 @@ docker compose --profile vaultwarden --env-file .env build VaultwardenPrebuild V
|
|||||||
|
|
||||||
## Configuration
|
## Configuration
|
||||||
|
|
||||||
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 `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)).
|
- `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
|
||||||
|
|
||||||
Use `docker compose --profile vaultWarden down`.
|
Use `docker compose --profile vaultwarden down`.
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
FROM playwright_oidc_vaultwarden_prebuilt AS prebuilt
|
FROM playwright_oidc_vaultwarden_prebuilt AS prebuilt
|
||||||
|
|
||||||
FROM node:18-bookworm AS build
|
FROM node:22-trixie AS build
|
||||||
|
|
||||||
ARG REPO_URL
|
ARG REPO_URL
|
||||||
ARG COMMIT_HASH
|
ARG COMMIT_HASH
|
||||||
@@ -14,7 +14,7 @@ COPY build.sh /build.sh
|
|||||||
RUN /build.sh
|
RUN /build.sh
|
||||||
|
|
||||||
######################## RUNTIME IMAGE ########################
|
######################## RUNTIME IMAGE ########################
|
||||||
FROM docker.io/library/debian:bookworm-slim
|
FROM docker.io/library/debian:trixie-slim
|
||||||
|
|
||||||
ENV DEBIAN_FRONTEND=noninteractive
|
ENV DEBIAN_FRONTEND=noninteractive
|
||||||
|
|
||||||
@@ -24,7 +24,7 @@ RUN mkdir /data && \
|
|||||||
--no-install-recommends \
|
--no-install-recommends \
|
||||||
ca-certificates \
|
ca-certificates \
|
||||||
curl \
|
curl \
|
||||||
libmariadb-dev-compat \
|
libmariadb-dev \
|
||||||
libpq5 \
|
libpq5 \
|
||||||
openssl && \
|
openssl && \
|
||||||
rm -rf /var/lib/apt/lists/*
|
rm -rf /var/lib/apt/lists/*
|
||||||
|
@@ -16,7 +16,6 @@ if [[ ! -z "$REPO_URL" ]] && [[ ! -z "$COMMIT_HASH" ]] ; then
|
|||||||
|
|
||||||
export VAULT_VERSION=$(cat Dockerfile | grep "ARG VAULT_VERSION" | cut -d "=" -f2)
|
export VAULT_VERSION=$(cat Dockerfile | grep "ARG VAULT_VERSION" | cut -d "=" -f2)
|
||||||
./scripts/checkout_web_vault.sh
|
./scripts/checkout_web_vault.sh
|
||||||
./scripts/patch_web_vault.sh
|
|
||||||
./scripts/build_web_vault.sh
|
./scripts/build_web_vault.sh
|
||||||
printf '{"version":"%s"}' "$COMMIT_HASH" > ./web-vault/apps/web/build/vw-version.json
|
printf '{"version":"%s"}' "$COMMIT_HASH" > ./web-vault/apps/web/build/vw-version.json
|
||||||
|
|
||||||
|
955
playwright/package-lock.json
generated
955
playwright/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -8,14 +8,14 @@
|
|||||||
"author": "",
|
"author": "",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@playwright/test": "^1.53.0",
|
"@playwright/test": "^1.54.2",
|
||||||
"dotenv": "^16.5.0",
|
"dotenv": "^16.6.1",
|
||||||
"dotenv-expand": "^12.0.2",
|
"dotenv-expand": "^12.0.2",
|
||||||
"maildev": "npm:@timshel_npm/maildev@^3.1.2"
|
"maildev": "npm:@timshel_npm/maildev@^3.2.1"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"mysql2": "^3.14.1",
|
"mysql2": "^3.14.3",
|
||||||
"otpauth": "^9.4.0",
|
"otpauth": "^9.4.0",
|
||||||
"pg": "^8.16.0"
|
"pg": "^8.16.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -26,9 +26,9 @@ export default defineConfig({
|
|||||||
* But short action/nav/expect timeouts to fail on specific step (raise locally if not enough).
|
* But short action/nav/expect timeouts to fail on specific step (raise locally if not enough).
|
||||||
*/
|
*/
|
||||||
timeout: 120 * 1000,
|
timeout: 120 * 1000,
|
||||||
actionTimeout: 10 * 1000,
|
actionTimeout: 20 * 1000,
|
||||||
navigationTimeout: 10 * 1000,
|
navigationTimeout: 20 * 1000,
|
||||||
expect: { timeout: 10 * 1000 },
|
expect: { timeout: 20 * 1000 },
|
||||||
|
|
||||||
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
|
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
|
||||||
use: {
|
use: {
|
||||||
|
@@ -43,7 +43,7 @@ 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
|
||||||
|
|
||||||
# Script parameters (use Keycloak and VaultWarden config too)
|
# Script parameters (use Keycloak and Vaultwarden config too)
|
||||||
TEST_REALM=test
|
TEST_REALM=test
|
||||||
DUMMY_REALM=dummy
|
DUMMY_REALM=dummy
|
||||||
DUMMY_AUTHORITY=http://${KC_HTTP_HOST}:${KC_HTTP_PORT}/realms/${DUMMY_REALM}
|
DUMMY_AUTHORITY=http://${KC_HTTP_HOST}:${KC_HTTP_PORT}/realms/${DUMMY_REALM}
|
||||||
@@ -52,7 +52,7 @@ DUMMY_AUTHORITY=http://${KC_HTTP_HOST}:${KC_HTTP_PORT}/realms/${DUMMY_REALM}
|
|||||||
# Vaultwarden Config #
|
# Vaultwarden Config #
|
||||||
######################
|
######################
|
||||||
ROCKET_PORT=8003
|
ROCKET_PORT=8003
|
||||||
DOMAIN=http://127.0.0.1:${ROCKET_PORT}
|
DOMAIN=http://localhost:${ROCKET_PORT}
|
||||||
LOG_LEVEL=info,oidcwarden::sso=debug
|
LOG_LEVEL=info,oidcwarden::sso=debug
|
||||||
LOGIN_RATELIMIT_MAX_BURST=100
|
LOGIN_RATELIMIT_MAX_BURST=100
|
||||||
|
|
||||||
@@ -66,6 +66,10 @@ SSO_CLIENT_SECRET=warden
|
|||||||
SSO_AUTHORITY=http://${KC_HTTP_HOST}:${KC_HTTP_PORT}/realms/${TEST_REALM}
|
SSO_AUTHORITY=http://${KC_HTTP_HOST}:${KC_HTTP_PORT}/realms/${TEST_REALM}
|
||||||
SSO_DEBUG_TOKENS=true
|
SSO_DEBUG_TOKENS=true
|
||||||
|
|
||||||
|
# Custom web-vault build
|
||||||
|
# PW_WV_REPO_URL=https://github.com/dani-garcia/bw_web_builds.git
|
||||||
|
# PW_WV_COMMIT_HASH=a5f5390895516bce2f48b7baadb6dc399e5fe75a
|
||||||
|
|
||||||
###########################
|
###########################
|
||||||
# Docker MariaDb container#
|
# Docker MariaDb container#
|
||||||
###########################
|
###########################
|
||||||
|
@@ -12,7 +12,7 @@ export async function logNewUser(
|
|||||||
test: Test,
|
test: Test,
|
||||||
page: Page,
|
page: Page,
|
||||||
user: { email: string, name: string, password: string },
|
user: { email: string, name: string, password: string },
|
||||||
options: { mailBuffer?: MailBuffer, override?: boolean } = {}
|
options: { mailBuffer?: MailBuffer } = {}
|
||||||
) {
|
) {
|
||||||
await test.step(`Create user ${user.name}`, async () => {
|
await test.step(`Create user ${user.name}`, async () => {
|
||||||
await page.context().clearCookies();
|
await page.context().clearCookies();
|
||||||
@@ -20,12 +20,8 @@ export async function logNewUser(
|
|||||||
await test.step('Landing page', async () => {
|
await test.step('Landing page', async () => {
|
||||||
await utils.cleanLanding(page);
|
await utils.cleanLanding(page);
|
||||||
|
|
||||||
if( options.override ) {
|
await page.locator("input[type=email].vw-email-sso").fill(user.email);
|
||||||
await page.getByRole('button', { name: 'Continue' }).click();
|
await page.getByRole('button', { name: /Use single sign-on/ }).click();
|
||||||
} else {
|
|
||||||
await page.getByLabel(/Email address/).fill(user.email);
|
|
||||||
await page.getByRole('button', { name: /Use single sign-on/ }).click();
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
await test.step('Keycloak login', async () => {
|
await test.step('Keycloak login', async () => {
|
||||||
@@ -69,7 +65,6 @@ export async function logUser(
|
|||||||
user: { email: string, password: string },
|
user: { email: string, password: string },
|
||||||
options: {
|
options: {
|
||||||
mailBuffer ?: MailBuffer,
|
mailBuffer ?: MailBuffer,
|
||||||
override?: boolean,
|
|
||||||
totp?: OTPAuth.TOTP,
|
totp?: OTPAuth.TOTP,
|
||||||
mail2fa?: boolean,
|
mail2fa?: boolean,
|
||||||
} = {}
|
} = {}
|
||||||
@@ -82,12 +77,8 @@ export async function logUser(
|
|||||||
await test.step('Landing page', async () => {
|
await test.step('Landing page', async () => {
|
||||||
await utils.cleanLanding(page);
|
await utils.cleanLanding(page);
|
||||||
|
|
||||||
if( options.override ) {
|
await page.locator("input[type=email].vw-email-sso").fill(user.email);
|
||||||
await page.getByRole('button', { name: 'Continue' }).click();
|
await page.getByRole('button', { name: /Use single sign-on/ }).click();
|
||||||
} else {
|
|
||||||
await page.getByLabel(/Email address/).fill(user.email);
|
|
||||||
await page.getByRole('button', { name: /Use single sign-on/ }).click();
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
await test.step('Keycloak login', async () => {
|
await test.step('Keycloak login', async () => {
|
||||||
|
@@ -29,8 +29,8 @@ test('SSO login', async ({ page }) => {
|
|||||||
test('Non SSO login', async ({ page }) => {
|
test('Non SSO login', async ({ page }) => {
|
||||||
// Landing page
|
// Landing page
|
||||||
await page.goto('/');
|
await page.goto('/');
|
||||||
await page.getByLabel(/Email address/).fill(users.user1.email);
|
await page.locator("input[type=email].vw-email-sso").fill(users.user1.email);
|
||||||
await page.getByRole('button', { name: 'Continue' }).click();
|
await page.getByRole('button', { name: 'Other' }).click();
|
||||||
|
|
||||||
// Unlock page
|
// Unlock page
|
||||||
await page.getByLabel('Master password').fill(users.user1.password);
|
await page.getByLabel('Master password').fill(users.user1.password);
|
||||||
@@ -58,20 +58,12 @@ test('Non SSO login impossible', async ({ page, browser }, testInfo: TestInfo) =
|
|||||||
|
|
||||||
// Landing page
|
// Landing page
|
||||||
await page.goto('/');
|
await page.goto('/');
|
||||||
await page.getByLabel(/Email address/).fill(users.user1.email);
|
|
||||||
|
|
||||||
// Check that SSO login is available
|
// Check that SSO login is available
|
||||||
await expect(page.getByRole('button', { name: /Use single sign-on/ })).toHaveCount(1);
|
await expect(page.getByRole('button', { name: /Use single sign-on/ })).toHaveCount(1);
|
||||||
|
|
||||||
await page.getByLabel(/Email address/).fill(users.user1.email);
|
// No Continue/Other
|
||||||
await page.getByRole('button', { name: 'Continue' }).click();
|
await expect(page.getByRole('button', { name: 'Other' })).toHaveCount(0);
|
||||||
|
|
||||||
// Unlock page
|
|
||||||
await page.getByLabel('Master password').fill(users.user1.password);
|
|
||||||
await page.getByRole('button', { name: 'Log in with master password' }).click();
|
|
||||||
|
|
||||||
// An error should appear
|
|
||||||
await page.getByLabel('SSO sign-in is required')
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
@@ -82,13 +74,12 @@ test('No SSO login', async ({ page }, testInfo: TestInfo) => {
|
|||||||
|
|
||||||
// Landing page
|
// Landing page
|
||||||
await page.goto('/');
|
await page.goto('/');
|
||||||
await page.getByLabel(/Email address/).fill(users.user1.email);
|
|
||||||
|
|
||||||
// No SSO button (rely on a correct selector checked in previous test)
|
// No SSO button (rely on a correct selector checked in previous test)
|
||||||
await page.getByLabel('Master password');
|
|
||||||
await expect(page.getByRole('button', { name: /Use single sign-on/ })).toHaveCount(0);
|
await expect(page.getByRole('button', { name: /Use single sign-on/ })).toHaveCount(0);
|
||||||
|
|
||||||
// Can continue to Master password
|
// Can continue to Master password
|
||||||
|
await page.getByLabel(/Email address/).fill(users.user1.email);
|
||||||
await page.getByRole('button', { name: 'Continue' }).click();
|
await page.getByRole('button', { name: 'Continue' }).click();
|
||||||
await expect(page.getByRole('button', { name: /Log in with master password/ })).toHaveCount(1);
|
await expect(page.getByRole('button', { name: 'Log in with master password' })).toHaveCount(1);
|
||||||
});
|
});
|
||||||
|
@@ -65,7 +65,7 @@ test('Enforce password policy', async ({ page }) => {
|
|||||||
await utils.logout(test, page, users.user1);
|
await utils.logout(test, page, users.user1);
|
||||||
|
|
||||||
await test.step(`Unlock trigger policy`, async () => {
|
await test.step(`Unlock trigger policy`, async () => {
|
||||||
await page.getByRole('textbox', { name: 'Email address (required)' }).fill(users.user1.email);
|
await page.locator("input[type=email].vw-email-sso").fill(users.user1.email);
|
||||||
await page.getByRole('button', { name: 'Use single sign-on' }).click();
|
await page.getByRole('button', { name: 'Use single sign-on' }).click();
|
||||||
|
|
||||||
await page.getByRole('textbox', { name: 'Master password (required)' }).fill(users.user1.password);
|
await page.getByRole('textbox', { name: 'Master password (required)' }).fill(users.user1.password);
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
[toolchain]
|
[toolchain]
|
||||||
channel = "1.88.0"
|
channel = "1.89.0"
|
||||||
components = [ "rustfmt", "clippy" ]
|
components = [ "rustfmt", "clippy" ]
|
||||||
profile = "minimal"
|
profile = "minimal"
|
||||||
|
@@ -342,11 +342,11 @@ async fn post_set_password(data: Json<SetPasswordData>, headers: Headers, mut co
|
|||||||
let mut user = headers.user;
|
let mut user = headers.user;
|
||||||
|
|
||||||
if user.private_key.is_some() {
|
if user.private_key.is_some() {
|
||||||
err!("Account already intialized cannot set password")
|
err!("Account already initialized, cannot set password")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check against the password hint setting here so if it fails, the user
|
// Check against the password hint setting here so if it fails,
|
||||||
// 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);
|
let password_hint = clean_password_hint(&data.master_password_hint);
|
||||||
enforce_password_hint_setting(&password_hint)?;
|
enforce_password_hint_setting(&password_hint)?;
|
||||||
|
|
||||||
|
@@ -78,6 +78,7 @@ pub fn routes() -> Vec<Route> {
|
|||||||
restore_cipher_put,
|
restore_cipher_put,
|
||||||
restore_cipher_put_admin,
|
restore_cipher_put_admin,
|
||||||
restore_cipher_selected,
|
restore_cipher_selected,
|
||||||
|
restore_cipher_selected_admin,
|
||||||
delete_all,
|
delete_all,
|
||||||
move_cipher_selected,
|
move_cipher_selected,
|
||||||
move_cipher_selected_put,
|
move_cipher_selected_put,
|
||||||
@@ -318,7 +319,7 @@ async fn post_ciphers_create(
|
|||||||
// or otherwise), we can just ignore this field entirely.
|
// or otherwise), we can just ignore this field entirely.
|
||||||
data.cipher.last_known_revision_date = None;
|
data.cipher.last_known_revision_date = None;
|
||||||
|
|
||||||
share_cipher_by_uuid(&cipher.uuid, data, &headers, &mut conn, &nt).await
|
share_cipher_by_uuid(&cipher.uuid, data, &headers, &mut conn, &nt, None).await
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Called when creating a new user-owned cipher.
|
/// Called when creating a new user-owned cipher.
|
||||||
@@ -920,7 +921,7 @@ async fn post_cipher_share(
|
|||||||
) -> JsonResult {
|
) -> JsonResult {
|
||||||
let data: ShareCipherData = data.into_inner();
|
let data: ShareCipherData = data.into_inner();
|
||||||
|
|
||||||
share_cipher_by_uuid(&cipher_id, data, &headers, &mut conn, &nt).await
|
share_cipher_by_uuid(&cipher_id, data, &headers, &mut conn, &nt, None).await
|
||||||
}
|
}
|
||||||
|
|
||||||
#[put("/ciphers/<cipher_id>/share", data = "<data>")]
|
#[put("/ciphers/<cipher_id>/share", data = "<data>")]
|
||||||
@@ -933,7 +934,7 @@ async fn put_cipher_share(
|
|||||||
) -> JsonResult {
|
) -> JsonResult {
|
||||||
let data: ShareCipherData = data.into_inner();
|
let data: ShareCipherData = data.into_inner();
|
||||||
|
|
||||||
share_cipher_by_uuid(&cipher_id, data, &headers, &mut conn, &nt).await
|
share_cipher_by_uuid(&cipher_id, data, &headers, &mut conn, &nt, None).await
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
@@ -973,11 +974,16 @@ async fn put_cipher_share_selected(
|
|||||||
};
|
};
|
||||||
|
|
||||||
match shared_cipher_data.cipher.id.take() {
|
match shared_cipher_data.cipher.id.take() {
|
||||||
Some(id) => share_cipher_by_uuid(&id, shared_cipher_data, &headers, &mut conn, &nt).await?,
|
Some(id) => {
|
||||||
|
share_cipher_by_uuid(&id, shared_cipher_data, &headers, &mut conn, &nt, Some(UpdateType::None)).await?
|
||||||
|
}
|
||||||
None => err!("Request missing ids field"),
|
None => err!("Request missing ids field"),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Multi share actions do not send out a push for each cipher, we need to send a general sync here
|
||||||
|
nt.send_user_update(UpdateType::SyncCiphers, &headers.user, &headers.device.push_uuid, &mut conn).await;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -987,6 +993,7 @@ async fn share_cipher_by_uuid(
|
|||||||
headers: &Headers,
|
headers: &Headers,
|
||||||
conn: &mut DbConn,
|
conn: &mut DbConn,
|
||||||
nt: &Notify<'_>,
|
nt: &Notify<'_>,
|
||||||
|
override_ut: Option<UpdateType>,
|
||||||
) -> JsonResult {
|
) -> JsonResult {
|
||||||
let mut cipher = match Cipher::find_by_uuid(cipher_id, conn).await {
|
let mut cipher = match Cipher::find_by_uuid(cipher_id, conn).await {
|
||||||
Some(cipher) => {
|
Some(cipher) => {
|
||||||
@@ -1018,7 +1025,10 @@ async fn share_cipher_by_uuid(
|
|||||||
};
|
};
|
||||||
|
|
||||||
// When LastKnownRevisionDate is None, it is a new cipher, so send CipherCreate.
|
// When LastKnownRevisionDate is None, it is a new cipher, so send CipherCreate.
|
||||||
let ut = if data.cipher.last_known_revision_date.is_some() {
|
// If there is an override, like when handling multiple items, we want to prevent a push notification for every single item
|
||||||
|
let ut = if let Some(ut) = override_ut {
|
||||||
|
ut
|
||||||
|
} else if data.cipher.last_known_revision_date.is_some() {
|
||||||
UpdateType::SyncCipherUpdate
|
UpdateType::SyncCipherUpdate
|
||||||
} else {
|
} else {
|
||||||
UpdateType::SyncCipherCreate
|
UpdateType::SyncCipherCreate
|
||||||
@@ -1517,7 +1527,7 @@ async fn delete_cipher_selected_put_admin(
|
|||||||
|
|
||||||
#[put("/ciphers/<cipher_id>/restore")]
|
#[put("/ciphers/<cipher_id>/restore")]
|
||||||
async fn restore_cipher_put(cipher_id: CipherId, headers: Headers, mut conn: DbConn, nt: Notify<'_>) -> JsonResult {
|
async fn restore_cipher_put(cipher_id: CipherId, headers: Headers, mut conn: DbConn, nt: Notify<'_>) -> JsonResult {
|
||||||
_restore_cipher_by_uuid(&cipher_id, &headers, &mut conn, &nt).await
|
_restore_cipher_by_uuid(&cipher_id, &headers, false, &mut conn, &nt).await
|
||||||
}
|
}
|
||||||
|
|
||||||
#[put("/ciphers/<cipher_id>/restore-admin")]
|
#[put("/ciphers/<cipher_id>/restore-admin")]
|
||||||
@@ -1527,7 +1537,17 @@ async fn restore_cipher_put_admin(
|
|||||||
mut conn: DbConn,
|
mut conn: DbConn,
|
||||||
nt: Notify<'_>,
|
nt: Notify<'_>,
|
||||||
) -> JsonResult {
|
) -> JsonResult {
|
||||||
_restore_cipher_by_uuid(&cipher_id, &headers, &mut conn, &nt).await
|
_restore_cipher_by_uuid(&cipher_id, &headers, false, &mut conn, &nt).await
|
||||||
|
}
|
||||||
|
|
||||||
|
#[put("/ciphers/restore-admin", data = "<data>")]
|
||||||
|
async fn restore_cipher_selected_admin(
|
||||||
|
data: Json<CipherIdsData>,
|
||||||
|
headers: Headers,
|
||||||
|
mut conn: DbConn,
|
||||||
|
nt: Notify<'_>,
|
||||||
|
) -> JsonResult {
|
||||||
|
_restore_multiple_ciphers(data, &headers, &mut conn, &nt).await
|
||||||
}
|
}
|
||||||
|
|
||||||
#[put("/ciphers/restore", data = "<data>")]
|
#[put("/ciphers/restore", data = "<data>")]
|
||||||
@@ -1555,35 +1575,47 @@ async fn move_cipher_selected(
|
|||||||
nt: Notify<'_>,
|
nt: Notify<'_>,
|
||||||
) -> EmptyResult {
|
) -> EmptyResult {
|
||||||
let data = data.into_inner();
|
let data = data.into_inner();
|
||||||
let user_id = headers.user.uuid;
|
let user_id = &headers.user.uuid;
|
||||||
|
|
||||||
if let Some(ref folder_id) = data.folder_id {
|
if let Some(ref folder_id) = data.folder_id {
|
||||||
if Folder::find_by_uuid_and_user(folder_id, &user_id, &mut conn).await.is_none() {
|
if Folder::find_by_uuid_and_user(folder_id, user_id, &mut conn).await.is_none() {
|
||||||
err!("Invalid folder", "Folder does not exist or belongs to another user");
|
err!("Invalid folder", "Folder does not exist or belongs to another user");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for cipher_id in data.ids {
|
let cipher_count = data.ids.len();
|
||||||
let Some(cipher) = Cipher::find_by_uuid(&cipher_id, &mut conn).await else {
|
let mut single_cipher: Option<Cipher> = None;
|
||||||
err!("Cipher doesn't exist")
|
|
||||||
};
|
|
||||||
|
|
||||||
if !cipher.is_accessible_to_user(&user_id, &mut conn).await {
|
// TODO: Convert this to use a single query (or at least less) to update all items
|
||||||
err!("Cipher is not accessible by user")
|
// Find all ciphers a user has access to, all others will be ignored
|
||||||
|
let accessible_ciphers = Cipher::find_by_user_and_ciphers(user_id, &data.ids, &mut conn).await;
|
||||||
|
let accessible_ciphers_count = accessible_ciphers.len();
|
||||||
|
for cipher in accessible_ciphers {
|
||||||
|
cipher.move_to_folder(data.folder_id.clone(), user_id, &mut conn).await?;
|
||||||
|
if cipher_count == 1 {
|
||||||
|
single_cipher = Some(cipher);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Move cipher
|
if let Some(cipher) = single_cipher {
|
||||||
cipher.move_to_folder(data.folder_id.clone(), &user_id, &mut conn).await?;
|
|
||||||
|
|
||||||
nt.send_cipher_update(
|
nt.send_cipher_update(
|
||||||
UpdateType::SyncCipherUpdate,
|
UpdateType::SyncCipherUpdate,
|
||||||
&cipher,
|
&cipher,
|
||||||
std::slice::from_ref(&user_id),
|
std::slice::from_ref(user_id),
|
||||||
&headers.device,
|
&headers.device,
|
||||||
None,
|
None,
|
||||||
&mut conn,
|
&mut conn,
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
|
} else {
|
||||||
|
// Multi move actions do not send out a push for each cipher, we need to send a general sync here
|
||||||
|
nt.send_user_update(UpdateType::SyncCiphers, &headers.user, &headers.device.push_uuid, &mut conn).await;
|
||||||
|
}
|
||||||
|
|
||||||
|
if cipher_count != accessible_ciphers_count {
|
||||||
|
err!(format!(
|
||||||
|
"Not all ciphers are moved! {accessible_ciphers_count} of the selected {cipher_count} were moved."
|
||||||
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -1764,6 +1796,7 @@ async fn _delete_multiple_ciphers(
|
|||||||
async fn _restore_cipher_by_uuid(
|
async fn _restore_cipher_by_uuid(
|
||||||
cipher_id: &CipherId,
|
cipher_id: &CipherId,
|
||||||
headers: &Headers,
|
headers: &Headers,
|
||||||
|
multi_restore: bool,
|
||||||
conn: &mut DbConn,
|
conn: &mut DbConn,
|
||||||
nt: &Notify<'_>,
|
nt: &Notify<'_>,
|
||||||
) -> JsonResult {
|
) -> JsonResult {
|
||||||
@@ -1778,15 +1811,17 @@ async fn _restore_cipher_by_uuid(
|
|||||||
cipher.deleted_at = None;
|
cipher.deleted_at = None;
|
||||||
cipher.save(conn).await?;
|
cipher.save(conn).await?;
|
||||||
|
|
||||||
nt.send_cipher_update(
|
if !multi_restore {
|
||||||
UpdateType::SyncCipherUpdate,
|
nt.send_cipher_update(
|
||||||
&cipher,
|
UpdateType::SyncCipherUpdate,
|
||||||
&cipher.update_users_revision(conn).await,
|
&cipher,
|
||||||
&headers.device,
|
&cipher.update_users_revision(conn).await,
|
||||||
None,
|
&headers.device,
|
||||||
conn,
|
None,
|
||||||
)
|
conn,
|
||||||
.await;
|
)
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
|
||||||
if let Some(org_id) = &cipher.organization_uuid {
|
if let Some(org_id) = &cipher.organization_uuid {
|
||||||
log_event(
|
log_event(
|
||||||
@@ -1814,12 +1849,15 @@ async fn _restore_multiple_ciphers(
|
|||||||
|
|
||||||
let mut ciphers: Vec<Value> = Vec::new();
|
let mut ciphers: Vec<Value> = Vec::new();
|
||||||
for cipher_id in data.ids {
|
for cipher_id in data.ids {
|
||||||
match _restore_cipher_by_uuid(&cipher_id, headers, conn, nt).await {
|
match _restore_cipher_by_uuid(&cipher_id, headers, true, conn, nt).await {
|
||||||
Ok(json) => ciphers.push(json.into_inner()),
|
Ok(json) => ciphers.push(json.into_inner()),
|
||||||
err => return err,
|
err => return err,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Multi move actions do not send out a push for each cipher, we need to send a general sync here
|
||||||
|
nt.send_user_update(UpdateType::SyncCiphers, &headers.user, &headers.device.push_uuid, conn).await;
|
||||||
|
|
||||||
Ok(Json(json!({
|
Ok(Json(json!({
|
||||||
"data": ciphers,
|
"data": ciphers,
|
||||||
"object": "list",
|
"object": "list",
|
||||||
|
@@ -2063,12 +2063,12 @@ async fn list_policies_token(org_id: OrganizationId, token: &str, mut conn: DbCo
|
|||||||
async fn get_master_password_policy(org_id: OrganizationId, _headers: Headers, mut conn: DbConn) -> JsonResult {
|
async fn get_master_password_policy(org_id: OrganizationId, _headers: Headers, mut conn: DbConn) -> JsonResult {
|
||||||
let policy =
|
let policy =
|
||||||
OrgPolicy::find_by_org_and_type(&org_id, OrgPolicyType::MasterPassword, &mut conn).await.unwrap_or_else(|| {
|
OrgPolicy::find_by_org_and_type(&org_id, OrgPolicyType::MasterPassword, &mut conn).await.unwrap_or_else(|| {
|
||||||
let data = match CONFIG.sso_master_password_policy() {
|
let (enabled, data) = match CONFIG.sso_master_password_policy_value() {
|
||||||
Some(policy) => policy,
|
Some(policy) if CONFIG.sso_enabled() => (true, policy.to_string()),
|
||||||
None => "null".to_string(),
|
_ => (false, "null".to_string()),
|
||||||
};
|
};
|
||||||
|
|
||||||
OrgPolicy::new(org_id, OrgPolicyType::MasterPassword, CONFIG.sso_master_password_policy().is_some(), data)
|
OrgPolicy::new(org_id, OrgPolicyType::MasterPassword, enabled, data)
|
||||||
});
|
});
|
||||||
|
|
||||||
Ok(Json(policy.to_json()))
|
Ok(Json(policy.to_json()))
|
||||||
@@ -2310,7 +2310,7 @@ struct OrgImportData {
|
|||||||
users: Vec<OrgImportUserData>,
|
users: Vec<OrgImportUserData>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// This function seems to be deprected
|
/// This function seems to be deprecated
|
||||||
/// It is only used with older directory connectors
|
/// It is only used with older directory connectors
|
||||||
/// TODO: Cleanup Tech debt
|
/// TODO: Cleanup Tech debt
|
||||||
#[post("/organizations/<org_id>/import", data = "<data>")]
|
#[post("/organizations/<org_id>/import", data = "<data>")]
|
||||||
|
@@ -24,6 +24,7 @@ pub fn routes() -> Vec<Route> {
|
|||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
struct SendEmailLoginData {
|
struct SendEmailLoginData {
|
||||||
|
#[serde(alias = "DeviceIdentifier")]
|
||||||
device_identifier: DeviceId,
|
device_identifier: DeviceId,
|
||||||
|
|
||||||
#[allow(unused)]
|
#[allow(unused)]
|
||||||
|
@@ -4,6 +4,7 @@ use crate::{
|
|||||||
EmptyResult, JsonResult, PasswordOrOtpData,
|
EmptyResult, JsonResult, PasswordOrOtpData,
|
||||||
},
|
},
|
||||||
auth::Headers,
|
auth::Headers,
|
||||||
|
crypto::ct_eq,
|
||||||
db::{
|
db::{
|
||||||
models::{EventType, TwoFactor, TwoFactorType, UserId},
|
models::{EventType, TwoFactor, TwoFactorType, UserId},
|
||||||
DbConn,
|
DbConn,
|
||||||
@@ -16,19 +17,19 @@ use rocket::serde::json::Json;
|
|||||||
use rocket::Route;
|
use rocket::Route;
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
use std::sync::{Arc, LazyLock};
|
use std::sync::LazyLock;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
use url::Url;
|
use url::Url;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
use webauthn_rs::prelude::{Base64UrlSafeData, SecurityKey, SecurityKeyAuthentication, SecurityKeyRegistration};
|
use webauthn_rs::prelude::{Base64UrlSafeData, Credential, Passkey, PasskeyAuthentication, PasskeyRegistration};
|
||||||
use webauthn_rs::{Webauthn, WebauthnBuilder};
|
use webauthn_rs::{Webauthn, WebauthnBuilder};
|
||||||
use webauthn_rs_proto::{
|
use webauthn_rs_proto::{
|
||||||
AuthenticationExtensionsClientOutputs, AuthenticatorAssertionResponseRaw, AuthenticatorAttestationResponseRaw,
|
AuthenticationExtensionsClientOutputs, AuthenticatorAssertionResponseRaw, AuthenticatorAttestationResponseRaw,
|
||||||
PublicKeyCredential, RegisterPublicKeyCredential, RegistrationExtensionsClientOutputs,
|
PublicKeyCredential, RegisterPublicKeyCredential, RegistrationExtensionsClientOutputs,
|
||||||
RequestAuthenticationExtensions,
|
RequestAuthenticationExtensions, UserVerificationPolicy,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub static WEBAUTHN_2FA_CONFIG: LazyLock<Arc<Webauthn>> = LazyLock::new(|| {
|
static WEBAUTHN: LazyLock<Webauthn> = LazyLock::new(|| {
|
||||||
let domain = CONFIG.domain();
|
let domain = CONFIG.domain();
|
||||||
let domain_origin = CONFIG.domain_origin();
|
let domain_origin = CONFIG.domain_origin();
|
||||||
let rp_id = Url::parse(&domain).map(|u| u.domain().map(str::to_owned)).ok().flatten().unwrap_or_default();
|
let rp_id = Url::parse(&domain).map(|u| u.domain().map(str::to_owned)).ok().flatten().unwrap_or_default();
|
||||||
@@ -37,14 +38,11 @@ pub static WEBAUTHN_2FA_CONFIG: LazyLock<Arc<Webauthn>> = LazyLock::new(|| {
|
|||||||
let webauthn = WebauthnBuilder::new(&rp_id, &rp_origin)
|
let webauthn = WebauthnBuilder::new(&rp_id, &rp_origin)
|
||||||
.expect("Creating WebauthnBuilder failed")
|
.expect("Creating WebauthnBuilder failed")
|
||||||
.rp_name(&domain)
|
.rp_name(&domain)
|
||||||
.timeout(Duration::from_millis(60000))
|
.timeout(Duration::from_millis(60000));
|
||||||
.danger_set_user_presence_only_security_keys(true);
|
|
||||||
|
|
||||||
Arc::new(webauthn.build().expect("Building Webauthn failed"))
|
webauthn.build().expect("Building Webauthn failed")
|
||||||
});
|
});
|
||||||
|
|
||||||
pub type Webauthn2FaConfig<'a> = &'a rocket::State<Arc<Webauthn>>;
|
|
||||||
|
|
||||||
pub fn routes() -> Vec<Route> {
|
pub fn routes() -> Vec<Route> {
|
||||||
routes![get_webauthn, generate_webauthn_challenge, activate_webauthn, activate_webauthn_put, delete_webauthn,]
|
routes![get_webauthn, generate_webauthn_challenge, activate_webauthn, activate_webauthn_put, delete_webauthn,]
|
||||||
}
|
}
|
||||||
@@ -77,7 +75,7 @@ pub struct WebauthnRegistration {
|
|||||||
pub name: String,
|
pub name: String,
|
||||||
pub migrated: bool,
|
pub migrated: bool,
|
||||||
|
|
||||||
pub credential: SecurityKey,
|
pub credential: Passkey,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl WebauthnRegistration {
|
impl WebauthnRegistration {
|
||||||
@@ -88,6 +86,24 @@ impl WebauthnRegistration {
|
|||||||
"migrated": self.migrated,
|
"migrated": self.migrated,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn set_backup_eligible(&mut self, backup_eligible: bool, backup_state: bool) -> bool {
|
||||||
|
let mut changed = false;
|
||||||
|
let mut cred: Credential = self.credential.clone().into();
|
||||||
|
|
||||||
|
if cred.backup_state != backup_state {
|
||||||
|
cred.backup_state = backup_state;
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if backup_eligible && !cred.backup_eligible {
|
||||||
|
cred.backup_eligible = true;
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.credential = cred.into();
|
||||||
|
changed
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[post("/two-factor/get-webauthn", data = "<data>")]
|
#[post("/two-factor/get-webauthn", data = "<data>")]
|
||||||
@@ -112,12 +128,7 @@ async fn get_webauthn(data: Json<PasswordOrOtpData>, headers: Headers, mut conn:
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[post("/two-factor/get-webauthn-challenge", data = "<data>")]
|
#[post("/two-factor/get-webauthn-challenge", data = "<data>")]
|
||||||
async fn generate_webauthn_challenge(
|
async fn generate_webauthn_challenge(data: Json<PasswordOrOtpData>, headers: Headers, mut conn: DbConn) -> JsonResult {
|
||||||
data: Json<PasswordOrOtpData>,
|
|
||||||
headers: Headers,
|
|
||||||
webauthn: Webauthn2FaConfig<'_>,
|
|
||||||
mut conn: DbConn,
|
|
||||||
) -> JsonResult {
|
|
||||||
let data: PasswordOrOtpData = data.into_inner();
|
let data: PasswordOrOtpData = data.into_inner();
|
||||||
let user = headers.user;
|
let user = headers.user;
|
||||||
|
|
||||||
@@ -130,18 +141,27 @@ async fn generate_webauthn_challenge(
|
|||||||
.map(|r| r.credential.cred_id().to_owned()) // We return the credentialIds to the clients to avoid double registering
|
.map(|r| r.credential.cred_id().to_owned()) // We return the credentialIds to the clients to avoid double registering
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
let (challenge, state) = webauthn.start_securitykey_registration(
|
let (mut challenge, state) = WEBAUTHN.start_passkey_registration(
|
||||||
Uuid::from_str(&user.uuid).expect("Failed to parse UUID"), // Should never fail
|
Uuid::from_str(&user.uuid).expect("Failed to parse UUID"), // Should never fail
|
||||||
&user.email,
|
&user.email,
|
||||||
&user.name,
|
&user.name,
|
||||||
Some(registrations),
|
Some(registrations),
|
||||||
None,
|
|
||||||
None,
|
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
|
let mut state = serde_json::to_value(&state)?;
|
||||||
|
state["rs"]["policy"] = Value::String("discouraged".to_string());
|
||||||
|
state["rs"]["extensions"].as_object_mut().unwrap().clear();
|
||||||
|
|
||||||
let type_ = TwoFactorType::WebauthnRegisterChallenge;
|
let type_ = TwoFactorType::WebauthnRegisterChallenge;
|
||||||
TwoFactor::new(user.uuid.clone(), type_, serde_json::to_string(&state)?).save(&mut conn).await?;
|
TwoFactor::new(user.uuid.clone(), type_, serde_json::to_string(&state)?).save(&mut conn).await?;
|
||||||
|
|
||||||
|
// Because for this flow we abuse the passkeys as 2FA, and use it more like a securitykey
|
||||||
|
// we need to modify some of the default settings defined by `start_passkey_registration()`.
|
||||||
|
challenge.public_key.extensions = None;
|
||||||
|
if let Some(asc) = challenge.public_key.authenticator_selection.as_mut() {
|
||||||
|
asc.user_verification = UserVerificationPolicy::Discouraged_DO_NOT_USE;
|
||||||
|
}
|
||||||
|
|
||||||
let mut challenge_value = serde_json::to_value(challenge.public_key)?;
|
let mut challenge_value = serde_json::to_value(challenge.public_key)?;
|
||||||
challenge_value["status"] = "ok".into();
|
challenge_value["status"] = "ok".into();
|
||||||
challenge_value["errorMessage"] = "".into();
|
challenge_value["errorMessage"] = "".into();
|
||||||
@@ -232,12 +252,7 @@ impl From<PublicKeyCredentialCopy> for PublicKeyCredential {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[post("/two-factor/webauthn", data = "<data>")]
|
#[post("/two-factor/webauthn", data = "<data>")]
|
||||||
async fn activate_webauthn(
|
async fn activate_webauthn(data: Json<EnableWebauthnData>, headers: Headers, mut conn: DbConn) -> JsonResult {
|
||||||
data: Json<EnableWebauthnData>,
|
|
||||||
headers: Headers,
|
|
||||||
webauthn: Webauthn2FaConfig<'_>,
|
|
||||||
mut conn: DbConn,
|
|
||||||
) -> JsonResult {
|
|
||||||
let data: EnableWebauthnData = data.into_inner();
|
let data: EnableWebauthnData = data.into_inner();
|
||||||
let mut user = headers.user;
|
let mut user = headers.user;
|
||||||
|
|
||||||
@@ -252,7 +267,7 @@ async fn activate_webauthn(
|
|||||||
let type_ = TwoFactorType::WebauthnRegisterChallenge as i32;
|
let type_ = TwoFactorType::WebauthnRegisterChallenge as i32;
|
||||||
let state = match TwoFactor::find_by_user_and_type(&user.uuid, type_, &mut conn).await {
|
let state = match TwoFactor::find_by_user_and_type(&user.uuid, type_, &mut conn).await {
|
||||||
Some(tf) => {
|
Some(tf) => {
|
||||||
let state: SecurityKeyRegistration = serde_json::from_str(&tf.data)?;
|
let state: PasskeyRegistration = serde_json::from_str(&tf.data)?;
|
||||||
tf.delete(&mut conn).await?;
|
tf.delete(&mut conn).await?;
|
||||||
state
|
state
|
||||||
}
|
}
|
||||||
@@ -260,7 +275,7 @@ async fn activate_webauthn(
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Verify the credentials with the saved state
|
// Verify the credentials with the saved state
|
||||||
let credential = webauthn.finish_securitykey_registration(&data.device_response.into(), &state)?;
|
let credential = WEBAUTHN.finish_passkey_registration(&data.device_response.into(), &state)?;
|
||||||
|
|
||||||
let mut registrations: Vec<_> = get_webauthn_registrations(&user.uuid, &mut conn).await?.1;
|
let mut registrations: Vec<_> = get_webauthn_registrations(&user.uuid, &mut conn).await?.1;
|
||||||
// TODO: Check for repeated ID's
|
// TODO: Check for repeated ID's
|
||||||
@@ -289,13 +304,8 @@ async fn activate_webauthn(
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[put("/two-factor/webauthn", data = "<data>")]
|
#[put("/two-factor/webauthn", data = "<data>")]
|
||||||
async fn activate_webauthn_put(
|
async fn activate_webauthn_put(data: Json<EnableWebauthnData>, headers: Headers, conn: DbConn) -> JsonResult {
|
||||||
data: Json<EnableWebauthnData>,
|
activate_webauthn(data, headers, conn).await
|
||||||
headers: Headers,
|
|
||||||
webauthn: Webauthn2FaConfig<'_>,
|
|
||||||
conn: DbConn,
|
|
||||||
) -> JsonResult {
|
|
||||||
activate_webauthn(data, headers, webauthn, conn).await
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
@@ -365,27 +375,27 @@ pub async fn get_webauthn_registrations(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn generate_webauthn_login(
|
pub async fn generate_webauthn_login(user_id: &UserId, conn: &mut DbConn) -> JsonResult {
|
||||||
user_id: &UserId,
|
|
||||||
webauthn: Webauthn2FaConfig<'_>,
|
|
||||||
conn: &mut DbConn,
|
|
||||||
) -> JsonResult {
|
|
||||||
// Load saved credentials
|
// Load saved credentials
|
||||||
let creds: Vec<_> = get_webauthn_registrations(user_id, conn).await?.1.into_iter().map(|r| r.credential).collect();
|
let creds: Vec<Passkey> =
|
||||||
|
get_webauthn_registrations(user_id, conn).await?.1.into_iter().map(|r| r.credential).collect();
|
||||||
|
|
||||||
if creds.is_empty() {
|
if creds.is_empty() {
|
||||||
err!("No Webauthn devices registered")
|
err!("No Webauthn devices registered")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate a challenge based on the credentials
|
// Generate a challenge based on the credentials
|
||||||
let (mut response, state) = webauthn.start_securitykey_authentication(&creds)?;
|
let (mut response, state) = WEBAUTHN.start_passkey_authentication(&creds)?;
|
||||||
|
|
||||||
// Modify to discourage user verification
|
// Modify to discourage user verification
|
||||||
let mut state = serde_json::to_value(&state)?;
|
let mut state = serde_json::to_value(&state)?;
|
||||||
|
state["ast"]["policy"] = Value::String("discouraged".to_string());
|
||||||
|
|
||||||
// Add appid, this is only needed for U2F compatibility, so maybe it can be removed as well
|
// Add appid, this is only needed for U2F compatibility, so maybe it can be removed as well
|
||||||
let app_id = format!("{}/app-id.json", &CONFIG.domain());
|
let app_id = format!("{}/app-id.json", &CONFIG.domain());
|
||||||
state["ast"]["appid"] = Value::String(app_id.clone());
|
state["ast"]["appid"] = Value::String(app_id.clone());
|
||||||
|
|
||||||
|
response.public_key.user_verification = UserVerificationPolicy::Discouraged_DO_NOT_USE;
|
||||||
response
|
response
|
||||||
.public_key
|
.public_key
|
||||||
.extensions
|
.extensions
|
||||||
@@ -405,16 +415,11 @@ pub async fn generate_webauthn_login(
|
|||||||
Ok(Json(serde_json::to_value(response.public_key)?))
|
Ok(Json(serde_json::to_value(response.public_key)?))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn validate_webauthn_login(
|
pub async fn validate_webauthn_login(user_id: &UserId, response: &str, conn: &mut DbConn) -> EmptyResult {
|
||||||
user_id: &UserId,
|
|
||||||
response: &str,
|
|
||||||
webauthn: Webauthn2FaConfig<'_>,
|
|
||||||
conn: &mut DbConn,
|
|
||||||
) -> EmptyResult {
|
|
||||||
let type_ = TwoFactorType::WebauthnLoginChallenge as i32;
|
let type_ = TwoFactorType::WebauthnLoginChallenge as i32;
|
||||||
let state = match TwoFactor::find_by_user_and_type(user_id, type_, conn).await {
|
let mut state = match TwoFactor::find_by_user_and_type(user_id, type_, conn).await {
|
||||||
Some(tf) => {
|
Some(tf) => {
|
||||||
let state: SecurityKeyAuthentication = serde_json::from_str(&tf.data)?;
|
let state: PasskeyAuthentication = serde_json::from_str(&tf.data)?;
|
||||||
tf.delete(conn).await?;
|
tf.delete(conn).await?;
|
||||||
state
|
state
|
||||||
}
|
}
|
||||||
@@ -431,15 +436,22 @@ pub async fn validate_webauthn_login(
|
|||||||
|
|
||||||
let mut registrations = get_webauthn_registrations(user_id, conn).await?.1;
|
let mut registrations = get_webauthn_registrations(user_id, conn).await?.1;
|
||||||
|
|
||||||
let authentication_result = webauthn.finish_securitykey_authentication(&rsp, &state)?;
|
// We need to check for and update the backup_eligible flag when needed.
|
||||||
|
// Vaultwarden did not have knowledge of this flag prior to migrating to webauthn-rs v0.5.x
|
||||||
|
// Because of this we check the flag at runtime and update the registrations and state when needed
|
||||||
|
check_and_update_backup_eligible(user_id, &rsp, &mut registrations, &mut state, conn).await?;
|
||||||
|
|
||||||
|
let authentication_result = WEBAUTHN.finish_passkey_authentication(&rsp, &state)?;
|
||||||
|
|
||||||
for reg in &mut registrations {
|
for reg in &mut registrations {
|
||||||
if reg.credential.cred_id() == authentication_result.cred_id() && authentication_result.needs_update() {
|
if ct_eq(reg.credential.cred_id(), authentication_result.cred_id()) {
|
||||||
reg.credential.update_credential(&authentication_result);
|
// If the cred id matches and the credential is updated, Some(true) is returned
|
||||||
|
// In those cases, update the record, else leave it alone
|
||||||
TwoFactor::new(user_id.clone(), TwoFactorType::Webauthn, serde_json::to_string(®istrations)?)
|
if reg.credential.update_credential(&authentication_result) == Some(true) {
|
||||||
.save(conn)
|
TwoFactor::new(user_id.clone(), TwoFactorType::Webauthn, serde_json::to_string(®istrations)?)
|
||||||
.await?;
|
.save(conn)
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -451,3 +463,66 @@ pub async fn validate_webauthn_login(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn check_and_update_backup_eligible(
|
||||||
|
user_id: &UserId,
|
||||||
|
rsp: &PublicKeyCredential,
|
||||||
|
registrations: &mut Vec<WebauthnRegistration>,
|
||||||
|
state: &mut PasskeyAuthentication,
|
||||||
|
conn: &mut DbConn,
|
||||||
|
) -> EmptyResult {
|
||||||
|
// The feature flags from the response
|
||||||
|
// For details see: https://www.w3.org/TR/webauthn-3/#sctn-authenticator-data
|
||||||
|
const FLAG_BACKUP_ELIGIBLE: u8 = 0b0000_1000;
|
||||||
|
const FLAG_BACKUP_STATE: u8 = 0b0001_0000;
|
||||||
|
|
||||||
|
if let Some(bits) = rsp.response.authenticator_data.get(32) {
|
||||||
|
let backup_eligible = 0 != (bits & FLAG_BACKUP_ELIGIBLE);
|
||||||
|
let backup_state = 0 != (bits & FLAG_BACKUP_STATE);
|
||||||
|
|
||||||
|
// If the current key is backup eligible, then we probably need to update one of the keys already stored in the database
|
||||||
|
// This is needed because Vaultwarden didn't store this information when using the previous version of webauthn-rs since it was a new addition to the protocol
|
||||||
|
// Because we store multiple keys in one json string, we need to fetch the correct key first, and update its information before we let it verify
|
||||||
|
if backup_eligible {
|
||||||
|
let rsp_id = rsp.raw_id.as_slice();
|
||||||
|
for reg in &mut *registrations {
|
||||||
|
if ct_eq(reg.credential.cred_id().as_slice(), rsp_id) {
|
||||||
|
// Try to update the key, and if needed also update the database, before the actual state check is done
|
||||||
|
if reg.set_backup_eligible(backup_eligible, backup_state) {
|
||||||
|
TwoFactor::new(
|
||||||
|
user_id.clone(),
|
||||||
|
TwoFactorType::Webauthn,
|
||||||
|
serde_json::to_string(®istrations)?,
|
||||||
|
)
|
||||||
|
.save(conn)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
// We also need to adjust the current state which holds the challenge used to start the authentication verification
|
||||||
|
// Because Vaultwarden supports multiple keys, we need to loop through the deserialized state and check which key to update
|
||||||
|
let mut raw_state = serde_json::to_value(&state)?;
|
||||||
|
if let Some(credentials) = raw_state
|
||||||
|
.get_mut("ast")
|
||||||
|
.and_then(|v| v.get_mut("credentials"))
|
||||||
|
.and_then(|v| v.as_array_mut())
|
||||||
|
{
|
||||||
|
for cred in credentials.iter_mut() {
|
||||||
|
if cred.get("cred_id").is_some_and(|v| {
|
||||||
|
// Deserialize to a [u8] so it can be compared using `ct_eq` with the `rsp_id`
|
||||||
|
let cred_id_slice: Base64UrlSafeData = serde_json::from_value(v.clone()).unwrap();
|
||||||
|
ct_eq(cred_id_slice, rsp_id)
|
||||||
|
}) {
|
||||||
|
cred["backup_eligible"] = Value::Bool(backup_eligible);
|
||||||
|
cred["backup_state"] = Value::Bool(backup_state);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
*state = serde_json::from_value(raw_state)?;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
@@ -641,9 +641,9 @@ async fn stream_to_bytes_limit(res: Response, max_size: usize) -> Result<Bytes,
|
|||||||
let mut buf = BytesMut::new();
|
let mut buf = BytesMut::new();
|
||||||
let mut size = 0;
|
let mut size = 0;
|
||||||
while let Some(chunk) = stream.next().await {
|
while let Some(chunk) = stream.next().await {
|
||||||
// It is possible that there might occure UnexpectedEof errors or others
|
// It is possible that there might occur UnexpectedEof errors or others
|
||||||
// This is most of the time no issue, and if there is no chunked data anymore or at all parsing the HTML will not happen anyway.
|
// This is most of the time no issue, and if there is no chunked data anymore or at all parsing the HTML will not happen anyway.
|
||||||
// Therfore if chunk is an err, just break and continue with the data be have received.
|
// Therefore if chunk is an err, just break and continue with the data be have received.
|
||||||
if chunk.is_err() {
|
if chunk.is_err() {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
@@ -9,7 +9,6 @@ use rocket::{
|
|||||||
};
|
};
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
|
|
||||||
use crate::api::core::two_factor::webauthn::Webauthn2FaConfig;
|
|
||||||
use crate::{
|
use crate::{
|
||||||
api::{
|
api::{
|
||||||
core::{
|
core::{
|
||||||
@@ -49,7 +48,6 @@ async fn login(
|
|||||||
data: Form<ConnectData>,
|
data: Form<ConnectData>,
|
||||||
client_header: ClientHeaders,
|
client_header: ClientHeaders,
|
||||||
client_version: Option<ClientVersion>,
|
client_version: Option<ClientVersion>,
|
||||||
webauthn: Webauthn2FaConfig<'_>,
|
|
||||||
mut conn: DbConn,
|
mut conn: DbConn,
|
||||||
) -> JsonResult {
|
) -> JsonResult {
|
||||||
let data: ConnectData = data.into_inner();
|
let data: ConnectData = data.into_inner();
|
||||||
@@ -72,7 +70,7 @@ async fn login(
|
|||||||
_check_is_some(&data.device_name, "device_name cannot be blank")?;
|
_check_is_some(&data.device_name, "device_name cannot be blank")?;
|
||||||
_check_is_some(&data.device_type, "device_type cannot be blank")?;
|
_check_is_some(&data.device_type, "device_type cannot be blank")?;
|
||||||
|
|
||||||
_password_login(data, &mut user_id, &mut conn, &client_header.ip, &client_version, webauthn).await
|
_password_login(data, &mut user_id, &mut conn, &client_header.ip, &client_version).await
|
||||||
}
|
}
|
||||||
"client_credentials" => {
|
"client_credentials" => {
|
||||||
_check_is_some(&data.client_id, "client_id cannot be blank")?;
|
_check_is_some(&data.client_id, "client_id cannot be blank")?;
|
||||||
@@ -93,7 +91,7 @@ async fn login(
|
|||||||
_check_is_some(&data.device_name, "device_name cannot be blank")?;
|
_check_is_some(&data.device_name, "device_name cannot be blank")?;
|
||||||
_check_is_some(&data.device_type, "device_type cannot be blank")?;
|
_check_is_some(&data.device_type, "device_type cannot be blank")?;
|
||||||
|
|
||||||
_sso_login(data, &mut user_id, &mut conn, &client_header.ip, &client_version, webauthn).await
|
_sso_login(data, &mut user_id, &mut conn, &client_header.ip, &client_version).await
|
||||||
}
|
}
|
||||||
"authorization_code" => err!("SSO sign-in is not available"),
|
"authorization_code" => err!("SSO sign-in is not available"),
|
||||||
t => err!("Invalid type", t),
|
t => err!("Invalid type", t),
|
||||||
@@ -171,7 +169,6 @@ async fn _sso_login(
|
|||||||
conn: &mut DbConn,
|
conn: &mut DbConn,
|
||||||
ip: &ClientIp,
|
ip: &ClientIp,
|
||||||
client_version: &Option<ClientVersion>,
|
client_version: &Option<ClientVersion>,
|
||||||
webauthn: Webauthn2FaConfig<'_>,
|
|
||||||
) -> JsonResult {
|
) -> JsonResult {
|
||||||
AuthMethod::Sso.check_scope(data.scope.as_ref())?;
|
AuthMethod::Sso.check_scope(data.scope.as_ref())?;
|
||||||
|
|
||||||
@@ -270,7 +267,7 @@ async fn _sso_login(
|
|||||||
}
|
}
|
||||||
Some((mut user, sso_user)) => {
|
Some((mut user, sso_user)) => {
|
||||||
let mut device = get_device(&data, conn, &user).await?;
|
let mut device = get_device(&data, conn, &user).await?;
|
||||||
let twofactor_token = twofactor_auth(&user, &data, &mut device, ip, client_version, webauthn, conn).await?;
|
let twofactor_token = twofactor_auth(&mut user, &data, &mut device, ip, client_version, conn).await?;
|
||||||
|
|
||||||
if user.private_key.is_none() {
|
if user.private_key.is_none() {
|
||||||
// User was invited a stub was created
|
// User was invited a stub was created
|
||||||
@@ -293,7 +290,7 @@ async fn _sso_login(
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// We passed 2FA get full user informations
|
// We passed 2FA get full user information
|
||||||
let auth_user = sso::redeem(&user_infos.state, conn).await?;
|
let auth_user = sso::redeem(&user_infos.state, conn).await?;
|
||||||
|
|
||||||
if sso_user.is_none() {
|
if sso_user.is_none() {
|
||||||
@@ -325,7 +322,6 @@ async fn _password_login(
|
|||||||
conn: &mut DbConn,
|
conn: &mut DbConn,
|
||||||
ip: &ClientIp,
|
ip: &ClientIp,
|
||||||
client_version: &Option<ClientVersion>,
|
client_version: &Option<ClientVersion>,
|
||||||
webauthn: Webauthn2FaConfig<'_>,
|
|
||||||
) -> JsonResult {
|
) -> JsonResult {
|
||||||
// Validate scope
|
// Validate scope
|
||||||
AuthMethod::Password.check_scope(data.scope.as_ref())?;
|
AuthMethod::Password.check_scope(data.scope.as_ref())?;
|
||||||
@@ -435,14 +431,13 @@ async fn _password_login(
|
|||||||
|
|
||||||
let mut device = get_device(&data, conn, &user).await?;
|
let mut device = get_device(&data, conn, &user).await?;
|
||||||
|
|
||||||
let twofactor_token = twofactor_auth(&user, &data, &mut device, ip, client_version, webauthn, conn).await?;
|
let twofactor_token = twofactor_auth(&mut user, &data, &mut device, ip, client_version, conn).await?;
|
||||||
|
|
||||||
let auth_tokens = auth::AuthTokens::new(&device, &user, AuthMethod::Password, data.client_id);
|
let auth_tokens = auth::AuthTokens::new(&device, &user, AuthMethod::Password, data.client_id);
|
||||||
|
|
||||||
authenticated_response(&user, &mut device, auth_tokens, twofactor_token, &now, conn, ip).await
|
authenticated_response(&user, &mut device, auth_tokens, twofactor_token, &now, conn, ip).await
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(clippy::too_many_arguments)]
|
|
||||||
async fn authenticated_response(
|
async fn authenticated_response(
|
||||||
user: &User,
|
user: &User,
|
||||||
device: &mut Device,
|
device: &mut Device,
|
||||||
@@ -663,12 +658,11 @@ async fn get_device(data: &ConnectData, conn: &mut DbConn, user: &User) -> ApiRe
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn twofactor_auth(
|
async fn twofactor_auth(
|
||||||
user: &User,
|
user: &mut User,
|
||||||
data: &ConnectData,
|
data: &ConnectData,
|
||||||
device: &mut Device,
|
device: &mut Device,
|
||||||
ip: &ClientIp,
|
ip: &ClientIp,
|
||||||
client_version: &Option<ClientVersion>,
|
client_version: &Option<ClientVersion>,
|
||||||
webauthn: Webauthn2FaConfig<'_>,
|
|
||||||
conn: &mut DbConn,
|
conn: &mut DbConn,
|
||||||
) -> ApiResult<Option<String>> {
|
) -> ApiResult<Option<String>> {
|
||||||
let twofactors = TwoFactor::find_by_user(&user.uuid, conn).await;
|
let twofactors = TwoFactor::find_by_user(&user.uuid, conn).await;
|
||||||
@@ -688,7 +682,7 @@ async fn twofactor_auth(
|
|||||||
Some(ref code) => code,
|
Some(ref code) => code,
|
||||||
None => {
|
None => {
|
||||||
err_json!(
|
err_json!(
|
||||||
_json_err_twofactor(&twofactor_ids, &user.uuid, data, client_version, webauthn, conn).await?,
|
_json_err_twofactor(&twofactor_ids, &user.uuid, data, client_version, conn).await?,
|
||||||
"2FA token not provided"
|
"2FA token not provided"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -705,9 +699,7 @@ async fn twofactor_auth(
|
|||||||
Some(TwoFactorType::Authenticator) => {
|
Some(TwoFactorType::Authenticator) => {
|
||||||
authenticator::validate_totp_code_str(&user.uuid, twofactor_code, &selected_data?, ip, conn).await?
|
authenticator::validate_totp_code_str(&user.uuid, twofactor_code, &selected_data?, ip, conn).await?
|
||||||
}
|
}
|
||||||
Some(TwoFactorType::Webauthn) => {
|
Some(TwoFactorType::Webauthn) => webauthn::validate_webauthn_login(&user.uuid, twofactor_code, conn).await?,
|
||||||
webauthn::validate_webauthn_login(&user.uuid, twofactor_code, webauthn, conn).await?
|
|
||||||
}
|
|
||||||
Some(TwoFactorType::YubiKey) => yubikey::validate_yubikey_login(twofactor_code, &selected_data?).await?,
|
Some(TwoFactorType::YubiKey) => yubikey::validate_yubikey_login(twofactor_code, &selected_data?).await?,
|
||||||
Some(TwoFactorType::Duo) => {
|
Some(TwoFactorType::Duo) => {
|
||||||
match CONFIG.duo_use_iframe() {
|
match CONFIG.duo_use_iframe() {
|
||||||
@@ -731,7 +723,6 @@ async fn twofactor_auth(
|
|||||||
Some(TwoFactorType::Email) => {
|
Some(TwoFactorType::Email) => {
|
||||||
email::validate_email_code_str(&user.uuid, twofactor_code, &selected_data?, &ip.ip, conn).await?
|
email::validate_email_code_str(&user.uuid, twofactor_code, &selected_data?, &ip.ip, conn).await?
|
||||||
}
|
}
|
||||||
|
|
||||||
Some(TwoFactorType::Remember) => {
|
Some(TwoFactorType::Remember) => {
|
||||||
match device.twofactor_remember {
|
match device.twofactor_remember {
|
||||||
Some(ref code) if !CONFIG.disable_2fa_remember() && ct_eq(code, twofactor_code) => {
|
Some(ref code) if !CONFIG.disable_2fa_remember() && ct_eq(code, twofactor_code) => {
|
||||||
@@ -739,12 +730,28 @@ async fn twofactor_auth(
|
|||||||
}
|
}
|
||||||
_ => {
|
_ => {
|
||||||
err_json!(
|
err_json!(
|
||||||
_json_err_twofactor(&twofactor_ids, &user.uuid, data, client_version, webauthn, conn).await?,
|
_json_err_twofactor(&twofactor_ids, &user.uuid, data, client_version, conn).await?,
|
||||||
"2FA Remember token not provided"
|
"2FA Remember token not provided"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Some(TwoFactorType::RecoveryCode) => {
|
||||||
|
// Check if recovery code is correct
|
||||||
|
if !user.check_valid_recovery_code(twofactor_code) {
|
||||||
|
err!("Recovery code is incorrect. Try again.")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove all twofactors from the user
|
||||||
|
TwoFactor::delete_all_by_user(&user.uuid, conn).await?;
|
||||||
|
enforce_2fa_policy(user, &user.uuid, device.atype, &ip.ip, conn).await?;
|
||||||
|
|
||||||
|
log_user_event(EventType::UserRecovered2fa as i32, &user.uuid, device.atype, &ip.ip, conn).await;
|
||||||
|
|
||||||
|
// Remove the recovery code, not needed without twofactors
|
||||||
|
user.totp_recover = None;
|
||||||
|
user.save(conn).await?;
|
||||||
|
}
|
||||||
_ => err!(
|
_ => err!(
|
||||||
"Invalid two factor provider",
|
"Invalid two factor provider",
|
||||||
ErrorEvent {
|
ErrorEvent {
|
||||||
@@ -773,7 +780,6 @@ async fn _json_err_twofactor(
|
|||||||
user_id: &UserId,
|
user_id: &UserId,
|
||||||
data: &ConnectData,
|
data: &ConnectData,
|
||||||
client_version: &Option<ClientVersion>,
|
client_version: &Option<ClientVersion>,
|
||||||
webauthn: Webauthn2FaConfig<'_>,
|
|
||||||
conn: &mut DbConn,
|
conn: &mut DbConn,
|
||||||
) -> ApiResult<Value> {
|
) -> ApiResult<Value> {
|
||||||
let mut result = json!({
|
let mut result = json!({
|
||||||
@@ -793,7 +799,7 @@ async fn _json_err_twofactor(
|
|||||||
Some(TwoFactorType::Authenticator) => { /* Nothing to do for TOTP */ }
|
Some(TwoFactorType::Authenticator) => { /* Nothing to do for TOTP */ }
|
||||||
|
|
||||||
Some(TwoFactorType::Webauthn) if CONFIG.domain_set() => {
|
Some(TwoFactorType::Webauthn) if CONFIG.domain_set() => {
|
||||||
let request = webauthn::generate_webauthn_login(user_id, webauthn, conn).await?;
|
let request = webauthn::generate_webauthn_login(user_id, conn).await?;
|
||||||
result["TwoFactorProviders2"][provider.to_string()] = request.0;
|
result["TwoFactorProviders2"][provider.to_string()] = request.0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1060,12 +1066,12 @@ async fn oidcsignin_redirect(
|
|||||||
wrapper: impl FnOnce(OIDCState) -> sso::OIDCCodeWrapper,
|
wrapper: impl FnOnce(OIDCState) -> sso::OIDCCodeWrapper,
|
||||||
conn: &DbConn,
|
conn: &DbConn,
|
||||||
) -> ApiResult<Redirect> {
|
) -> ApiResult<Redirect> {
|
||||||
let state = sso::deocde_state(base64_state)?;
|
let state = sso::decode_state(base64_state)?;
|
||||||
let code = sso::encode_code_claims(wrapper(state.clone()));
|
let code = sso::encode_code_claims(wrapper(state.clone()));
|
||||||
|
|
||||||
let nonce = match SsoNonce::find(&state, conn).await {
|
let nonce = match SsoNonce::find(&state, conn).await {
|
||||||
Some(n) => n,
|
Some(n) => n,
|
||||||
None => err!(format!("Failed to retrive redirect_uri with {state}")),
|
None => err!(format!("Failed to retrieve redirect_uri with {state}")),
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut url = match url::Url::parse(&nonce.redirect_uri) {
|
let mut url = match url::Url::parse(&nonce.redirect_uri) {
|
||||||
|
@@ -110,8 +110,8 @@ async fn master_password_policy(user: &User, conn: &DbConn) -> Value {
|
|||||||
enforce_on_login: acc.enforce_on_login || policy.enforce_on_login,
|
enforce_on_login: acc.enforce_on_login || policy.enforce_on_login,
|
||||||
}
|
}
|
||||||
}))
|
}))
|
||||||
} else if let Some(policy_str) = CONFIG.sso_master_password_policy().filter(|_| CONFIG.sso_enabled()) {
|
} else if CONFIG.sso_enabled() {
|
||||||
serde_json::from_str(&policy_str).unwrap_or(json!({}))
|
CONFIG.sso_master_password_policy_value().unwrap_or(json!({}))
|
||||||
} else {
|
} else {
|
||||||
json!({})
|
json!({})
|
||||||
};
|
};
|
||||||
|
@@ -61,9 +61,10 @@ fn vaultwarden_css() -> Cached<Css<String>> {
|
|||||||
"mail_enabled": CONFIG.mail_enabled(),
|
"mail_enabled": CONFIG.mail_enabled(),
|
||||||
"sends_allowed": CONFIG.sends_allowed(),
|
"sends_allowed": CONFIG.sends_allowed(),
|
||||||
"signup_disabled": CONFIG.is_signup_disabled(),
|
"signup_disabled": CONFIG.is_signup_disabled(),
|
||||||
"sso_disabled": !CONFIG.sso_enabled(),
|
"sso_enabled": CONFIG.sso_enabled(),
|
||||||
"sso_only": CONFIG.sso_enabled() && CONFIG.sso_only(),
|
"sso_only": CONFIG.sso_enabled() && CONFIG.sso_only(),
|
||||||
"yubico_enabled": CONFIG._enable_yubico() && CONFIG.yubico_client_id().is_some() && CONFIG.yubico_secret_key().is_some(),
|
"yubico_enabled": CONFIG._enable_yubico() && CONFIG.yubico_client_id().is_some() && CONFIG.yubico_secret_key().is_some(),
|
||||||
|
"webauthn_2fa_supported": CONFIG.is_webauthn_2fa_supported(),
|
||||||
});
|
});
|
||||||
|
|
||||||
let scss = match CONFIG.render_template("scss/vaultwarden.scss", &css_options) {
|
let scss = match CONFIG.render_template("scss/vaultwarden.scss", &css_options) {
|
||||||
|
@@ -1174,7 +1174,7 @@ impl AuthTokens {
|
|||||||
|
|
||||||
let access_claims = LoginJwtClaims::default(device, user, &sub, client_id);
|
let access_claims = LoginJwtClaims::default(device, user, &sub, client_id);
|
||||||
|
|
||||||
let validity = if DeviceType::is_mobile(&device.atype) {
|
let validity = if device.is_mobile() {
|
||||||
*MOBILE_REFRESH_VALIDITY
|
*MOBILE_REFRESH_VALIDITY
|
||||||
} else {
|
} else {
|
||||||
*DEFAULT_REFRESH_VALIDITY
|
*DEFAULT_REFRESH_VALIDITY
|
||||||
|
@@ -283,6 +283,9 @@ macro_rules! make_config {
|
|||||||
"smtp_host",
|
"smtp_host",
|
||||||
"smtp_username",
|
"smtp_username",
|
||||||
"_smtp_img_src",
|
"_smtp_img_src",
|
||||||
|
"sso_client_id",
|
||||||
|
"sso_authority",
|
||||||
|
"sso_callback_path",
|
||||||
];
|
];
|
||||||
|
|
||||||
let cfg = {
|
let cfg = {
|
||||||
@@ -636,9 +639,15 @@ make_config! {
|
|||||||
/// Timeout when acquiring database connection
|
/// Timeout when acquiring database connection
|
||||||
database_timeout: u64, false, def, 30;
|
database_timeout: u64, false, def, 30;
|
||||||
|
|
||||||
/// Database connection pool size
|
/// Timeout in seconds before idle connections to the database are closed
|
||||||
|
database_idle_timeout: u64, false, def, 600;
|
||||||
|
|
||||||
|
/// Database connection max pool size
|
||||||
database_max_conns: u32, false, def, 10;
|
database_max_conns: u32, false, def, 10;
|
||||||
|
|
||||||
|
/// Database connection min pool size
|
||||||
|
database_min_conns: u32, false, def, 2;
|
||||||
|
|
||||||
/// Database connection init |> SQL statements to run when creating a new database connection, mainly useful for connection-scoped pragmas. If empty, a database-specific default is used.
|
/// Database connection init |> SQL statements to run when creating a new database connection, mainly useful for connection-scoped pragmas. If empty, a database-specific default is used.
|
||||||
database_conn_init: String, false, def, String::new();
|
database_conn_init: String, false, def, String::new();
|
||||||
|
|
||||||
@@ -688,7 +697,7 @@ make_config! {
|
|||||||
/// Allow email association |> Associate existing non-SSO user based on email
|
/// Allow email association |> Associate existing non-SSO user based on email
|
||||||
sso_signups_match_email: bool, true, def, true;
|
sso_signups_match_email: bool, true, def, true;
|
||||||
/// Allow unknown email verification status |> Allowing this with `SSO_SIGNUPS_MATCH_EMAIL=true` open potential account takeover.
|
/// Allow unknown email verification status |> Allowing this with `SSO_SIGNUPS_MATCH_EMAIL=true` open potential account takeover.
|
||||||
sso_allow_unknown_email_verification: bool, false, def, false;
|
sso_allow_unknown_email_verification: bool, true, def, false;
|
||||||
/// Client ID
|
/// Client ID
|
||||||
sso_client_id: String, true, def, String::new();
|
sso_client_id: String, true, def, String::new();
|
||||||
/// Client Key
|
/// Client Key
|
||||||
@@ -709,7 +718,7 @@ make_config! {
|
|||||||
sso_master_password_policy: String, true, option;
|
sso_master_password_policy: String, true, option;
|
||||||
/// Use SSO only for auth not the session lifecycle |> Use default Vaultwarden session lifecycle (Idle refresh token valid for 30days)
|
/// Use SSO only for auth not the session lifecycle |> Use default Vaultwarden session lifecycle (Idle refresh token valid for 30days)
|
||||||
sso_auth_only_not_session: bool, true, def, false;
|
sso_auth_only_not_session: bool, true, def, false;
|
||||||
/// Client cache for discovery endpoint. |> Duration in seconds (0 or less to disable). More details: https://github.com/dani-garcia/vaultwarden/blob/sso-support/SSO.md#client-cache
|
/// Client cache for discovery endpoint. |> Duration in seconds (0 or less to disable). More details: https://github.com/dani-garcia/vaultwarden/wiki/Enabling-SSO-support-using-OpenId-Connect#client-cache
|
||||||
sso_client_cache_expiration: u64, true, def, 0;
|
sso_client_cache_expiration: u64, true, def, 0;
|
||||||
/// Log all tokens |> `LOG_LEVEL=debug` or `LOG_LEVEL=info,vaultwarden::sso=debug` is required
|
/// Log all tokens |> `LOG_LEVEL=debug` or `LOG_LEVEL=info,vaultwarden::sso=debug` is required
|
||||||
sso_debug_tokens: bool, true, def, false;
|
sso_debug_tokens: bool, true, def, false;
|
||||||
@@ -773,7 +782,7 @@ make_config! {
|
|||||||
smtp_auth_mechanism: String, true, option;
|
smtp_auth_mechanism: String, true, option;
|
||||||
/// SMTP connection timeout |> Number of seconds when to stop trying to connect to the SMTP server
|
/// SMTP connection timeout |> Number of seconds when to stop trying to connect to the SMTP server
|
||||||
smtp_timeout: u64, true, def, 15;
|
smtp_timeout: u64, true, def, 15;
|
||||||
/// Server name sent during HELO |> By default this value should be is on the machine's hostname, but might need to be changed in case it trips some anti-spam filters
|
/// Server name sent during HELO |> By default this value should be the machine's hostname, but might need to be changed in case it trips some anti-spam filters
|
||||||
helo_name: String, true, option;
|
helo_name: String, true, option;
|
||||||
/// Embed images as email attachments.
|
/// Embed images as email attachments.
|
||||||
smtp_embed_images: bool, true, def, true;
|
smtp_embed_images: bool, true, def, true;
|
||||||
@@ -825,6 +834,14 @@ fn validate_config(cfg: &ConfigItems) -> Result<(), Error> {
|
|||||||
err!(format!("`DATABASE_MAX_CONNS` contains an invalid value. Ensure it is between 1 and {limit}.",));
|
err!(format!("`DATABASE_MAX_CONNS` contains an invalid value. Ensure it is between 1 and {limit}.",));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if cfg.database_min_conns < 1 || cfg.database_min_conns > limit {
|
||||||
|
err!(format!("`DATABASE_MIN_CONNS` contains an invalid value. Ensure it is between 1 and {limit}.",));
|
||||||
|
}
|
||||||
|
|
||||||
|
if cfg.database_min_conns > cfg.database_max_conns {
|
||||||
|
err!(format!("`DATABASE_MIN_CONNS` must be smaller than or equal to `DATABASE_MAX_CONNS`.",));
|
||||||
|
}
|
||||||
|
|
||||||
if let Some(log_file) = &cfg.log_file {
|
if let Some(log_file) = &cfg.log_file {
|
||||||
if std::fs::OpenOptions::new().append(true).create(true).open(log_file).is_err() {
|
if std::fs::OpenOptions::new().append(true).create(true).open(log_file).is_err() {
|
||||||
err!("Unable to write to log file", log_file);
|
err!("Unable to write to log file", log_file);
|
||||||
@@ -957,7 +974,7 @@ fn validate_config(cfg: &ConfigItems) -> Result<(), Error> {
|
|||||||
|
|
||||||
validate_internal_sso_issuer_url(&cfg.sso_authority)?;
|
validate_internal_sso_issuer_url(&cfg.sso_authority)?;
|
||||||
validate_internal_sso_redirect_url(&cfg.sso_callback_path)?;
|
validate_internal_sso_redirect_url(&cfg.sso_callback_path)?;
|
||||||
check_master_password_policy(&cfg.sso_master_password_policy)?;
|
validate_sso_master_password_policy(&cfg.sso_master_password_policy)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
if cfg._enable_yubico {
|
if cfg._enable_yubico {
|
||||||
@@ -1139,7 +1156,7 @@ fn validate_config(cfg: &ConfigItems) -> Result<(), Error> {
|
|||||||
|
|
||||||
fn validate_internal_sso_issuer_url(sso_authority: &String) -> Result<openidconnect::IssuerUrl, Error> {
|
fn validate_internal_sso_issuer_url(sso_authority: &String) -> Result<openidconnect::IssuerUrl, Error> {
|
||||||
match openidconnect::IssuerUrl::new(sso_authority.clone()) {
|
match openidconnect::IssuerUrl::new(sso_authority.clone()) {
|
||||||
Err(err) => err!(format!("Invalid sso_authority UR ({sso_authority}): {err}")),
|
Err(err) => err!(format!("Invalid sso_authority URL ({sso_authority}): {err}")),
|
||||||
Ok(issuer_url) => Ok(issuer_url),
|
Ok(issuer_url) => Ok(issuer_url),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1151,12 +1168,19 @@ fn validate_internal_sso_redirect_url(sso_callback_path: &String) -> Result<open
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn check_master_password_policy(sso_master_password_policy: &Option<String>) -> Result<(), Error> {
|
fn validate_sso_master_password_policy(
|
||||||
|
sso_master_password_policy: &Option<String>,
|
||||||
|
) -> Result<Option<serde_json::Value>, Error> {
|
||||||
let policy = sso_master_password_policy.as_ref().map(|mpp| serde_json::from_str::<serde_json::Value>(mpp));
|
let policy = sso_master_password_policy.as_ref().map(|mpp| serde_json::from_str::<serde_json::Value>(mpp));
|
||||||
if let Some(Err(error)) = policy {
|
|
||||||
err!(format!("Invalid sso_master_password_policy ({error}), Ensure that it's correctly escaped with ''"))
|
match policy {
|
||||||
|
None => Ok(None),
|
||||||
|
Some(Ok(jsobject @ serde_json::Value::Object(_))) => Ok(Some(jsobject)),
|
||||||
|
Some(Ok(_)) => err!("Invalid sso_master_password_policy: parsed value is not a JSON object"),
|
||||||
|
Some(Err(error)) => {
|
||||||
|
err!(format!("Invalid sso_master_password_policy ({error}), Ensure that it's correctly escaped with ''"))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Extracts an RFC 6454 web origin from a URL.
|
/// Extracts an RFC 6454 web origin from a URL.
|
||||||
@@ -1501,6 +1525,10 @@ impl Config {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn is_webauthn_2fa_supported(&self) -> bool {
|
||||||
|
Url::parse(&self.domain()).expect("DOMAIN not a valid URL").domain().is_some()
|
||||||
|
}
|
||||||
|
|
||||||
/// Tests whether the admin token is set to a non-empty value.
|
/// Tests whether the admin token is set to a non-empty value.
|
||||||
pub fn is_admin_token_set(&self) -> bool {
|
pub fn is_admin_token_set(&self) -> bool {
|
||||||
let token = self.admin_token();
|
let token = self.admin_token();
|
||||||
@@ -1561,6 +1589,10 @@ impl Config {
|
|||||||
validate_internal_sso_redirect_url(&self.sso_callback_path())
|
validate_internal_sso_redirect_url(&self.sso_callback_path())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn sso_master_password_policy_value(&self) -> Option<serde_json::Value> {
|
||||||
|
validate_sso_master_password_policy(&self.sso_master_password_policy()).ok().flatten()
|
||||||
|
}
|
||||||
|
|
||||||
pub fn sso_scopes_vec(&self) -> Vec<String> {
|
pub fn sso_scopes_vec(&self) -> Vec<String> {
|
||||||
self.sso_scopes().split_whitespace().map(str::to_string).collect()
|
self.sso_scopes().split_whitespace().map(str::to_string).collect()
|
||||||
}
|
}
|
||||||
|
@@ -134,6 +134,8 @@ macro_rules! generate_connections {
|
|||||||
let manager = ConnectionManager::new(&url);
|
let manager = ConnectionManager::new(&url);
|
||||||
let pool = Pool::builder()
|
let pool = Pool::builder()
|
||||||
.max_size(CONFIG.database_max_conns())
|
.max_size(CONFIG.database_max_conns())
|
||||||
|
.min_idle(Some(CONFIG.database_min_conns()))
|
||||||
|
.idle_timeout(Some(Duration::from_secs(CONFIG.database_idle_timeout())))
|
||||||
.connection_timeout(Duration::from_secs(CONFIG.database_timeout()))
|
.connection_timeout(Duration::from_secs(CONFIG.database_timeout()))
|
||||||
.connection_customizer(Box::new(DbConnOptions{
|
.connection_customizer(Box::new(DbConnOptions{
|
||||||
init_stmts: conn_type.get_init_stmts()
|
init_stmts: conn_type.get_init_stmts()
|
||||||
|
@@ -6,7 +6,7 @@ use macros::UuidFromParam;
|
|||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
|
|
||||||
db_object! {
|
db_object! {
|
||||||
#[derive(Debug, Identifiable, Queryable, Insertable, AsChangeset, Deserialize, Serialize)]
|
#[derive(Identifiable, Queryable, Insertable, AsChangeset, Deserialize, Serialize)]
|
||||||
#[diesel(table_name = auth_requests)]
|
#[diesel(table_name = auth_requests)]
|
||||||
#[diesel(treat_none_as_null = true)]
|
#[diesel(treat_none_as_null = true)]
|
||||||
#[diesel(primary_key(uuid))]
|
#[diesel(primary_key(uuid))]
|
||||||
|
@@ -783,7 +783,12 @@ impl Cipher {
|
|||||||
// true, then the non-interesting ciphers will not be returned. As a
|
// true, then the non-interesting ciphers will not be returned. As a
|
||||||
// result, those ciphers will not appear in "My Vault" for the org
|
// result, those ciphers will not appear in "My Vault" for the org
|
||||||
// owner/admin, but they can still be accessed via the org vault view.
|
// owner/admin, but they can still be accessed via the org vault view.
|
||||||
pub async fn find_by_user(user_uuid: &UserId, visible_only: bool, conn: &mut DbConn) -> Vec<Self> {
|
pub async fn find_by_user(
|
||||||
|
user_uuid: &UserId,
|
||||||
|
visible_only: bool,
|
||||||
|
cipher_uuids: &Vec<CipherId>,
|
||||||
|
conn: &mut DbConn,
|
||||||
|
) -> Vec<Self> {
|
||||||
if CONFIG.org_groups_enabled() {
|
if CONFIG.org_groups_enabled() {
|
||||||
db_run! {conn: {
|
db_run! {conn: {
|
||||||
let mut query = ciphers::table
|
let mut query = ciphers::table
|
||||||
@@ -821,7 +826,14 @@ impl Cipher {
|
|||||||
if !visible_only {
|
if !visible_only {
|
||||||
query = query.or_filter(
|
query = query.or_filter(
|
||||||
users_organizations::atype.le(MembershipType::Admin as i32) // Org admin/owner
|
users_organizations::atype.le(MembershipType::Admin as i32) // Org admin/owner
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only filter for one specific cipher
|
||||||
|
if !cipher_uuids.is_empty() {
|
||||||
|
query = query.filter(
|
||||||
|
ciphers::uuid.eq_any(cipher_uuids)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
query
|
query
|
||||||
@@ -850,11 +862,18 @@ impl Cipher {
|
|||||||
.or_filter(users_collections::user_uuid.eq(user_uuid)) // Access to collection
|
.or_filter(users_collections::user_uuid.eq(user_uuid)) // Access to collection
|
||||||
.into_boxed();
|
.into_boxed();
|
||||||
|
|
||||||
if !visible_only {
|
if !visible_only {
|
||||||
query = query.or_filter(
|
query = query.or_filter(
|
||||||
users_organizations::atype.le(MembershipType::Admin as i32) // Org admin/owner
|
users_organizations::atype.le(MembershipType::Admin as i32) // Org admin/owner
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Only filter for one specific cipher
|
||||||
|
if !cipher_uuids.is_empty() {
|
||||||
|
query = query.filter(
|
||||||
|
ciphers::uuid.eq_any(cipher_uuids)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
query
|
query
|
||||||
.select(ciphers::all_columns)
|
.select(ciphers::all_columns)
|
||||||
@@ -866,7 +885,23 @@ impl Cipher {
|
|||||||
|
|
||||||
// Find all ciphers visible to the specified user.
|
// Find all ciphers visible to the specified user.
|
||||||
pub async fn find_by_user_visible(user_uuid: &UserId, conn: &mut DbConn) -> Vec<Self> {
|
pub async fn find_by_user_visible(user_uuid: &UserId, conn: &mut DbConn) -> Vec<Self> {
|
||||||
Self::find_by_user(user_uuid, true, conn).await
|
Self::find_by_user(user_uuid, true, &vec![], conn).await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn find_by_user_and_ciphers(
|
||||||
|
user_uuid: &UserId,
|
||||||
|
cipher_uuids: &Vec<CipherId>,
|
||||||
|
conn: &mut DbConn,
|
||||||
|
) -> Vec<Self> {
|
||||||
|
Self::find_by_user(user_uuid, true, cipher_uuids, conn).await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn find_by_user_and_cipher(
|
||||||
|
user_uuid: &UserId,
|
||||||
|
cipher_uuid: &CipherId,
|
||||||
|
conn: &mut DbConn,
|
||||||
|
) -> Option<Self> {
|
||||||
|
Self::find_by_user(user_uuid, true, &vec![cipher_uuid.clone()], conn).await.pop()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find all ciphers directly owned by the specified user.
|
// Find all ciphers directly owned by the specified user.
|
||||||
|
@@ -70,6 +70,10 @@ impl Device {
|
|||||||
pub fn is_cli(&self) -> bool {
|
pub fn is_cli(&self) -> bool {
|
||||||
matches!(DeviceType::from_i32(self.atype), DeviceType::WindowsCLI | DeviceType::MacOsCLI | DeviceType::LinuxCLI)
|
matches!(DeviceType::from_i32(self.atype), DeviceType::WindowsCLI | DeviceType::MacOsCLI | DeviceType::LinuxCLI)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn is_mobile(&self) -> bool {
|
||||||
|
matches!(DeviceType::from_i32(self.atype), DeviceType::Android | DeviceType::Ios)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct DeviceWithAuthRequest {
|
pub struct DeviceWithAuthRequest {
|
||||||
@@ -353,10 +357,6 @@ impl DeviceType {
|
|||||||
_ => DeviceType::UnknownBrowser,
|
_ => DeviceType::UnknownBrowser,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn is_mobile(value: &i32) -> bool {
|
|
||||||
*value == DeviceType::Android as i32 || *value == DeviceType::Ios as i32
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(
|
#[derive(
|
||||||
|
@@ -135,7 +135,7 @@ impl CollectionGroup {
|
|||||||
// If both read_only and hide_passwords are false, then manage should be true
|
// If both read_only and hide_passwords are false, then manage should be true
|
||||||
// You can't have an entry with read_only and manage, or hide_passwords and manage
|
// You can't have an entry with read_only and manage, or hide_passwords and manage
|
||||||
// Or an entry with everything to false
|
// Or an entry with everything to false
|
||||||
// For backwards compaibility and migration proposes we keep checking read_only and hide_password
|
// For backwards compatibility and migration proposes we keep checking read_only and hide_password
|
||||||
json!({
|
json!({
|
||||||
"id": self.groups_uuid,
|
"id": self.groups_uuid,
|
||||||
"readOnly": self.read_only,
|
"readOnly": self.read_only,
|
||||||
|
@@ -31,6 +31,7 @@ pub enum TwoFactorType {
|
|||||||
Remember = 5,
|
Remember = 5,
|
||||||
OrganizationDuo = 6,
|
OrganizationDuo = 6,
|
||||||
Webauthn = 7,
|
Webauthn = 7,
|
||||||
|
RecoveryCode = 8,
|
||||||
|
|
||||||
// These are implementation details
|
// These are implementation details
|
||||||
U2fRegisterChallenge = 1000,
|
U2fRegisterChallenge = 1000,
|
||||||
|
@@ -61,7 +61,6 @@ mod sso_client;
|
|||||||
mod util;
|
mod util;
|
||||||
|
|
||||||
use crate::api::core::two_factor::duo_oidc::purge_duo_contexts;
|
use crate::api::core::two_factor::duo_oidc::purge_duo_contexts;
|
||||||
use crate::api::core::two_factor::webauthn::WEBAUTHN_2FA_CONFIG;
|
|
||||||
use crate::api::purge_auth_requests;
|
use crate::api::purge_auth_requests;
|
||||||
use crate::api::{WS_ANONYMOUS_SUBSCRIPTIONS, WS_USERS};
|
use crate::api::{WS_ANONYMOUS_SUBSCRIPTIONS, WS_USERS};
|
||||||
pub use config::{PathType, CONFIG};
|
pub use config::{PathType, CONFIG};
|
||||||
@@ -601,7 +600,6 @@ async fn launch_rocket(pool: db::DbPool, extra_debug: bool) -> Result<(), Error>
|
|||||||
.manage(pool)
|
.manage(pool)
|
||||||
.manage(Arc::clone(&WS_USERS))
|
.manage(Arc::clone(&WS_USERS))
|
||||||
.manage(Arc::clone(&WS_ANONYMOUS_SUBSCRIPTIONS))
|
.manage(Arc::clone(&WS_ANONYMOUS_SUBSCRIPTIONS))
|
||||||
.manage(Arc::clone(&WEBAUTHN_2FA_CONFIG))
|
|
||||||
.attach(util::AppHeaders())
|
.attach(util::AppHeaders())
|
||||||
.attach(util::Cors())
|
.attach(util::Cors())
|
||||||
.attach(util::BetterLogging(extra_debug))
|
.attach(util::BetterLogging(extra_debug))
|
||||||
|
@@ -151,7 +151,7 @@ fn decode_token_claims(token_name: &str, token: &str) -> ApiResult<BasicTokenCla
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn deocde_state(base64_state: String) -> ApiResult<OIDCState> {
|
pub fn decode_state(base64_state: String) -> ApiResult<OIDCState> {
|
||||||
let state = match data_encoding::BASE64.decode(base64_state.as_bytes()) {
|
let state = match data_encoding::BASE64.decode(base64_state.as_bytes()) {
|
||||||
Ok(vec) => match String::from_utf8(vec) {
|
Ok(vec) => match String::from_utf8(vec) {
|
||||||
Ok(valid) => OIDCState(valid),
|
Ok(valid) => OIDCState(valid),
|
||||||
@@ -316,7 +316,7 @@ pub async fn exchange_code(wrapped_code: &str, conn: &mut DbConn) -> ApiResult<U
|
|||||||
user_name: user_name.clone(),
|
user_name: user_name.clone(),
|
||||||
};
|
};
|
||||||
|
|
||||||
debug!("Authentified user {authenticated_user:?}");
|
debug!("Authenticated user {authenticated_user:?}");
|
||||||
|
|
||||||
AC_CACHE.insert(state.clone(), authenticated_user);
|
AC_CACHE.insert(state.clone(), authenticated_user);
|
||||||
|
|
||||||
@@ -443,7 +443,7 @@ pub async fn exchange_refresh_token(
|
|||||||
err_silent!("Access token is close to expiration but we have no refresh token")
|
err_silent!("Access token is close to expiration but we have no refresh token")
|
||||||
}
|
}
|
||||||
|
|
||||||
Client::check_validaty(access_token.clone()).await?;
|
Client::check_validity(access_token.clone()).await?;
|
||||||
|
|
||||||
let access_claims = auth::LoginJwtClaims::new(
|
let access_claims = auth::LoginJwtClaims::new(
|
||||||
device,
|
device,
|
||||||
|
@@ -203,7 +203,7 @@ impl Client {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn check_validaty(access_token: String) -> EmptyResult {
|
pub async fn check_validity(access_token: String) -> EmptyResult {
|
||||||
let client = Client::cached().await?;
|
let client = Client::cached().await?;
|
||||||
match client.user_info(AccessToken::new(access_token)).await {
|
match client.user_info(AccessToken::new(access_token)).await {
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
|
@@ -21,21 +21,21 @@ a[href$="/settings/sponsored-families"] {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* Hide the sso `Email` input field */
|
/* Hide the sso `Email` input field */
|
||||||
{{#if sso_disabled}}
|
{{#if (not sso_enabled)}}
|
||||||
.vw-email-sso {
|
.vw-email-sso {
|
||||||
@extend %vw-hide;
|
@extend %vw-hide;
|
||||||
}
|
}
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
|
||||||
/* Hide the default/continue `Email` input field */
|
/* Hide the default/continue `Email` input field */
|
||||||
{{#if (not sso_disabled)}}
|
{{#if sso_enabled}}
|
||||||
.vw-email-continue {
|
.vw-email-continue {
|
||||||
@extend %vw-hide;
|
@extend %vw-hide;
|
||||||
}
|
}
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
|
||||||
/* Hide the `Continue` button on the login page */
|
/* Hide the `Continue` button on the login page */
|
||||||
{{#if (not sso_disabled)}}
|
{{#if sso_enabled}}
|
||||||
.vw-continue-login {
|
.vw-continue-login {
|
||||||
@extend %vw-hide;
|
@extend %vw-hide;
|
||||||
}
|
}
|
||||||
@@ -43,7 +43,7 @@ a[href$="/settings/sponsored-families"] {
|
|||||||
|
|
||||||
/* Hide the `Enterprise Single Sign-On` button on the login page */
|
/* Hide the `Enterprise Single Sign-On` button on the login page */
|
||||||
{{#if (webver ">=2025.5.1")}}
|
{{#if (webver ">=2025.5.1")}}
|
||||||
{{#if sso_disabled}}
|
{{#if (not sso_enabled)}}
|
||||||
.vw-sso-login {
|
.vw-sso-login {
|
||||||
@extend %vw-hide;
|
@extend %vw-hide;
|
||||||
}
|
}
|
||||||
@@ -55,7 +55,7 @@ app-root ng-component > form > div:nth-child(1) > div > button[buttontype="secon
|
|||||||
{{/if}}
|
{{/if}}
|
||||||
|
|
||||||
/* Hide the `Log in with passkey` settings */
|
/* Hide the `Log in with passkey` settings */
|
||||||
app-change-password app-webauthn-login-settings {
|
app-user-layout app-password-settings app-webauthn-login-settings {
|
||||||
@extend %vw-hide;
|
@extend %vw-hide;
|
||||||
}
|
}
|
||||||
/* Hide Log in with passkey on the login page */
|
/* Hide Log in with passkey on the login page */
|
||||||
@@ -71,7 +71,7 @@ app-root ng-component > form > div:nth-child(1) > div > button[buttontype="secon
|
|||||||
|
|
||||||
/* Hide the or text followed by the two buttons hidden above */
|
/* Hide the or text followed by the two buttons hidden above */
|
||||||
{{#if (webver ">=2025.5.1")}}
|
{{#if (webver ">=2025.5.1")}}
|
||||||
{{#if (or sso_disabled sso_only)}}
|
{{#if (or (not sso_enabled) sso_only)}}
|
||||||
.vw-or-text {
|
.vw-or-text {
|
||||||
@extend %vw-hide;
|
@extend %vw-hide;
|
||||||
}
|
}
|
||||||
@@ -83,7 +83,7 @@ app-root ng-component > form > div:nth-child(1) > div:nth-child(3) > div:nth-chi
|
|||||||
{{/if}}
|
{{/if}}
|
||||||
|
|
||||||
/* Hide the `Other` button on the login page */
|
/* Hide the `Other` button on the login page */
|
||||||
{{#if (or sso_disabled sso_only)}}
|
{{#if (or (not sso_enabled) sso_only)}}
|
||||||
.vw-other-login {
|
.vw-other-login {
|
||||||
@extend %vw-hide;
|
@extend %vw-hide;
|
||||||
}
|
}
|
||||||
@@ -133,6 +133,10 @@ bit-nav-logo bit-nav-item a:before {
|
|||||||
bit-nav-logo bit-nav-item .bwi-shield {
|
bit-nav-logo bit-nav-item .bwi-shield {
|
||||||
@extend %vw-hide;
|
@extend %vw-hide;
|
||||||
}
|
}
|
||||||
|
/* Hide Device Login Protection button on user settings page */
|
||||||
|
app-user-layout app-danger-zone button:nth-child(1) {
|
||||||
|
@extend %vw-hide;
|
||||||
|
}
|
||||||
/**** END Static Vaultwarden Changes ****/
|
/**** END Static Vaultwarden Changes ****/
|
||||||
/**** START Dynamic Vaultwarden Changes ****/
|
/**** START Dynamic Vaultwarden Changes ****/
|
||||||
{{#if signup_disabled}}
|
{{#if signup_disabled}}
|
||||||
@@ -168,6 +172,13 @@ app-root a[routerlink="/signup"] {
|
|||||||
}
|
}
|
||||||
{{/unless}}
|
{{/unless}}
|
||||||
|
|
||||||
|
{{#unless webauthn_2fa_supported}}
|
||||||
|
/* Hide `Passkey` 2FA if it is not supported */
|
||||||
|
.providers-2fa-7 {
|
||||||
|
@extend %vw-hide;
|
||||||
|
}
|
||||||
|
{{/unless}}
|
||||||
|
|
||||||
{{#unless emergency_access_allowed}}
|
{{#unless emergency_access_allowed}}
|
||||||
/* Hide Emergency Access if not allowed */
|
/* Hide Emergency Access if not allowed */
|
||||||
bit-nav-item[route="settings/emergency-access"] {
|
bit-nav-item[route="settings/emergency-access"] {
|
||||||
|
Reference in New Issue
Block a user