mirror of
https://github.com/dani-garcia/vaultwarden.git
synced 2025-09-11 03:05:58 +03:00
Compare commits
18 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
45e5f06b86 | ||
|
620ad92331 | ||
|
c9860af11c | ||
|
d7adce97df | ||
|
71b3d3c818 | ||
|
da3701c0cf | ||
|
96813b1317 | ||
|
b0b953f348 | ||
|
cdfdc6ff4f | ||
|
2393c3f3c0 | ||
|
0d16b38a68 | ||
|
ff33534c07 | ||
|
adb21d5c1a | ||
|
e927b8aa5e | ||
|
ba48ca68fc | ||
|
294b429436 | ||
|
37c14c3c69 | ||
|
d0581da638 |
@@ -280,12 +280,13 @@
|
|||||||
## The default for new users. If changed, it will be updated during login for existing users.
|
## The default for new users. If changed, it will be updated during login for existing users.
|
||||||
# PASSWORD_ITERATIONS=600000
|
# PASSWORD_ITERATIONS=600000
|
||||||
|
|
||||||
## Controls whether users can set password hints. This setting applies globally to all users.
|
## Controls whether users can set or show password hints. This setting applies globally to all users.
|
||||||
# PASSWORD_HINTS_ALLOWED=true
|
# PASSWORD_HINTS_ALLOWED=true
|
||||||
|
|
||||||
## Controls whether a password hint should be shown directly in the web page if
|
## Controls whether a password hint should be shown directly in the web page if
|
||||||
## SMTP service is not configured. Not recommended for publicly-accessible instances
|
## SMTP service is not configured and password hints are allowed.
|
||||||
## as this provides unauthenticated access to potentially sensitive data.
|
## Not recommended for publicly-accessible instances because this provides
|
||||||
|
## unauthenticated access to potentially sensitive data.
|
||||||
# SHOW_PASSWORD_HINT=false
|
# SHOW_PASSWORD_HINT=false
|
||||||
|
|
||||||
#########################
|
#########################
|
||||||
@@ -349,6 +350,8 @@
|
|||||||
## - "browser-fileless-import": Directly import credentials from other providers without a file.
|
## - "browser-fileless-import": Directly import credentials from other providers without a file.
|
||||||
## - "extension-refresh": Temporarily enable the new extension design until general availability (should be used with the beta Chrome extension)
|
## - "extension-refresh": Temporarily enable the new extension design until general availability (should be used with the beta Chrome extension)
|
||||||
## - "fido2-vault-credentials": Enable the use of FIDO2 security keys as second factor.
|
## - "fido2-vault-credentials": Enable the use of FIDO2 security keys as second factor.
|
||||||
|
## - "ssh-key-vault-item": Enable the creation and use of SSH key vault items. (Needs clients >=2024.12.0)
|
||||||
|
## - "ssh-agent": Enable SSH agent support on Desktop. (Needs desktop >=2024.12.0)
|
||||||
# EXPERIMENTAL_CLIENT_FEATURE_FLAGS=fido2-vault-credentials
|
# EXPERIMENTAL_CLIENT_FEATURE_FLAGS=fido2-vault-credentials
|
||||||
|
|
||||||
## Require new device emails. When a user logs in an email is required to be sent.
|
## Require new device emails. When a user logs in an email is required to be sent.
|
||||||
|
15
.github/workflows/build.yml
vendored
15
.github/workflows/build.yml
vendored
@@ -47,7 +47,7 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
# Checkout the repo
|
# Checkout the repo
|
||||||
- name: "Checkout"
|
- name: "Checkout"
|
||||||
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 #v4.2.1
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 #v4.2.2
|
||||||
# End Checkout the repo
|
# End Checkout the repo
|
||||||
|
|
||||||
|
|
||||||
@@ -75,7 +75,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@7b1c307e0dcbda6122208f10795a713336a9b35a # master @ Aug 8, 2024, 7:36 PM GMT+2
|
uses: dtolnay/rust-toolchain@315e265cd78dad1e1dcf3a5074f6d6c47029d5aa # master @ Nov 18, 2024, 5:36 AM GMT+1
|
||||||
if: ${{ matrix.channel == 'rust-toolchain' }}
|
if: ${{ matrix.channel == 'rust-toolchain' }}
|
||||||
with:
|
with:
|
||||||
toolchain: "${{steps.toolchain.outputs.RUST_TOOLCHAIN}}"
|
toolchain: "${{steps.toolchain.outputs.RUST_TOOLCHAIN}}"
|
||||||
@@ -85,7 +85,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@7b1c307e0dcbda6122208f10795a713336a9b35a # master @ Aug 8, 2024, 7:36 PM GMT+2
|
uses: dtolnay/rust-toolchain@315e265cd78dad1e1dcf3a5074f6d6c47029d5aa # master @ Nov 18, 2024, 5:36 AM GMT+1
|
||||||
if: ${{ matrix.channel != 'rust-toolchain' }}
|
if: ${{ matrix.channel != 'rust-toolchain' }}
|
||||||
with:
|
with:
|
||||||
toolchain: "${{steps.toolchain.outputs.RUST_TOOLCHAIN}}"
|
toolchain: "${{steps.toolchain.outputs.RUST_TOOLCHAIN}}"
|
||||||
@@ -107,7 +107,7 @@ jobs:
|
|||||||
# End Show environment
|
# End Show environment
|
||||||
|
|
||||||
# Enable Rust Caching
|
# Enable Rust Caching
|
||||||
- uses: Swatinem/rust-cache@23bce251a8cd2ffc3c1075eaa2367cf899916d84 # v2.7.3
|
- uses: Swatinem/rust-cache@82a92a6e8fbeee089604da2575dc567ae9ddeaab # v2.7.5
|
||||||
with:
|
with:
|
||||||
# Use a custom prefix-key to force a fresh start. This is sometimes needed with bigger changes.
|
# Use a custom prefix-key to force a fresh start. This is sometimes needed with bigger changes.
|
||||||
# Like changing the build host from Ubuntu 20.04 to 22.04 for example.
|
# Like changing the build host from Ubuntu 20.04 to 22.04 for example.
|
||||||
@@ -117,6 +117,12 @@ jobs:
|
|||||||
|
|
||||||
# Run cargo tests
|
# Run cargo tests
|
||||||
# First test all features together, afterwards test them separately.
|
# First test all features together, afterwards test them separately.
|
||||||
|
- name: "test features: sqlite,mysql,postgresql,enable_mimalloc,query_logger"
|
||||||
|
id: test_sqlite_mysql_postgresql_mimalloc_logger
|
||||||
|
if: $${{ always() }}
|
||||||
|
run: |
|
||||||
|
cargo test --features sqlite,mysql,postgresql,enable_mimalloc,query_logger
|
||||||
|
|
||||||
- name: "test features: sqlite,mysql,postgresql,enable_mimalloc"
|
- name: "test features: sqlite,mysql,postgresql,enable_mimalloc"
|
||||||
id: test_sqlite_mysql_postgresql_mimalloc
|
id: test_sqlite_mysql_postgresql_mimalloc
|
||||||
if: $${{ always() }}
|
if: $${{ always() }}
|
||||||
@@ -176,6 +182,7 @@ jobs:
|
|||||||
echo "" >> $GITHUB_STEP_SUMMARY
|
echo "" >> $GITHUB_STEP_SUMMARY
|
||||||
echo "|Job|Status|" >> $GITHUB_STEP_SUMMARY
|
echo "|Job|Status|" >> $GITHUB_STEP_SUMMARY
|
||||||
echo "|---|------|" >> $GITHUB_STEP_SUMMARY
|
echo "|---|------|" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "|test (sqlite,mysql,postgresql,enable_mimalloc,query_logger)|${{ steps.test_sqlite_mysql_postgresql_mimalloc_logger.outcome }}|" >> $GITHUB_STEP_SUMMARY
|
||||||
echo "|test (sqlite,mysql,postgresql,enable_mimalloc)|${{ steps.test_sqlite_mysql_postgresql_mimalloc.outcome }}|" >> $GITHUB_STEP_SUMMARY
|
echo "|test (sqlite,mysql,postgresql,enable_mimalloc)|${{ steps.test_sqlite_mysql_postgresql_mimalloc.outcome }}|" >> $GITHUB_STEP_SUMMARY
|
||||||
echo "|test (sqlite,mysql,postgresql)|${{ steps.test_sqlite_mysql_postgresql.outcome }}|" >> $GITHUB_STEP_SUMMARY
|
echo "|test (sqlite,mysql,postgresql)|${{ steps.test_sqlite_mysql_postgresql.outcome }}|" >> $GITHUB_STEP_SUMMARY
|
||||||
echo "|test (sqlite)|${{ steps.test_sqlite.outcome }}|" >> $GITHUB_STEP_SUMMARY
|
echo "|test (sqlite)|${{ steps.test_sqlite.outcome }}|" >> $GITHUB_STEP_SUMMARY
|
||||||
|
2
.github/workflows/hadolint.yml
vendored
2
.github/workflows/hadolint.yml
vendored
@@ -13,7 +13,7 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
# Checkout the repo
|
# Checkout the repo
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 #v4.2.1
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 #v4.2.2
|
||||||
# End Checkout the repo
|
# End Checkout the repo
|
||||||
|
|
||||||
# Start Docker Buildx
|
# Start Docker Buildx
|
||||||
|
2
.github/workflows/release.yml
vendored
2
.github/workflows/release.yml
vendored
@@ -58,7 +58,7 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
# Checkout the repo
|
# Checkout the repo
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 #v4.2.1
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 #v4.2.2
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
|
9
.github/workflows/trivy.yml
vendored
9
.github/workflows/trivy.yml
vendored
@@ -28,10 +28,13 @@ jobs:
|
|||||||
actions: read
|
actions: read
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 #v4.2.1
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 #v4.2.2
|
||||||
|
|
||||||
- name: Run Trivy vulnerability scanner
|
- name: Run Trivy vulnerability scanner
|
||||||
uses: aquasecurity/trivy-action@5681af892cd0f4997658e2bacc62bd0a894cf564 # v0.27.0
|
uses: aquasecurity/trivy-action@18f2510ee396bbf400402947b394f2dd8c87dbb0 # v0.29.0
|
||||||
|
env:
|
||||||
|
TRIVY_DB_REPOSITORY: docker.io/aquasec/trivy-db:2,public.ecr.aws/aquasecurity/trivy-db:2,ghcr.io/aquasecurity/trivy-db:2
|
||||||
|
TRIVY_JAVA_DB_REPOSITORY: docker.io/aquasec/trivy-java-db:1,public.ecr.aws/aquasecurity/trivy-java-db:1,ghcr.io/aquasecurity/trivy-java-db:1
|
||||||
with:
|
with:
|
||||||
scan-type: repo
|
scan-type: repo
|
||||||
ignore-unfixed: true
|
ignore-unfixed: true
|
||||||
@@ -40,6 +43,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@2bbafcdd7fbf96243689e764c2f15d9735164f33 # v3.26.6
|
uses: github/codeql-action/upload-sarif@86b04fb0e47484f7282357688f21d5d0e32175fe # v3.27.5
|
||||||
with:
|
with:
|
||||||
sarif_file: 'trivy-results.sarif'
|
sarif_file: 'trivy-results.sarif'
|
||||||
|
614
Cargo.lock
generated
614
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
51
Cargo.toml
51
Cargo.toml
@@ -3,7 +3,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.80.0"
|
rust-version = "1.82.0"
|
||||||
resolver = "2"
|
resolver = "2"
|
||||||
|
|
||||||
repository = "https://github.com/dani-garcia/vaultwarden"
|
repository = "https://github.com/dani-garcia/vaultwarden"
|
||||||
@@ -36,13 +36,13 @@ unstable = []
|
|||||||
|
|
||||||
[target."cfg(unix)".dependencies]
|
[target."cfg(unix)".dependencies]
|
||||||
# Logging
|
# Logging
|
||||||
syslog = "6.1.1"
|
syslog = "7.0.0"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
# Logging
|
# Logging
|
||||||
log = "0.4.22"
|
log = "0.4.22"
|
||||||
fern = { version = "0.7.0", features = ["syslog-6", "reopen-1"] }
|
fern = { version = "0.7.0", features = ["syslog-7", "reopen-1"] }
|
||||||
tracing = { version = "0.1.40", features = ["log"] } # Needed to have lettre and webauthn-rs trace logging to work
|
tracing = { version = "0.1.41", features = ["log"] } # Needed to have lettre and webauthn-rs trace logging to work
|
||||||
|
|
||||||
# A `dotenv` implementation for Rust
|
# A `dotenv` implementation for Rust
|
||||||
dotenvy = { version = "0.15.7", default-features = false }
|
dotenvy = { version = "0.15.7", default-features = false }
|
||||||
@@ -53,7 +53,7 @@ once_cell = "1.20.2"
|
|||||||
# Numerical libraries
|
# Numerical libraries
|
||||||
num-traits = "0.2.19"
|
num-traits = "0.2.19"
|
||||||
num-derive = "0.4.2"
|
num-derive = "0.4.2"
|
||||||
bigdecimal = "0.4.6"
|
bigdecimal = "0.4.7"
|
||||||
|
|
||||||
# Web framework
|
# Web framework
|
||||||
rocket = { version = "0.5.1", features = ["tls", "json"], default-features = false }
|
rocket = { version = "0.5.1", features = ["tls", "json"], default-features = false }
|
||||||
@@ -67,16 +67,16 @@ dashmap = "6.1.0"
|
|||||||
|
|
||||||
# Async futures
|
# Async futures
|
||||||
futures = "0.3.31"
|
futures = "0.3.31"
|
||||||
tokio = { version = "1.41.1", features = ["rt-multi-thread", "fs", "io-util", "parking_lot", "time", "signal", "net"] }
|
tokio = { version = "1.42.0", features = ["rt-multi-thread", "fs", "io-util", "parking_lot", "time", "signal", "net"] }
|
||||||
|
|
||||||
# A generic serialization/deserialization framework
|
# A generic serialization/deserialization framework
|
||||||
serde = { version = "1.0.214", features = ["derive"] }
|
serde = { version = "1.0.215", features = ["derive"] }
|
||||||
serde_json = "1.0.132"
|
serde_json = "1.0.133"
|
||||||
|
|
||||||
# A safe, extensible ORM and Query builder
|
# A safe, extensible ORM and Query builder
|
||||||
diesel = { version = "2.2.4", features = ["chrono", "r2d2", "numeric"] }
|
diesel = { version = "2.2.6", features = ["chrono", "r2d2", "numeric"] }
|
||||||
diesel_migrations = "2.2.0"
|
diesel_migrations = "2.2.0"
|
||||||
diesel_logger = { version = "0.3.0", optional = true }
|
diesel_logger = { version = "0.4.0", optional = true }
|
||||||
|
|
||||||
# Bundled/Static SQLite
|
# Bundled/Static SQLite
|
||||||
libsqlite3-sys = { version = "0.30.1", features = ["bundled"], optional = true }
|
libsqlite3-sys = { version = "0.30.1", features = ["bundled"], optional = true }
|
||||||
@@ -89,9 +89,9 @@ ring = "0.17.8"
|
|||||||
uuid = { version = "1.11.0", features = ["v4"] }
|
uuid = { version = "1.11.0", features = ["v4"] }
|
||||||
|
|
||||||
# Date and time libraries
|
# Date and time libraries
|
||||||
chrono = { version = "0.4.38", features = ["clock", "serde"], default-features = false }
|
chrono = { version = "0.4.39", features = ["clock", "serde"], default-features = false }
|
||||||
chrono-tz = "0.10.0"
|
chrono-tz = "0.10.0"
|
||||||
time = "0.3.36"
|
time = "0.3.37"
|
||||||
|
|
||||||
# Job scheduler
|
# Job scheduler
|
||||||
job_scheduler_ng = "2.0.5"
|
job_scheduler_ng = "2.0.5"
|
||||||
@@ -106,16 +106,16 @@ jsonwebtoken = "9.3.0"
|
|||||||
totp-lite = "2.0.1"
|
totp-lite = "2.0.1"
|
||||||
|
|
||||||
# Yubico Library
|
# Yubico Library
|
||||||
yubico = { version = "0.11.0", features = ["online-tokio"], default-features = false }
|
yubico = { version = "0.12.0", features = ["online-tokio"], default-features = false }
|
||||||
|
|
||||||
# WebAuthn libraries
|
# WebAuthn libraries
|
||||||
webauthn-rs = "0.3.2"
|
webauthn-rs = "0.3.2"
|
||||||
|
|
||||||
# Handling of URL's for WebAuthn and favicons
|
# Handling of URL's for WebAuthn and favicons
|
||||||
url = "2.5.3"
|
url = "2.5.4"
|
||||||
|
|
||||||
# Email libraries
|
# Email libraries
|
||||||
lettre = { version = "0.11.10", features = ["smtp-transport", "sendmail-transport", "builder", "serde", "tokio1-native-tls", "hostname", "tracing", "tokio1"], default-features = false }
|
lettre = { version = "0.11.11", features = ["smtp-transport", "sendmail-transport", "builder", "serde", "tokio1-native-tls", "hostname", "tracing", "tokio1"], default-features = false }
|
||||||
percent-encoding = "2.3.1" # URL encoding library used for URL's in the emails
|
percent-encoding = "2.3.1" # URL encoding library used for URL's in the emails
|
||||||
email_address = "0.2.9"
|
email_address = "0.2.9"
|
||||||
|
|
||||||
@@ -124,13 +124,13 @@ handlebars = { version = "6.2.0", 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.9", features = ["native-tls-alpn", "stream", "json", "gzip", "brotli", "socks", "cookies"] }
|
reqwest = { version = "0.12.9", features = ["native-tls-alpn", "stream", "json", "gzip", "brotli", "socks", "cookies"] }
|
||||||
hickory-resolver = "0.24.1"
|
hickory-resolver = "0.24.2"
|
||||||
|
|
||||||
# Favicon extraction libraries
|
# Favicon extraction libraries
|
||||||
html5gum = "0.6.1"
|
html5gum = "0.7.0"
|
||||||
regex = { version = "1.11.1", features = ["std", "perf", "unicode-perl"], default-features = false }
|
regex = { version = "1.11.1", features = ["std", "perf", "unicode-perl"], default-features = false }
|
||||||
data-url = "0.3.1"
|
data-url = "0.3.1"
|
||||||
bytes = "1.8.0"
|
bytes = "1.9.0"
|
||||||
|
|
||||||
# Cache function results (Used for version check and favicon fetching)
|
# Cache function results (Used for version check and favicon fetching)
|
||||||
cached = { version = "0.54.0", features = ["async"] }
|
cached = { version = "0.54.0", features = ["async"] }
|
||||||
@@ -163,6 +163,15 @@ argon2 = "0.5.3"
|
|||||||
# Reading a password from the cli for generating the Argon2id ADMIN_TOKEN
|
# Reading a password from the cli for generating the Argon2id ADMIN_TOKEN
|
||||||
rpassword = "7.3.1"
|
rpassword = "7.3.1"
|
||||||
|
|
||||||
|
# Loading a dynamic CSS Stylesheet
|
||||||
|
grass_compiler = { version = "0.13.4", default-features = false }
|
||||||
|
|
||||||
|
[patch.crates-io]
|
||||||
|
# Patch fern to support syslog v7
|
||||||
|
fern = { git = "https://github.com/daboross/fern", rev = "3e775ccfafe7d24baee39826d38011981b2e55b5" }
|
||||||
|
# Patch yubico to remove duplicate crates of older versions
|
||||||
|
yubico = { git = "https://github.com/BlackDex/yubico-rs", rev = "00df14811f58155c0f02e3ab10f1570ed3e115c6" }
|
||||||
|
|
||||||
# Strip debuginfo from the release builds
|
# Strip debuginfo from the release builds
|
||||||
# The symbols are the provide better panic traces
|
# The symbols are the provide better panic traces
|
||||||
# Also enable fat LTO and use 1 codegen unit for optimizations
|
# Also enable fat LTO and use 1 codegen unit for optimizations
|
||||||
@@ -213,7 +222,8 @@ noop_method_call = "deny"
|
|||||||
refining_impl_trait = { level = "deny", priority = -1 }
|
refining_impl_trait = { level = "deny", priority = -1 }
|
||||||
rust_2018_idioms = { level = "deny", priority = -1 }
|
rust_2018_idioms = { level = "deny", priority = -1 }
|
||||||
rust_2021_compatibility = { level = "deny", priority = -1 }
|
rust_2021_compatibility = { level = "deny", priority = -1 }
|
||||||
# rust_2024_compatibility = { level = "deny", priority = -1 } # Enable once we are at MSRV 1.81.0
|
rust_2024_compatibility = { level = "deny", priority = -1 }
|
||||||
|
edition_2024_expr_fragment_specifier = "allow" # Once changed to Rust 2024 this should be removed and macro's should be validated again
|
||||||
single_use_lifetimes = "deny"
|
single_use_lifetimes = "deny"
|
||||||
trivial_casts = "deny"
|
trivial_casts = "deny"
|
||||||
trivial_numeric_casts = "deny"
|
trivial_numeric_casts = "deny"
|
||||||
@@ -222,9 +232,6 @@ unused_import_braces = "deny"
|
|||||||
unused_lifetimes = "deny"
|
unused_lifetimes = "deny"
|
||||||
unused_qualifications = "deny"
|
unused_qualifications = "deny"
|
||||||
variant_size_differences = "deny"
|
variant_size_differences = "deny"
|
||||||
# The lints below are part of the rust_2024_compatibility group
|
|
||||||
static-mut-refs = "deny"
|
|
||||||
unsafe-op-in-unsafe-fn = "deny"
|
|
||||||
|
|
||||||
# https://rust-lang.github.io/rust-clippy/stable/index.html
|
# https://rust-lang.github.io/rust-clippy/stable/index.html
|
||||||
[lints.clippy]
|
[lints.clippy]
|
||||||
|
@@ -21,7 +21,7 @@ notify us. We welcome working with you to resolve the issue promptly. Thanks in
|
|||||||
The following bug classes are out-of scope:
|
The following bug classes are out-of scope:
|
||||||
|
|
||||||
- Bugs that are already reported on Vaultwarden's issue tracker (https://github.com/dani-garcia/vaultwarden/issues)
|
- Bugs that are already reported on Vaultwarden's issue tracker (https://github.com/dani-garcia/vaultwarden/issues)
|
||||||
- Bugs that are not part of Vaultwarden, like on the the web-vault or mobile and desktop clients. These issues need to be reported in the respective project issue tracker at https://github.com/bitwarden to which we are not associated
|
- Bugs that are not part of Vaultwarden, like on the web-vault or mobile and desktop clients. These issues need to be reported in the respective project issue tracker at https://github.com/bitwarden to which we are not associated
|
||||||
- Issues in an upstream software dependency (ex: Rust, or External Libraries) which are already reported to the upstream maintainer
|
- Issues in an upstream software dependency (ex: Rust, or External Libraries) which are already reported to the upstream maintainer
|
||||||
- Attacks requiring physical access to a user's device
|
- Attacks requiring physical access to a user's device
|
||||||
- Issues related to software or protocols not under Vaultwarden's control
|
- Issues related to software or protocols not under Vaultwarden's control
|
||||||
|
@@ -5,9 +5,9 @@ vault_image_digest: "sha256:409ab328ca931439cb916b388a4bb784bd44220717aaf74cf716
|
|||||||
# 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:1978e7a58a1777cb0ef0dde76bad60b7914b21da57cfa88047875e4f364297aa"
|
xx_image_digest: "sha256:1978e7a58a1777cb0ef0dde76bad60b7914b21da57cfa88047875e4f364297aa"
|
||||||
rust_version: 1.82.0 # Rust version to be used
|
rust_version: 1.83.0 # Rust version to be used
|
||||||
debian_version: bookworm # Debian release name to be used
|
debian_version: bookworm # Debian release name to be used
|
||||||
alpine_version: "3.20" # Alpine version to be used
|
alpine_version: "3.21" # 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"]
|
||||||
# Determine the build images per OS/Arch
|
# Determine the build images per OS/Arch
|
||||||
|
@@ -32,10 +32,10 @@ FROM --platform=linux/amd64 docker.io/vaultwarden/web-vault@sha256:409ab328ca931
|
|||||||
########################## 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.82.0 AS build_amd64
|
FROM --platform=linux/amd64 ghcr.io/blackdex/rust-musl:x86_64-musl-stable-1.83.0 AS build_amd64
|
||||||
FROM --platform=linux/amd64 ghcr.io/blackdex/rust-musl:aarch64-musl-stable-1.82.0 AS build_arm64
|
FROM --platform=linux/amd64 ghcr.io/blackdex/rust-musl:aarch64-musl-stable-1.83.0 AS build_arm64
|
||||||
FROM --platform=linux/amd64 ghcr.io/blackdex/rust-musl:armv7-musleabihf-stable-1.82.0 AS build_armv7
|
FROM --platform=linux/amd64 ghcr.io/blackdex/rust-musl:armv7-musleabihf-stable-1.83.0 AS build_armv7
|
||||||
FROM --platform=linux/amd64 ghcr.io/blackdex/rust-musl:arm-musleabi-stable-1.82.0 AS build_armv6
|
FROM --platform=linux/amd64 ghcr.io/blackdex/rust-musl:arm-musleabi-stable-1.83.0 AS build_armv6
|
||||||
|
|
||||||
########################## BUILD IMAGE ##########################
|
########################## BUILD IMAGE ##########################
|
||||||
# hadolint ignore=DL3006
|
# hadolint ignore=DL3006
|
||||||
@@ -126,7 +126,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/alpine:3.20
|
FROM --platform=$TARGETPLATFORM docker.io/library/alpine:3.21
|
||||||
|
|
||||||
ENV ROCKET_PROFILE="release" \
|
ENV ROCKET_PROFILE="release" \
|
||||||
ROCKET_ADDRESS=0.0.0.0 \
|
ROCKET_ADDRESS=0.0.0.0 \
|
||||||
|
@@ -36,7 +36,7 @@ FROM --platform=linux/amd64 docker.io/tonistiigi/xx@sha256:1978e7a58a1777cb0ef0d
|
|||||||
|
|
||||||
########################## BUILD IMAGE ##########################
|
########################## BUILD IMAGE ##########################
|
||||||
# hadolint ignore=DL3006
|
# hadolint ignore=DL3006
|
||||||
FROM --platform=$BUILDPLATFORM docker.io/library/rust:1.82.0-slim-bookworm AS build
|
FROM --platform=$BUILDPLATFORM docker.io/library/rust:1.83.0-slim-bookworm AS build
|
||||||
COPY --from=xx / /
|
COPY --from=xx / /
|
||||||
ARG TARGETARCH
|
ARG TARGETARCH
|
||||||
ARG TARGETVARIANT
|
ARG TARGETVARIANT
|
||||||
|
@@ -46,7 +46,7 @@ There also is an option to use an other docker container to provide support for
|
|||||||
```bash
|
```bash
|
||||||
# To install and activate
|
# To install and activate
|
||||||
docker run --privileged --rm tonistiigi/binfmt --install arm64,arm
|
docker run --privileged --rm tonistiigi/binfmt --install arm64,arm
|
||||||
# To unistall
|
# To uninstall
|
||||||
docker run --privileged --rm tonistiigi/binfmt --uninstall 'qemu-*'
|
docker run --privileged --rm tonistiigi/binfmt --uninstall 'qemu-*'
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@@ -17,7 +17,7 @@ variable "SOURCE_REPOSITORY_URL" {
|
|||||||
default = null
|
default = null
|
||||||
}
|
}
|
||||||
|
|
||||||
// The commit hash of of the current commit this build was triggered on
|
// The commit hash of the current commit this build was triggered on
|
||||||
variable "SOURCE_COMMIT" {
|
variable "SOURCE_COMMIT" {
|
||||||
default = null
|
default = null
|
||||||
}
|
}
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
[toolchain]
|
[toolchain]
|
||||||
channel = "1.82.0"
|
channel = "1.83.0"
|
||||||
components = [ "rustfmt", "clippy" ]
|
components = [ "rustfmt", "clippy" ]
|
||||||
profile = "minimal"
|
profile = "minimal"
|
||||||
|
@@ -62,6 +62,7 @@ pub fn routes() -> Vec<Route> {
|
|||||||
diagnostics,
|
diagnostics,
|
||||||
get_diagnostics_config,
|
get_diagnostics_config,
|
||||||
resend_user_invite,
|
resend_user_invite,
|
||||||
|
get_diagnostics_http,
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -713,6 +714,7 @@ async fn diagnostics(_token: AdminToken, ip_header: IpHeader, mut conn: DbConn)
|
|||||||
"ip_header_name": ip_header_name,
|
"ip_header_name": ip_header_name,
|
||||||
"ip_header_config": &CONFIG.ip_header(),
|
"ip_header_config": &CONFIG.ip_header(),
|
||||||
"uses_proxy": uses_proxy,
|
"uses_proxy": uses_proxy,
|
||||||
|
"enable_websocket": &CONFIG.enable_websocket(),
|
||||||
"db_type": *DB_TYPE,
|
"db_type": *DB_TYPE,
|
||||||
"db_version": get_sql_server_version(&mut conn).await,
|
"db_version": get_sql_server_version(&mut conn).await,
|
||||||
"admin_url": format!("{}/diagnostics", admin_url()),
|
"admin_url": format!("{}/diagnostics", admin_url()),
|
||||||
@@ -734,6 +736,11 @@ fn get_diagnostics_config(_token: AdminToken) -> Json<Value> {
|
|||||||
Json(support_json)
|
Json(support_json)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[get("/diagnostics/http?<code>")]
|
||||||
|
fn get_diagnostics_http(code: u16, _token: AdminToken) -> EmptyResult {
|
||||||
|
err_code!(format!("Testing error {code} response"), code);
|
||||||
|
}
|
||||||
|
|
||||||
#[post("/config", data = "<data>")]
|
#[post("/config", data = "<data>")]
|
||||||
fn post_config(data: Json<ConfigBuilder>, _token: AdminToken) -> EmptyResult {
|
fn post_config(data: Json<ConfigBuilder>, _token: AdminToken) -> EmptyResult {
|
||||||
let data: ConfigBuilder = data.into_inner();
|
let data: ConfigBuilder = data.into_inner();
|
||||||
|
@@ -1,3 +1,5 @@
|
|||||||
|
use std::collections::HashSet;
|
||||||
|
|
||||||
use crate::db::DbPool;
|
use crate::db::DbPool;
|
||||||
use chrono::Utc;
|
use chrono::Utc;
|
||||||
use rocket::serde::json::Json;
|
use rocket::serde::json::Json;
|
||||||
@@ -477,6 +479,60 @@ struct KeyData {
|
|||||||
private_key: String,
|
private_key: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn validate_keydata(
|
||||||
|
data: &KeyData,
|
||||||
|
existing_ciphers: &[Cipher],
|
||||||
|
existing_folders: &[Folder],
|
||||||
|
existing_emergency_access: &[EmergencyAccess],
|
||||||
|
existing_user_orgs: &[UserOrganization],
|
||||||
|
existing_sends: &[Send],
|
||||||
|
) -> EmptyResult {
|
||||||
|
// Check that we're correctly rotating all the user's ciphers
|
||||||
|
let existing_cipher_ids = existing_ciphers.iter().map(|c| c.uuid.as_str()).collect::<HashSet<_>>();
|
||||||
|
let provided_cipher_ids = data
|
||||||
|
.ciphers
|
||||||
|
.iter()
|
||||||
|
.filter(|c| c.organization_id.is_none())
|
||||||
|
.filter_map(|c| c.id.as_deref())
|
||||||
|
.collect::<HashSet<_>>();
|
||||||
|
if !provided_cipher_ids.is_superset(&existing_cipher_ids) {
|
||||||
|
err!("All existing ciphers must be included in the rotation")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check that we're correctly rotating all the user's folders
|
||||||
|
let existing_folder_ids = existing_folders.iter().map(|f| f.uuid.as_str()).collect::<HashSet<_>>();
|
||||||
|
let provided_folder_ids = data.folders.iter().filter_map(|f| f.id.as_deref()).collect::<HashSet<_>>();
|
||||||
|
if !provided_folder_ids.is_superset(&existing_folder_ids) {
|
||||||
|
err!("All existing folders must be included in the rotation")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check that we're correctly rotating all the user's emergency access keys
|
||||||
|
let existing_emergency_access_ids =
|
||||||
|
existing_emergency_access.iter().map(|ea| ea.uuid.as_str()).collect::<HashSet<_>>();
|
||||||
|
let provided_emergency_access_ids =
|
||||||
|
data.emergency_access_keys.iter().map(|ea| ea.id.as_str()).collect::<HashSet<_>>();
|
||||||
|
if !provided_emergency_access_ids.is_superset(&existing_emergency_access_ids) {
|
||||||
|
err!("All existing emergency access keys must be included in the rotation")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check that we're correctly rotating all the user's reset password keys
|
||||||
|
let existing_reset_password_ids = existing_user_orgs.iter().map(|uo| uo.org_uuid.as_str()).collect::<HashSet<_>>();
|
||||||
|
let provided_reset_password_ids =
|
||||||
|
data.reset_password_keys.iter().map(|rp| rp.organization_id.as_str()).collect::<HashSet<_>>();
|
||||||
|
if !provided_reset_password_ids.is_superset(&existing_reset_password_ids) {
|
||||||
|
err!("All existing reset password keys must be included in the rotation")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check that we're correctly rotating all the user's sends
|
||||||
|
let existing_send_ids = existing_sends.iter().map(|s| s.uuid.as_str()).collect::<HashSet<_>>();
|
||||||
|
let provided_send_ids = data.sends.iter().filter_map(|s| s.id.as_deref()).collect::<HashSet<_>>();
|
||||||
|
if !provided_send_ids.is_superset(&existing_send_ids) {
|
||||||
|
err!("All existing sends must be included in the rotation")
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
#[post("/accounts/key", data = "<data>")]
|
#[post("/accounts/key", data = "<data>")]
|
||||||
async fn post_rotatekey(data: Json<KeyData>, headers: Headers, mut conn: DbConn, nt: Notify<'_>) -> EmptyResult {
|
async fn post_rotatekey(data: Json<KeyData>, headers: Headers, mut conn: DbConn, nt: Notify<'_>) -> EmptyResult {
|
||||||
// TODO: See if we can wrap everything within a SQL Transaction. If something fails it should revert everything.
|
// TODO: See if we can wrap everything within a SQL Transaction. If something fails it should revert everything.
|
||||||
@@ -494,20 +550,35 @@ async fn post_rotatekey(data: Json<KeyData>, headers: Headers, mut conn: DbConn,
|
|||||||
|
|
||||||
let user_uuid = &headers.user.uuid;
|
let user_uuid = &headers.user.uuid;
|
||||||
|
|
||||||
|
// TODO: Ideally we'd do everything after this point in a single transaction.
|
||||||
|
|
||||||
|
let mut existing_ciphers = Cipher::find_owned_by_user(user_uuid, &mut conn).await;
|
||||||
|
let mut existing_folders = Folder::find_by_user(user_uuid, &mut conn).await;
|
||||||
|
let mut existing_emergency_access = EmergencyAccess::find_all_by_grantor_uuid(user_uuid, &mut conn).await;
|
||||||
|
let mut existing_user_orgs = UserOrganization::find_by_user(user_uuid, &mut conn).await;
|
||||||
|
// We only rotate the reset password key if it is set.
|
||||||
|
existing_user_orgs.retain(|uo| uo.reset_password_key.is_some());
|
||||||
|
let mut existing_sends = Send::find_by_user(user_uuid, &mut conn).await;
|
||||||
|
|
||||||
|
validate_keydata(
|
||||||
|
&data,
|
||||||
|
&existing_ciphers,
|
||||||
|
&existing_folders,
|
||||||
|
&existing_emergency_access,
|
||||||
|
&existing_user_orgs,
|
||||||
|
&existing_sends,
|
||||||
|
)?;
|
||||||
|
|
||||||
// Update folder data
|
// Update folder data
|
||||||
for folder_data in data.folders {
|
for folder_data in data.folders {
|
||||||
// Skip `null` folder id entries.
|
// Skip `null` folder id entries.
|
||||||
// See: https://github.com/bitwarden/clients/issues/8453
|
// See: https://github.com/bitwarden/clients/issues/8453
|
||||||
if let Some(folder_id) = folder_data.id {
|
if let Some(folder_id) = folder_data.id {
|
||||||
let mut saved_folder = match Folder::find_by_uuid(&folder_id, &mut conn).await {
|
let saved_folder = match existing_folders.iter_mut().find(|f| f.uuid == folder_id) {
|
||||||
Some(folder) => folder,
|
Some(folder) => folder,
|
||||||
None => err!("Folder doesn't exist"),
|
None => err!("Folder doesn't exist"),
|
||||||
};
|
};
|
||||||
|
|
||||||
if &saved_folder.user_uuid != user_uuid {
|
|
||||||
err!("The folder is not owned by the user")
|
|
||||||
}
|
|
||||||
|
|
||||||
saved_folder.name = folder_data.name;
|
saved_folder.name = folder_data.name;
|
||||||
saved_folder.save(&mut conn).await?
|
saved_folder.save(&mut conn).await?
|
||||||
}
|
}
|
||||||
@@ -515,9 +586,8 @@ async fn post_rotatekey(data: Json<KeyData>, headers: Headers, mut conn: DbConn,
|
|||||||
|
|
||||||
// Update emergency access data
|
// Update emergency access data
|
||||||
for emergency_access_data in data.emergency_access_keys {
|
for emergency_access_data in data.emergency_access_keys {
|
||||||
let mut saved_emergency_access =
|
let saved_emergency_access =
|
||||||
match EmergencyAccess::find_by_uuid_and_grantor_uuid(&emergency_access_data.id, user_uuid, &mut conn).await
|
match existing_emergency_access.iter_mut().find(|ea| ea.uuid == emergency_access_data.id) {
|
||||||
{
|
|
||||||
Some(emergency_access) => emergency_access,
|
Some(emergency_access) => emergency_access,
|
||||||
None => err!("Emergency access doesn't exist or is not owned by the user"),
|
None => err!("Emergency access doesn't exist or is not owned by the user"),
|
||||||
};
|
};
|
||||||
@@ -528,9 +598,7 @@ async fn post_rotatekey(data: Json<KeyData>, headers: Headers, mut conn: DbConn,
|
|||||||
|
|
||||||
// Update reset password data
|
// Update reset password data
|
||||||
for reset_password_data in data.reset_password_keys {
|
for reset_password_data in data.reset_password_keys {
|
||||||
let mut user_org =
|
let user_org = match existing_user_orgs.iter_mut().find(|uo| uo.org_uuid == reset_password_data.organization_id)
|
||||||
match UserOrganization::find_by_user_and_org(user_uuid, &reset_password_data.organization_id, &mut conn)
|
|
||||||
.await
|
|
||||||
{
|
{
|
||||||
Some(reset_password) => reset_password,
|
Some(reset_password) => reset_password,
|
||||||
None => err!("Reset password doesn't exist"),
|
None => err!("Reset password doesn't exist"),
|
||||||
@@ -542,12 +610,12 @@ async fn post_rotatekey(data: Json<KeyData>, headers: Headers, mut conn: DbConn,
|
|||||||
|
|
||||||
// Update send data
|
// Update send data
|
||||||
for send_data in data.sends {
|
for send_data in data.sends {
|
||||||
let mut send = match Send::find_by_uuid(send_data.id.as_ref().unwrap(), &mut conn).await {
|
let send = match existing_sends.iter_mut().find(|s| &s.uuid == send_data.id.as_ref().unwrap()) {
|
||||||
Some(send) => send,
|
Some(send) => send,
|
||||||
None => err!("Send doesn't exist"),
|
None => err!("Send doesn't exist"),
|
||||||
};
|
};
|
||||||
|
|
||||||
update_send_from_data(&mut send, send_data, &headers, &mut conn, &nt, UpdateType::None).await?;
|
update_send_from_data(send, send_data, &headers, &mut conn, &nt, UpdateType::None).await?;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update cipher data
|
// Update cipher data
|
||||||
@@ -555,20 +623,15 @@ async fn post_rotatekey(data: Json<KeyData>, headers: Headers, mut conn: DbConn,
|
|||||||
|
|
||||||
for cipher_data in data.ciphers {
|
for cipher_data in data.ciphers {
|
||||||
if cipher_data.organization_id.is_none() {
|
if cipher_data.organization_id.is_none() {
|
||||||
let mut saved_cipher = match Cipher::find_by_uuid(cipher_data.id.as_ref().unwrap(), &mut conn).await {
|
let saved_cipher = match existing_ciphers.iter_mut().find(|c| &c.uuid == cipher_data.id.as_ref().unwrap()) {
|
||||||
Some(cipher) => cipher,
|
Some(cipher) => cipher,
|
||||||
None => err!("Cipher doesn't exist"),
|
None => err!("Cipher doesn't exist"),
|
||||||
};
|
};
|
||||||
|
|
||||||
if saved_cipher.user_uuid.as_ref().unwrap() != user_uuid {
|
|
||||||
err!("The cipher is not owned by the user")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Prevent triggering cipher updates via WebSockets by settings UpdateType::None
|
// Prevent triggering cipher updates via WebSockets by settings UpdateType::None
|
||||||
// The user sessions are invalidated because all the ciphers were re-encrypted and thus triggering an update could cause issues.
|
// The user sessions are invalidated because all the ciphers were re-encrypted and thus triggering an update could cause issues.
|
||||||
// We force the users to logout after the user has been saved to try and prevent these issues.
|
// We force the users to logout after the user has been saved to try and prevent these issues.
|
||||||
update_cipher_from_data(&mut saved_cipher, cipher_data, &headers, None, &mut conn, &nt, UpdateType::None)
|
update_cipher_from_data(saved_cipher, cipher_data, &headers, None, &mut conn, &nt, UpdateType::None).await?
|
||||||
.await?
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -842,7 +905,7 @@ struct PasswordHintData {
|
|||||||
|
|
||||||
#[post("/accounts/password-hint", data = "<data>")]
|
#[post("/accounts/password-hint", data = "<data>")]
|
||||||
async fn password_hint(data: Json<PasswordHintData>, mut conn: DbConn) -> EmptyResult {
|
async fn password_hint(data: Json<PasswordHintData>, mut conn: DbConn) -> EmptyResult {
|
||||||
if !CONFIG.mail_enabled() && !CONFIG.show_password_hint() {
|
if !CONFIG.password_hints_allowed() || (!CONFIG.mail_enabled() && !CONFIG.show_password_hint()) {
|
||||||
err!("This server is not configured to provide password hints.");
|
err!("This server is not configured to provide password hints.");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1136,15 +1199,15 @@ async fn post_auth_request(
|
|||||||
|
|
||||||
#[get("/auth-requests/<uuid>")]
|
#[get("/auth-requests/<uuid>")]
|
||||||
async fn get_auth_request(uuid: &str, headers: Headers, mut conn: DbConn) -> JsonResult {
|
async fn get_auth_request(uuid: &str, headers: Headers, mut conn: DbConn) -> JsonResult {
|
||||||
if headers.user.uuid != uuid {
|
|
||||||
err!("AuthRequest doesn't exist", "User uuid's do not match")
|
|
||||||
}
|
|
||||||
|
|
||||||
let auth_request = match AuthRequest::find_by_uuid(uuid, &mut conn).await {
|
let auth_request = match AuthRequest::find_by_uuid(uuid, &mut conn).await {
|
||||||
Some(auth_request) => auth_request,
|
Some(auth_request) => auth_request,
|
||||||
None => err!("AuthRequest doesn't exist", "Record not found"),
|
None => err!("AuthRequest doesn't exist", "Record not found"),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if headers.user.uuid != auth_request.user_uuid {
|
||||||
|
err!("AuthRequest doesn't exist", "User uuid's do not match")
|
||||||
|
}
|
||||||
|
|
||||||
let response_date_utc = auth_request.response_date.map(|response_date| format_date(&response_date));
|
let response_date_utc = auth_request.response_date.map(|response_date| format_date(&response_date));
|
||||||
|
|
||||||
Ok(Json(json!({
|
Ok(Json(json!({
|
||||||
@@ -1190,19 +1253,28 @@ async fn put_auth_request(
|
|||||||
err!("AuthRequest doesn't exist", "User uuid's do not match")
|
err!("AuthRequest doesn't exist", "User uuid's do not match")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if auth_request.approved.is_some() {
|
||||||
|
err!("An authentication request with the same device already exists")
|
||||||
|
}
|
||||||
|
|
||||||
|
let response_date = Utc::now().naive_utc();
|
||||||
|
let response_date_utc = format_date(&response_date);
|
||||||
|
|
||||||
|
if data.request_approved {
|
||||||
auth_request.approved = Some(data.request_approved);
|
auth_request.approved = Some(data.request_approved);
|
||||||
auth_request.enc_key = Some(data.key);
|
auth_request.enc_key = Some(data.key);
|
||||||
auth_request.master_password_hash = data.master_password_hash;
|
auth_request.master_password_hash = data.master_password_hash;
|
||||||
auth_request.response_device_id = Some(data.device_identifier.clone());
|
auth_request.response_device_id = Some(data.device_identifier.clone());
|
||||||
|
auth_request.response_date = Some(response_date);
|
||||||
auth_request.save(&mut conn).await?;
|
auth_request.save(&mut conn).await?;
|
||||||
|
|
||||||
if auth_request.approved.unwrap_or(false) {
|
|
||||||
ant.send_auth_response(&auth_request.user_uuid, &auth_request.uuid).await;
|
ant.send_auth_response(&auth_request.user_uuid, &auth_request.uuid).await;
|
||||||
nt.send_auth_response(&auth_request.user_uuid, &auth_request.uuid, data.device_identifier, &mut conn).await;
|
nt.send_auth_response(&auth_request.user_uuid, &auth_request.uuid, data.device_identifier, &mut conn).await;
|
||||||
|
} else {
|
||||||
|
// If denied, there's no reason to keep the request
|
||||||
|
auth_request.delete(&mut conn).await?;
|
||||||
}
|
}
|
||||||
|
|
||||||
let response_date_utc = auth_request.response_date.map(|response_date| format_date(&response_date));
|
|
||||||
|
|
||||||
Ok(Json(json!({
|
Ok(Json(json!({
|
||||||
"id": uuid,
|
"id": uuid,
|
||||||
"publicKey": auth_request.public_key,
|
"publicKey": auth_request.public_key,
|
||||||
@@ -1231,8 +1303,8 @@ async fn get_auth_request_response(
|
|||||||
};
|
};
|
||||||
|
|
||||||
if auth_request.device_type != client_headers.device_type
|
if auth_request.device_type != client_headers.device_type
|
||||||
&& auth_request.request_ip != client_headers.ip.ip.to_string()
|
|| auth_request.request_ip != client_headers.ip.ip.to_string()
|
||||||
&& !auth_request.check_access_code(code)
|
|| !auth_request.check_access_code(code)
|
||||||
{
|
{
|
||||||
err!("AuthRequest doesn't exist", "Invalid device, IP or code")
|
err!("AuthRequest doesn't exist", "Invalid device, IP or code")
|
||||||
}
|
}
|
||||||
|
@@ -10,6 +10,7 @@ use rocket::{
|
|||||||
};
|
};
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
|
|
||||||
|
use crate::auth::ClientVersion;
|
||||||
use crate::util::NumberOrString;
|
use crate::util::NumberOrString;
|
||||||
use crate::{
|
use crate::{
|
||||||
api::{self, core::log_event, EmptyResult, JsonResult, Notify, PasswordOrOtpData, UpdateType},
|
api::{self, core::log_event, EmptyResult, JsonResult, Notify, PasswordOrOtpData, UpdateType},
|
||||||
@@ -104,11 +105,27 @@ struct SyncData {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[get("/sync?<data..>")]
|
#[get("/sync?<data..>")]
|
||||||
async fn sync(data: SyncData, headers: Headers, mut conn: DbConn) -> Json<Value> {
|
async fn sync(
|
||||||
|
data: SyncData,
|
||||||
|
headers: Headers,
|
||||||
|
client_version: Option<ClientVersion>,
|
||||||
|
mut conn: DbConn,
|
||||||
|
) -> Json<Value> {
|
||||||
let user_json = headers.user.to_json(&mut conn).await;
|
let user_json = headers.user.to_json(&mut conn).await;
|
||||||
|
|
||||||
// Get all ciphers which are visible by the user
|
// Get all ciphers which are visible by the user
|
||||||
let ciphers = Cipher::find_by_user_visible(&headers.user.uuid, &mut conn).await;
|
let mut ciphers = Cipher::find_by_user_visible(&headers.user.uuid, &mut conn).await;
|
||||||
|
|
||||||
|
// Filter out SSH keys if the client version is less than 2024.12.0
|
||||||
|
let show_ssh_keys = if let Some(client_version) = client_version {
|
||||||
|
let ver_match = semver::VersionReq::parse(">=2024.12.0").unwrap();
|
||||||
|
ver_match.matches(&client_version.0)
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
};
|
||||||
|
if !show_ssh_keys {
|
||||||
|
ciphers.retain(|c| c.atype != 5);
|
||||||
|
}
|
||||||
|
|
||||||
let cipher_sync_data = CipherSyncData::new(&headers.user.uuid, CipherSyncType::User, &mut conn).await;
|
let cipher_sync_data = CipherSyncData::new(&headers.user.uuid, CipherSyncType::User, &mut conn).await;
|
||||||
|
|
||||||
@@ -205,7 +222,7 @@ pub struct CipherData {
|
|||||||
// Id is optional as it is included only in bulk share
|
// Id is optional as it is included only in bulk share
|
||||||
pub id: Option<String>,
|
pub id: Option<String>,
|
||||||
// Folder id is not included in import
|
// Folder id is not included in import
|
||||||
folder_id: Option<String>,
|
pub folder_id: Option<String>,
|
||||||
// TODO: Some of these might appear all the time, no need for Option
|
// TODO: Some of these might appear all the time, no need for Option
|
||||||
#[serde(alias = "organizationID")]
|
#[serde(alias = "organizationID")]
|
||||||
pub organization_id: Option<String>,
|
pub organization_id: Option<String>,
|
||||||
@@ -216,7 +233,8 @@ pub struct CipherData {
|
|||||||
Login = 1,
|
Login = 1,
|
||||||
SecureNote = 2,
|
SecureNote = 2,
|
||||||
Card = 3,
|
Card = 3,
|
||||||
Identity = 4
|
Identity = 4,
|
||||||
|
SshKey = 5
|
||||||
*/
|
*/
|
||||||
pub r#type: i32,
|
pub r#type: i32,
|
||||||
pub name: String,
|
pub name: String,
|
||||||
@@ -228,6 +246,7 @@ pub struct CipherData {
|
|||||||
secure_note: Option<Value>,
|
secure_note: Option<Value>,
|
||||||
card: Option<Value>,
|
card: Option<Value>,
|
||||||
identity: Option<Value>,
|
identity: Option<Value>,
|
||||||
|
ssh_key: Option<Value>,
|
||||||
|
|
||||||
favorite: Option<bool>,
|
favorite: Option<bool>,
|
||||||
reprompt: Option<i32>,
|
reprompt: Option<i32>,
|
||||||
@@ -469,6 +488,7 @@ pub async fn update_cipher_from_data(
|
|||||||
2 => data.secure_note,
|
2 => data.secure_note,
|
||||||
3 => data.card,
|
3 => data.card,
|
||||||
4 => data.identity,
|
4 => data.identity,
|
||||||
|
5 => data.ssh_key,
|
||||||
_ => err!("Invalid type"),
|
_ => err!("Invalid type"),
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -491,7 +511,7 @@ pub async fn update_cipher_from_data(
|
|||||||
cipher.fields = data.fields.map(|f| _clean_cipher_data(f).to_string());
|
cipher.fields = data.fields.map(|f| _clean_cipher_data(f).to_string());
|
||||||
cipher.data = type_data.to_string();
|
cipher.data = type_data.to_string();
|
||||||
cipher.password_history = data.password_history.map(|f| f.to_string());
|
cipher.password_history = data.password_history.map(|f| f.to_string());
|
||||||
cipher.reprompt = data.reprompt;
|
cipher.reprompt = data.reprompt.filter(|r| *r == RepromptType::None as i32 || *r == RepromptType::Password as i32);
|
||||||
|
|
||||||
cipher.save(conn).await?;
|
cipher.save(conn).await?;
|
||||||
cipher.move_to_folder(data.folder_id, &headers.user.uuid, conn).await?;
|
cipher.move_to_folder(data.folder_id, &headers.user.uuid, conn).await?;
|
||||||
@@ -565,11 +585,11 @@ async fn post_ciphers_import(
|
|||||||
Cipher::validate_cipher_data(&data.ciphers)?;
|
Cipher::validate_cipher_data(&data.ciphers)?;
|
||||||
|
|
||||||
// Read and create the folders
|
// Read and create the folders
|
||||||
let existing_folders: Vec<String> =
|
let existing_folders: HashSet<Option<String>> =
|
||||||
Folder::find_by_user(&headers.user.uuid, &mut conn).await.into_iter().map(|f| f.uuid).collect();
|
Folder::find_by_user(&headers.user.uuid, &mut conn).await.into_iter().map(|f| Some(f.uuid)).collect();
|
||||||
let mut folders: Vec<String> = Vec::with_capacity(data.folders.len());
|
let mut folders: Vec<String> = Vec::with_capacity(data.folders.len());
|
||||||
for folder in data.folders.into_iter() {
|
for folder in data.folders.into_iter() {
|
||||||
let folder_uuid = if folder.id.is_some() && existing_folders.contains(folder.id.as_ref().unwrap()) {
|
let folder_uuid = if existing_folders.contains(&folder.id) {
|
||||||
folder.id.unwrap()
|
folder.id.unwrap()
|
||||||
} else {
|
} else {
|
||||||
let mut new_folder = Folder::new(headers.user.uuid.clone(), folder.name);
|
let mut new_folder = Folder::new(headers.user.uuid.clone(), folder.name);
|
||||||
@@ -581,8 +601,8 @@ async fn post_ciphers_import(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Read the relations between folders and ciphers
|
// Read the relations between folders and ciphers
|
||||||
|
// Ciphers can only be in one folder at the same time
|
||||||
let mut relations_map = HashMap::with_capacity(data.folder_relationships.len());
|
let mut relations_map = HashMap::with_capacity(data.folder_relationships.len());
|
||||||
|
|
||||||
for relation in data.folder_relationships {
|
for relation in data.folder_relationships {
|
||||||
relations_map.insert(relation.key, relation.value);
|
relations_map.insert(relation.key, relation.value);
|
||||||
}
|
}
|
||||||
|
@@ -136,8 +136,8 @@ async fn put_eq_domains(data: Json<EquivDomainData>, headers: Headers, conn: DbC
|
|||||||
|
|
||||||
#[get("/hibp/breach?<username>")]
|
#[get("/hibp/breach?<username>")]
|
||||||
async fn hibp_breach(username: &str, _headers: Headers) -> JsonResult {
|
async fn hibp_breach(username: &str, _headers: Headers) -> JsonResult {
|
||||||
if let Some(api_key) = crate::CONFIG.hibp_api_key() {
|
|
||||||
let username: String = url::form_urlencoded::byte_serialize(username.as_bytes()).collect();
|
let username: String = url::form_urlencoded::byte_serialize(username.as_bytes()).collect();
|
||||||
|
if let Some(api_key) = crate::CONFIG.hibp_api_key() {
|
||||||
let url = format!(
|
let url = format!(
|
||||||
"https://haveibeenpwned.com/api/v3/breachedaccount/{username}?truncateResponse=false&includeUnverified=false"
|
"https://haveibeenpwned.com/api/v3/breachedaccount/{username}?truncateResponse=false&includeUnverified=false"
|
||||||
);
|
);
|
||||||
|
@@ -9,9 +9,8 @@ use crate::{
|
|||||||
core::{log_event, two_factor, CipherSyncData, CipherSyncType},
|
core::{log_event, two_factor, CipherSyncData, CipherSyncType},
|
||||||
EmptyResult, JsonResult, Notify, PasswordOrOtpData, UpdateType,
|
EmptyResult, JsonResult, Notify, PasswordOrOtpData, UpdateType,
|
||||||
},
|
},
|
||||||
auth::{decode_invite, AdminHeaders, Headers, ManagerHeaders, ManagerHeadersLoose, OwnerHeaders},
|
auth::{decode_invite, AdminHeaders, ClientVersion, Headers, ManagerHeaders, ManagerHeadersLoose, OwnerHeaders},
|
||||||
db::{models::*, DbConn},
|
db::{models::*, DbConn},
|
||||||
error::Error,
|
|
||||||
mail,
|
mail,
|
||||||
util::{convert_json_key_lcase_first, NumberOrString},
|
util::{convert_json_key_lcase_first, NumberOrString},
|
||||||
CONFIG,
|
CONFIG,
|
||||||
@@ -127,6 +126,7 @@ struct NewCollectionData {
|
|||||||
name: String,
|
name: String,
|
||||||
groups: Vec<NewCollectionObjectData>,
|
groups: Vec<NewCollectionObjectData>,
|
||||||
users: Vec<NewCollectionObjectData>,
|
users: Vec<NewCollectionObjectData>,
|
||||||
|
id: Option<String>,
|
||||||
external_id: Option<String>,
|
external_id: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1598,40 +1598,43 @@ async fn post_org_import(
|
|||||||
// TODO: See if we can optimize the whole cipher adding/importing and prevent duplicate code and checks.
|
// TODO: See if we can optimize the whole cipher adding/importing and prevent duplicate code and checks.
|
||||||
Cipher::validate_cipher_data(&data.ciphers)?;
|
Cipher::validate_cipher_data(&data.ciphers)?;
|
||||||
|
|
||||||
let mut collections = Vec::new();
|
let existing_collections: HashSet<Option<String>> =
|
||||||
|
Collection::find_by_organization(&org_id, &mut conn).await.into_iter().map(|c| (Some(c.uuid))).collect();
|
||||||
|
let mut collections: Vec<String> = Vec::with_capacity(data.collections.len());
|
||||||
for coll in data.collections {
|
for coll in data.collections {
|
||||||
let collection = Collection::new(org_id.clone(), coll.name, coll.external_id);
|
let collection_uuid = if existing_collections.contains(&coll.id) {
|
||||||
if collection.save(&mut conn).await.is_err() {
|
coll.id.unwrap()
|
||||||
collections.push(Err(Error::new("Failed to create Collection", "Failed to create Collection")));
|
|
||||||
} else {
|
} else {
|
||||||
collections.push(Ok(collection));
|
let new_collection = Collection::new(org_id.clone(), coll.name, coll.external_id);
|
||||||
}
|
new_collection.save(&mut conn).await?;
|
||||||
|
new_collection.uuid
|
||||||
|
};
|
||||||
|
|
||||||
|
collections.push(collection_uuid);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Read the relations between collections and ciphers
|
// Read the relations between collections and ciphers
|
||||||
let mut relations = Vec::new();
|
// Ciphers can be in multiple collections at the same time
|
||||||
|
let mut relations = Vec::with_capacity(data.collection_relationships.len());
|
||||||
for relation in data.collection_relationships {
|
for relation in data.collection_relationships {
|
||||||
relations.push((relation.key, relation.value));
|
relations.push((relation.key, relation.value));
|
||||||
}
|
}
|
||||||
|
|
||||||
let headers: Headers = headers.into();
|
let headers: Headers = headers.into();
|
||||||
|
|
||||||
let mut ciphers = Vec::new();
|
let mut ciphers: Vec<String> = Vec::with_capacity(data.ciphers.len());
|
||||||
for cipher_data in data.ciphers {
|
for mut cipher_data in data.ciphers {
|
||||||
|
// Always clear folder_id's via an organization import
|
||||||
|
cipher_data.folder_id = None;
|
||||||
let mut cipher = Cipher::new(cipher_data.r#type, cipher_data.name.clone());
|
let mut cipher = Cipher::new(cipher_data.r#type, cipher_data.name.clone());
|
||||||
update_cipher_from_data(&mut cipher, cipher_data, &headers, None, &mut conn, &nt, UpdateType::None).await.ok();
|
update_cipher_from_data(&mut cipher, cipher_data, &headers, None, &mut conn, &nt, UpdateType::None).await.ok();
|
||||||
ciphers.push(cipher);
|
ciphers.push(cipher.uuid);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Assign the collections
|
// Assign the collections
|
||||||
for (cipher_index, coll_index) in relations {
|
for (cipher_index, coll_index) in relations {
|
||||||
let cipher_id = &ciphers[cipher_index].uuid;
|
let cipher_id = &ciphers[cipher_index];
|
||||||
let coll = &collections[coll_index];
|
let coll_id = &collections[coll_index];
|
||||||
let coll_id = match coll {
|
|
||||||
Ok(coll) => coll.uuid.as_str(),
|
|
||||||
Err(_) => err!("Failed to assign to collection"),
|
|
||||||
};
|
|
||||||
|
|
||||||
CollectionCipher::save(cipher_id, coll_id, &mut conn).await?;
|
CollectionCipher::save(cipher_id, coll_id, &mut conn).await?;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1649,7 +1652,7 @@ struct BulkCollectionsData {
|
|||||||
remove_collections: bool,
|
remove_collections: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
// This endpoint is only reachable via the organization view, therefor this endpoint is located here
|
// This endpoint is only reachable via the organization view, therefore this endpoint is located here
|
||||||
// Also Bitwarden does not send out Notifications for these changes, it only does this for individual cipher collection updates
|
// Also Bitwarden does not send out Notifications for these changes, it only does this for individual cipher collection updates
|
||||||
#[post("/ciphers/bulk-collections", data = "<data>")]
|
#[post("/ciphers/bulk-collections", data = "<data>")]
|
||||||
async fn post_bulk_collections(data: Json<BulkCollectionsData>, headers: Headers, mut conn: DbConn) -> EmptyResult {
|
async fn post_bulk_collections(data: Json<BulkCollectionsData>, headers: Headers, mut conn: DbConn) -> EmptyResult {
|
||||||
@@ -2305,14 +2308,14 @@ async fn _restore_organization_user(
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[get("/organizations/<org_id>/groups")]
|
#[get("/organizations/<org_id>/groups")]
|
||||||
async fn get_groups(org_id: &str, headers: ManagerHeadersLoose, mut conn: DbConn) -> JsonResult {
|
async fn get_groups(org_id: &str, _headers: ManagerHeadersLoose, mut conn: DbConn) -> JsonResult {
|
||||||
let groups: Vec<Value> = if CONFIG.org_groups_enabled() {
|
let groups: Vec<Value> = if CONFIG.org_groups_enabled() {
|
||||||
// Group::find_by_organization(&org_id, &mut conn).await.iter().map(Group::to_json).collect::<Value>()
|
// Group::find_by_organization(&org_id, &mut conn).await.iter().map(Group::to_json).collect::<Value>()
|
||||||
let groups = Group::find_by_organization(org_id, &mut conn).await;
|
let groups = Group::find_by_organization(org_id, &mut conn).await;
|
||||||
let mut groups_json = Vec::with_capacity(groups.len());
|
let mut groups_json = Vec::with_capacity(groups.len());
|
||||||
|
|
||||||
for g in groups {
|
for g in groups {
|
||||||
groups_json.push(g.to_json_details(&headers.org_user.atype, &mut conn).await)
|
groups_json.push(g.to_json_details(&mut conn).await)
|
||||||
}
|
}
|
||||||
groups_json
|
groups_json
|
||||||
} else {
|
} else {
|
||||||
@@ -2500,7 +2503,7 @@ async fn add_update_group(
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[get("/organizations/<_org_id>/groups/<group_id>/details")]
|
#[get("/organizations/<_org_id>/groups/<group_id>/details")]
|
||||||
async fn get_group_details(_org_id: &str, group_id: &str, headers: AdminHeaders, mut conn: DbConn) -> JsonResult {
|
async fn get_group_details(_org_id: &str, group_id: &str, _headers: AdminHeaders, mut conn: DbConn) -> JsonResult {
|
||||||
if !CONFIG.org_groups_enabled() {
|
if !CONFIG.org_groups_enabled() {
|
||||||
err!("Group support is disabled");
|
err!("Group support is disabled");
|
||||||
}
|
}
|
||||||
@@ -2510,7 +2513,7 @@ async fn get_group_details(_org_id: &str, group_id: &str, headers: AdminHeaders,
|
|||||||
_ => err!("Group could not be found!"),
|
_ => err!("Group could not be found!"),
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(Json(group.to_json_details(&(headers.org_user_type as i32), &mut conn).await))
|
Ok(Json(group.to_json_details(&mut conn).await))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[post("/organizations/<org_id>/groups/<group_id>/delete")]
|
#[post("/organizations/<org_id>/groups/<group_id>/delete")]
|
||||||
@@ -2786,7 +2789,7 @@ struct OrganizationUserResetPasswordRequest {
|
|||||||
key: String,
|
key: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Upstrem reports this is the renamed endpoint instead of `/keys`
|
// Upstream reports this is the renamed endpoint instead of `/keys`
|
||||||
// But the clients do not seem to use this at all
|
// But the clients do not seem to use this at all
|
||||||
// Just add it here in case they will
|
// Just add it here in case they will
|
||||||
#[get("/organizations/<org_id>/public-key")]
|
#[get("/organizations/<org_id>/public-key")]
|
||||||
@@ -2999,18 +3002,20 @@ async fn put_reset_password_enrollment(
|
|||||||
// We need to convert all keys so they have the first character to be a lowercase.
|
// We need to convert all keys so they have the first character to be a lowercase.
|
||||||
// Else the export will be just an empty JSON file.
|
// Else the export will be just an empty JSON file.
|
||||||
#[get("/organizations/<org_id>/export")]
|
#[get("/organizations/<org_id>/export")]
|
||||||
async fn get_org_export(org_id: &str, headers: AdminHeaders, mut conn: DbConn) -> Json<Value> {
|
async fn get_org_export(
|
||||||
use semver::{Version, VersionReq};
|
org_id: &str,
|
||||||
|
headers: AdminHeaders,
|
||||||
|
client_version: Option<ClientVersion>,
|
||||||
|
mut conn: DbConn,
|
||||||
|
) -> Json<Value> {
|
||||||
// Since version v2023.1.0 the format of the export is different.
|
// Since version v2023.1.0 the format of the export is different.
|
||||||
// Also, this endpoint was created since v2022.9.0.
|
// Also, this endpoint was created since v2022.9.0.
|
||||||
// Therefore, we will check for any version smaller then v2023.1.0 and return a different response.
|
// Therefore, we will check for any version smaller then v2023.1.0 and return a different response.
|
||||||
// If we can't determine the version, we will use the latest default v2023.1.0 and higher.
|
// If we can't determine the version, we will use the latest default v2023.1.0 and higher.
|
||||||
// https://github.com/bitwarden/server/blob/9ca93381ce416454734418c3a9f99ab49747f1b6/src/Api/Controllers/OrganizationExportController.cs#L44
|
// https://github.com/bitwarden/server/blob/9ca93381ce416454734418c3a9f99ab49747f1b6/src/Api/Controllers/OrganizationExportController.cs#L44
|
||||||
let use_list_response_model = if let Some(client_version) = headers.client_version {
|
let use_list_response_model = if let Some(client_version) = client_version {
|
||||||
let ver_match = VersionReq::parse("<2023.1.0").unwrap();
|
let ver_match = semver::VersionReq::parse("<2023.1.0").unwrap();
|
||||||
let client_version = Version::parse(&client_version).unwrap();
|
ver_match.matches(&client_version.0)
|
||||||
ver_match.matches(&client_version)
|
|
||||||
} else {
|
} else {
|
||||||
false
|
false
|
||||||
};
|
};
|
||||||
|
@@ -211,10 +211,7 @@ impl DuoClient {
|
|||||||
nonce,
|
nonce,
|
||||||
};
|
};
|
||||||
|
|
||||||
let token = match self.encode_duo_jwt(jwt_payload) {
|
let token = self.encode_duo_jwt(jwt_payload)?;
|
||||||
Ok(token) => token,
|
|
||||||
Err(e) => return Err(e),
|
|
||||||
};
|
|
||||||
|
|
||||||
let authz_endpoint = format!("https://{}/oauth/v1/authorize", self.api_host);
|
let authz_endpoint = format!("https://{}/oauth/v1/authorize", self.api_host);
|
||||||
let mut auth_url = match Url::parse(authz_endpoint.as_str()) {
|
let mut auth_url = match Url::parse(authz_endpoint.as_str()) {
|
||||||
|
@@ -19,7 +19,7 @@ use tokio::{
|
|||||||
io::{AsyncReadExt, AsyncWriteExt},
|
io::{AsyncReadExt, AsyncWriteExt},
|
||||||
};
|
};
|
||||||
|
|
||||||
use html5gum::{Emitter, HtmlString, InfallibleTokenizer, Readable, StringReader, Tokenizer};
|
use html5gum::{Emitter, HtmlString, Readable, StringReader, Tokenizer};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
error::Error,
|
error::Error,
|
||||||
@@ -261,11 +261,7 @@ impl Icon {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_favicons_node(
|
fn get_favicons_node(dom: Tokenizer<StringReader<'_>, FaviconEmitter>, icons: &mut Vec<Icon>, url: &url::Url) {
|
||||||
dom: InfallibleTokenizer<StringReader<'_>, FaviconEmitter>,
|
|
||||||
icons: &mut Vec<Icon>,
|
|
||||||
url: &url::Url,
|
|
||||||
) {
|
|
||||||
const TAG_LINK: &[u8] = b"link";
|
const TAG_LINK: &[u8] = b"link";
|
||||||
const TAG_BASE: &[u8] = b"base";
|
const TAG_BASE: &[u8] = b"base";
|
||||||
const TAG_HEAD: &[u8] = b"head";
|
const TAG_HEAD: &[u8] = b"head";
|
||||||
@@ -274,7 +270,7 @@ fn get_favicons_node(
|
|||||||
|
|
||||||
let mut base_url = url.clone();
|
let mut base_url = url.clone();
|
||||||
let mut icon_tags: Vec<Tag> = Vec::new();
|
let mut icon_tags: Vec<Tag> = Vec::new();
|
||||||
for token in dom {
|
for Ok(token) in dom {
|
||||||
let tag_name: &[u8] = &token.tag.name;
|
let tag_name: &[u8] = &token.tag.name;
|
||||||
match tag_name {
|
match tag_name {
|
||||||
TAG_LINK => {
|
TAG_LINK => {
|
||||||
@@ -401,7 +397,7 @@ async fn get_icon_url(domain: &str) -> Result<IconUrlResult, Error> {
|
|||||||
// 384KB should be more than enough for the HTML, though as we only really need the HTML header.
|
// 384KB should be more than enough for the HTML, though as we only really need the HTML header.
|
||||||
let limited_reader = stream_to_bytes_limit(content, 384 * 1024).await?.to_vec();
|
let limited_reader = stream_to_bytes_limit(content, 384 * 1024).await?.to_vec();
|
||||||
|
|
||||||
let dom = Tokenizer::new_with_emitter(limited_reader.to_reader(), FaviconEmitter::default()).infallible();
|
let dom = Tokenizer::new_with_emitter(limited_reader.to_reader(), FaviconEmitter::default());
|
||||||
get_favicons_node(dom, &mut iconlist, &url);
|
get_favicons_node(dom, &mut iconlist, &url);
|
||||||
} else {
|
} else {
|
||||||
// Add the default favicon.ico to the list with just the given domain
|
// Add the default favicon.ico to the list with just the given domain
|
||||||
@@ -662,7 +658,7 @@ impl reqwest::cookie::CookieStore for Jar {
|
|||||||
/// The FaviconEmitter is using an optimized version of the DefaultEmitter.
|
/// The FaviconEmitter is using an optimized version of the DefaultEmitter.
|
||||||
/// This prevents emitting tags like comments, doctype and also strings between the tags.
|
/// This prevents emitting tags like comments, doctype and also strings between the tags.
|
||||||
/// But it will also only emit the tags we need and only if they have the correct attributes
|
/// But it will also only emit the tags we need and only if they have the correct attributes
|
||||||
/// Therefor parsing the HTML content is faster.
|
/// Therefore parsing the HTML content is faster.
|
||||||
use std::collections::BTreeMap;
|
use std::collections::BTreeMap;
|
||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
|
@@ -165,22 +165,42 @@ async fn _password_login(
|
|||||||
// Set the user_uuid here to be passed back used for event logging.
|
// Set the user_uuid here to be passed back used for event logging.
|
||||||
*user_uuid = Some(user.uuid.clone());
|
*user_uuid = Some(user.uuid.clone());
|
||||||
|
|
||||||
// Check password
|
// Check if the user is disabled
|
||||||
let password = data.password.as_ref().unwrap();
|
if !user.enabled {
|
||||||
if let Some(auth_request_uuid) = data.auth_request.clone() {
|
|
||||||
if let Some(auth_request) = AuthRequest::find_by_uuid(auth_request_uuid.as_str(), conn).await {
|
|
||||||
if !auth_request.check_access_code(password) {
|
|
||||||
err!(
|
err!(
|
||||||
"Username or access code is incorrect. Try again",
|
"This user has been disabled",
|
||||||
|
format!("IP: {}. Username: {}.", ip.ip, username),
|
||||||
|
ErrorEvent {
|
||||||
|
event: EventType::UserFailedLogIn
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
let password = data.password.as_ref().unwrap();
|
||||||
|
|
||||||
|
// If we get an auth request, we don't check the user's password, but the access code of the auth request
|
||||||
|
if let Some(ref auth_request_uuid) = data.auth_request {
|
||||||
|
let Some(auth_request) = AuthRequest::find_by_uuid(auth_request_uuid.as_str(), conn).await else {
|
||||||
|
err!(
|
||||||
|
"Auth request not found. Try again.",
|
||||||
format!("IP: {}. Username: {}.", ip.ip, username),
|
format!("IP: {}. Username: {}.", ip.ip, username),
|
||||||
ErrorEvent {
|
ErrorEvent {
|
||||||
event: EventType::UserFailedLogIn,
|
event: EventType::UserFailedLogIn,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
};
|
||||||
} else {
|
|
||||||
|
let expiration_time = auth_request.creation_date + chrono::Duration::minutes(5);
|
||||||
|
let request_expired = Utc::now().naive_utc() >= expiration_time;
|
||||||
|
|
||||||
|
if auth_request.user_uuid != user.uuid
|
||||||
|
|| !auth_request.approved.unwrap_or(false)
|
||||||
|
|| request_expired
|
||||||
|
|| ip.ip.to_string() != auth_request.request_ip
|
||||||
|
|| !auth_request.check_access_code(password)
|
||||||
|
{
|
||||||
err!(
|
err!(
|
||||||
"Auth request not found. Try again.",
|
"Username or access code is incorrect. Try again",
|
||||||
format!("IP: {}. Username: {}.", ip.ip, username),
|
format!("IP: {}. Username: {}.", ip.ip, username),
|
||||||
ErrorEvent {
|
ErrorEvent {
|
||||||
event: EventType::UserFailedLogIn,
|
event: EventType::UserFailedLogIn,
|
||||||
@@ -197,8 +217,8 @@ async fn _password_login(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Change the KDF Iterations
|
// Change the KDF Iterations (only when not logging in with an auth request)
|
||||||
if user.password_iterations != CONFIG.password_iterations() {
|
if data.auth_request.is_none() && user.password_iterations != CONFIG.password_iterations() {
|
||||||
user.password_iterations = CONFIG.password_iterations();
|
user.password_iterations = CONFIG.password_iterations();
|
||||||
user.set_password(password, None, false, None);
|
user.set_password(password, None, false, None);
|
||||||
|
|
||||||
@@ -207,17 +227,6 @@ async fn _password_login(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if the user is disabled
|
|
||||||
if !user.enabled {
|
|
||||||
err!(
|
|
||||||
"This user has been disabled",
|
|
||||||
format!("IP: {}. Username: {}.", ip.ip, username),
|
|
||||||
ErrorEvent {
|
|
||||||
event: EventType::UserFailedLogIn
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
let now = Utc::now().naive_utc();
|
let now = Utc::now().naive_utc();
|
||||||
|
|
||||||
if user.verified_at.is_none() && CONFIG.mail_enabled() && CONFIG.signups_verify() {
|
if user.verified_at.is_none() && CONFIG.mail_enabled() && CONFIG.signups_verify() {
|
||||||
|
@@ -9,6 +9,7 @@ use crate::{
|
|||||||
api::{ApiResult, EmptyResult, UpdateType},
|
api::{ApiResult, EmptyResult, UpdateType},
|
||||||
db::models::{Cipher, Device, Folder, Send, User},
|
db::models::{Cipher, Device, Folder, Send, User},
|
||||||
http_client::make_http_request,
|
http_client::make_http_request,
|
||||||
|
util::format_date,
|
||||||
CONFIG,
|
CONFIG,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -170,10 +171,10 @@ pub async fn push_cipher_update(
|
|||||||
"identifier": acting_device_uuid,
|
"identifier": acting_device_uuid,
|
||||||
"type": ut as i32,
|
"type": ut as i32,
|
||||||
"payload": {
|
"payload": {
|
||||||
"id": cipher.uuid,
|
"Id": cipher.uuid,
|
||||||
"userId": cipher.user_uuid,
|
"UserId": cipher.user_uuid,
|
||||||
"organizationId": (),
|
"OrganizationId": (),
|
||||||
"revisionDate": cipher.updated_at
|
"RevisionDate": format_date(&cipher.updated_at)
|
||||||
}
|
}
|
||||||
}))
|
}))
|
||||||
.await;
|
.await;
|
||||||
@@ -190,8 +191,8 @@ pub fn push_logout(user: &User, acting_device_uuid: Option<String>) {
|
|||||||
"identifier": acting_device_uuid,
|
"identifier": acting_device_uuid,
|
||||||
"type": UpdateType::LogOut as i32,
|
"type": UpdateType::LogOut as i32,
|
||||||
"payload": {
|
"payload": {
|
||||||
"userId": user.uuid,
|
"UserId": user.uuid,
|
||||||
"date": user.updated_at
|
"Date": format_date(&user.updated_at)
|
||||||
}
|
}
|
||||||
})));
|
})));
|
||||||
}
|
}
|
||||||
@@ -204,8 +205,8 @@ pub fn push_user_update(ut: UpdateType, user: &User) {
|
|||||||
"identifier": (),
|
"identifier": (),
|
||||||
"type": ut as i32,
|
"type": ut as i32,
|
||||||
"payload": {
|
"payload": {
|
||||||
"userId": user.uuid,
|
"UserId": user.uuid,
|
||||||
"date": user.updated_at
|
"Date": format_date(&user.updated_at)
|
||||||
}
|
}
|
||||||
})));
|
})));
|
||||||
}
|
}
|
||||||
@@ -224,9 +225,9 @@ pub async fn push_folder_update(
|
|||||||
"identifier": acting_device_uuid,
|
"identifier": acting_device_uuid,
|
||||||
"type": ut as i32,
|
"type": ut as i32,
|
||||||
"payload": {
|
"payload": {
|
||||||
"id": folder.uuid,
|
"Id": folder.uuid,
|
||||||
"userId": folder.user_uuid,
|
"UserId": folder.user_uuid,
|
||||||
"revisionDate": folder.updated_at
|
"RevisionDate": format_date(&folder.updated_at)
|
||||||
}
|
}
|
||||||
})));
|
})));
|
||||||
}
|
}
|
||||||
@@ -242,9 +243,9 @@ pub async fn push_send_update(ut: UpdateType, send: &Send, acting_device_uuid: &
|
|||||||
"identifier": acting_device_uuid,
|
"identifier": acting_device_uuid,
|
||||||
"type": ut as i32,
|
"type": ut as i32,
|
||||||
"payload": {
|
"payload": {
|
||||||
"id": send.uuid,
|
"Id": send.uuid,
|
||||||
"userId": send.user_uuid,
|
"UserId": send.user_uuid,
|
||||||
"revisionDate": send.revision_date
|
"RevisionDate": format_date(&send.revision_date)
|
||||||
}
|
}
|
||||||
})));
|
})));
|
||||||
}
|
}
|
||||||
@@ -295,8 +296,8 @@ pub async fn push_auth_request(user_uuid: String, auth_request_uuid: String, con
|
|||||||
"identifier": null,
|
"identifier": null,
|
||||||
"type": UpdateType::AuthRequest as i32,
|
"type": UpdateType::AuthRequest as i32,
|
||||||
"payload": {
|
"payload": {
|
||||||
"id": auth_request_uuid,
|
"Id": auth_request_uuid,
|
||||||
"userId": user_uuid,
|
"UserId": user_uuid,
|
||||||
}
|
}
|
||||||
})));
|
})));
|
||||||
}
|
}
|
||||||
@@ -316,8 +317,8 @@ pub async fn push_auth_response(
|
|||||||
"identifier": approving_device_uuid,
|
"identifier": approving_device_uuid,
|
||||||
"type": UpdateType::AuthRequestResponse as i32,
|
"type": UpdateType::AuthRequestResponse as i32,
|
||||||
"payload": {
|
"payload": {
|
||||||
"id": auth_request_uuid,
|
"Id": auth_request_uuid,
|
||||||
"userId": user_uuid,
|
"UserId": user_uuid,
|
||||||
}
|
}
|
||||||
})));
|
})));
|
||||||
}
|
}
|
||||||
|
103
src/api/web.rs
103
src/api/web.rs
@@ -1,13 +1,20 @@
|
|||||||
|
use once_cell::sync::Lazy;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
use rocket::{fs::NamedFile, http::ContentType, response::content::RawHtml as Html, serde::json::Json, Catcher, Route};
|
use rocket::{
|
||||||
|
fs::NamedFile,
|
||||||
|
http::ContentType,
|
||||||
|
response::{content::RawCss as Css, content::RawHtml as Html, Redirect},
|
||||||
|
serde::json::Json,
|
||||||
|
Catcher, Route,
|
||||||
|
};
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
api::{core::now, ApiResult, EmptyResult},
|
api::{core::now, ApiResult, EmptyResult},
|
||||||
auth::decode_file_download,
|
auth::decode_file_download,
|
||||||
error::Error,
|
error::Error,
|
||||||
util::{Cached, SafeString},
|
util::{get_web_vault_version, Cached, SafeString},
|
||||||
CONFIG,
|
CONFIG,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -16,7 +23,7 @@ pub fn routes() -> Vec<Route> {
|
|||||||
// crate::utils::LOGGED_ROUTES to make sure they appear in the log
|
// crate::utils::LOGGED_ROUTES to make sure they appear in the log
|
||||||
let mut routes = routes![attachments, alive, alive_head, static_files];
|
let mut routes = routes![attachments, alive, alive_head, static_files];
|
||||||
if CONFIG.web_vault_enabled() {
|
if CONFIG.web_vault_enabled() {
|
||||||
routes.append(&mut routes![web_index, web_index_head, app_id, web_files]);
|
routes.append(&mut routes![web_index, web_index_direct, web_index_head, app_id, web_files, vaultwarden_css]);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(debug_assertions)]
|
#[cfg(debug_assertions)]
|
||||||
@@ -45,11 +52,101 @@ fn not_found() -> ApiResult<Html<String>> {
|
|||||||
Ok(Html(text))
|
Ok(Html(text))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[get("/css/vaultwarden.css")]
|
||||||
|
fn vaultwarden_css() -> Cached<Css<String>> {
|
||||||
|
// Configure the web-vault version as an integer so it can be used as a comparison smaller or greater then.
|
||||||
|
// The default is based upon the version since this feature is added.
|
||||||
|
static WEB_VAULT_VERSION: Lazy<u32> = Lazy::new(|| {
|
||||||
|
let re = regex::Regex::new(r"(\d{4})\.(\d{1,2})\.(\d{1,2})").unwrap();
|
||||||
|
let vault_version = get_web_vault_version();
|
||||||
|
|
||||||
|
let (major, minor, patch) = match re.captures(&vault_version) {
|
||||||
|
Some(c) if c.len() == 4 => (
|
||||||
|
c.get(1).unwrap().as_str().parse().unwrap(),
|
||||||
|
c.get(2).unwrap().as_str().parse().unwrap(),
|
||||||
|
c.get(3).unwrap().as_str().parse().unwrap(),
|
||||||
|
),
|
||||||
|
_ => (2024, 6, 2),
|
||||||
|
};
|
||||||
|
format!("{major}{minor:02}{patch:02}").parse::<u32>().unwrap()
|
||||||
|
});
|
||||||
|
|
||||||
|
// Configure the Vaultwarden version as an integer so it can be used as a comparison smaller or greater then.
|
||||||
|
// The default is based upon the version since this feature is added.
|
||||||
|
static VW_VERSION: Lazy<u32> = Lazy::new(|| {
|
||||||
|
let re = regex::Regex::new(r"(\d{1})\.(\d{1,2})\.(\d{1,2})").unwrap();
|
||||||
|
let vw_version = crate::VERSION.unwrap_or("1.32.1");
|
||||||
|
|
||||||
|
let (major, minor, patch) = match re.captures(vw_version) {
|
||||||
|
Some(c) if c.len() == 4 => (
|
||||||
|
c.get(1).unwrap().as_str().parse().unwrap(),
|
||||||
|
c.get(2).unwrap().as_str().parse().unwrap(),
|
||||||
|
c.get(3).unwrap().as_str().parse().unwrap(),
|
||||||
|
),
|
||||||
|
_ => (1, 32, 1),
|
||||||
|
};
|
||||||
|
format!("{major}{minor:02}{patch:02}").parse::<u32>().unwrap()
|
||||||
|
});
|
||||||
|
|
||||||
|
let css_options = json!({
|
||||||
|
"web_vault_version": *WEB_VAULT_VERSION,
|
||||||
|
"vw_version": *VW_VERSION,
|
||||||
|
"signup_disabled": !CONFIG.signups_allowed() && CONFIG.signups_domains_whitelist().is_empty(),
|
||||||
|
"mail_enabled": CONFIG.mail_enabled(),
|
||||||
|
"yubico_enabled": CONFIG._enable_yubico() && (CONFIG.yubico_client_id().is_some() == CONFIG.yubico_secret_key().is_some()),
|
||||||
|
"emergency_access_allowed": CONFIG.emergency_access_allowed(),
|
||||||
|
"sends_allowed": CONFIG.sends_allowed(),
|
||||||
|
"load_user_scss": true,
|
||||||
|
});
|
||||||
|
|
||||||
|
let scss = match CONFIG.render_template("scss/vaultwarden.scss", &css_options) {
|
||||||
|
Ok(t) => t,
|
||||||
|
Err(e) => {
|
||||||
|
// Something went wrong loading the template. Use the fallback
|
||||||
|
warn!("Loading scss/vaultwarden.scss.hbs or scss/user.vaultwarden.scss.hbs failed. {e}");
|
||||||
|
CONFIG
|
||||||
|
.render_fallback_template("scss/vaultwarden.scss", &css_options)
|
||||||
|
.expect("Fallback scss/vaultwarden.scss.hbs to render")
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let css = match grass_compiler::from_string(
|
||||||
|
scss,
|
||||||
|
&grass_compiler::Options::default().style(grass_compiler::OutputStyle::Compressed),
|
||||||
|
) {
|
||||||
|
Ok(css) => css,
|
||||||
|
Err(e) => {
|
||||||
|
// Something went wrong compiling the scss. Use the fallback
|
||||||
|
warn!("Compiling the Vaultwarden SCSS styles failed. {e}");
|
||||||
|
let mut css_options = css_options;
|
||||||
|
css_options["load_user_scss"] = json!(false);
|
||||||
|
let scss = CONFIG
|
||||||
|
.render_fallback_template("scss/vaultwarden.scss", &css_options)
|
||||||
|
.expect("Fallback scss/vaultwarden.scss.hbs to render");
|
||||||
|
grass_compiler::from_string(
|
||||||
|
scss,
|
||||||
|
&grass_compiler::Options::default().style(grass_compiler::OutputStyle::Compressed),
|
||||||
|
)
|
||||||
|
.expect("SCSS to compile")
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Cache for one day should be enough and not too much
|
||||||
|
Cached::ttl(Css(css), 86_400, false)
|
||||||
|
}
|
||||||
|
|
||||||
#[get("/")]
|
#[get("/")]
|
||||||
async fn web_index() -> Cached<Option<NamedFile>> {
|
async fn web_index() -> Cached<Option<NamedFile>> {
|
||||||
Cached::short(NamedFile::open(Path::new(&CONFIG.web_vault_folder()).join("index.html")).await.ok(), false)
|
Cached::short(NamedFile::open(Path::new(&CONFIG.web_vault_folder()).join("index.html")).await.ok(), false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Make sure that `/index.html` redirect to actual domain path.
|
||||||
|
// If not, this might cause issues with the web-vault
|
||||||
|
#[get("/index.html")]
|
||||||
|
fn web_index_direct() -> Redirect {
|
||||||
|
Redirect::to(format!("{}/", CONFIG.domain_path()))
|
||||||
|
}
|
||||||
|
|
||||||
#[head("/")]
|
#[head("/")]
|
||||||
fn web_index_head() -> EmptyResult {
|
fn web_index_head() -> EmptyResult {
|
||||||
// Add an explicit HEAD route to prevent uptime monitoring services from
|
// Add an explicit HEAD route to prevent uptime monitoring services from
|
||||||
|
24
src/auth.rs
24
src/auth.rs
@@ -615,7 +615,6 @@ pub struct AdminHeaders {
|
|||||||
pub device: Device,
|
pub device: Device,
|
||||||
pub user: User,
|
pub user: User,
|
||||||
pub org_user_type: UserOrgType,
|
pub org_user_type: UserOrgType,
|
||||||
pub client_version: Option<String>,
|
|
||||||
pub ip: ClientIp,
|
pub ip: ClientIp,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -625,14 +624,12 @@ impl<'r> FromRequest<'r> for AdminHeaders {
|
|||||||
|
|
||||||
async fn from_request(request: &'r Request<'_>) -> Outcome<Self, Self::Error> {
|
async fn from_request(request: &'r Request<'_>) -> Outcome<Self, Self::Error> {
|
||||||
let headers = try_outcome!(OrgHeaders::from_request(request).await);
|
let headers = try_outcome!(OrgHeaders::from_request(request).await);
|
||||||
let client_version = request.headers().get_one("Bitwarden-Client-Version").map(String::from);
|
|
||||||
if headers.org_user_type >= UserOrgType::Admin {
|
if headers.org_user_type >= UserOrgType::Admin {
|
||||||
Outcome::Success(Self {
|
Outcome::Success(Self {
|
||||||
host: headers.host,
|
host: headers.host,
|
||||||
device: headers.device,
|
device: headers.device,
|
||||||
user: headers.user,
|
user: headers.user,
|
||||||
org_user_type: headers.org_user_type,
|
org_user_type: headers.org_user_type,
|
||||||
client_version,
|
|
||||||
ip: headers.ip,
|
ip: headers.ip,
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
@@ -900,3 +897,24 @@ impl<'r> FromRequest<'r> for WsAccessTokenHeader {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub struct ClientVersion(pub semver::Version);
|
||||||
|
|
||||||
|
#[rocket::async_trait]
|
||||||
|
impl<'r> FromRequest<'r> for ClientVersion {
|
||||||
|
type Error = &'static str;
|
||||||
|
|
||||||
|
async fn from_request(request: &'r Request<'_>) -> Outcome<Self, Self::Error> {
|
||||||
|
let headers = request.headers();
|
||||||
|
|
||||||
|
let Some(version) = headers.get_one("Bitwarden-Client-Version") else {
|
||||||
|
err_handler!("No Bitwarden-Client-Version header provided")
|
||||||
|
};
|
||||||
|
|
||||||
|
let Ok(version) = semver::Version::parse(version) else {
|
||||||
|
err_handler!("Invalid Bitwarden-Client-Version header provided")
|
||||||
|
};
|
||||||
|
|
||||||
|
Outcome::Success(ClientVersion(version))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@@ -497,11 +497,11 @@ make_config! {
|
|||||||
/// Password iterations |> Number of server-side passwords hashing iterations for the password hash.
|
/// Password iterations |> Number of server-side passwords hashing iterations for the password hash.
|
||||||
/// The default for new users. If changed, it will be updated during login for existing users.
|
/// The default for new users. If changed, it will be updated during login for existing users.
|
||||||
password_iterations: i32, true, def, 600_000;
|
password_iterations: i32, true, def, 600_000;
|
||||||
/// Allow password hints |> Controls whether users can set password hints. This setting applies globally to all users.
|
/// Allow password hints |> Controls whether users can set or show password hints. This setting applies globally to all users.
|
||||||
password_hints_allowed: bool, true, def, true;
|
password_hints_allowed: bool, true, def, true;
|
||||||
/// Show password hint |> Controls whether a password hint should be shown directly in the web page
|
/// Show password hint (Know the risks!) |> Controls whether a password hint should be shown directly in the web page
|
||||||
/// if SMTP service is not configured. Not recommended for publicly-accessible instances as this
|
/// if SMTP service is not configured and password hints are allowed. Not recommended for publicly-accessible instances
|
||||||
/// provides unauthenticated access to potentially sensitive data.
|
/// because this provides unauthenticated access to potentially sensitive data.
|
||||||
show_password_hint: bool, true, def, false;
|
show_password_hint: bool, true, def, false;
|
||||||
|
|
||||||
/// Admin token/Argon2 PHC |> The plain text token or Argon2 PHC string used to authenticate in this very same page. Changing it here will not deauthorize the current session!
|
/// Admin token/Argon2 PHC |> The plain text token or Argon2 PHC string used to authenticate in this very same page. Changing it here will not deauthorize the current session!
|
||||||
@@ -811,8 +811,15 @@ fn validate_config(cfg: &ConfigItems) -> Result<(), Error> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// TODO: deal with deprecated flags so they can be removed from this list, cf. #4263
|
// TODO: deal with deprecated flags so they can be removed from this list, cf. #4263
|
||||||
const KNOWN_FLAGS: &[&str] =
|
const KNOWN_FLAGS: &[&str] = &[
|
||||||
&["autofill-overlay", "autofill-v2", "browser-fileless-import", "extension-refresh", "fido2-vault-credentials"];
|
"autofill-overlay",
|
||||||
|
"autofill-v2",
|
||||||
|
"browser-fileless-import",
|
||||||
|
"extension-refresh",
|
||||||
|
"fido2-vault-credentials",
|
||||||
|
"ssh-key-vault-item",
|
||||||
|
"ssh-agent",
|
||||||
|
];
|
||||||
let configured_flags = parse_experimental_client_feature_flags(&cfg.experimental_client_feature_flags);
|
let configured_flags = parse_experimental_client_feature_flags(&cfg.experimental_client_feature_flags);
|
||||||
let invalid_flags: Vec<_> = configured_flags.keys().filter(|flag| !KNOWN_FLAGS.contains(&flag.as_str())).collect();
|
let invalid_flags: Vec<_> = configured_flags.keys().filter(|flag| !KNOWN_FLAGS.contains(&flag.as_str())).collect();
|
||||||
if !invalid_flags.is_empty() {
|
if !invalid_flags.is_empty() {
|
||||||
@@ -1269,11 +1276,16 @@ impl Config {
|
|||||||
let hb = load_templates(CONFIG.templates_folder());
|
let hb = load_templates(CONFIG.templates_folder());
|
||||||
hb.render(name, data).map_err(Into::into)
|
hb.render(name, data).map_err(Into::into)
|
||||||
} else {
|
} else {
|
||||||
let hb = &CONFIG.inner.read().unwrap().templates;
|
let hb = &self.inner.read().unwrap().templates;
|
||||||
hb.render(name, data).map_err(Into::into)
|
hb.render(name, data).map_err(Into::into)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn render_fallback_template<T: serde::ser::Serialize>(&self, name: &str, data: &T) -> Result<String, Error> {
|
||||||
|
let hb = &self.inner.read().unwrap().templates;
|
||||||
|
hb.render(&format!("fallback_{name}"), data).map_err(Into::into)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn set_rocket_shutdown_handle(&self, handle: rocket::Shutdown) {
|
pub fn set_rocket_shutdown_handle(&self, handle: rocket::Shutdown) {
|
||||||
self.inner.write().unwrap().rocket_shutdown_handle = Some(handle);
|
self.inner.write().unwrap().rocket_shutdown_handle = Some(handle);
|
||||||
}
|
}
|
||||||
@@ -1312,6 +1324,11 @@ where
|
|||||||
reg!($name);
|
reg!($name);
|
||||||
reg!(concat!($name, $ext));
|
reg!(concat!($name, $ext));
|
||||||
}};
|
}};
|
||||||
|
(@withfallback $name:expr) => {{
|
||||||
|
let template = include_str!(concat!("static/templates/", $name, ".hbs"));
|
||||||
|
hb.register_template_string($name, template).unwrap();
|
||||||
|
hb.register_template_string(concat!("fallback_", $name), template).unwrap();
|
||||||
|
}};
|
||||||
}
|
}
|
||||||
|
|
||||||
// First register default templates here
|
// First register default templates here
|
||||||
@@ -1355,6 +1372,9 @@ where
|
|||||||
|
|
||||||
reg!("404");
|
reg!("404");
|
||||||
|
|
||||||
|
reg!(@withfallback "scss/vaultwarden.scss");
|
||||||
|
reg!("scss/user.vaultwarden.scss");
|
||||||
|
|
||||||
// And then load user templates to overwrite the defaults
|
// And then load user templates to overwrite the defaults
|
||||||
// Use .hbs extension for the files
|
// Use .hbs extension for the files
|
||||||
// Templates get registered with their relative name
|
// Templates get registered with their relative name
|
||||||
|
@@ -373,14 +373,6 @@ pub async fn backup_database(conn: &mut DbConn) -> Result<String, Error> {
|
|||||||
err!("PostgreSQL and MySQL/MariaDB do not support this backup feature");
|
err!("PostgreSQL and MySQL/MariaDB do not support this backup feature");
|
||||||
}
|
}
|
||||||
sqlite {
|
sqlite {
|
||||||
backup_sqlite_database(conn)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(sqlite)]
|
|
||||||
pub fn backup_sqlite_database(conn: &mut diesel::sqlite::SqliteConnection) -> Result<String, Error> {
|
|
||||||
use diesel::RunQueryDsl;
|
|
||||||
let db_url = CONFIG.database_url();
|
let db_url = CONFIG.database_url();
|
||||||
let db_path = std::path::Path::new(&db_url).parent().unwrap();
|
let db_path = std::path::Path::new(&db_url).parent().unwrap();
|
||||||
let backup_file = db_path
|
let backup_file = db_path
|
||||||
@@ -389,6 +381,8 @@ pub fn backup_sqlite_database(conn: &mut diesel::sqlite::SqliteConnection) -> Re
|
|||||||
.into_owned();
|
.into_owned();
|
||||||
diesel::sql_query(format!("VACUUM INTO '{backup_file}'")).execute(conn)?;
|
diesel::sql_query(format!("VACUUM INTO '{backup_file}'")).execute(conn)?;
|
||||||
Ok(backup_file)
|
Ok(backup_file)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the SQL Server version
|
/// Get the SQL Server version
|
||||||
|
@@ -30,7 +30,8 @@ db_object! {
|
|||||||
Login = 1,
|
Login = 1,
|
||||||
SecureNote = 2,
|
SecureNote = 2,
|
||||||
Card = 3,
|
Card = 3,
|
||||||
Identity = 4
|
Identity = 4,
|
||||||
|
SshKey = 5
|
||||||
*/
|
*/
|
||||||
pub atype: i32,
|
pub atype: i32,
|
||||||
pub name: String,
|
pub name: String,
|
||||||
@@ -45,10 +46,9 @@ db_object! {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(dead_code)]
|
|
||||||
pub enum RepromptType {
|
pub enum RepromptType {
|
||||||
None = 0,
|
None = 0,
|
||||||
Password = 1, // not currently used in server
|
Password = 1,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Local methods
|
/// Local methods
|
||||||
@@ -295,7 +295,7 @@ impl Cipher {
|
|||||||
"creationDate": format_date(&self.created_at),
|
"creationDate": format_date(&self.created_at),
|
||||||
"revisionDate": format_date(&self.updated_at),
|
"revisionDate": format_date(&self.updated_at),
|
||||||
"deletedDate": self.deleted_at.map_or(Value::Null, |d| Value::String(format_date(&d))),
|
"deletedDate": self.deleted_at.map_or(Value::Null, |d| Value::String(format_date(&d))),
|
||||||
"reprompt": self.reprompt.unwrap_or(RepromptType::None as i32),
|
"reprompt": self.reprompt.filter(|r| *r == RepromptType::None as i32 || *r == RepromptType::Password as i32).unwrap_or(RepromptType::None as i32),
|
||||||
"organizationId": self.organization_uuid,
|
"organizationId": self.organization_uuid,
|
||||||
"key": self.key,
|
"key": self.key,
|
||||||
"attachments": attachments_json,
|
"attachments": attachments_json,
|
||||||
@@ -319,6 +319,7 @@ impl Cipher {
|
|||||||
"secureNote": null,
|
"secureNote": null,
|
||||||
"card": null,
|
"card": null,
|
||||||
"identity": null,
|
"identity": null,
|
||||||
|
"sshKey": null,
|
||||||
});
|
});
|
||||||
|
|
||||||
// These values are only needed for user/default syncs
|
// These values are only needed for user/default syncs
|
||||||
@@ -347,6 +348,7 @@ impl Cipher {
|
|||||||
2 => "secureNote",
|
2 => "secureNote",
|
||||||
3 => "card",
|
3 => "card",
|
||||||
4 => "identity",
|
4 => "identity",
|
||||||
|
5 => "sshKey",
|
||||||
_ => panic!("Wrong type"),
|
_ => panic!("Wrong type"),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
use super::{User, UserOrgType, UserOrganization};
|
use super::{User, UserOrganization};
|
||||||
use crate::api::EmptyResult;
|
use crate::api::EmptyResult;
|
||||||
use crate::db::DbConn;
|
use crate::db::DbConn;
|
||||||
use crate::error::MapResult;
|
use crate::error::MapResult;
|
||||||
@@ -73,7 +73,7 @@ impl Group {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn to_json_details(&self, user_org_type: &i32, conn: &mut DbConn) -> Value {
|
pub async fn to_json_details(&self, conn: &mut DbConn) -> Value {
|
||||||
let collections_groups: Vec<Value> = CollectionGroup::find_by_group(&self.uuid, conn)
|
let collections_groups: Vec<Value> = CollectionGroup::find_by_group(&self.uuid, conn)
|
||||||
.await
|
.await
|
||||||
.iter()
|
.iter()
|
||||||
@@ -82,7 +82,7 @@ impl Group {
|
|||||||
"id": entry.collections_uuid,
|
"id": entry.collections_uuid,
|
||||||
"readOnly": entry.read_only,
|
"readOnly": entry.read_only,
|
||||||
"hidePasswords": entry.hide_passwords,
|
"hidePasswords": entry.hide_passwords,
|
||||||
"manage": *user_org_type >= UserOrgType::Admin || (*user_org_type == UserOrgType::Manager && !entry.read_only && !entry.hide_passwords)
|
"manage": false
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
@@ -18,7 +18,7 @@ mod user;
|
|||||||
|
|
||||||
pub use self::attachment::Attachment;
|
pub use self::attachment::Attachment;
|
||||||
pub use self::auth_request::AuthRequest;
|
pub use self::auth_request::AuthRequest;
|
||||||
pub use self::cipher::Cipher;
|
pub use self::cipher::{Cipher, RepromptType};
|
||||||
pub use self::collection::{Collection, CollectionCipher, CollectionUser};
|
pub use self::collection::{Collection, CollectionCipher, CollectionUser};
|
||||||
pub use self::device::{Device, DeviceType};
|
pub use self::device::{Device, DeviceType};
|
||||||
pub use self::emergency_access::{EmergencyAccess, EmergencyAccessStatus, EmergencyAccessType};
|
pub use self::emergency_access::{EmergencyAccess, EmergencyAccessStatus, EmergencyAccessType};
|
||||||
|
@@ -462,7 +462,13 @@ impl UserOrganization {
|
|||||||
Vec::with_capacity(0)
|
Vec::with_capacity(0)
|
||||||
};
|
};
|
||||||
|
|
||||||
let collections: Vec<Value> = if include_collections {
|
// Check if a user is in a group which has access to all collections
|
||||||
|
// If that is the case, we should not return individual collections!
|
||||||
|
let full_access_group =
|
||||||
|
CONFIG.org_groups_enabled() && Group::is_in_full_access_group(&self.user_uuid, &self.org_uuid, conn).await;
|
||||||
|
|
||||||
|
// If collections are to be included, only include them if the user does not have full access via a group or defined to the user it self
|
||||||
|
let collections: Vec<Value> = if include_collections && !(full_access_group || self.has_full_access()) {
|
||||||
// Get all collections for the user here already to prevent more queries
|
// Get all collections for the user here already to prevent more queries
|
||||||
let cu: HashMap<String, CollectionUser> =
|
let cu: HashMap<String, CollectionUser> =
|
||||||
CollectionUser::find_by_organization_and_user_uuid(&self.org_uuid, &self.user_uuid, conn)
|
CollectionUser::find_by_organization_and_user_uuid(&self.org_uuid, &self.user_uuid, conn)
|
||||||
|
31
src/main.rs
31
src/main.rs
@@ -67,7 +67,7 @@ pub use util::is_running_in_container;
|
|||||||
|
|
||||||
#[rocket::main]
|
#[rocket::main]
|
||||||
async fn main() -> Result<(), Error> {
|
async fn main() -> Result<(), Error> {
|
||||||
parse_args();
|
parse_args().await;
|
||||||
launch_info();
|
launch_info();
|
||||||
|
|
||||||
let level = init_logging()?;
|
let level = init_logging()?;
|
||||||
@@ -115,7 +115,7 @@ PRESETS: m= t= p=
|
|||||||
|
|
||||||
pub const VERSION: Option<&str> = option_env!("VW_VERSION");
|
pub const VERSION: Option<&str> = option_env!("VW_VERSION");
|
||||||
|
|
||||||
fn parse_args() {
|
async fn parse_args() {
|
||||||
let mut pargs = pico_args::Arguments::from_env();
|
let mut pargs = pico_args::Arguments::from_env();
|
||||||
let version = VERSION.unwrap_or("(Version info from Git not present)");
|
let version = VERSION.unwrap_or("(Version info from Git not present)");
|
||||||
|
|
||||||
@@ -186,7 +186,7 @@ fn parse_args() {
|
|||||||
exit(1);
|
exit(1);
|
||||||
}
|
}
|
||||||
} else if command == "backup" {
|
} else if command == "backup" {
|
||||||
match backup_sqlite() {
|
match backup_sqlite().await {
|
||||||
Ok(f) => {
|
Ok(f) => {
|
||||||
println!("Backup to '{f}' was successful");
|
println!("Backup to '{f}' was successful");
|
||||||
exit(0);
|
exit(0);
|
||||||
@@ -201,26 +201,21 @@ fn parse_args() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn backup_sqlite() -> Result<String, Error> {
|
async fn backup_sqlite() -> Result<String, Error> {
|
||||||
#[cfg(sqlite)]
|
use crate::db::{backup_database, DbConnType};
|
||||||
{
|
|
||||||
use crate::db::{backup_sqlite_database, DbConnType};
|
|
||||||
if DbConnType::from_url(&CONFIG.database_url()).map(|t| t == DbConnType::sqlite).unwrap_or(false) {
|
if DbConnType::from_url(&CONFIG.database_url()).map(|t| t == DbConnType::sqlite).unwrap_or(false) {
|
||||||
use diesel::Connection;
|
|
||||||
let url = CONFIG.database_url();
|
|
||||||
|
|
||||||
// Establish a connection to the sqlite database
|
// Establish a connection to the sqlite database
|
||||||
let mut conn = diesel::sqlite::SqliteConnection::establish(&url)?;
|
let mut conn = db::DbPool::from_config()
|
||||||
let backup_file = backup_sqlite_database(&mut conn)?;
|
.expect("SQLite database connection failed")
|
||||||
|
.get()
|
||||||
|
.await
|
||||||
|
.expect("Unable to get SQLite db pool");
|
||||||
|
|
||||||
|
let backup_file = backup_database(&mut conn).await?;
|
||||||
Ok(backup_file)
|
Ok(backup_file)
|
||||||
} else {
|
} else {
|
||||||
err_silent!("The database type is not SQLite. Backups only works for SQLite databases")
|
err_silent!("The database type is not SQLite. Backups only works for SQLite databases")
|
||||||
}
|
}
|
||||||
}
|
|
||||||
#[cfg(not(sqlite))]
|
|
||||||
{
|
|
||||||
err_silent!("The 'sqlite' feature is not enabled. Backups only works for SQLite databases")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn launch_info() {
|
fn launch_info() {
|
||||||
@@ -610,7 +605,7 @@ async fn launch_rocket(pool: db::DbPool, extra_debug: bool) -> Result<(), Error>
|
|||||||
// If we need more signals to act upon, we might want to use select! here.
|
// If we need more signals to act upon, we might want to use select! here.
|
||||||
// With only one item to listen for this is enough.
|
// With only one item to listen for this is enough.
|
||||||
let _ = signal_user1.recv().await;
|
let _ = signal_user1.recv().await;
|
||||||
match backup_sqlite() {
|
match backup_sqlite().await {
|
||||||
Ok(f) => info!("Backup to '{f}' was successful"),
|
Ok(f) => info!("Backup to '{f}' was successful"),
|
||||||
Err(e) => error!("Backup failed. {e:?}"),
|
Err(e) => error!("Backup failed. {e:?}"),
|
||||||
}
|
}
|
||||||
|
4
src/static/scripts/admin.css
vendored
4
src/static/scripts/admin.css
vendored
@@ -38,8 +38,8 @@ img {
|
|||||||
max-width: 130px;
|
max-width: 130px;
|
||||||
}
|
}
|
||||||
#users-table .vw-actions, #orgs-table .vw-actions {
|
#users-table .vw-actions, #orgs-table .vw-actions {
|
||||||
min-width: 130px;
|
min-width: 135px;
|
||||||
max-width: 130px;
|
max-width: 140px;
|
||||||
}
|
}
|
||||||
#users-table .vw-org-cell {
|
#users-table .vw-org-cell {
|
||||||
max-height: 120px;
|
max-height: 120px;
|
||||||
|
212
src/static/scripts/admin_diagnostics.js
vendored
212
src/static/scripts/admin_diagnostics.js
vendored
@@ -7,6 +7,8 @@ var timeCheck = false;
|
|||||||
var ntpTimeCheck = false;
|
var ntpTimeCheck = false;
|
||||||
var domainCheck = false;
|
var domainCheck = false;
|
||||||
var httpsCheck = false;
|
var httpsCheck = false;
|
||||||
|
var websocketCheck = false;
|
||||||
|
var httpResponseCheck = false;
|
||||||
|
|
||||||
// ================================
|
// ================================
|
||||||
// Date & Time Check
|
// Date & Time Check
|
||||||
@@ -76,18 +78,15 @@ async function generateSupportString(event, dj) {
|
|||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
|
|
||||||
let supportString = "### Your environment (Generated via diagnostics page)\n";
|
let supportString = "### Your environment (Generated via diagnostics page)\n\n";
|
||||||
|
|
||||||
supportString += `* Vaultwarden version: v${dj.current_release}\n`;
|
supportString += `* Vaultwarden version: v${dj.current_release}\n`;
|
||||||
supportString += `* Web-vault version: v${dj.web_vault_version}\n`;
|
supportString += `* Web-vault version: v${dj.web_vault_version}\n`;
|
||||||
supportString += `* OS/Arch: ${dj.host_os}/${dj.host_arch}\n`;
|
supportString += `* OS/Arch: ${dj.host_os}/${dj.host_arch}\n`;
|
||||||
supportString += `* Running within a container: ${dj.running_within_container} (Base: ${dj.container_base_image})\n`;
|
supportString += `* Running within a container: ${dj.running_within_container} (Base: ${dj.container_base_image})\n`;
|
||||||
supportString += "* Environment settings overridden: ";
|
supportString += `* Database type: ${dj.db_type}\n`;
|
||||||
if (dj.overrides != "") {
|
supportString += `* Database version: ${dj.db_version}\n`;
|
||||||
supportString += "true\n";
|
supportString += `* Environment settings overridden!: ${dj.overrides !== ""}\n`;
|
||||||
} else {
|
|
||||||
supportString += "false\n";
|
|
||||||
}
|
|
||||||
supportString += `* Uses a reverse proxy: ${dj.ip_header_exists}\n`;
|
supportString += `* Uses a reverse proxy: ${dj.ip_header_exists}\n`;
|
||||||
if (dj.ip_header_exists) {
|
if (dj.ip_header_exists) {
|
||||||
supportString += `* IP Header check: ${dj.ip_header_match} (${dj.ip_header_name})\n`;
|
supportString += `* IP Header check: ${dj.ip_header_match} (${dj.ip_header_name})\n`;
|
||||||
@@ -99,11 +98,12 @@ async function generateSupportString(event, dj) {
|
|||||||
supportString += `* Server/NTP Time Check: ${ntpTimeCheck}\n`;
|
supportString += `* Server/NTP Time Check: ${ntpTimeCheck}\n`;
|
||||||
supportString += `* Domain Configuration Check: ${domainCheck}\n`;
|
supportString += `* Domain Configuration Check: ${domainCheck}\n`;
|
||||||
supportString += `* HTTPS Check: ${httpsCheck}\n`;
|
supportString += `* HTTPS Check: ${httpsCheck}\n`;
|
||||||
supportString += `* Database type: ${dj.db_type}\n`;
|
if (dj.enable_websocket) {
|
||||||
supportString += `* Database version: ${dj.db_version}\n`;
|
supportString += `* Websocket Check: ${websocketCheck}\n`;
|
||||||
supportString += "* Clients used: \n";
|
} else {
|
||||||
supportString += "* Reverse proxy and version: \n";
|
supportString += "* Websocket Check: disabled\n";
|
||||||
supportString += "* Other relevant information: \n";
|
}
|
||||||
|
supportString += `* HTTP Response Checks: ${httpResponseCheck}\n`;
|
||||||
|
|
||||||
const jsonResponse = await fetch(`${BASE_URL}/admin/diagnostics/config`, {
|
const jsonResponse = await fetch(`${BASE_URL}/admin/diagnostics/config`, {
|
||||||
"headers": { "Accept": "application/json" }
|
"headers": { "Accept": "application/json" }
|
||||||
@@ -113,10 +113,30 @@ async function generateSupportString(event, dj) {
|
|||||||
throw new Error(jsonResponse);
|
throw new Error(jsonResponse);
|
||||||
}
|
}
|
||||||
const configJson = await jsonResponse.json();
|
const configJson = await jsonResponse.json();
|
||||||
supportString += "\n### Config (Generated via diagnostics page)\n<details><summary>Show Running Config</summary>\n";
|
|
||||||
supportString += `\n**Environment settings which are overridden:** ${dj.overrides}\n`;
|
|
||||||
supportString += "\n\n```json\n" + JSON.stringify(configJson, undefined, 2) + "\n```\n</details>\n";
|
|
||||||
|
|
||||||
|
// Start Config and Details section within a details block which is collapsed by default
|
||||||
|
supportString += "\n### Config & Details (Generated via diagnostics page)\n\n";
|
||||||
|
supportString += "<details><summary>Show Config & Details</summary>\n";
|
||||||
|
|
||||||
|
// Add overrides if they exists
|
||||||
|
if (dj.overrides != "") {
|
||||||
|
supportString += `\n**Environment settings which are overridden:** ${dj.overrides}\n`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add http response check messages if they exists
|
||||||
|
if (httpResponseCheck === false) {
|
||||||
|
supportString += "\n**Failed HTTP Checks:**\n";
|
||||||
|
// We use `innerText` here since that will convert <br> into new-lines
|
||||||
|
supportString += "\n```yaml\n" + document.getElementById("http-response-errors").innerText.trim() + "\n```\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add the current config in json form
|
||||||
|
supportString += "\n**Config:**\n";
|
||||||
|
supportString += "\n```json\n" + JSON.stringify(configJson, undefined, 2) + "\n```\n";
|
||||||
|
|
||||||
|
supportString += "\n</details>\n";
|
||||||
|
|
||||||
|
// Add the support string to the textbox so it can be viewed and copied
|
||||||
document.getElementById("support-string").textContent = supportString;
|
document.getElementById("support-string").textContent = supportString;
|
||||||
document.getElementById("support-string").classList.remove("d-none");
|
document.getElementById("support-string").classList.remove("d-none");
|
||||||
document.getElementById("copy-support").classList.remove("d-none");
|
document.getElementById("copy-support").classList.remove("d-none");
|
||||||
@@ -199,6 +219,162 @@ function checkDns(dns_resolved) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function fetchCheckUrl(url) {
|
||||||
|
try {
|
||||||
|
const response = await fetch(url);
|
||||||
|
return { headers: response.headers, status: response.status, text: await response.text() };
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Error fetching ${url}: ${error}`);
|
||||||
|
return { error };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function checkSecurityHeaders(headers, omit) {
|
||||||
|
let securityHeaders = {
|
||||||
|
"x-frame-options": ["SAMEORIGIN"],
|
||||||
|
"x-content-type-options": ["nosniff"],
|
||||||
|
"referrer-policy": ["same-origin"],
|
||||||
|
"x-xss-protection": ["0"],
|
||||||
|
"x-robots-tag": ["noindex", "nofollow"],
|
||||||
|
"content-security-policy": [
|
||||||
|
"default-src 'self'",
|
||||||
|
"base-uri 'self'",
|
||||||
|
"form-action 'self'",
|
||||||
|
"object-src 'self' blob:",
|
||||||
|
"script-src 'self' 'wasm-unsafe-eval'",
|
||||||
|
"style-src 'self' 'unsafe-inline'",
|
||||||
|
"child-src 'self' https://*.duosecurity.com https://*.duofederal.com",
|
||||||
|
"frame-src 'self' https://*.duosecurity.com https://*.duofederal.com",
|
||||||
|
"frame-ancestors 'self' chrome-extension://nngceckbapebfimnlniiiahkandclblb chrome-extension://jbkfoedolllekgbhcbcoahefnbanhhlh moz-extension://*",
|
||||||
|
"img-src 'self' data: https://haveibeenpwned.com",
|
||||||
|
"connect-src 'self' https://api.pwnedpasswords.com https://api.2fa.directory https://app.simplelogin.io/api/ https://app.addy.io/api/ https://api.fastmail.com/ https://api.forwardemail.net",
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
let messages = [];
|
||||||
|
for (let header in securityHeaders) {
|
||||||
|
// Skip some headers for specific endpoints if needed
|
||||||
|
if (typeof omit === "object" && omit.includes(header) === true) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// If the header exists, check if the contents matches what we expect it to be
|
||||||
|
let headerValue = headers.get(header);
|
||||||
|
if (headerValue !== null) {
|
||||||
|
securityHeaders[header].forEach((expectedValue) => {
|
||||||
|
if (headerValue.indexOf(expectedValue) === -1) {
|
||||||
|
messages.push(`'${header}' does not contain '${expectedValue}'`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
messages.push(`'${header}' is missing!`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return messages;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function checkHttpResponse() {
|
||||||
|
const [apiConfig, webauthnConnector, notFound, notFoundApi, badRequest, unauthorized, forbidden] = await Promise.all([
|
||||||
|
fetchCheckUrl(`${BASE_URL}/api/config`),
|
||||||
|
fetchCheckUrl(`${BASE_URL}/webauthn-connector.html`),
|
||||||
|
fetchCheckUrl(`${BASE_URL}/admin/does-not-exist`),
|
||||||
|
fetchCheckUrl(`${BASE_URL}/admin/diagnostics/http?code=404`),
|
||||||
|
fetchCheckUrl(`${BASE_URL}/admin/diagnostics/http?code=400`),
|
||||||
|
fetchCheckUrl(`${BASE_URL}/admin/diagnostics/http?code=401`),
|
||||||
|
fetchCheckUrl(`${BASE_URL}/admin/diagnostics/http?code=403`),
|
||||||
|
]);
|
||||||
|
|
||||||
|
const respErrorElm = document.getElementById("http-response-errors");
|
||||||
|
|
||||||
|
// Check and validate the default API header responses
|
||||||
|
let apiErrors = checkSecurityHeaders(apiConfig.headers);
|
||||||
|
if (apiErrors.length >= 1) {
|
||||||
|
respErrorElm.innerHTML += "<b>API calls:</b><br>";
|
||||||
|
apiErrors.forEach((errMsg) => {
|
||||||
|
respErrorElm.innerHTML += `<b>Header:</b> ${errMsg}<br>`;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check the special `-connector.html` headers, these should have some headers omitted.
|
||||||
|
const omitConnectorHeaders = ["x-frame-options", "content-security-policy"];
|
||||||
|
let connectorErrors = checkSecurityHeaders(webauthnConnector.headers, omitConnectorHeaders);
|
||||||
|
omitConnectorHeaders.forEach((header) => {
|
||||||
|
if (webauthnConnector.headers.get(header) !== null) {
|
||||||
|
connectorErrors.push(`'${header}' is present while it should not`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (connectorErrors.length >= 1) {
|
||||||
|
respErrorElm.innerHTML += "<b>2FA Connector calls:</b><br>";
|
||||||
|
connectorErrors.forEach((errMsg) => {
|
||||||
|
respErrorElm.innerHTML += `<b>Header:</b> ${errMsg}<br>`;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check specific error code responses if they are not re-written by a reverse proxy
|
||||||
|
let responseErrors = [];
|
||||||
|
if (notFound.status !== 404 || notFound.text.indexOf("return to the web-vault") === -1) {
|
||||||
|
responseErrors.push("404 (Not Found) HTML is invalid");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (notFoundApi.status !== 404 || notFoundApi.text.indexOf("\"message\":\"Testing error 404 response\",") === -1) {
|
||||||
|
responseErrors.push("404 (Not Found) JSON is invalid");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (badRequest.status !== 400 || badRequest.text.indexOf("\"message\":\"Testing error 400 response\",") === -1) {
|
||||||
|
responseErrors.push("400 (Bad Request) is invalid");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (unauthorized.status !== 401 || unauthorized.text.indexOf("\"message\":\"Testing error 401 response\",") === -1) {
|
||||||
|
responseErrors.push("401 (Unauthorized) is invalid");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (forbidden.status !== 403 || forbidden.text.indexOf("\"message\":\"Testing error 403 response\",") === -1) {
|
||||||
|
responseErrors.push("403 (Forbidden) is invalid");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (responseErrors.length >= 1) {
|
||||||
|
respErrorElm.innerHTML += "<b>HTTP error responses:</b><br>";
|
||||||
|
responseErrors.forEach((errMsg) => {
|
||||||
|
respErrorElm.innerHTML += `<b>Response to:</b> ${errMsg}<br>`;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (responseErrors.length >= 1 || connectorErrors.length >= 1 || apiErrors.length >= 1) {
|
||||||
|
document.getElementById("http-response-warning").classList.remove("d-none");
|
||||||
|
} else {
|
||||||
|
httpResponseCheck = true;
|
||||||
|
document.getElementById("http-response-success").classList.remove("d-none");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchWsUrl(wsUrl) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
try {
|
||||||
|
const ws = new WebSocket(wsUrl);
|
||||||
|
ws.onopen = () => {
|
||||||
|
ws.close();
|
||||||
|
resolve(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
ws.onerror = () => {
|
||||||
|
reject(false);
|
||||||
|
};
|
||||||
|
} catch (_) {
|
||||||
|
reject(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function checkWebsocketConnection() {
|
||||||
|
// Test Websocket connections via the anonymous (login with device) connection
|
||||||
|
const isConnected = await fetchWsUrl(`${BASE_URL}/notifications/anonymous-hub?token=admin-diagnostics`).catch(() => false);
|
||||||
|
if (isConnected) {
|
||||||
|
websocketCheck = true;
|
||||||
|
document.getElementById("websocket-success").classList.remove("d-none");
|
||||||
|
} else {
|
||||||
|
document.getElementById("websocket-error").classList.remove("d-none");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function init(dj) {
|
function init(dj) {
|
||||||
// Time check
|
// Time check
|
||||||
document.getElementById("time-browser-string").textContent = browserUTC;
|
document.getElementById("time-browser-string").textContent = browserUTC;
|
||||||
@@ -225,6 +401,12 @@ function init(dj) {
|
|||||||
|
|
||||||
// DNS Check
|
// DNS Check
|
||||||
checkDns(dj.dns_resolved);
|
checkDns(dj.dns_resolved);
|
||||||
|
|
||||||
|
checkHttpResponse();
|
||||||
|
|
||||||
|
if (dj.enable_websocket) {
|
||||||
|
checkWebsocketConnection();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// onLoad events
|
// onLoad events
|
||||||
|
42
src/static/scripts/datatables.css
vendored
42
src/static/scripts/datatables.css
vendored
@@ -4,10 +4,10 @@
|
|||||||
*
|
*
|
||||||
* To rebuild or modify this file with the latest versions of the included
|
* To rebuild or modify this file with the latest versions of the included
|
||||||
* software please visit:
|
* software please visit:
|
||||||
* https://datatables.net/download/#bs5/dt-2.0.8
|
* https://datatables.net/download/#bs5/dt-2.1.8
|
||||||
*
|
*
|
||||||
* Included libraries:
|
* Included libraries:
|
||||||
* DataTables 2.0.8
|
* DataTables 2.1.8
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@charset "UTF-8";
|
@charset "UTF-8";
|
||||||
@@ -45,15 +45,21 @@ table.dataTable tr.dt-hasChild td.dt-control:before {
|
|||||||
}
|
}
|
||||||
|
|
||||||
html.dark table.dataTable td.dt-control:before,
|
html.dark table.dataTable td.dt-control:before,
|
||||||
:root[data-bs-theme=dark] table.dataTable td.dt-control:before {
|
:root[data-bs-theme=dark] table.dataTable td.dt-control:before,
|
||||||
|
:root[data-theme=dark] table.dataTable td.dt-control:before {
|
||||||
border-left-color: rgba(255, 255, 255, 0.5);
|
border-left-color: rgba(255, 255, 255, 0.5);
|
||||||
}
|
}
|
||||||
html.dark table.dataTable tr.dt-hasChild td.dt-control:before,
|
html.dark table.dataTable tr.dt-hasChild td.dt-control:before,
|
||||||
:root[data-bs-theme=dark] table.dataTable tr.dt-hasChild td.dt-control:before {
|
:root[data-bs-theme=dark] table.dataTable tr.dt-hasChild td.dt-control:before,
|
||||||
|
:root[data-theme=dark] table.dataTable tr.dt-hasChild td.dt-control:before {
|
||||||
border-top-color: rgba(255, 255, 255, 0.5);
|
border-top-color: rgba(255, 255, 255, 0.5);
|
||||||
border-left-color: transparent;
|
border-left-color: transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
div.dt-scroll {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
div.dt-scroll-body thead tr,
|
div.dt-scroll-body thead tr,
|
||||||
div.dt-scroll-body tfoot tr {
|
div.dt-scroll-body tfoot tr {
|
||||||
height: 0;
|
height: 0;
|
||||||
@@ -377,6 +383,31 @@ table.table.dataTable.table-hover > tbody > tr.selected:hover > * {
|
|||||||
box-shadow: inset 0 0 0 9999px rgba(var(--dt-row-selected), 0.975);
|
box-shadow: inset 0 0 0 9999px rgba(var(--dt-row-selected), 0.975);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
div.dt-container div.dt-layout-start > *:not(:last-child) {
|
||||||
|
margin-right: 1em;
|
||||||
|
}
|
||||||
|
div.dt-container div.dt-layout-end > *:not(:first-child) {
|
||||||
|
margin-left: 1em;
|
||||||
|
}
|
||||||
|
div.dt-container div.dt-layout-full {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
div.dt-container div.dt-layout-full > *:only-child {
|
||||||
|
margin-left: auto;
|
||||||
|
margin-right: auto;
|
||||||
|
}
|
||||||
|
div.dt-container div.dt-layout-table > div {
|
||||||
|
display: block !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 767px) {
|
||||||
|
div.dt-container div.dt-layout-start > *:not(:last-child) {
|
||||||
|
margin-right: 0;
|
||||||
|
}
|
||||||
|
div.dt-container div.dt-layout-end > *:not(:first-child) {
|
||||||
|
margin-left: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
div.dt-container div.dt-length label {
|
div.dt-container div.dt-length label {
|
||||||
font-weight: normal;
|
font-weight: normal;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
@@ -400,9 +431,6 @@ div.dt-container div.dt-search input {
|
|||||||
display: inline-block;
|
display: inline-block;
|
||||||
width: auto;
|
width: auto;
|
||||||
}
|
}
|
||||||
div.dt-container div.dt-info {
|
|
||||||
padding-top: 0.85em;
|
|
||||||
}
|
|
||||||
div.dt-container div.dt-paging {
|
div.dt-container div.dt-paging {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
1142
src/static/scripts/datatables.js
vendored
1142
src/static/scripts/datatables.js
vendored
File diff suppressed because it is too large
Load Diff
@@ -132,6 +132,21 @@
|
|||||||
<span class="d-block" title="We have direct internet access, no outgoing proxy configured."><b>No</b></span>
|
<span class="d-block" title="We have direct internet access, no outgoing proxy configured."><b>No</b></span>
|
||||||
{{/unless}}
|
{{/unless}}
|
||||||
</dd>
|
</dd>
|
||||||
|
<dt class="col-sm-5">Websocket enabled
|
||||||
|
{{#if page_data.enable_websocket}}
|
||||||
|
<span class="badge bg-success d-none" id="websocket-success" title="Websocket connection is working.">Ok</span>
|
||||||
|
<span class="badge bg-danger d-none" id="websocket-error" title="Websocket connection error, validate your reverse proxy configuration!">Error</span>
|
||||||
|
{{/if}}
|
||||||
|
</dt>
|
||||||
|
<dd class="col-sm-7">
|
||||||
|
{{#if page_data.enable_websocket}}
|
||||||
|
<span class="d-block" title="Websocket connections are enabled (ENABLE_WEBSOCKET is true)."><b>Yes</b></span>
|
||||||
|
{{/if}}
|
||||||
|
{{#unless page_data.enable_websocket}}
|
||||||
|
<span class="d-block" title="Websocket connections are disabled (ENABLE_WEBSOCKET is false)."><b>No</b></span>
|
||||||
|
{{/unless}}
|
||||||
|
</dd>
|
||||||
|
|
||||||
<dt class="col-sm-5">DNS (github.com)
|
<dt class="col-sm-5">DNS (github.com)
|
||||||
<span class="badge bg-success d-none" id="dns-success" title="DNS Resolving works!">Ok</span>
|
<span class="badge bg-success d-none" id="dns-success" title="DNS Resolving works!">Ok</span>
|
||||||
<span class="badge bg-danger d-none" id="dns-warning" title="DNS Resolving failed. Please fix.">Error</span>
|
<span class="badge bg-danger d-none" id="dns-warning" title="DNS Resolving failed. Please fix.">Error</span>
|
||||||
@@ -167,6 +182,14 @@
|
|||||||
<span id="domain-server" class="d-block"><b>Server:</b> <span id="domain-server-string">{{page_data.admin_url}}</span></span>
|
<span id="domain-server" class="d-block"><b>Server:</b> <span id="domain-server-string">{{page_data.admin_url}}</span></span>
|
||||||
<span id="domain-browser" class="d-block"><b>Browser:</b> <span id="domain-browser-string"></span></span>
|
<span id="domain-browser" class="d-block"><b>Browser:</b> <span id="domain-browser-string"></span></span>
|
||||||
</dd>
|
</dd>
|
||||||
|
|
||||||
|
<dt class="col-sm-5">HTTP Response validation
|
||||||
|
<span class="badge bg-success d-none" id="http-response-success" title="All headers and HTTP request responses seem to be ok.">Ok</span>
|
||||||
|
<span class="badge bg-danger d-none" id="http-response-warning" title="Some headers or HTTP request responses return invalid data!">Error</span>
|
||||||
|
</dt>
|
||||||
|
<dd class="col-sm-7">
|
||||||
|
<span id="http-response-errors" class="d-block"></span>
|
||||||
|
</dd>
|
||||||
</dl>
|
</dl>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -19,7 +19,7 @@
|
|||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
<svg width="48" height="48" class="float-start me-2 rounded" data-jdenticon-value="{{email}}">
|
<svg width="48" height="48" class="float-start me-2 rounded" data-jdenticon-value="{{email}}">
|
||||||
<div class="float-start">
|
<div>
|
||||||
<strong>{{name}}</strong>
|
<strong>{{name}}</strong>
|
||||||
<span class="d-block">{{email}}</span>
|
<span class="d-block">{{email}}</span>
|
||||||
<span class="d-block">
|
<span class="d-block">
|
||||||
|
1
src/static/templates/scss/user.vaultwarden.scss.hbs
Normal file
1
src/static/templates/scss/user.vaultwarden.scss.hbs
Normal file
@@ -0,0 +1 @@
|
|||||||
|
/* See the wiki for examples and details: https://github.com/dani-garcia/vaultwarden/wiki/Customize-Vaultwarden-CSS */
|
105
src/static/templates/scss/vaultwarden.scss.hbs
Normal file
105
src/static/templates/scss/vaultwarden.scss.hbs
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
/**** START Static Vaultwarden changes ****/
|
||||||
|
/* This combines all selectors extending it into one */
|
||||||
|
%vw-hide {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* This allows searching for the combined style in the browsers dev-tools (look into the head tag) */
|
||||||
|
.vw-hide,
|
||||||
|
head {
|
||||||
|
@extend %vw-hide;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Hide the Subscription Page tab */
|
||||||
|
bit-nav-item[route="settings/subscription"] {
|
||||||
|
@extend %vw-hide;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Hide any link pointing to Free Bitwarden Families */
|
||||||
|
a[href$="/settings/sponsored-families"] {
|
||||||
|
@extend %vw-hide;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Hide the `Enterprise Single Sign-On` button on the login page */
|
||||||
|
a[routerlink="/sso"] {
|
||||||
|
@extend %vw-hide;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Hide Two-Factor menu in Organization settings */
|
||||||
|
bit-nav-item[route="settings/two-factor"],
|
||||||
|
a[href$="/settings/two-factor"] {
|
||||||
|
@extend %vw-hide;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Hide Business Owned checkbox */
|
||||||
|
app-org-info > form:nth-child(1) > div:nth-child(3) {
|
||||||
|
@extend %vw-hide;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Hide the `This account is owned by a business` checkbox and label */
|
||||||
|
#ownedBusiness,
|
||||||
|
label[for^="ownedBusiness"] {
|
||||||
|
@extend %vw-hide;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Hide the radio button and label for the `Custom` org user type */
|
||||||
|
#userTypeCustom,
|
||||||
|
label[for^="userTypeCustom"] {
|
||||||
|
@extend %vw-hide;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Hide Business Name */
|
||||||
|
app-org-account form div bit-form-field.tw-block:nth-child(3) {
|
||||||
|
@extend %vw-hide;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Hide organization plans */
|
||||||
|
app-organization-plans > form > bit-section:nth-child(2) {
|
||||||
|
@extend %vw-hide;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Hide Device Verification form at the Two Step Login screen */
|
||||||
|
app-security > app-two-factor-setup > form {
|
||||||
|
@extend %vw-hide;
|
||||||
|
}
|
||||||
|
/**** END Static Vaultwarden Changes ****/
|
||||||
|
/**** START Dynamic Vaultwarden Changes ****/
|
||||||
|
{{#if signup_disabled}}
|
||||||
|
/* Hide the register link on the login screen */
|
||||||
|
app-frontend-layout > app-login > form > div > div > div > p {
|
||||||
|
@extend %vw-hide;
|
||||||
|
}
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
|
/* Hide `Email` 2FA if mail is not enabled */
|
||||||
|
{{#unless mail_enabled}}
|
||||||
|
app-two-factor-setup ul.list-group.list-group-2fa li.list-group-item:nth-child(5) {
|
||||||
|
@extend %vw-hide;
|
||||||
|
}
|
||||||
|
{{/unless}}
|
||||||
|
|
||||||
|
/* Hide `YubiKey OTP security key` 2FA if it is not enabled */
|
||||||
|
{{#unless yubico_enabled}}
|
||||||
|
app-two-factor-setup ul.list-group.list-group-2fa li.list-group-item:nth-child(2) {
|
||||||
|
@extend %vw-hide;
|
||||||
|
}
|
||||||
|
{{/unless}}
|
||||||
|
|
||||||
|
/* Hide Emergency Access if not allowed */
|
||||||
|
{{#unless emergency_access_allowed}}
|
||||||
|
bit-nav-item[route="settings/emergency-access"] {
|
||||||
|
@extend %vw-hide;
|
||||||
|
}
|
||||||
|
{{/unless}}
|
||||||
|
|
||||||
|
/* Hide Sends if not allowed */
|
||||||
|
{{#unless sends_allowed}}
|
||||||
|
bit-nav-item[route="sends"] {
|
||||||
|
@extend %vw-hide;
|
||||||
|
}
|
||||||
|
{{/unless}}
|
||||||
|
/**** End Dynamic Vaultwarden Changes ****/
|
||||||
|
/**** Include a special user stylesheet for custom changes ****/
|
||||||
|
{{#if load_user_scss}}
|
||||||
|
{{> scss/user.vaultwarden.scss }}
|
||||||
|
{{/if}}
|
@@ -51,9 +51,11 @@ impl Fairing for AppHeaders {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NOTE: When modifying or adding security headers be sure to also update the diagnostic checks in `src/static/scripts/admin_diagnostics.js` in `checkSecurityHeaders`
|
||||||
res.set_raw_header("Permissions-Policy", "accelerometer=(), ambient-light-sensor=(), autoplay=(), battery=(), camera=(), display-capture=(), document-domain=(), encrypted-media=(), execution-while-not-rendered=(), execution-while-out-of-viewport=(), fullscreen=(), geolocation=(), gyroscope=(), keyboard-map=(), magnetometer=(), microphone=(), midi=(), payment=(), picture-in-picture=(), screen-wake-lock=(), sync-xhr=(), usb=(), web-share=(), xr-spatial-tracking=()");
|
res.set_raw_header("Permissions-Policy", "accelerometer=(), ambient-light-sensor=(), autoplay=(), battery=(), camera=(), display-capture=(), document-domain=(), encrypted-media=(), execution-while-not-rendered=(), execution-while-out-of-viewport=(), fullscreen=(), geolocation=(), gyroscope=(), keyboard-map=(), magnetometer=(), microphone=(), midi=(), payment=(), picture-in-picture=(), screen-wake-lock=(), sync-xhr=(), usb=(), web-share=(), xr-spatial-tracking=()");
|
||||||
res.set_raw_header("Referrer-Policy", "same-origin");
|
res.set_raw_header("Referrer-Policy", "same-origin");
|
||||||
res.set_raw_header("X-Content-Type-Options", "nosniff");
|
res.set_raw_header("X-Content-Type-Options", "nosniff");
|
||||||
|
res.set_raw_header("X-Robots-Tag", "noindex, nofollow");
|
||||||
// Obsolete in modern browsers, unsafe (XS-Leak), and largely replaced by CSP
|
// Obsolete in modern browsers, unsafe (XS-Leak), and largely replaced by CSP
|
||||||
res.set_raw_header("X-XSS-Protection", "0");
|
res.set_raw_header("X-XSS-Protection", "0");
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user