From c28eab74dd52c04104527b0c25e7acfb3dac13b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Garc=C3=ADa?= Date: Sun, 28 Dec 2025 23:44:50 +0100 Subject: [PATCH 01/89] Cached config operations --- src/api/web.rs | 32 +++++++++-------- src/config.rs | 94 ++++++++++++++++++++++++++++++++++++-------------- 2 files changed, 85 insertions(+), 41 deletions(-) diff --git a/src/api/web.rs b/src/api/web.rs index 98d51a5e..b2ab1e44 100644 --- a/src/api/web.rs +++ b/src/api/web.rs @@ -12,6 +12,7 @@ use serde_json::Value; use crate::{ api::{core::now, ApiResult, EmptyResult}, auth::decode_file_download, + config::CachedConfigOperation, db::models::{AttachmentId, CipherId}, error::Error, util::Cached, @@ -52,19 +53,18 @@ fn not_found() -> ApiResult> { Ok(Html(text)) } -#[get("/css/vaultwarden.css")] -fn vaultwarden_css() -> Cached> { +static VAULTWARDEN_CSS_CACHE: CachedConfigOperation = CachedConfigOperation::new(|config| { let css_options = json!({ - "emergency_access_allowed": CONFIG.emergency_access_allowed(), + "emergency_access_allowed": config.emergency_access_allowed(), "load_user_scss": true, - "mail_2fa_enabled": CONFIG._enable_email_2fa(), - "mail_enabled": CONFIG.mail_enabled(), - "sends_allowed": CONFIG.sends_allowed(), - "signup_disabled": CONFIG.is_signup_disabled(), - "sso_enabled": CONFIG.sso_enabled(), - "sso_only": CONFIG.sso_enabled() && CONFIG.sso_only(), - "yubico_enabled": CONFIG._enable_yubico() && CONFIG.yubico_client_id().is_some() && CONFIG.yubico_secret_key().is_some(), - "webauthn_2fa_supported": CONFIG.is_webauthn_2fa_supported(), + "mail_2fa_enabled": config._enable_email_2fa(), + "mail_enabled": config.mail_enabled(), + "sends_allowed": config.sends_allowed(), + "signup_disabled": config.is_signup_disabled(), + "sso_enabled": config.sso_enabled(), + "sso_only": config.sso_enabled() && config.sso_only(), + "yubico_enabled": config._enable_yubico() && config.yubico_client_id().is_some() && config.yubico_secret_key().is_some(), + "webauthn_2fa_supported": config.is_webauthn_2fa_supported(), }); let scss = match CONFIG.render_template("scss/vaultwarden.scss", &css_options) { @@ -78,7 +78,7 @@ fn vaultwarden_css() -> Cached> { } }; - let css = match grass_compiler::from_string( + match grass_compiler::from_string( scss, &grass_compiler::Options::default().style(grass_compiler::OutputStyle::Compressed), ) { @@ -97,10 +97,12 @@ fn vaultwarden_css() -> Cached> { ) .expect("SCSS to compile") } - }; + } +}); - // Cache for one day should be enough and not too much - Cached::ttl(Css(css), 86_400, false) +#[get("/css/vaultwarden.css")] +fn vaultwarden_css() -> Css { + Css(CONFIG.cached_operation(&VAULTWARDEN_CSS_CACHE)) } #[get("/")] diff --git a/src/config.rs b/src/config.rs index 812b12f6..93dcd166 100644 --- a/src/config.rs +++ b/src/config.rs @@ -3,7 +3,7 @@ use std::{ fmt, process::exit, sync::{ - atomic::{AtomicBool, Ordering}, + atomic::{AtomicBool, AtomicUsize, Ordering}, LazyLock, RwLock, }, }; @@ -103,6 +103,7 @@ macro_rules! make_config { struct Inner { rocket_shutdown_handle: Option, + revision: usize, templates: Handlebars<'static>, config: ConfigItems, @@ -322,7 +323,7 @@ macro_rules! make_config { } #[derive(Clone, Default)] - struct ConfigItems { $($( $name: make_config! {@type $ty, $none_action}, )+)+ } + struct ConfigItems { $($( pub $name: make_config! {@type $ty, $none_action}, )+)+ } #[derive(Serialize)] struct ElementDoc { @@ -1467,6 +1468,23 @@ pub enum PathType { RsaKey, } +pub struct CachedConfigOperation { + generator: fn(&Config) -> T, + value_cache: RwLock>, + revision: AtomicUsize, +} + +impl CachedConfigOperation { + #[allow(private_interfaces)] + pub const fn new(generator: fn(&Config) -> T) -> Self { + CachedConfigOperation { + generator, + value_cache: RwLock::new(None), + revision: AtomicUsize::new(0), + } + } +} + impl Config { pub async fn load() -> Result { // Loading from env and file @@ -1486,6 +1504,7 @@ impl Config { Ok(Config { inner: RwLock::new(Inner { rocket_shutdown_handle: None, + revision: 1, templates: load_templates(&config.templates_folder), config, _env, @@ -1524,6 +1543,7 @@ impl Config { writer.config = config; writer._usr = builder; writer._overrides = overrides; + writer.revision += 1; } //Save to file @@ -1542,6 +1562,51 @@ impl Config { self.update_config(builder, false).await } + pub async fn delete_user_config(&self) -> Result<(), Error> { + let operator = opendal_operator_for_path(&CONFIG_FILE_PARENT_DIR)?; + operator.delete(&CONFIG_FILENAME).await?; + + // Empty user config + let usr = ConfigBuilder::default(); + + // Config now is env + defaults + let config = { + let env = &self.inner.read().unwrap()._env; + env.build() + }; + + // Save configs + { + let mut writer = self.inner.write().unwrap(); + writer.config = config; + writer._usr = usr; + writer._overrides = Vec::new(); + writer.revision += 1; + } + + Ok(()) + } + + pub fn cached_operation(&self, operation: &CachedConfigOperation) -> T { + let config_revision = self.inner.read().unwrap().revision; + let cache_revision = operation.revision.load(Ordering::Relaxed); + + // If the current revision matches the cached revision, return the cached value + if cache_revision == config_revision { + let reader = operation.value_cache.read().unwrap(); + return reader.as_ref().unwrap().clone(); + } + + // Otherwise, compute the value, update the cache and revision, and return the new value + let value = (operation.generator)(&CONFIG); + { + let mut writer = operation.value_cache.write().unwrap(); + *writer = Some(value.clone()); + operation.revision.store(config_revision, Ordering::Relaxed); + } + value + } + /// Tests whether an email's domain is allowed. A domain is allowed if it /// is in signups_domains_whitelist, or if no whitelist is set (so there /// are no domain restrictions in effect). @@ -1591,33 +1656,10 @@ impl Config { } } - pub async fn delete_user_config(&self) -> Result<(), Error> { - let operator = opendal_operator_for_path(&CONFIG_FILE_PARENT_DIR)?; - operator.delete(&CONFIG_FILENAME).await?; - - // Empty user config - let usr = ConfigBuilder::default(); - - // Config now is env + defaults - let config = { - let env = &self.inner.read().unwrap()._env; - env.build() - }; - - // Save configs - { - let mut writer = self.inner.write().unwrap(); - writer.config = config; - writer._usr = usr; - writer._overrides = Vec::new(); - } - - Ok(()) - } - pub fn private_rsa_key(&self) -> String { format!("{}.pem", self.rsa_key_filename()) } + pub fn mail_enabled(&self) -> bool { let inner = &self.inner.read().unwrap().config; inner._enable_smtp && (inner.smtp_host.is_some() || inner.use_sendmail) From c4f6c4e63ba1690fe1f1dd68e9a9d4b948658711 Mon Sep 17 00:00:00 2001 From: Daniel Date: Mon, 29 Dec 2025 23:25:15 +0200 Subject: [PATCH 02/89] Re-add `alpine` tag (#6626) - fixes https://github.com/dani-garcia/vaultwarden/issues/6619 - also optimize the process while at it --- .github/workflows/release.yml | 38 +++++++++++++++++------------------ 1 file changed, 18 insertions(+), 20 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 5cbb2346..378682d3 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -313,45 +313,43 @@ jobs: # Determine Base Tags - name: Determine Base Tags env: + BASE_IMAGE_TAG: "${{ matrix.base_image != 'debian' && format('-{0}', matrix.base_image) || '' }}" REF_TYPE: ${{ github.ref_type }} run: | # Check which main tag we are going to build determined by ref_type if [[ "${REF_TYPE}" == "tag" ]]; then - echo "BASE_TAGS=latest,${GITHUB_REF#refs/*/}" | tee -a "${GITHUB_ENV}" + echo "BASE_TAGS=latest${BASE_IMAGE_TAG},${GITHUB_REF#refs/*/}${BASE_IMAGE_TAG}${BASE_IMAGE_TAG//-/,}" | tee -a "${GITHUB_ENV}" elif [[ "${REF_TYPE}" == "branch" ]]; then - echo "BASE_TAGS=testing" | tee -a "${GITHUB_ENV}" + echo "BASE_TAGS=testing${BASE_IMAGE_TAG}" | tee -a "${GITHUB_ENV}" fi - name: Create manifest list, push it and extract digest SHA working-directory: ${{ runner.temp }}/digests env: - BASE_IMAGE_TAG: "${{ matrix.base_image != 'debian' && format('-{0}', matrix.base_image) || '' }}" BASE_TAGS: "${{ env.BASE_TAGS }}" CONTAINER_REGISTRIES: "${{ env.CONTAINER_REGISTRIES }}" run: | - set +e IFS=',' read -ra IMAGES <<< "${CONTAINER_REGISTRIES}" IFS=',' read -ra TAGS <<< "${BASE_TAGS}" + + TAG_ARGS=() for img in "${IMAGES[@]}"; do for tag in "${TAGS[@]}"; do - echo "Creating manifest for ${img}:${tag}${BASE_IMAGE_TAG}" - - OUTPUT=$(docker buildx imagetools create \ - -t "${img}:${tag}${BASE_IMAGE_TAG}" \ - $(printf "${img}@sha256:%s " *) 2>&1) - STATUS=$? - - if [ ${STATUS} -ne 0 ]; then - echo "Manifest creation failed for ${img}:${tag}${BASE_IMAGE_TAG}" - echo "${OUTPUT}" - exit ${STATUS} - fi - - echo "Manifest created for ${img}:${tag}${BASE_IMAGE_TAG}" - echo "${OUTPUT}" + TAG_ARGS+=("-t" "${img}:${tag}") done done - set -e + + echo "Creating manifest" + if ! OUTPUT=$(docker buildx imagetools create \ + "${TAG_ARGS[@]}" \ + $(printf "${IMAGES[0]}@sha256:%s " *) 2>&1); then + echo "Manifest creation failed" + echo "${OUTPUT}" + exit 1 + fi + + echo "Manifest created successfully" + echo "${OUTPUT}" # Extract digest SHA for subsequent steps GET_DIGEST_SHA="$(echo "${OUTPUT}" | grep -oE 'sha256:[a-f0-9]{64}' | tail -1)" From 2af9d2115820342b6853bc541649f91ee60e5668 Mon Sep 17 00:00:00 2001 From: Mathijs van Veluw Date: Mon, 29 Dec 2025 21:27:12 +0000 Subject: [PATCH 03/89] Misc updates (#6627) - Update crates and toml - Update web-vault to v2025.12.1 - Update workflows Signed-off-by: BlackDex --- .github/workflows/typos.yml | 2 +- .pre-commit-config.yaml | 2 +- Cargo.lock | 26 ++++++++++++++++---------- Cargo.toml | 6 +++--- docker/DockerSettings.yaml | 4 ++-- docker/Dockerfile.alpine | 12 ++++++------ docker/Dockerfile.debian | 12 ++++++------ 7 files changed, 35 insertions(+), 29 deletions(-) diff --git a/.github/workflows/typos.yml b/.github/workflows/typos.yml index 1210a194..b3dae9b7 100644 --- a/.github/workflows/typos.yml +++ b/.github/workflows/typos.yml @@ -19,4 +19,4 @@ jobs: # When this version is updated, do not forget to update this in `.pre-commit-config.yaml` too - name: Spell Check Repo - uses: crate-ci/typos@2d0ce569feab1f8752f1dde43cc2f2aa53236e06 # v1.40.0 + uses: crate-ci/typos@1a319b54cc9e3b333fed6a5c88ba1a90324da514 # v1.40.1 diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 757afca2..448ccbeb 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -53,6 +53,6 @@ repos: - "cd docker && make" # When this version is updated, do not forget to update this in `.github/workflows/typos.yaml` too - repo: https://github.com/crate-ci/typos - rev: 2d0ce569feab1f8752f1dde43cc2f2aa53236e06 # v1.40.0 + rev: 1a319b54cc9e3b333fed6a5c88ba1a90324da514 # v1.40.1 hooks: - id: typos diff --git a/Cargo.lock b/Cargo.lock index 4d642585..07f5a49b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -718,9 +718,9 @@ dependencies = [ [[package]] name = "bigdecimal" -version = "0.4.9" +version = "0.4.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "560f42649de9fa436b73517378a147ec21f6c997a546581df4b4b31677828934" +checksum = "4d6867f1565b3aad85681f1015055b087fcfd840d6aeee6eee7f2da317603695" dependencies = [ "autocfg", "libm", @@ -2660,9 +2660,9 @@ checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" [[package]] name = "iri-string" -version = "0.7.9" +version = "0.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f867b9d1d896b67beb18518eda36fdb77a32ea590de864f1325b294a6d14397" +checksum = "c91338f0783edbd6195decb37bae672fd3b165faffb89bf7b9e6942f8b1a731a" dependencies = [ "memchr", "serde", @@ -3126,7 +3126,7 @@ dependencies = [ "libc", "log", "openssl", - "openssl-probe", + "openssl-probe 0.1.6", "openssl-sys", "schannel", "security-framework 2.11.1", @@ -3415,6 +3415,12 @@ version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" +[[package]] +name = "openssl-probe" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f50d9b3dabb09ecd771ad0aa242ca6894994c130308ca3d7684634df8037391" + [[package]] name = "openssl-src" version = "300.5.4+3.5.4" @@ -4520,11 +4526,11 @@ dependencies = [ [[package]] name = "rustls-native-certs" -version = "0.8.2" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9980d917ebb0c0536119ba501e90834767bffc3d60641457fd84a1f3fd337923" +checksum = "612460d5f7bea540c490b2b6395d8e34a953e52b491accd6c86c8164c5932a63" dependencies = [ - "openssl-probe", + "openssl-probe 0.2.0", "rustls-pki-types", "schannel", "security-framework 3.5.1", @@ -6624,9 +6630,9 @@ dependencies = [ [[package]] name = "zmij" -version = "1.0.0" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6d6085d62852e35540689d1f97ad663e3971fc19cf5eceab364d62c646ea167" +checksum = "0f4a4e8e9dc5c62d159f04fcdbe07f4c3fb710415aab4754bf11505501e3251d" [[package]] name = "zstd" diff --git a/Cargo.toml b/Cargo.toml index 277301ef..ea2d5ecb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -65,14 +65,14 @@ dotenvy = { version = "0.15.7", default-features = false } # Numerical libraries num-traits = "0.2.19" num-derive = "0.4.2" -bigdecimal = "0.4.9" +bigdecimal = "0.4.10" # Web framework rocket = { version = "0.5.1", features = ["tls", "json"], default-features = false } rocket_ws = { version ="0.1.1" } # WebSockets libraries -rmpv = "1.3.0" # MessagePack library +rmpv = "1.3.1" # MessagePack library # Concurrent HashMap used for WebSocket messaging and favicons dashmap = "6.1.0" @@ -84,7 +84,7 @@ tokio-util = { version = "0.7.17", features = ["compat"]} # A generic serialization/deserialization framework serde = { version = "1.0.228", features = ["derive"] } -serde_json = "1.0.145" +serde_json = "1.0.148" # A safe, extensible ORM and Query builder # Currently pinned diesel to v2.3.3 as newer version break MySQL/MariaDB compatibility diff --git a/docker/DockerSettings.yaml b/docker/DockerSettings.yaml index 9709f3ea..6635d99c 100644 --- a/docker/DockerSettings.yaml +++ b/docker/DockerSettings.yaml @@ -1,6 +1,6 @@ --- -vault_version: "v2025.12.0" -vault_image_digest: "sha256:bb7303efafdb7e2b41bee2c772e14f67676ae2c9047bd7bba80d3544d4162613" +vault_version: "v2025.12.1" +vault_image_digest: "sha256:dc718ffec13eccab8a849d65dd436b38730577b9b46be4672d97debc88e2c0ad" # Cross Compile Docker Helper Scripts v1.9.0 # We use the linux/amd64 platform shell scripts since there is no difference between the different platform scripts # https://github.com/tonistiigi/xx | https://hub.docker.com/r/tonistiigi/xx/tags diff --git a/docker/Dockerfile.alpine b/docker/Dockerfile.alpine index bfa91622..d30b00d9 100644 --- a/docker/Dockerfile.alpine +++ b/docker/Dockerfile.alpine @@ -19,15 +19,15 @@ # - From https://hub.docker.com/r/vaultwarden/web-vault/tags, # click the tag name to view the digest of the image it currently points to. # - From the command line: -# $ docker pull docker.io/vaultwarden/web-vault:v2025.12.0 -# $ docker image inspect --format "{{.RepoDigests}}" docker.io/vaultwarden/web-vault:v2025.12.0 -# [docker.io/vaultwarden/web-vault@sha256:bb7303efafdb7e2b41bee2c772e14f67676ae2c9047bd7bba80d3544d4162613] +# $ docker pull docker.io/vaultwarden/web-vault:v2025.12.1 +# $ docker image inspect --format "{{.RepoDigests}}" docker.io/vaultwarden/web-vault:v2025.12.1 +# [docker.io/vaultwarden/web-vault@sha256:dc718ffec13eccab8a849d65dd436b38730577b9b46be4672d97debc88e2c0ad] # # - Conversely, to get the tag name from the digest: -# $ docker image inspect --format "{{.RepoTags}}" docker.io/vaultwarden/web-vault@sha256:bb7303efafdb7e2b41bee2c772e14f67676ae2c9047bd7bba80d3544d4162613 -# [docker.io/vaultwarden/web-vault:v2025.12.0] +# $ docker image inspect --format "{{.RepoTags}}" docker.io/vaultwarden/web-vault@sha256:dc718ffec13eccab8a849d65dd436b38730577b9b46be4672d97debc88e2c0ad +# [docker.io/vaultwarden/web-vault:v2025.12.1] # -FROM --platform=linux/amd64 docker.io/vaultwarden/web-vault@sha256:bb7303efafdb7e2b41bee2c772e14f67676ae2c9047bd7bba80d3544d4162613 AS vault +FROM --platform=linux/amd64 docker.io/vaultwarden/web-vault@sha256:dc718ffec13eccab8a849d65dd436b38730577b9b46be4672d97debc88e2c0ad AS vault ########################## ALPINE BUILD IMAGES ########################## ## NOTE: The Alpine Base Images do not support other platforms then linux/amd64 and linux/arm64 diff --git a/docker/Dockerfile.debian b/docker/Dockerfile.debian index d66ee556..8e6b69e2 100644 --- a/docker/Dockerfile.debian +++ b/docker/Dockerfile.debian @@ -19,15 +19,15 @@ # - From https://hub.docker.com/r/vaultwarden/web-vault/tags, # click the tag name to view the digest of the image it currently points to. # - From the command line: -# $ docker pull docker.io/vaultwarden/web-vault:v2025.12.0 -# $ docker image inspect --format "{{.RepoDigests}}" docker.io/vaultwarden/web-vault:v2025.12.0 -# [docker.io/vaultwarden/web-vault@sha256:bb7303efafdb7e2b41bee2c772e14f67676ae2c9047bd7bba80d3544d4162613] +# $ docker pull docker.io/vaultwarden/web-vault:v2025.12.1 +# $ docker image inspect --format "{{.RepoDigests}}" docker.io/vaultwarden/web-vault:v2025.12.1 +# [docker.io/vaultwarden/web-vault@sha256:dc718ffec13eccab8a849d65dd436b38730577b9b46be4672d97debc88e2c0ad] # # - Conversely, to get the tag name from the digest: -# $ docker image inspect --format "{{.RepoTags}}" docker.io/vaultwarden/web-vault@sha256:bb7303efafdb7e2b41bee2c772e14f67676ae2c9047bd7bba80d3544d4162613 -# [docker.io/vaultwarden/web-vault:v2025.12.0] +# $ docker image inspect --format "{{.RepoTags}}" docker.io/vaultwarden/web-vault@sha256:dc718ffec13eccab8a849d65dd436b38730577b9b46be4672d97debc88e2c0ad +# [docker.io/vaultwarden/web-vault:v2025.12.1] # -FROM --platform=linux/amd64 docker.io/vaultwarden/web-vault@sha256:bb7303efafdb7e2b41bee2c772e14f67676ae2c9047bd7bba80d3544d4162613 AS vault +FROM --platform=linux/amd64 docker.io/vaultwarden/web-vault@sha256:dc718ffec13eccab8a849d65dd436b38730577b9b46be4672d97debc88e2c0ad AS vault ########################## Cross Compile Docker Helper Scripts ########################## ## We use the linux/amd64 no matter which Build Platform, since these are all bash scripts From 3e2cef7e8b27cf33cb735d428553f835bc5dd6c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Garc=C3=ADa?= Date: Mon, 29 Dec 2025 22:54:51 +0100 Subject: [PATCH 04/89] Try old refresh token if we fail to decode jwt (#6629) --- src/auth.rs | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/src/auth.rs b/src/auth.rs index 6360aaf6..ab41898f 100644 --- a/src/auth.rs +++ b/src/auth.rs @@ -1210,8 +1210,20 @@ pub async fn refresh_tokens( ) -> ApiResult<(Device, AuthTokens)> { let refresh_claims = match decode_refresh(refresh_token) { Err(err) => { - debug!("Failed to decode {} refresh_token: {refresh_token}", ip.ip); - err_silent!(format!("Impossible to read refresh_token: {}", err.message())) + error!("Failed to decode {} refresh_token: {refresh_token}: {err:?}", ip.ip); + //err_silent!(format!("Impossible to read refresh_token: {}", err.message())) + + // If the token failed to decode, it was probably one of the old style tokens that was just a Base64 string. + // We can generate a claim for them for backwards compatibility. Note that the password refresh claims don't + // check expiration or issuer, so they're not included here. + RefreshJwtClaims { + nbf: 0, + exp: 0, + iss: String::new(), + sub: AuthMethod::Password, + device_token: refresh_token.into(), + token: None, + } } Ok(claims) => claims, }; From bf37657c08aa3dd8b9c871d15d00c3a7bbcc756c Mon Sep 17 00:00:00 2001 From: Stefan Melmuk <509385+stefan0xC@users.noreply.github.com> Date: Thu, 1 Jan 2026 16:52:11 +0000 Subject: [PATCH 05/89] update web-vault to fix org creation (#6646) --- docker/DockerSettings.yaml | 4 ++-- docker/Dockerfile.alpine | 12 ++++++------ docker/Dockerfile.debian | 12 ++++++------ 3 files changed, 14 insertions(+), 14 deletions(-) diff --git a/docker/DockerSettings.yaml b/docker/DockerSettings.yaml index 6635d99c..e74f979c 100644 --- a/docker/DockerSettings.yaml +++ b/docker/DockerSettings.yaml @@ -1,6 +1,6 @@ --- -vault_version: "v2025.12.1" -vault_image_digest: "sha256:dc718ffec13eccab8a849d65dd436b38730577b9b46be4672d97debc88e2c0ad" +vault_version: "v2025.12.1.1" +vault_image_digest: "sha256:90261e5d5438b67a00cb12d8615cf3f130a65e81f33a3f5ff190c6202bf0e457" # Cross Compile Docker Helper Scripts v1.9.0 # We use the linux/amd64 platform shell scripts since there is no difference between the different platform scripts # https://github.com/tonistiigi/xx | https://hub.docker.com/r/tonistiigi/xx/tags diff --git a/docker/Dockerfile.alpine b/docker/Dockerfile.alpine index d30b00d9..6453ba1f 100644 --- a/docker/Dockerfile.alpine +++ b/docker/Dockerfile.alpine @@ -19,15 +19,15 @@ # - From https://hub.docker.com/r/vaultwarden/web-vault/tags, # click the tag name to view the digest of the image it currently points to. # - From the command line: -# $ docker pull docker.io/vaultwarden/web-vault:v2025.12.1 -# $ docker image inspect --format "{{.RepoDigests}}" docker.io/vaultwarden/web-vault:v2025.12.1 -# [docker.io/vaultwarden/web-vault@sha256:dc718ffec13eccab8a849d65dd436b38730577b9b46be4672d97debc88e2c0ad] +# $ docker pull docker.io/vaultwarden/web-vault:v2025.12.1.1 +# $ docker image inspect --format "{{.RepoDigests}}" docker.io/vaultwarden/web-vault:v2025.12.1.1 +# [docker.io/vaultwarden/web-vault@sha256:90261e5d5438b67a00cb12d8615cf3f130a65e81f33a3f5ff190c6202bf0e457] # # - Conversely, to get the tag name from the digest: -# $ docker image inspect --format "{{.RepoTags}}" docker.io/vaultwarden/web-vault@sha256:dc718ffec13eccab8a849d65dd436b38730577b9b46be4672d97debc88e2c0ad -# [docker.io/vaultwarden/web-vault:v2025.12.1] +# $ docker image inspect --format "{{.RepoTags}}" docker.io/vaultwarden/web-vault@sha256:90261e5d5438b67a00cb12d8615cf3f130a65e81f33a3f5ff190c6202bf0e457 +# [docker.io/vaultwarden/web-vault:v2025.12.1.1] # -FROM --platform=linux/amd64 docker.io/vaultwarden/web-vault@sha256:dc718ffec13eccab8a849d65dd436b38730577b9b46be4672d97debc88e2c0ad AS vault +FROM --platform=linux/amd64 docker.io/vaultwarden/web-vault@sha256:90261e5d5438b67a00cb12d8615cf3f130a65e81f33a3f5ff190c6202bf0e457 AS vault ########################## ALPINE BUILD IMAGES ########################## ## NOTE: The Alpine Base Images do not support other platforms then linux/amd64 and linux/arm64 diff --git a/docker/Dockerfile.debian b/docker/Dockerfile.debian index 8e6b69e2..25545f32 100644 --- a/docker/Dockerfile.debian +++ b/docker/Dockerfile.debian @@ -19,15 +19,15 @@ # - From https://hub.docker.com/r/vaultwarden/web-vault/tags, # click the tag name to view the digest of the image it currently points to. # - From the command line: -# $ docker pull docker.io/vaultwarden/web-vault:v2025.12.1 -# $ docker image inspect --format "{{.RepoDigests}}" docker.io/vaultwarden/web-vault:v2025.12.1 -# [docker.io/vaultwarden/web-vault@sha256:dc718ffec13eccab8a849d65dd436b38730577b9b46be4672d97debc88e2c0ad] +# $ docker pull docker.io/vaultwarden/web-vault:v2025.12.1.1 +# $ docker image inspect --format "{{.RepoDigests}}" docker.io/vaultwarden/web-vault:v2025.12.1.1 +# [docker.io/vaultwarden/web-vault@sha256:90261e5d5438b67a00cb12d8615cf3f130a65e81f33a3f5ff190c6202bf0e457] # # - Conversely, to get the tag name from the digest: -# $ docker image inspect --format "{{.RepoTags}}" docker.io/vaultwarden/web-vault@sha256:dc718ffec13eccab8a849d65dd436b38730577b9b46be4672d97debc88e2c0ad -# [docker.io/vaultwarden/web-vault:v2025.12.1] +# $ docker image inspect --format "{{.RepoTags}}" docker.io/vaultwarden/web-vault@sha256:90261e5d5438b67a00cb12d8615cf3f130a65e81f33a3f5ff190c6202bf0e457 +# [docker.io/vaultwarden/web-vault:v2025.12.1.1] # -FROM --platform=linux/amd64 docker.io/vaultwarden/web-vault@sha256:dc718ffec13eccab8a849d65dd436b38730577b9b46be4672d97debc88e2c0ad AS vault +FROM --platform=linux/amd64 docker.io/vaultwarden/web-vault@sha256:90261e5d5438b67a00cb12d8615cf3f130a65e81f33a3f5ff190c6202bf0e457 AS vault ########################## Cross Compile Docker Helper Scripts ########################## ## We use the linux/amd64 no matter which Build Platform, since these are all bash scripts From 1e1f9957cd037fad87e5cd33245720f865942016 Mon Sep 17 00:00:00 2001 From: Stefan Melmuk <509385+stefan0xC@users.noreply.github.com> Date: Mon, 5 Jan 2026 19:52:24 +0100 Subject: [PATCH 06/89] return no content with status code 204 (#6665) --- src/api/identity.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/api/identity.rs b/src/api/identity.rs index 59aba4a9..e763ef46 100644 --- a/src/api/identity.rs +++ b/src/api/identity.rs @@ -919,6 +919,7 @@ struct RegisterVerificationData { #[derive(rocket::Responder)] enum RegisterVerificationResponse { + #[response(status = 204)] NoContent(()), Token(Json), } From 9f1df422595cdfb04b8aea6968ae52a434887abc Mon Sep 17 00:00:00 2001 From: Stefan Melmuk <509385+stefan0xC@users.noreply.github.com> Date: Tue, 6 Jan 2026 15:24:05 +0100 Subject: [PATCH 07/89] allow MasterPasswordHash for Android (#6673) --- src/api/mod.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/api/mod.rs b/src/api/mod.rs index b988f053..ecdf9408 100644 --- a/src/api/mod.rs +++ b/src/api/mod.rs @@ -47,6 +47,7 @@ pub type EmptyResult = ApiResult<()>; #[derive(Deserialize)] #[serde(rename_all = "camelCase")] struct PasswordOrOtpData { + #[serde(alias = "MasterPasswordHash")] master_password_hash: Option, otp: Option, } From 8d08697cf84a65e87920cc05bbc6d9e815a106d2 Mon Sep 17 00:00:00 2001 From: Stefan Melmuk <509385+stefan0xC@users.noreply.github.com> Date: Tue, 6 Jan 2026 18:10:00 +0100 Subject: [PATCH 08/89] improve sso callback path (#6676) * normalize base_url for sso_callback_path * clean url when embedding images --- src/config.rs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/config.rs b/src/config.rs index 812b12f6..6bfdea80 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1325,12 +1325,16 @@ fn generate_smtp_img_src(embed_images: bool, domain: &str) -> String { if embed_images { "cid:".to_string() } else { - format!("{domain}/vw_static/") + // normalize base_url + let base_url = domain.trim_end_matches('/'); + format!("{base_url}/vw_static/") } } fn generate_sso_callback_path(domain: &str) -> String { - format!("{domain}/identity/connect/oidc-signin") + // normalize base_url + let base_url = domain.trim_end_matches('/'); + format!("{base_url}/identity/connect/oidc-signin") } /// Generate the correct URL for the icon service. From 4352fffeec7915e45559b46dce18640a25f46801 Mon Sep 17 00:00:00 2001 From: Mathijs van Veluw Date: Fri, 9 Jan 2026 12:21:10 +0000 Subject: [PATCH 09/89] Fix web-vault version check and update web-vault (#6686) --- docker/DockerSettings.yaml | 4 +- docker/Dockerfile.alpine | 12 ++-- docker/Dockerfile.debian | 12 ++-- docker/Dockerfile.j2 | 6 +- src/api/admin.rs | 78 ++++++++++++++++------ src/config.rs | 4 +- src/main.rs | 2 +- src/static/scripts/admin_diagnostics.js | 12 ++-- src/static/templates/admin/diagnostics.hbs | 4 +- src/util.rs | 2 +- 10 files changed, 87 insertions(+), 49 deletions(-) diff --git a/docker/DockerSettings.yaml b/docker/DockerSettings.yaml index e74f979c..dd87a9e3 100644 --- a/docker/DockerSettings.yaml +++ b/docker/DockerSettings.yaml @@ -1,6 +1,6 @@ --- -vault_version: "v2025.12.1.1" -vault_image_digest: "sha256:90261e5d5438b67a00cb12d8615cf3f130a65e81f33a3f5ff190c6202bf0e457" +vault_version: "v2025.12.1+build.3" +vault_image_digest: "sha256:bf5aa55dc7bcb99f85d2a88ff44d32cdc832e934a0603fe28e5c3f92904bad42" # Cross Compile Docker Helper Scripts v1.9.0 # We use the linux/amd64 platform shell scripts since there is no difference between the different platform scripts # https://github.com/tonistiigi/xx | https://hub.docker.com/r/tonistiigi/xx/tags diff --git a/docker/Dockerfile.alpine b/docker/Dockerfile.alpine index 6453ba1f..2a6cf9f2 100644 --- a/docker/Dockerfile.alpine +++ b/docker/Dockerfile.alpine @@ -19,15 +19,15 @@ # - From https://hub.docker.com/r/vaultwarden/web-vault/tags, # click the tag name to view the digest of the image it currently points to. # - From the command line: -# $ docker pull docker.io/vaultwarden/web-vault:v2025.12.1.1 -# $ docker image inspect --format "{{.RepoDigests}}" docker.io/vaultwarden/web-vault:v2025.12.1.1 -# [docker.io/vaultwarden/web-vault@sha256:90261e5d5438b67a00cb12d8615cf3f130a65e81f33a3f5ff190c6202bf0e457] +# $ docker pull docker.io/vaultwarden/web-vault:v2025.12.1_build.3 +# $ docker image inspect --format "{{.RepoDigests}}" docker.io/vaultwarden/web-vault:v2025.12.1_build.3 +# [docker.io/vaultwarden/web-vault@sha256:bf5aa55dc7bcb99f85d2a88ff44d32cdc832e934a0603fe28e5c3f92904bad42] # # - Conversely, to get the tag name from the digest: -# $ docker image inspect --format "{{.RepoTags}}" docker.io/vaultwarden/web-vault@sha256:90261e5d5438b67a00cb12d8615cf3f130a65e81f33a3f5ff190c6202bf0e457 -# [docker.io/vaultwarden/web-vault:v2025.12.1.1] +# $ docker image inspect --format "{{.RepoTags}}" docker.io/vaultwarden/web-vault@sha256:bf5aa55dc7bcb99f85d2a88ff44d32cdc832e934a0603fe28e5c3f92904bad42 +# [docker.io/vaultwarden/web-vault:v2025.12.1_build.3] # -FROM --platform=linux/amd64 docker.io/vaultwarden/web-vault@sha256:90261e5d5438b67a00cb12d8615cf3f130a65e81f33a3f5ff190c6202bf0e457 AS vault +FROM --platform=linux/amd64 docker.io/vaultwarden/web-vault@sha256:bf5aa55dc7bcb99f85d2a88ff44d32cdc832e934a0603fe28e5c3f92904bad42 AS vault ########################## ALPINE BUILD IMAGES ########################## ## NOTE: The Alpine Base Images do not support other platforms then linux/amd64 and linux/arm64 diff --git a/docker/Dockerfile.debian b/docker/Dockerfile.debian index 25545f32..03c0faba 100644 --- a/docker/Dockerfile.debian +++ b/docker/Dockerfile.debian @@ -19,15 +19,15 @@ # - From https://hub.docker.com/r/vaultwarden/web-vault/tags, # click the tag name to view the digest of the image it currently points to. # - From the command line: -# $ docker pull docker.io/vaultwarden/web-vault:v2025.12.1.1 -# $ docker image inspect --format "{{.RepoDigests}}" docker.io/vaultwarden/web-vault:v2025.12.1.1 -# [docker.io/vaultwarden/web-vault@sha256:90261e5d5438b67a00cb12d8615cf3f130a65e81f33a3f5ff190c6202bf0e457] +# $ docker pull docker.io/vaultwarden/web-vault:v2025.12.1_build.3 +# $ docker image inspect --format "{{.RepoDigests}}" docker.io/vaultwarden/web-vault:v2025.12.1_build.3 +# [docker.io/vaultwarden/web-vault@sha256:bf5aa55dc7bcb99f85d2a88ff44d32cdc832e934a0603fe28e5c3f92904bad42] # # - Conversely, to get the tag name from the digest: -# $ docker image inspect --format "{{.RepoTags}}" docker.io/vaultwarden/web-vault@sha256:90261e5d5438b67a00cb12d8615cf3f130a65e81f33a3f5ff190c6202bf0e457 -# [docker.io/vaultwarden/web-vault:v2025.12.1.1] +# $ docker image inspect --format "{{.RepoTags}}" docker.io/vaultwarden/web-vault@sha256:bf5aa55dc7bcb99f85d2a88ff44d32cdc832e934a0603fe28e5c3f92904bad42 +# [docker.io/vaultwarden/web-vault:v2025.12.1_build.3] # -FROM --platform=linux/amd64 docker.io/vaultwarden/web-vault@sha256:90261e5d5438b67a00cb12d8615cf3f130a65e81f33a3f5ff190c6202bf0e457 AS vault +FROM --platform=linux/amd64 docker.io/vaultwarden/web-vault@sha256:bf5aa55dc7bcb99f85d2a88ff44d32cdc832e934a0603fe28e5c3f92904bad42 AS vault ########################## Cross Compile Docker Helper Scripts ########################## ## We use the linux/amd64 no matter which Build Platform, since these are all bash scripts diff --git a/docker/Dockerfile.j2 b/docker/Dockerfile.j2 index cf8106bd..f745780e 100644 --- a/docker/Dockerfile.j2 +++ b/docker/Dockerfile.j2 @@ -19,13 +19,13 @@ # - From https://hub.docker.com/r/vaultwarden/web-vault/tags, # click the tag name to view the digest of the image it currently points to. # - From the command line: -# $ docker pull docker.io/vaultwarden/web-vault:{{ vault_version }} -# $ docker image inspect --format "{{ '{{' }}.RepoDigests}}" docker.io/vaultwarden/web-vault:{{ vault_version }} +# $ docker pull docker.io/vaultwarden/web-vault:{{ vault_version | replace('+', '_') }} +# $ docker image inspect --format "{{ '{{' }}.RepoDigests}}" docker.io/vaultwarden/web-vault:{{ vault_version | replace('+', '_') }} # [docker.io/vaultwarden/web-vault@{{ vault_image_digest }}] # # - Conversely, to get the tag name from the digest: # $ docker image inspect --format "{{ '{{' }}.RepoTags}}" docker.io/vaultwarden/web-vault@{{ vault_image_digest }} -# [docker.io/vaultwarden/web-vault:{{ vault_version }}] +# [docker.io/vaultwarden/web-vault:{{ vault_version | replace('+', '_') }}] # FROM --platform=linux/amd64 docker.io/vaultwarden/web-vault@{{ vault_image_digest }} AS vault diff --git a/src/api/admin.rs b/src/api/admin.rs index d36da8f9..badfaa3a 100644 --- a/src/api/admin.rs +++ b/src/api/admin.rs @@ -31,7 +31,7 @@ use crate::{ http_client::make_http_request, mail, util::{ - container_base_image, format_naive_datetime_local, get_display_size, get_web_vault_version, + container_base_image, format_naive_datetime_local, get_active_web_release, get_display_size, is_running_in_container, NumberOrString, }, CONFIG, VERSION, @@ -689,6 +689,26 @@ async fn get_ntp_time(has_http_access: bool) -> String { String::from("Unable to fetch NTP time.") } +fn web_vault_compare(active: &str, latest: &str) -> i8 { + use semver::Version; + use std::cmp::Ordering; + + let active_semver = Version::parse(active).unwrap_or_else(|e| { + warn!("Unable to parse active web-vault version '{active}': {e}"); + Version::parse("2025.1.1").unwrap() + }); + let latest_semver = Version::parse(latest).unwrap_or_else(|e| { + warn!("Unable to parse latest web-vault version '{latest}': {e}"); + Version::parse("2025.1.1").unwrap() + }); + + match active_semver.cmp(&latest_semver) { + Ordering::Less => -1, + Ordering::Equal => 0, + Ordering::Greater => 1, + } +} + #[get("/diagnostics")] async fn diagnostics(_token: AdminToken, ip_header: IpHeader, conn: DbConn) -> ApiResult> { use chrono::prelude::*; @@ -708,32 +728,21 @@ async fn diagnostics(_token: AdminToken, ip_header: IpHeader, conn: DbConn) -> A _ => "Unable to resolve domain name.".to_string(), }; - let (latest_release, latest_commit, latest_web_build) = get_release_info(has_http_access).await; + let (latest_vw_release, latest_vw_commit, latest_web_release) = get_release_info(has_http_access).await; + let active_web_release = get_active_web_release(); + let web_vault_compare = web_vault_compare(&active_web_release, &latest_web_release); let ip_header_name = &ip_header.0.unwrap_or_default(); - // Get current running versions - let web_vault_version = get_web_vault_version(); - - // Check if the running version is newer than the latest stable released version - let web_vault_pre_release = if let Ok(web_ver_match) = semver::VersionReq::parse(&format!(">{latest_web_build}")) { - web_ver_match.matches( - &semver::Version::parse(&web_vault_version).unwrap_or_else(|_| semver::Version::parse("2025.1.1").unwrap()), - ) - } else { - error!("Unable to parse latest_web_build: '{latest_web_build}'"); - false - }; - let diagnostics_json = json!({ "dns_resolved": dns_resolved, "current_release": VERSION, - "latest_release": latest_release, - "latest_commit": latest_commit, + "latest_release": latest_vw_release, + "latest_commit": latest_vw_commit, "web_vault_enabled": &CONFIG.web_vault_enabled(), - "web_vault_version": web_vault_version, - "latest_web_build": latest_web_build, - "web_vault_pre_release": web_vault_pre_release, + "active_web_release": active_web_release, + "latest_web_release": latest_web_release, + "web_vault_compare": web_vault_compare, "running_within_container": running_within_container, "container_base_image": if running_within_container { container_base_image() } else { "Not applicable" }, "has_http_access": has_http_access, @@ -844,3 +853,32 @@ impl<'r> FromRequest<'r> for AdminToken { }) } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn validate_web_vault_compare() { + // web_vault_compare(active, latest) + // Test normal versions + assert!(web_vault_compare("2025.12.0", "2025.12.1") == -1); + assert!(web_vault_compare("2025.12.1", "2025.12.1") == 0); + assert!(web_vault_compare("2025.12.2", "2025.12.1") == 1); + + // Test patched/+build.n versions + // Newer latest version + assert!(web_vault_compare("2025.12.0+build.1", "2025.12.1") == -1); + assert!(web_vault_compare("2025.12.1", "2025.12.1+build.1") == -1); + assert!(web_vault_compare("2025.12.0+build.1", "2025.12.1+build.1") == -1); + assert!(web_vault_compare("2025.12.1+build.1", "2025.12.1+build.2") == -1); + // Equal versions + assert!(web_vault_compare("2025.12.1+build.1", "2025.12.1+build.1") == 0); + assert!(web_vault_compare("2025.12.2+build.2", "2025.12.2+build.2") == 0); + // Newer active version + assert!(web_vault_compare("2025.12.1+build.1", "2025.12.1") == 1); + assert!(web_vault_compare("2025.12.2", "2025.12.1+build.1") == 1); + assert!(web_vault_compare("2025.12.2+build.1", "2025.12.1+build.1") == 1); + assert!(web_vault_compare("2025.12.1+build.3", "2025.12.1+build.2") == 1); + } +} diff --git a/src/config.rs b/src/config.rs index 6bfdea80..4fb103fa 100644 --- a/src/config.rs +++ b/src/config.rs @@ -14,7 +14,7 @@ use serde::de::{self, Deserialize, Deserializer, MapAccess, Visitor}; use crate::{ error::Error, - util::{get_env, get_env_bool, get_web_vault_version, is_valid_email, parse_experimental_client_feature_flags}, + util::{get_active_web_release, get_env, get_env_bool, is_valid_email, parse_experimental_client_feature_flags}, }; static CONFIG_FILE: LazyLock = LazyLock::new(|| { @@ -1849,7 +1849,7 @@ fn to_json<'reg, 'rc>( // 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: LazyLock = LazyLock::new(|| { - let vault_version = get_web_vault_version(); + let vault_version = get_active_web_release(); // Use a single regex capture to extract version components let re = regex::Regex::new(r"(\d{4})\.(\d{1,2})\.(\d{1,2})").unwrap(); re.captures(&vault_version) diff --git a/src/main.rs b/src/main.rs index b5ff93ae..8eef2e8c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -126,7 +126,7 @@ fn parse_args() { exit(0); } else if pargs.contains(["-v", "--version"]) { config::SKIP_CONFIG_VALIDATION.store(true, Ordering::Relaxed); - let web_vault_version = util::get_web_vault_version(); + let web_vault_version = util::get_active_web_release(); println!("Vaultwarden {version}"); println!("Web-Vault {web_vault_version}"); exit(0); diff --git a/src/static/scripts/admin_diagnostics.js b/src/static/scripts/admin_diagnostics.js index 108034dd..5594b439 100644 --- a/src/static/scripts/admin_diagnostics.js +++ b/src/static/scripts/admin_diagnostics.js @@ -29,7 +29,7 @@ function isValidIp(ip) { return ipv4Regex.test(ip) || ipv6Regex.test(ip); } -function checkVersions(platform, installed, latest, commit=null, pre_release=false) { +function checkVersions(platform, installed, latest, commit=null, compare_order=0) { if (installed === "-" || latest === "-") { document.getElementById(`${platform}-failed`).classList.remove("d-none"); return; @@ -37,7 +37,7 @@ function checkVersions(platform, installed, latest, commit=null, pre_release=fal // Only check basic versions, no commit revisions if (commit === null || installed.indexOf("-") === -1) { - if (platform === "web" && pre_release === true) { + if (platform === "web" && compare_order === 1) { document.getElementById(`${platform}-prerelease`).classList.remove("d-none"); } else if (installed == latest) { document.getElementById(`${platform}-success`).classList.remove("d-none"); @@ -83,7 +83,7 @@ async function generateSupportString(event, dj) { let supportString = "### Your environment (Generated via diagnostics page)\n\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.active_web_release}\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 += `* Database type: ${dj.db_type}\n`; @@ -208,9 +208,9 @@ function initVersionCheck(dj) { } checkVersions("server", serverInstalled, serverLatest, serverLatestCommit); - const webInstalled = dj.web_vault_version; - const webLatest = dj.latest_web_build; - checkVersions("web", webInstalled, webLatest, null, dj.web_vault_pre_release); + const webInstalled = dj.active_web_release; + const webLatest = dj.latest_web_release; + checkVersions("web", webInstalled, webLatest, null, dj.web_vault_compare); } function checkDns(dns_resolved) { diff --git a/src/static/templates/admin/diagnostics.hbs b/src/static/templates/admin/diagnostics.hbs index 503c6954..f8edabb2 100644 --- a/src/static/templates/admin/diagnostics.hbs +++ b/src/static/templates/admin/diagnostics.hbs @@ -27,13 +27,13 @@ Pre-Release
- {{page_data.web_vault_version}} + {{page_data.active_web_release}}
Web Latest Unknown
- {{page_data.latest_web_build}} + {{page_data.latest_web_release}}
{{/if}} {{#unless page_data.web_vault_enabled}} diff --git a/src/util.rs b/src/util.rs index c7ba9ed1..aa4e7914 100644 --- a/src/util.rs +++ b/src/util.rs @@ -531,7 +531,7 @@ struct WebVaultVersion { version: String, } -pub fn get_web_vault_version() -> String { +pub fn get_active_web_release() -> String { let version_files = [ format!("{}/vw-version.json", CONFIG.web_vault_folder()), format!("{}/version.json", CONFIG.web_vault_folder()), From b2cd556f3e79673d3eff1dac9b7402c18aa69d69 Mon Sep 17 00:00:00 2001 From: Mathijs van Veluw Date: Wed, 14 Jan 2026 13:11:43 +0100 Subject: [PATCH 10/89] Fix User API Key login (#6712) When using the latest Bitwarden CLI and logging in using the API Key, it expects some extra fields, same as for normal login. This PR adds those fields and login is possible again via API Key. Fixes #6709 Signed-off-by: BlackDex --- src/api/identity.rs | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/src/api/identity.rs b/src/api/identity.rs index e763ef46..722b3eab 100644 --- a/src/api/identity.rs +++ b/src/api/identity.rs @@ -610,6 +610,25 @@ async fn _user_api_key_login( info!("User {} logged in successfully via API key. IP: {}", user.email, ip.ip); + let has_master_password = !user.password_hash.is_empty(); + let master_password_unlock = if has_master_password { + json!({ + "Kdf": { + "KdfType": user.client_kdf_type, + "Iterations": user.client_kdf_iter, + "Memory": user.client_kdf_memory, + "Parallelism": user.client_kdf_parallelism + }, + // This field is named inconsistently and will be removed and replaced by the "wrapped" variant in the apps. + // https://github.com/bitwarden/android/blob/release/2025.12-rc41/network/src/main/kotlin/com/bitwarden/network/model/MasterPasswordUnlockDataJson.kt#L22-L26 + "MasterKeyEncryptedUserKey": user.akey, + "MasterKeyWrappedUserKey": user.akey, + "Salt": user.email + }) + } else { + Value::Null + }; + // Note: No refresh_token is returned. The CLI just repeats the // client_credentials login flow when the existing token expires. let result = json!({ @@ -625,6 +644,11 @@ async fn _user_api_key_login( "KdfParallelism": user.client_kdf_parallelism, "ResetMasterPassword": false, // TODO: according to official server seems something like: user.password_hash.is_empty(), but would need testing "scope": AuthMethod::UserApiKey.scope(), + "UserDecryptionOptions": { + "HasMasterPassword": has_master_password, + "MasterPasswordUnlock": master_password_unlock, + "Object": "userDecryptionOptions" + }, }); Ok(Json(result)) From 25a71d913f8309e3a7bc36cb2e806d348e610ee9 Mon Sep 17 00:00:00 2001 From: Stefan Melmuk <509385+stefan0xC@users.noreply.github.com> Date: Sun, 18 Jan 2026 15:23:21 +0100 Subject: [PATCH 11/89] use email instead of empty name for webauhn (#6733) * if empty use email instead of name for webauhn * use email as display name if name is empty --- src/api/core/organizations.rs | 2 +- src/api/core/two_factor/webauthn.rs | 2 +- src/api/identity.rs | 4 ++-- src/db/models/user.rs | 9 +++++++++ 4 files changed, 13 insertions(+), 4 deletions(-) diff --git a/src/api/core/organizations.rs b/src/api/core/organizations.rs index 285945eb..356d7786 100644 --- a/src/api/core/organizations.rs +++ b/src/api/core/organizations.rs @@ -3207,7 +3207,7 @@ async fn put_reset_password( // Sending email before resetting password to ensure working email configuration and the resulting // user notification. Also this might add some protection against security flaws and misuse - if let Err(e) = mail::send_admin_reset_password(&user.email, &user.name, &org.name).await { + if let Err(e) = mail::send_admin_reset_password(&user.email, user.display_name(), &org.name).await { err!(format!("Error sending user reset password email: {e:#?}")); } diff --git a/src/api/core/two_factor/webauthn.rs b/src/api/core/two_factor/webauthn.rs index 3b88302c..b10a5ded 100644 --- a/src/api/core/two_factor/webauthn.rs +++ b/src/api/core/two_factor/webauthn.rs @@ -144,7 +144,7 @@ async fn generate_webauthn_challenge(data: Json, headers: Hea let (mut challenge, state) = WEBAUTHN.start_passkey_registration( Uuid::from_str(&user.uuid).expect("Failed to parse UUID"), // Should never fail &user.email, - &user.name, + user.display_name(), Some(registrations), )?; diff --git a/src/api/identity.rs b/src/api/identity.rs index 722b3eab..9eaa6b36 100644 --- a/src/api/identity.rs +++ b/src/api/identity.rs @@ -266,7 +266,7 @@ async fn _sso_login( Some((user, _)) if !user.enabled => { err!( "This user has been disabled", - format!("IP: {}. Username: {}.", ip.ip, user.name), + format!("IP: {}. Username: {}.", ip.ip, user.display_name()), ErrorEvent { event: EventType::UserFailedLogIn } @@ -521,7 +521,7 @@ async fn authenticated_response( result["TwoFactorToken"] = Value::String(token); } - info!("User {} logged in successfully. IP: {}", &user.name, ip.ip); + info!("User {} logged in successfully. IP: {}", user.display_name(), ip.ip); Ok(Json(result)) } diff --git a/src/db/models/user.rs b/src/db/models/user.rs index c96e0fe7..e88c7296 100644 --- a/src/db/models/user.rs +++ b/src/db/models/user.rs @@ -231,6 +231,15 @@ impl User { pub fn reset_stamp_exception(&mut self) { self.stamp_exception = None; } + + pub fn display_name(&self) -> &str { + // default to email if name is empty + if !&self.name.is_empty() { + &self.name + } else { + &self.email + } + } } /// Database methods From 0c6817cb4e24964deaf765fd676da6c49e47d099 Mon Sep 17 00:00:00 2001 From: Stefan Melmuk <509385+stefan0xC@users.noreply.github.com> Date: Sun, 18 Jan 2026 15:25:20 +0100 Subject: [PATCH 12/89] hide password hints via CSS (#6726) --- src/api/web.rs | 3 ++- src/static/templates/scss/vaultwarden.scss.hbs | 13 +++++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/src/api/web.rs b/src/api/web.rs index 98d51a5e..91191968 100644 --- a/src/api/web.rs +++ b/src/api/web.rs @@ -60,11 +60,12 @@ fn vaultwarden_css() -> Cached> { "mail_2fa_enabled": CONFIG._enable_email_2fa(), "mail_enabled": CONFIG.mail_enabled(), "sends_allowed": CONFIG.sends_allowed(), + "password_hints_allowed": CONFIG.password_hints_allowed(), "signup_disabled": CONFIG.is_signup_disabled(), "sso_enabled": CONFIG.sso_enabled(), "sso_only": CONFIG.sso_enabled() && CONFIG.sso_only(), - "yubico_enabled": CONFIG._enable_yubico() && CONFIG.yubico_client_id().is_some() && CONFIG.yubico_secret_key().is_some(), "webauthn_2fa_supported": CONFIG.is_webauthn_2fa_supported(), + "yubico_enabled": CONFIG._enable_yubico() && CONFIG.yubico_client_id().is_some() && CONFIG.yubico_secret_key().is_some(), }); let scss = match CONFIG.render_template("scss/vaultwarden.scss", &css_options) { diff --git a/src/static/templates/scss/vaultwarden.scss.hbs b/src/static/templates/scss/vaultwarden.scss.hbs index 2b84cbb9..1859c1ea 100644 --- a/src/static/templates/scss/vaultwarden.scss.hbs +++ b/src/static/templates/scss/vaultwarden.scss.hbs @@ -192,6 +192,19 @@ bit-nav-item[route="sends"] { @extend %vw-hide; } {{/unless}} + +{{#unless password_hints_allowed}} +/* Hide password hints if not allowed */ +a[routerlink="/hint"], +{{#if (webver "<2025.12.2")}} +app-change-password > form > .form-group:nth-child(5), +auth-input-password > form > bit-form-field:nth-child(4) { +{{else}} +.vw-password-hint { +{{/if}} + @extend %vw-hide; +} +{{/unless}} /**** End Dynamic Vaultwarden Changes ****/ /**** Include a special user stylesheet for custom changes ****/ {{#if load_user_scss}} From 4737192853a1f0e4bb4517484f308bd9227596d7 Mon Sep 17 00:00:00 2001 From: Stefan Melmuk <509385+stefan0xC@users.noreply.github.com> Date: Thu, 22 Jan 2026 23:25:11 +0100 Subject: [PATCH 13/89] fix email as 2fa with auth requests (#6736) * fix email as 2fa with auth requests * increase expiry time of auth_requests to 15 minutes --- src/api/core/two_factor/email.rs | 45 +++++++++++++++++++++++++------- src/db/models/auth_request.rs | 4 ++- 2 files changed, 38 insertions(+), 11 deletions(-) diff --git a/src/api/core/two_factor/email.rs b/src/api/core/two_factor/email.rs index b8724cf1..25218069 100644 --- a/src/api/core/two_factor/email.rs +++ b/src/api/core/two_factor/email.rs @@ -7,10 +7,10 @@ use crate::{ core::{log_user_event, two_factor::_generate_recover_code}, EmptyResult, JsonResult, PasswordOrOtpData, }, - auth::Headers, + auth::{ClientHeaders, Headers}, crypto, db::{ - models::{DeviceId, EventType, TwoFactor, TwoFactorType, User, UserId}, + models::{AuthRequest, AuthRequestId, DeviceId, EventType, TwoFactor, TwoFactorType, User, UserId}, DbConn, }, error::{Error, MapResult}, @@ -30,12 +30,14 @@ struct SendEmailLoginData { email: Option, #[serde(alias = "MasterPasswordHash")] master_password_hash: Option, + auth_request_id: Option, + auth_request_access_code: Option, } /// User is trying to login and wants to use email 2FA. /// Does not require Bearer token #[post("/two-factor/send-email-login", data = "")] // JsonResult -async fn send_email_login(data: Json, conn: DbConn) -> EmptyResult { +async fn send_email_login(data: Json, client_headers: ClientHeaders, conn: DbConn) -> EmptyResult { let data: SendEmailLoginData = data.into_inner(); if !CONFIG._enable_email_2fa() { @@ -47,18 +49,41 @@ async fn send_email_login(data: Json, conn: DbConn) -> Empty Some(email) if !email.is_empty() => Some(email), _ => None, }; - let user = if let Some(email) = email { - let Some(master_password_hash) = &data.master_password_hash else { - err!("No password hash has been submitted.") - }; + let master_password_hash = match &data.master_password_hash { + Some(password_hash) if !password_hash.is_empty() => Some(password_hash), + _ => None, + }; + let auth_request_id = match &data.auth_request_id { + Some(auth_request_id) if !auth_request_id.is_empty() => Some(auth_request_id), + _ => None, + }; + let user = if let Some(email) = email { let Some(user) = User::find_by_mail(email, &conn).await else { err!("Username or password is incorrect. Try again.") }; - // Check password - if !user.check_valid_password(master_password_hash) { - err!("Username or password is incorrect. Try again.") + if let Some(master_password_hash) = master_password_hash { + // Check password + if !user.check_valid_password(master_password_hash) { + err!("Username or password is incorrect. Try again.") + } + } else if let Some(auth_request_id) = auth_request_id { + let Some(auth_request) = AuthRequest::find_by_uuid(auth_request_id, &conn).await else { + err!("AuthRequest doesn't exist", "User not found") + }; + let Some(code) = &data.auth_request_access_code else { + err!("no auth request access code") + }; + + if auth_request.device_type != client_headers.device_type + || auth_request.request_ip != client_headers.ip.ip.to_string() + || !auth_request.check_access_code(code) + { + err!("AuthRequest doesn't exist", "Invalid device, IP or code") + } + } else { + err!("No password hash has been submitted.") } user diff --git a/src/db/models/auth_request.rs b/src/db/models/auth_request.rs index c2af8d74..93c6e445 100644 --- a/src/db/models/auth_request.rs +++ b/src/db/models/auth_request.rs @@ -177,7 +177,9 @@ impl AuthRequest { } pub async fn purge_expired_auth_requests(conn: &DbConn) { - let expiry_time = Utc::now().naive_utc() - chrono::TimeDelta::try_minutes(5).unwrap(); //after 5 minutes, clients reject the request + // delete auth requests older than 15 minutes which is functionally equivalent to upstream: + // https://github.com/bitwarden/server/blob/f8ee2270409f7a13125cd414c450740af605a175/src/Sql/dbo/Auth/Stored%20Procedures/AuthRequest_DeleteIfExpired.sql + let expiry_time = Utc::now().naive_utc() - chrono::TimeDelta::try_minutes(15).unwrap(); for auth_request in Self::find_created_before(&expiry_time, conn).await { auth_request.delete(conn).await.ok(); } From cc80f689ed626ac9cfbbaa810601ed02225a9398 Mon Sep 17 00:00:00 2001 From: Mathijs van Veluw Date: Thu, 22 Jan 2026 23:40:39 +0100 Subject: [PATCH 14/89] Update crates, web-vault, js, workflows (#6749) - Updated all crates - Updated web-vault to v2025.12.2 - Updated all JavaScript files - Updated all GitHub Action Workflows Also added the `concurrency` option to all workflows. Signed-off-by: BlackDex --- .github/workflows/build.yml | 9 +- .github/workflows/check-templates.yml | 8 + .github/workflows/hadolint.yml | 14 +- .github/workflows/release.yml | 12 +- .github/workflows/releasecache-cleanup.yml | 4 + .github/workflows/trivy.yml | 6 +- .github/workflows/typos.yml | 8 +- .github/workflows/zizmor.yml | 9 +- .pre-commit-config.yaml | 2 +- Cargo.lock | 339 +- Cargo.toml | 18 +- docker/DockerSettings.yaml | 4 +- docker/Dockerfile.alpine | 12 +- docker/Dockerfile.debian | 12 +- macros/Cargo.toml | 4 +- src/api/web.rs | 4 +- src/static/scripts/datatables.css | 111 +- src/static/scripts/datatables.js | 82 +- src/static/scripts/jdenticon-3.3.0.js | 2920 ++++---- ...ery-3.7.1.slim.js => jquery-4.0.0.slim.js} | 6125 ++++++----------- src/static/templates/admin/organizations.hbs | 2 +- src/static/templates/admin/users.hbs | 2 +- 22 files changed, 4009 insertions(+), 5698 deletions(-) rename src/static/scripts/{jquery-3.7.1.slim.js => jquery-4.0.0.slim.js} (64%) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 8901ea41..3e7818ec 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,6 +1,10 @@ name: Build permissions: {} +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + on: push: paths: @@ -30,6 +34,10 @@ on: - "docker/DockerSettings.yaml" - "macros/**" +defaults: + run: + shell: bash + jobs: build: name: Build and Test ${{ matrix.channel }} @@ -63,7 +71,6 @@ jobs: # Determine rust-toolchain version - name: Init Variables id: toolchain - shell: bash env: CHANNEL: ${{ matrix.channel }} run: | diff --git a/.github/workflows/check-templates.yml b/.github/workflows/check-templates.yml index 2e02f574..a8415dde 100644 --- a/.github/workflows/check-templates.yml +++ b/.github/workflows/check-templates.yml @@ -1,8 +1,16 @@ name: Check templates permissions: {} +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + on: [ push, pull_request ] +defaults: + run: + shell: bash + jobs: docker-templates: name: Validate docker templates diff --git a/.github/workflows/hadolint.yml b/.github/workflows/hadolint.yml index 8a6d1218..1e0fa48c 100644 --- a/.github/workflows/hadolint.yml +++ b/.github/workflows/hadolint.yml @@ -1,8 +1,15 @@ name: Hadolint - -on: [ push, pull_request ] permissions: {} +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +on: [ push, pull_request ] + +defaults: + run: + shell: bash jobs: hadolint: @@ -25,7 +32,6 @@ jobs: # Download hadolint - https://github.com/hadolint/hadolint/releases - name: Download hadolint - shell: bash run: | sudo curl -L https://github.com/hadolint/hadolint/releases/download/v${HADOLINT_VERSION}/hadolint-$(uname -s)-$(uname -m) -o /usr/local/bin/hadolint && \ sudo chmod +x /usr/local/bin/hadolint @@ -41,13 +47,11 @@ jobs: # Test Dockerfiles with hadolint - name: Run hadolint - shell: bash run: hadolint docker/Dockerfile.{debian,alpine} # End Test Dockerfiles with hadolint # Test Dockerfiles with docker build checks - name: Run docker build check - shell: bash run: | echo "Checking docker/Dockerfile.debian" docker build --check . -f docker/Dockerfile.debian diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 378682d3..48cca0bb 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,6 +1,12 @@ name: Release permissions: {} +concurrency: + # Apply concurrency control only on the upstream repo + group: ${{ github.repository == 'dani-garcia/vaultwarden' && format('{0}-{1}', github.workflow, github.ref) || github.run_id }} + # Don't cancel other runs when creating a tag + cancel-in-progress: ${{ github.ref_type == 'branch' }} + on: push: branches: @@ -10,12 +16,6 @@ on: # https://docs.github.com/en/actions/writing-workflows/workflow-syntax-for-github-actions#filter-pattern-cheat-sheet - '[1-2].[0-9]+.[0-9]+' -concurrency: - # Apply concurrency control only on the upstream repo - group: ${{ github.repository == 'dani-garcia/vaultwarden' && format('{0}-{1}', github.workflow, github.ref) || github.run_id }} - # Don't cancel other runs when creating a tag - cancel-in-progress: ${{ github.ref_type == 'branch' }} - defaults: run: shell: bash diff --git a/.github/workflows/releasecache-cleanup.yml b/.github/workflows/releasecache-cleanup.yml index 22d98fa2..66bdf228 100644 --- a/.github/workflows/releasecache-cleanup.yml +++ b/.github/workflows/releasecache-cleanup.yml @@ -1,6 +1,10 @@ name: Cleanup permissions: {} +concurrency: + group: ${{ github.workflow }} + cancel-in-progress: false + on: workflow_dispatch: inputs: diff --git a/.github/workflows/trivy.yml b/.github/workflows/trivy.yml index bd1043a0..4aeb43b1 100644 --- a/.github/workflows/trivy.yml +++ b/.github/workflows/trivy.yml @@ -1,6 +1,10 @@ name: Trivy permissions: {} +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + on: push: branches: @@ -46,6 +50,6 @@ jobs: severity: CRITICAL,HIGH - name: Upload Trivy scan results to GitHub Security tab - uses: github/codeql-action/upload-sarif@5d4e8d1aca955e8d8589aabd499c5cae939e33c7 # v4.31.9 + uses: github/codeql-action/upload-sarif@cdefb33c0f6224e58673d9004f47f7cb3e328b89 # v4.31.10 with: sarif_file: 'trivy-results.sarif' diff --git a/.github/workflows/typos.yml b/.github/workflows/typos.yml index b3dae9b7..45a596ce 100644 --- a/.github/workflows/typos.yml +++ b/.github/workflows/typos.yml @@ -1,7 +1,11 @@ name: Code Spell Checking +permissions: {} + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true on: [ push, pull_request ] -permissions: {} jobs: typos: @@ -19,4 +23,4 @@ jobs: # When this version is updated, do not forget to update this in `.pre-commit-config.yaml` too - name: Spell Check Repo - uses: crate-ci/typos@1a319b54cc9e3b333fed6a5c88ba1a90324da514 # v1.40.1 + uses: crate-ci/typos@65120634e79d8374d1aa2f27e54baa0c364fff5a # v1.42.1 diff --git a/.github/workflows/zizmor.yml b/.github/workflows/zizmor.yml index 8ea25a4a..6083ef95 100644 --- a/.github/workflows/zizmor.yml +++ b/.github/workflows/zizmor.yml @@ -1,4 +1,9 @@ name: Security Analysis with zizmor +permissions: {} + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true on: push: @@ -6,8 +11,6 @@ on: pull_request: branches: ["**"] -permissions: {} - jobs: zizmor: name: Run zizmor @@ -21,7 +24,7 @@ jobs: persist-credentials: false - name: Run zizmor - uses: zizmorcore/zizmor-action@e639db99335bc9038abc0e066dfcd72e23d26fb4 # v0.3.0 + uses: zizmorcore/zizmor-action@135698455da5c3b3e55f73f4419e481ab68cdd95 # v0.4.1 with: # intentionally not scanning the entire repository, # since it contains integration tests. diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 448ccbeb..6da57526 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -53,6 +53,6 @@ repos: - "cd docker && make" # When this version is updated, do not forget to update this in `.github/workflows/typos.yaml` too - repo: https://github.com/crate-ci/typos - rev: 1a319b54cc9e3b333fed6a5c88ba1a90324da514 # v1.40.1 + rev: 65120634e79d8374d1aa2f27e54baa0c364fff5a # v1.42.1 hooks: - id: typos diff --git a/Cargo.lock b/Cargo.lock index 07f5a49b..a4697493 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -161,13 +161,12 @@ dependencies = [ [[package]] name = "async-compression" -version = "0.4.36" +version = "0.4.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "98ec5f6c2f8bc326c994cb9e241cc257ddaba9afa8555a43cffbb5dd86efaa37" +checksum = "d10e4f991a553474232bc0a31799f6d24b034a84c0971d80d2e2f78b2e576e40" dependencies = [ "compression-codecs", "compression-core", - "futures-core", "pin-project-lite", "tokio", ] @@ -403,9 +402,9 @@ dependencies = [ [[package]] name = "aws-runtime" -version = "1.5.17" +version = "1.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d81b5b2898f6798ad58f484856768bca817e3cd9de0974c24ae0f1113fe88f1b" +checksum = "959dab27ce613e6c9658eb3621064d0e2027e5f2acb65bc526a43577facea557" dependencies = [ "aws-credential-types", "aws-sigv4", @@ -427,15 +426,16 @@ dependencies = [ [[package]] name = "aws-sdk-sso" -version = "1.91.0" +version = "1.92.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ee6402a36f27b52fe67661c6732d684b2635152b676aa2babbfb5204f99115d" +checksum = "b7d63bd2bdeeb49aa3f9b00c15e18583503b778b2e792fc06284d54e7d5b6566" dependencies = [ "aws-credential-types", "aws-runtime", "aws-smithy-async", "aws-smithy-http", "aws-smithy-json", + "aws-smithy-observability", "aws-smithy-runtime", "aws-smithy-runtime-api", "aws-smithy-types", @@ -449,15 +449,16 @@ dependencies = [ [[package]] name = "aws-sdk-ssooidc" -version = "1.93.0" +version = "1.94.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a45a7f750bbd170ee3677671ad782d90b894548f4e4ae168302c57ec9de5cb3e" +checksum = "532d93574bf731f311bafb761366f9ece345a0416dbcc273d81d6d1a1205239b" dependencies = [ "aws-credential-types", "aws-runtime", "aws-smithy-async", "aws-smithy-http", "aws-smithy-json", + "aws-smithy-observability", "aws-smithy-runtime", "aws-smithy-runtime-api", "aws-smithy-types", @@ -471,15 +472,16 @@ dependencies = [ [[package]] name = "aws-sdk-sts" -version = "1.95.0" +version = "1.96.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55542378e419558e6b1f398ca70adb0b2088077e79ad9f14eb09441f2f7b2164" +checksum = "357e9a029c7524db6a0099cd77fbd5da165540339e7296cca603531bc783b56c" dependencies = [ "aws-credential-types", "aws-runtime", "aws-smithy-async", "aws-smithy-http", "aws-smithy-json", + "aws-smithy-observability", "aws-smithy-query", "aws-smithy-runtime", "aws-smithy-runtime-api", @@ -557,9 +559,9 @@ dependencies = [ [[package]] name = "aws-smithy-observability" -version = "0.1.5" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17f616c3f2260612fe44cede278bafa18e73e6479c4e393e2c4518cf2a9a228a" +checksum = "ef1fcbefc7ece1d70dcce29e490f269695dfca2d2bacdeaf9e5c3f799e4e6a42" dependencies = [ "aws-smithy-runtime-api", ] @@ -576,9 +578,9 @@ dependencies = [ [[package]] name = "aws-smithy-runtime" -version = "1.9.5" +version = "1.9.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a392db6c583ea4a912538afb86b7be7c5d8887d91604f50eb55c262ee1b4a5f5" +checksum = "bb5b6167fcdf47399024e81ac08e795180c576a20e4d4ce67949f9a88ae37dc1" dependencies = [ "aws-smithy-async", "aws-smithy-http", @@ -599,9 +601,9 @@ dependencies = [ [[package]] name = "aws-smithy-runtime-api" -version = "1.9.3" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab0d43d899f9e508300e587bf582ba54c27a452dd0a9ea294690669138ae14a2" +checksum = "efce7aaaf59ad53c5412f14fc19b2d5c6ab2c3ec688d272fd31f76ec12f44fb0" dependencies = [ "aws-smithy-async", "aws-smithy-types", @@ -616,9 +618,9 @@ dependencies = [ [[package]] name = "aws-smithy-types" -version = "1.3.5" +version = "1.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "905cb13a9895626d49cf2ced759b062d913834c7482c38e49557eac4e6193f01" +checksum = "65f172bcb02424eb94425db8aed1b6d583b5104d4d5ddddf22402c661a320048" dependencies = [ "base64-simd", "bytes", @@ -701,9 +703,9 @@ dependencies = [ [[package]] name = "base64ct" -version = "1.8.1" +version = "1.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e050f626429857a27ddccb31e0aca21356bfa709c04041aefddac081a8f068a" +checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" [[package]] name = "base64urlsafedata" @@ -855,7 +857,7 @@ dependencies = [ "futures", "hashbrown 0.15.5", "once_cell", - "thiserror 2.0.17", + "thiserror 2.0.18", "tokio", "web-time", ] @@ -920,9 +922,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.51" +version = "1.2.53" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a0aeaff4ff1a90589618835a598e545176939b97874f7abc7851caa0618f203" +checksum = "755d2fce177175ffca841e9a06afdb2c4ab0f593d53b4dee48147dfaade85932" dependencies = [ "find-msvc-tools", "jobserver", @@ -944,9 +946,9 @@ checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" [[package]] name = "chrono" -version = "0.4.42" +version = "0.4.43" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2" +checksum = "fac4744fb15ae8337dc853fee7fb3f4e48c0fbaa23d0afe49c447b4fab126118" dependencies = [ "iana-time-zone", "js-sys", @@ -994,9 +996,9 @@ checksum = "b9e769b5c8c8283982a987c6e948e540254f1058d5a74b8794914d4ef5fc2a24" [[package]] name = "compression-codecs" -version = "0.4.35" +version = "0.4.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0f7ac3e5b97fdce45e8922fb05cae2c37f7bbd63d30dd94821dacfd8f3f2bf2" +checksum = "00828ba6fd27b45a448e57dbfe84f1029d4c9f26b368157e9a448a5f49a2ec2a" dependencies = [ "brotli", "compression-core", @@ -1042,7 +1044,7 @@ version = "0.1.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f9d839f2a20b0aee515dc581a6172f2321f96cab76c1a38a4c584a194955390e" dependencies = [ - "getrandom 0.2.16", + "getrandom 0.2.17", "once_cell", "tiny-keccak", ] @@ -1333,9 +1335,9 @@ dependencies = [ [[package]] name = "data-encoding" -version = "2.9.0" +version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a2330da5de22e8a3cb63252ce2abb30116bf5265e89c0e01bc17015ce30a476" +checksum = "d7a1e2f27636f116493b8b860f5546edb47c8d8f8ea73e1d2a20be88e28d1fea" [[package]] name = "data-url" @@ -1821,15 +1823,15 @@ dependencies = [ [[package]] name = "find-msvc-tools" -version = "0.1.6" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "645cbb3a84e60b7531617d5ae4e57f7e27308f6445f5abf653209ea76dec8dff" +checksum = "8591b0bcc8a98a64310a2fae1bb3e9b8564dd10e381e6e28010fde8e8e8568db" [[package]] name = "flate2" -version = "1.1.5" +version = "1.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfe33edd8e85a12a67454e37f8c75e730830d83e313556ab9ebf9ee7fbeb3bfb" +checksum = "b375d6465b98090a5f25b1c7703f3859783755aa9a80433b36e0379a3ec2f369" dependencies = [ "crc32fast", "miniz_oxide", @@ -2011,9 +2013,9 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.16" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" dependencies = [ "cfg-if", "js-sys", @@ -2084,7 +2086,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2d9e3df7f0222ce5184154973d247c591d9aadc28ce7a73c6cd31100c9facff6" dependencies = [ "codemap", - "indexmap 2.12.1", + "indexmap 2.13.0", "lasso", "once_cell", "phf 0.11.3", @@ -2103,9 +2105,9 @@ dependencies = [ [[package]] name = "h2" -version = "0.4.12" +version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3c0b69cfcb4e1b9f1bf2f53f95f766e4661169728ec61cd3fe5a0166f2d1386" +checksum = "2f44da3a8150a6703ed5d34e164b875fd14c2cdab9af1252a9a1020bde2bdc54" dependencies = [ "atomic-waker", "bytes", @@ -2113,7 +2115,7 @@ dependencies = [ "futures-core", "futures-sink", "http 1.4.0", - "indexmap 2.12.1", + "indexmap 2.13.0", "slab", "tokio", "tokio-util", @@ -2133,9 +2135,9 @@ dependencies = [ [[package]] name = "handlebars" -version = "6.3.2" +version = "6.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "759e2d5aea3287cb1190c8ec394f42866cb5bf74fcbf213f354e3c856ea26098" +checksum = "9b3f9296c208515b87bd915a2f5d1163d4b3f863ba83337d7713cf478055948e" dependencies = [ "derive_builder", "log", @@ -2144,7 +2146,7 @@ dependencies = [ "pest_derive", "serde", "serde_json", - "thiserror 2.0.17", + "thiserror 2.0.18", "walkdir", ] @@ -2222,7 +2224,7 @@ dependencies = [ "once_cell", "rand 0.9.2", "ring", - "thiserror 2.0.17", + "thiserror 2.0.18", "tinyvec", "tokio", "tracing", @@ -2245,7 +2247,7 @@ dependencies = [ "rand 0.9.2", "resolv-conf", "smallvec", - "thiserror 2.0.17", + "thiserror 2.0.18", "tokio", "tracing", ] @@ -2418,7 +2420,7 @@ dependencies = [ "http 1.4.0", "hyper 1.8.1", "hyper-util", - "rustls 0.23.35", + "rustls 0.23.36", "rustls-native-certs", "rustls-pki-types", "tokio", @@ -2614,9 +2616,9 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.12.1" +version = "2.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ad4bb2b565bca0645f4d68c5c9af97fba094e9791da685bf83cb5f3ce74acf2" +checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" dependencies = [ "equivalent", "hashbrown 0.16.1", @@ -2702,9 +2704,9 @@ checksum = "47f142fe24a9c9944451e8349de0a56af5f3e7226dc46f3ed4d4ecc0b85af75e" [[package]] name = "jiff" -version = "0.2.17" +version = "0.2.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a87d9b8105c23642f50cbbae03d1f75d8422c5cb98ce7ee9271f7ff7505be6b8" +checksum = "e67e8da4c49d6d9909fe03361f9b620f58898859f5c7aded68351e85e71ecf50" dependencies = [ "jiff-static", "jiff-tzdb-platform", @@ -2717,9 +2719,9 @@ dependencies = [ [[package]] name = "jiff-static" -version = "0.2.17" +version = "0.2.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b787bebb543f8969132630c51fd0afab173a86c6abae56ff3b9e5e3e3f9f6e58" +checksum = "e0c84ee7f197eca9a86c6fd6cb771e55eb991632f15f2bc3ca6ec838929e6e78" dependencies = [ "proc-macro2", "quote", @@ -2764,9 +2766,9 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.83" +version = "0.3.85" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "464a3709c7f55f1f721e5389aa6ea4e3bc6aba669353300af094b29ffbdde1d8" +checksum = "8c942ebf8e95485ca0d52d97da7c5a2c387d0e7f0ba4c35e93bfcaee045955b3" dependencies = [ "once_cell", "wasm-bindgen", @@ -2795,7 +2797,7 @@ checksum = "c76e1c7d7df3e34443b3621b459b066a7b79644f059fc8b2db7070c825fd417e" dependencies = [ "base64 0.22.1", "ed25519-dalek", - "getrandom 0.2.16", + "getrandom 0.2.17", "hmac", "js-sys", "p256", @@ -2859,7 +2861,7 @@ dependencies = [ "nom 8.0.0", "percent-encoding", "quoted_printable", - "rustls 0.23.35", + "rustls 0.23.36", "rustls-native-certs", "serde", "socket2 0.6.1", @@ -2871,9 +2873,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.178" +version = "0.2.180" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37c93d8daa9d8a012fd8ab92f088405fb202ea0b6ab73ee2482ae66af4f42091" +checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc" [[package]] name = "libm" @@ -2999,7 +3001,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "36c791ecdf977c99f45f23280405d7723727470f6689a5e6dbf513ac547ae10d" dependencies = [ "serde", - "toml 0.9.10+spec-1.1.0", + "toml 0.9.11+spec-1.1.0", ] [[package]] @@ -3283,7 +3285,7 @@ checksum = "51e219e79014df21a225b1860a479e2dcd7cbd9130f4defd4bd0e191ea31d67d" dependencies = [ "base64 0.22.1", "chrono", - "getrandom 0.2.16", + "getrandom 0.2.17", "http 1.4.0", "rand 0.8.5", "reqwest", @@ -3335,7 +3337,7 @@ dependencies = [ "bytes", "crc32c", "futures", - "getrandom 0.2.16", + "getrandom 0.2.17", "http 1.4.0", "http-body 1.0.1", "jiff", @@ -3417,9 +3419,9 @@ checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" [[package]] name = "openssl-probe" -version = "0.2.0" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f50d9b3dabb09ecd771ad0aa242ca6894994c130308ca3d7684634df8037391" +checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" [[package]] name = "openssl-src" @@ -3604,9 +3606,9 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "pest" -version = "2.8.4" +version = "2.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cbcfd20a6d4eeba40179f05735784ad32bdaef05ce8e8af05f180d45bb3e7e22" +checksum = "2c9eb05c21a464ea704b53158d358a31e6425db2f63a1a7312268b05fe2b75f7" dependencies = [ "memchr", "ucd-trie", @@ -3614,9 +3616,9 @@ dependencies = [ [[package]] name = "pest_derive" -version = "2.8.4" +version = "2.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51f72981ade67b1ca6adc26ec221be9f463f2b5839c7508998daa17c23d94d7f" +checksum = "68f9dbced329c441fa79d80472764b1a2c7e57123553b8519b36663a2fb234ed" dependencies = [ "pest", "pest_generator", @@ -3624,9 +3626,9 @@ dependencies = [ [[package]] name = "pest_generator" -version = "2.8.4" +version = "2.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dee9efd8cdb50d719a80088b76f81aec7c41ed6d522ee750178f83883d271625" +checksum = "3bb96d5051a78f44f43c8f712d8e810adb0ebf923fc9ed2655a7f66f63ba8ee5" dependencies = [ "pest", "pest_meta", @@ -3637,9 +3639,9 @@ dependencies = [ [[package]] name = "pest_meta" -version = "2.8.4" +version = "2.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf1d70880e76bdc13ba52eafa6239ce793d85c8e43896507e43dd8984ff05b82" +checksum = "602113b5b5e8621770cfd490cfd90b9f84ab29bd2b0e49ad83eb6d186cef2365" dependencies = [ "pest", "sha2", @@ -3853,9 +3855,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.104" +version = "1.0.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9695f8df41bb4f3d222c95a67532365f569318332d03d5f3f67f37b20e6ebdf0" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" dependencies = [ "unicode-ident", ] @@ -3963,9 +3965,9 @@ dependencies = [ "quinn-proto", "quinn-udp", "rustc-hash", - "rustls 0.23.35", + "rustls 0.23.36", "socket2 0.6.1", - "thiserror 2.0.17", + "thiserror 2.0.18", "tokio", "tracing", "web-time", @@ -3983,10 +3985,10 @@ dependencies = [ "rand 0.9.2", "ring", "rustc-hash", - "rustls 0.23.35", + "rustls 0.23.36", "rustls-pki-types", "slab", - "thiserror 2.0.17", + "thiserror 2.0.18", "tinyvec", "tracing", "web-time", @@ -4008,9 +4010,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.42" +version = "1.0.43" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f" +checksum = "dc74d9a594b72ae6656596548f56f667211f8a97b3d4c3d467150794690dc40a" dependencies = [ "proc-macro2", ] @@ -4056,7 +4058,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" dependencies = [ "rand_chacha 0.9.0", - "rand_core 0.9.3", + "rand_core 0.9.5", ] [[package]] @@ -4076,7 +4078,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" dependencies = [ "ppv-lite86", - "rand_core 0.9.3", + "rand_core 0.9.5", ] [[package]] @@ -4085,14 +4087,14 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ - "getrandom 0.2.16", + "getrandom 0.2.17", ] [[package]] name = "rand_core" -version = "0.9.3" +version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" dependencies = [ "getrandom 0.3.4", ] @@ -4192,7 +4194,7 @@ dependencies = [ "base64 0.22.1", "chrono", "form_urlencoded", - "getrandom 0.2.16", + "getrandom 0.2.17", "hex", "hmac", "home", @@ -4243,7 +4245,7 @@ dependencies = [ "percent-encoding", "pin-project-lite", "quinn", - "rustls 0.23.35", + "rustls 0.23.36", "rustls-native-certs", "rustls-pki-types", "serde", @@ -4289,7 +4291,7 @@ checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" dependencies = [ "cc", "cfg-if", - "getrandom 0.2.16", + "getrandom 0.2.17", "libc", "untrusted", "windows-sys 0.52.0", @@ -4327,7 +4329,7 @@ dependencies = [ "either", "figment", "futures", - "indexmap 2.12.1", + "indexmap 2.13.0", "log", "memchr", "multer", @@ -4359,7 +4361,7 @@ checksum = "575d32d7ec1a9770108c879fc7c47815a80073f96ca07ff9525a94fcede1dd46" dependencies = [ "devise", "glob", - "indexmap 2.12.1", + "indexmap 2.13.0", "proc-macro2", "quote", "rocket_http", @@ -4379,7 +4381,7 @@ dependencies = [ "futures", "http 0.2.12", "hyper 0.14.32", - "indexmap 2.12.1", + "indexmap 2.13.0", "log", "memchr", "pear", @@ -4421,9 +4423,9 @@ dependencies = [ [[package]] name = "rsa" -version = "0.9.9" +version = "0.9.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40a0376c50d0358279d9d643e4bf7b7be212f1f4ff1da9070a7b54d22ef75c88" +checksum = "b8573f03f5883dcaebdfcf4725caa1ecb9c15b2ef50c43a07b816e06799bb12d" dependencies = [ "const-oid", "digest", @@ -4440,6 +4442,16 @@ dependencies = [ "zeroize", ] +[[package]] +name = "rsqlite-vfs" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8a1f2315036ef6b1fbacd1972e8ee7688030b0a2121edfc2a6550febd41574d" +dependencies = [ + "hashbrown 0.16.1", + "thiserror 2.0.18", +] + [[package]] name = "rtoolbox" version = "0.0.3" @@ -4511,15 +4523,15 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.35" +version = "0.23.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "533f54bc6a7d4f647e46ad909549eda97bf5afc1585190ef692b4286b198bd8f" +checksum = "c665f33d38cea657d9614f766881e4d510e0eda4239891eea56b4cadcf01801b" dependencies = [ "log", "once_cell", "ring", "rustls-pki-types", - "rustls-webpki 0.103.8", + "rustls-webpki 0.103.9", "subtle", "zeroize", ] @@ -4530,7 +4542,7 @@ version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "612460d5f7bea540c490b2b6395d8e34a953e52b491accd6c86c8164c5932a63" dependencies = [ - "openssl-probe 0.2.0", + "openssl-probe 0.2.1", "rustls-pki-types", "schannel", "security-framework 3.5.1", @@ -4547,9 +4559,9 @@ dependencies = [ [[package]] name = "rustls-pki-types" -version = "1.13.2" +version = "1.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21e6f2ab2928ca4291b86736a8bd920a277a399bba1589409d72154ff87c1282" +checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd" dependencies = [ "web-time", "zeroize", @@ -4567,9 +4579,9 @@ dependencies = [ [[package]] name = "rustls-webpki" -version = "0.103.8" +version = "0.103.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ffdfa2f5286e2247234e03f680868ac2815974dc39e00ea15adc445d0aafe52" +checksum = "d7df23109aa6c1567d1c575b9952556388da57401e4ace1d15f79eedad0d8f53" dependencies = [ "ring", "rustls-pki-types", @@ -4793,9 +4805,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.148" +version = "1.0.149" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3084b546a1dd6289475996f182a22aba973866ea8e8b02c51d9f46b1336a22da" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" dependencies = [ "itoa", "memchr", @@ -4864,7 +4876,7 @@ dependencies = [ "chrono", "hex", "indexmap 1.9.3", - "indexmap 2.12.1", + "indexmap 2.13.0", "schemars 0.9.0", "schemars 1.2.0", "serde_core", @@ -4966,7 +4978,7 @@ checksum = "297f631f50729c8c99b84667867963997ec0b50f32b2a7dbcab828ef0541e8bb" dependencies = [ "num-bigint", "num-traits", - "thiserror 2.0.17", + "thiserror 2.0.18", "time", ] @@ -5050,14 +5062,13 @@ dependencies = [ [[package]] name = "sqlite-wasm-rs" -version = "0.5.1" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05e98301bf8b0540c7de45ecd760539b9c62f5772aed172f08efba597c11cd5d" +checksum = "2f4206ed3a67690b9c29b77d728f6acc3ce78f16bf846d83c94f76400320181b" dependencies = [ "cc", - "hashbrown 0.16.1", "js-sys", - "thiserror 2.0.17", + "rsqlite-vfs", "wasm-bindgen", ] @@ -5126,9 +5137,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.111" +version = "2.0.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "390cc9a294ab71bdb1aa2e99d13be9c753cd2d7bd6560c77118597410c4d2e87" +checksum = "d4d107df263a3013ef9b1879b0df87d706ff80f65a86ea879bd9c31f9b307c2a" dependencies = [ "proc-macro2", "quote", @@ -5218,11 +5229,11 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.17" +version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" dependencies = [ - "thiserror-impl 2.0.17", + "thiserror-impl 2.0.18", ] [[package]] @@ -5238,9 +5249,9 @@ dependencies = [ [[package]] name = "thiserror-impl" -version = "2.0.17" +version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" dependencies = [ "proc-macro2", "quote", @@ -5267,9 +5278,9 @@ dependencies = [ [[package]] name = "time" -version = "0.3.44" +version = "0.3.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91e7d9e3bb61134e77bde20dd4825b97c010155709965fedf0f49bb138e52a9d" +checksum = "f9e442fc33d7fdb45aa9bfeb312c095964abdf596f7567261062b2a7107aaabd" dependencies = [ "deranged", "itoa", @@ -5277,22 +5288,22 @@ dependencies = [ "num-conv", "num_threads", "powerfmt", - "serde", + "serde_core", "time-core", "time-macros", ] [[package]] name = "time-core" -version = "0.1.6" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40868e7c1d2f0b8d73e4a8c7f0ff63af4f6d19be117e90bd73eb1d62cf831c6b" +checksum = "8b36ee98fd31ec7426d599183e8fe26932a8dc1fb76ddb6214d05493377d34ca" [[package]] name = "time-macros" -version = "0.2.24" +version = "0.2.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30cfb0125f12d9c277f35663a0a33f8c30190f4e4574868a330595412d34ebf3" +checksum = "71e552d1249bf61ac2a52db88179fd0673def1e1ad8243a00d9ec9ed71fee3dd" dependencies = [ "num-conv", "time-core", @@ -5334,9 +5345,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.48.0" +version = "1.49.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff360e02eab121e0bc37a2d3b4d4dc622e6eda3a8e5253d5435ecf5bd4c68408" +checksum = "72a2903cd7736441aac9df9d7688bd0ce48edccaadf181c3b90be801e81d3d86" dependencies = [ "bytes", "libc", @@ -5386,15 +5397,15 @@ version = "0.26.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" dependencies = [ - "rustls 0.23.35", + "rustls 0.23.36", "tokio", ] [[package]] name = "tokio-stream" -version = "0.1.17" +version = "0.1.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eca58d7bba4a75707817a2c44174253f9236b2d5fbd055602e9d5c07c139a047" +checksum = "32da49809aab5c3bc678af03902d4ccddea2a87d028d86392a4b1560c6906c70" dependencies = [ "futures-core", "pin-project-lite", @@ -5415,9 +5426,9 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.17" +version = "0.7.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2efa149fe76073d6e8fd97ef4f4eca7b67f599660115591483572e406e165594" +checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" dependencies = [ "bytes", "futures-core", @@ -5441,9 +5452,9 @@ dependencies = [ [[package]] name = "toml" -version = "0.9.10+spec-1.1.0" +version = "0.9.11+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0825052159284a1a8b4d6c0c86cbc801f2da5afd2b225fa548c72f2e74002f48" +checksum = "f3afc9a848309fe1aaffaed6e1546a7a14de1f935dc9d89d32afd9a44bab7c46" dependencies = [ "serde_core", "serde_spanned 1.0.4", @@ -5476,7 +5487,7 @@ version = "0.22.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" dependencies = [ - "indexmap 2.12.1", + "indexmap 2.13.0", "serde", "serde_spanned 0.6.9", "toml_datetime 0.6.11", @@ -5513,9 +5524,9 @@ dependencies = [ [[package]] name = "tower" -version = "0.5.2" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" dependencies = [ "futures-core", "futures-util", @@ -5687,9 +5698,9 @@ dependencies = [ [[package]] name = "unicase" -version = "2.8.1" +version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539" +checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142" [[package]] name = "unicode-ident" @@ -5717,14 +5728,15 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" [[package]] name = "url" -version = "2.5.7" +version = "2.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08bc136a29a3d1758e07a9cca267be308aeebf5cfd5a10f3f67ab2097683ef5b" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" dependencies = [ "form_urlencoded", "idna", "percent-encoding", "serde", + "serde_derive", ] [[package]] @@ -5892,18 +5904,18 @@ checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] name = "wasip2" -version = "1.0.1+wasi-0.2.4" +version = "1.0.2+wasi-0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" +checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" dependencies = [ "wit-bindgen", ] [[package]] name = "wasm-bindgen" -version = "0.2.106" +version = "0.2.108" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d759f433fa64a2d763d1340820e46e111a7a5ab75f993d1852d70b03dbb80fd" +checksum = "64024a30ec1e37399cf85a7ffefebdb72205ca1c972291c51512360d90bd8566" dependencies = [ "cfg-if", "once_cell", @@ -5914,11 +5926,12 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.56" +version = "0.4.58" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "836d9622d604feee9e5de25ac10e3ea5f2d65b41eac0d9ce72eb5deae707ce7c" +checksum = "70a6e77fd0ae8029c9ea0063f87c46fde723e7d887703d74ad2616d792e51e6f" dependencies = [ "cfg-if", + "futures-util", "js-sys", "once_cell", "wasm-bindgen", @@ -5927,9 +5940,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.106" +version = "0.2.108" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48cb0d2638f8baedbc542ed444afc0644a29166f1595371af4fecf8ce1e7eeb3" +checksum = "008b239d9c740232e71bd39e8ef6429d27097518b6b30bdf9086833bd5b6d608" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -5937,9 +5950,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.106" +version = "0.2.108" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cefb59d5cd5f92d9dcf80e4683949f15ca4b511f4ac0a6e14d4e1ac60c6ecd40" +checksum = "5256bae2d58f54820e6490f9839c49780dff84c65aeab9e772f15d5f0e913a55" dependencies = [ "bumpalo", "proc-macro2", @@ -5950,9 +5963,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.106" +version = "0.2.108" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cbc538057e648b67f72a982e708d485b2efa771e1ac05fec311f9f63e5800db4" +checksum = "1f01b580c9ac74c8d8f0c0e4afb04eeef2acf145458e52c03845ee9cd23e3d12" dependencies = [ "unicode-ident", ] @@ -5972,9 +5985,9 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.83" +version = "0.3.85" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b32828d774c412041098d182a8b38b16ea816958e07cf40eec2bc080ae137ac" +checksum = "312e32e551d92129218ea9a2452120f4aabc03529ef03e4d0d82fb2780608598" dependencies = [ "js-sys", "wasm-bindgen", @@ -6060,9 +6073,9 @@ dependencies = [ [[package]] name = "webpki-roots" -version = "1.0.4" +version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2878ef029c47c6e8cf779119f20fcf52bde7ad42a731b2a304bc221df17571e" +checksum = "12bed680863276c63889429bfd6cab3b99943659923822de1c8a39c49e4d722c" dependencies = [ "rustls-pki-types", ] @@ -6461,9 +6474,9 @@ checksum = "d135d17ab770252ad95e9a872d365cf3090e3be864a34ab46f48555993efc904" [[package]] name = "wit-bindgen" -version = "0.46.0" +version = "0.51.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" [[package]] name = "writeable" @@ -6550,18 +6563,18 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.8.31" +version = "0.8.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd74ec98b9250adb3ca554bdde269adf631549f51d8a8f8f0a10b50f1cb298c3" +checksum = "668f5168d10b9ee831de31933dc111a459c97ec93225beb307aed970d1372dfd" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.31" +version = "0.8.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8a8d209fdf45cf5138cbb5a506f6b52522a25afccc534d1475dad8e31105c6a" +checksum = "2c7962b26b0a8685668b671ee4b54d007a67d4eaf05fda79ac0ecf41e32270f1" dependencies = [ "proc-macro2", "quote", @@ -6630,9 +6643,9 @@ dependencies = [ [[package]] name = "zmij" -version = "1.0.2" +version = "1.0.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f4a4e8e9dc5c62d159f04fcdbe07f4c3fb710415aab4754bf11505501e3251d" +checksum = "dfcd145825aace48cff44a8844de64bf75feec3080e0aa5cdbde72961ae51a65" [[package]] name = "zstd" diff --git a/Cargo.toml b/Cargo.toml index ea2d5ecb..9d54590e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -79,12 +79,12 @@ dashmap = "6.1.0" # Async futures futures = "0.3.31" -tokio = { version = "1.48.0", features = ["rt-multi-thread", "fs", "io-util", "parking_lot", "time", "signal", "net"] } -tokio-util = { version = "0.7.17", features = ["compat"]} +tokio = { version = "1.49.0", features = ["rt-multi-thread", "fs", "io-util", "parking_lot", "time", "signal", "net"] } +tokio-util = { version = "0.7.18", features = ["compat"]} # A generic serialization/deserialization framework serde = { version = "1.0.228", features = ["derive"] } -serde_json = "1.0.148" +serde_json = "1.0.149" # A safe, extensible ORM and Query builder # Currently pinned diesel to v2.3.3 as newer version break MySQL/MariaDB compatibility @@ -106,15 +106,15 @@ subtle = "2.6.1" uuid = { version = "1.19.0", features = ["v4"] } # Date and time libraries -chrono = { version = "0.4.42", features = ["clock", "serde"], default-features = false } +chrono = { version = "0.4.43", features = ["clock", "serde"], default-features = false } chrono-tz = "0.10.4" -time = "0.3.44" +time = "0.3.45" # Job scheduler job_scheduler_ng = "2.4.0" # Data encoding library Hex/Base32/Base64 -data-encoding = "2.9.0" +data-encoding = "2.10.0" # JWT library jsonwebtoken = { version = "10.2.0", features = ["use_pem", "rust_crypto"], default-features = false } @@ -133,7 +133,7 @@ webauthn-rs-proto = "0.5.4" webauthn-rs-core = "0.5.4" # Handling of URL's for WebAuthn and favicons -url = "2.5.7" +url = "2.5.8" # Email libraries lettre = { version = "0.11.19", features = ["smtp-transport", "sendmail-transport", "builder", "serde", "hostname", "tracing", "tokio1-rustls", "ring", "rustls-native-certs"], default-features = false } @@ -141,7 +141,7 @@ percent-encoding = "2.3.2" # URL encoding library used for URL's in the emails email_address = "0.2.9" # HTML Template library -handlebars = { version = "6.3.2", features = ["dir_source"] } +handlebars = { version = "6.4.0", features = ["dir_source"] } # HTTP client (Used for favicons, version check, DUO and HIBP API) reqwest = { version = "0.12.28", features = ["rustls-tls", "rustls-tls-native-roots", "stream", "json", "deflate", "gzip", "brotli", "zstd", "socks", "cookies", "charset", "http2", "system-proxy"], default-features = false} @@ -200,7 +200,7 @@ opendal = { version = "0.55.0", features = ["services-fs"], default-features = f anyhow = { version = "1.0.100", optional = true } aws-config = { version = "1.8.12", features = ["behavior-version-latest", "rt-tokio", "credentials-process", "sso"], default-features = false, optional = true } aws-credential-types = { version = "1.2.11", optional = true } -aws-smithy-runtime-api = { version = "1.9.3", optional = true } +aws-smithy-runtime-api = { version = "1.10.0", optional = true } http = { version = "1.4.0", optional = true } reqsign = { version = "0.16.5", optional = true } diff --git a/docker/DockerSettings.yaml b/docker/DockerSettings.yaml index dd87a9e3..02cd166b 100644 --- a/docker/DockerSettings.yaml +++ b/docker/DockerSettings.yaml @@ -1,6 +1,6 @@ --- -vault_version: "v2025.12.1+build.3" -vault_image_digest: "sha256:bf5aa55dc7bcb99f85d2a88ff44d32cdc832e934a0603fe28e5c3f92904bad42" +vault_version: "v2025.12.2" +vault_image_digest: "sha256:3c9aec4924c4f529af5e48888cfddd559e553ee62ce3e81372b50103bbaf20f7" # Cross Compile Docker Helper Scripts v1.9.0 # We use the linux/amd64 platform shell scripts since there is no difference between the different platform scripts # https://github.com/tonistiigi/xx | https://hub.docker.com/r/tonistiigi/xx/tags diff --git a/docker/Dockerfile.alpine b/docker/Dockerfile.alpine index 2a6cf9f2..95aae642 100644 --- a/docker/Dockerfile.alpine +++ b/docker/Dockerfile.alpine @@ -19,15 +19,15 @@ # - From https://hub.docker.com/r/vaultwarden/web-vault/tags, # click the tag name to view the digest of the image it currently points to. # - From the command line: -# $ docker pull docker.io/vaultwarden/web-vault:v2025.12.1_build.3 -# $ docker image inspect --format "{{.RepoDigests}}" docker.io/vaultwarden/web-vault:v2025.12.1_build.3 -# [docker.io/vaultwarden/web-vault@sha256:bf5aa55dc7bcb99f85d2a88ff44d32cdc832e934a0603fe28e5c3f92904bad42] +# $ docker pull docker.io/vaultwarden/web-vault:v2025.12.2 +# $ docker image inspect --format "{{.RepoDigests}}" docker.io/vaultwarden/web-vault:v2025.12.2 +# [docker.io/vaultwarden/web-vault@sha256:3c9aec4924c4f529af5e48888cfddd559e553ee62ce3e81372b50103bbaf20f7] # # - Conversely, to get the tag name from the digest: -# $ docker image inspect --format "{{.RepoTags}}" docker.io/vaultwarden/web-vault@sha256:bf5aa55dc7bcb99f85d2a88ff44d32cdc832e934a0603fe28e5c3f92904bad42 -# [docker.io/vaultwarden/web-vault:v2025.12.1_build.3] +# $ docker image inspect --format "{{.RepoTags}}" docker.io/vaultwarden/web-vault@sha256:3c9aec4924c4f529af5e48888cfddd559e553ee62ce3e81372b50103bbaf20f7 +# [docker.io/vaultwarden/web-vault:v2025.12.2] # -FROM --platform=linux/amd64 docker.io/vaultwarden/web-vault@sha256:bf5aa55dc7bcb99f85d2a88ff44d32cdc832e934a0603fe28e5c3f92904bad42 AS vault +FROM --platform=linux/amd64 docker.io/vaultwarden/web-vault@sha256:3c9aec4924c4f529af5e48888cfddd559e553ee62ce3e81372b50103bbaf20f7 AS vault ########################## ALPINE BUILD IMAGES ########################## ## NOTE: The Alpine Base Images do not support other platforms then linux/amd64 and linux/arm64 diff --git a/docker/Dockerfile.debian b/docker/Dockerfile.debian index 03c0faba..113304b8 100644 --- a/docker/Dockerfile.debian +++ b/docker/Dockerfile.debian @@ -19,15 +19,15 @@ # - From https://hub.docker.com/r/vaultwarden/web-vault/tags, # click the tag name to view the digest of the image it currently points to. # - From the command line: -# $ docker pull docker.io/vaultwarden/web-vault:v2025.12.1_build.3 -# $ docker image inspect --format "{{.RepoDigests}}" docker.io/vaultwarden/web-vault:v2025.12.1_build.3 -# [docker.io/vaultwarden/web-vault@sha256:bf5aa55dc7bcb99f85d2a88ff44d32cdc832e934a0603fe28e5c3f92904bad42] +# $ docker pull docker.io/vaultwarden/web-vault:v2025.12.2 +# $ docker image inspect --format "{{.RepoDigests}}" docker.io/vaultwarden/web-vault:v2025.12.2 +# [docker.io/vaultwarden/web-vault@sha256:3c9aec4924c4f529af5e48888cfddd559e553ee62ce3e81372b50103bbaf20f7] # # - Conversely, to get the tag name from the digest: -# $ docker image inspect --format "{{.RepoTags}}" docker.io/vaultwarden/web-vault@sha256:bf5aa55dc7bcb99f85d2a88ff44d32cdc832e934a0603fe28e5c3f92904bad42 -# [docker.io/vaultwarden/web-vault:v2025.12.1_build.3] +# $ docker image inspect --format "{{.RepoTags}}" docker.io/vaultwarden/web-vault@sha256:3c9aec4924c4f529af5e48888cfddd559e553ee62ce3e81372b50103bbaf20f7 +# [docker.io/vaultwarden/web-vault:v2025.12.2] # -FROM --platform=linux/amd64 docker.io/vaultwarden/web-vault@sha256:bf5aa55dc7bcb99f85d2a88ff44d32cdc832e934a0603fe28e5c3f92904bad42 AS vault +FROM --platform=linux/amd64 docker.io/vaultwarden/web-vault@sha256:3c9aec4924c4f529af5e48888cfddd559e553ee62ce3e81372b50103bbaf20f7 AS vault ########################## Cross Compile Docker Helper Scripts ########################## ## We use the linux/amd64 no matter which Build Platform, since these are all bash scripts diff --git a/macros/Cargo.toml b/macros/Cargo.toml index 9855c56e..933d46d3 100644 --- a/macros/Cargo.toml +++ b/macros/Cargo.toml @@ -13,8 +13,8 @@ path = "src/lib.rs" proc-macro = true [dependencies] -quote = "1.0.42" -syn = "2.0.111" +quote = "1.0.43" +syn = "2.0.114" [lints] workspace = true diff --git a/src/api/web.rs b/src/api/web.rs index 91191968..d1ca0db4 100644 --- a/src/api/web.rs +++ b/src/api/web.rs @@ -239,8 +239,8 @@ pub fn static_files(filename: &str) -> Result<(ContentType, &'static [u8]), Erro "jdenticon-3.3.0.js" => Ok((ContentType::JavaScript, include_bytes!("../static/scripts/jdenticon-3.3.0.js"))), "datatables.js" => Ok((ContentType::JavaScript, include_bytes!("../static/scripts/datatables.js"))), "datatables.css" => Ok((ContentType::CSS, include_bytes!("../static/scripts/datatables.css"))), - "jquery-3.7.1.slim.js" => { - Ok((ContentType::JavaScript, include_bytes!("../static/scripts/jquery-3.7.1.slim.js"))) + "jquery-4.0.0.slim.js" => { + Ok((ContentType::JavaScript, include_bytes!("../static/scripts/jquery-4.0.0.slim.js"))) } _ => err!(format!("Static file not found: {filename}")), } diff --git a/src/static/scripts/datatables.css b/src/static/scripts/datatables.css index af6a9b1e..fe087655 100644 --- a/src/static/scripts/datatables.css +++ b/src/static/scripts/datatables.css @@ -4,10 +4,10 @@ * * To rebuild or modify this file with the latest versions of the included * software please visit: - * https://datatables.net/download/#bs5/dt-2.3.5 + * https://datatables.net/download/#bs5/dt-2.3.6 * * Included libraries: - * DataTables 2.3.5 + * DataTables 2.3.6 */ :root { @@ -88,42 +88,42 @@ table.dataTable thead > tr > th:active, table.dataTable thead > tr > td:active { outline: none; } -table.dataTable thead > tr > th.dt-orderable-asc span.dt-column-order:before, table.dataTable thead > tr > th.dt-ordering-asc span.dt-column-order:before, -table.dataTable thead > tr > td.dt-orderable-asc span.dt-column-order:before, -table.dataTable thead > tr > td.dt-ordering-asc span.dt-column-order:before { +table.dataTable thead > tr > th.dt-orderable-asc .dt-column-order:before, table.dataTable thead > tr > th.dt-ordering-asc .dt-column-order:before, +table.dataTable thead > tr > td.dt-orderable-asc .dt-column-order:before, +table.dataTable thead > tr > td.dt-ordering-asc .dt-column-order:before { position: absolute; display: block; bottom: 50%; content: "\25B2"; content: "\25B2"/""; } -table.dataTable thead > tr > th.dt-orderable-desc span.dt-column-order:after, table.dataTable thead > tr > th.dt-ordering-desc span.dt-column-order:after, -table.dataTable thead > tr > td.dt-orderable-desc span.dt-column-order:after, -table.dataTable thead > tr > td.dt-ordering-desc span.dt-column-order:after { +table.dataTable thead > tr > th.dt-orderable-desc .dt-column-order:after, table.dataTable thead > tr > th.dt-ordering-desc .dt-column-order:after, +table.dataTable thead > tr > td.dt-orderable-desc .dt-column-order:after, +table.dataTable thead > tr > td.dt-ordering-desc .dt-column-order:after { position: absolute; display: block; top: 50%; content: "\25BC"; content: "\25BC"/""; } -table.dataTable thead > tr > th.dt-orderable-asc span.dt-column-order, table.dataTable thead > tr > th.dt-orderable-desc span.dt-column-order, table.dataTable thead > tr > th.dt-ordering-asc span.dt-column-order, table.dataTable thead > tr > th.dt-ordering-desc span.dt-column-order, -table.dataTable thead > tr > td.dt-orderable-asc span.dt-column-order, -table.dataTable thead > tr > td.dt-orderable-desc span.dt-column-order, -table.dataTable thead > tr > td.dt-ordering-asc span.dt-column-order, -table.dataTable thead > tr > td.dt-ordering-desc span.dt-column-order { +table.dataTable thead > tr > th.dt-orderable-asc .dt-column-order, table.dataTable thead > tr > th.dt-orderable-desc .dt-column-order, table.dataTable thead > tr > th.dt-ordering-asc .dt-column-order, table.dataTable thead > tr > th.dt-ordering-desc .dt-column-order, +table.dataTable thead > tr > td.dt-orderable-asc .dt-column-order, +table.dataTable thead > tr > td.dt-orderable-desc .dt-column-order, +table.dataTable thead > tr > td.dt-ordering-asc .dt-column-order, +table.dataTable thead > tr > td.dt-ordering-desc .dt-column-order { position: relative; width: 12px; - height: 24px; + height: 20px; } -table.dataTable thead > tr > th.dt-orderable-asc span.dt-column-order:before, table.dataTable thead > tr > th.dt-orderable-asc span.dt-column-order:after, table.dataTable thead > tr > th.dt-orderable-desc span.dt-column-order:before, table.dataTable thead > tr > th.dt-orderable-desc span.dt-column-order:after, table.dataTable thead > tr > th.dt-ordering-asc span.dt-column-order:before, table.dataTable thead > tr > th.dt-ordering-asc span.dt-column-order:after, table.dataTable thead > tr > th.dt-ordering-desc span.dt-column-order:before, table.dataTable thead > tr > th.dt-ordering-desc span.dt-column-order:after, -table.dataTable thead > tr > td.dt-orderable-asc span.dt-column-order:before, -table.dataTable thead > tr > td.dt-orderable-asc span.dt-column-order:after, -table.dataTable thead > tr > td.dt-orderable-desc span.dt-column-order:before, -table.dataTable thead > tr > td.dt-orderable-desc span.dt-column-order:after, -table.dataTable thead > tr > td.dt-ordering-asc span.dt-column-order:before, -table.dataTable thead > tr > td.dt-ordering-asc span.dt-column-order:after, -table.dataTable thead > tr > td.dt-ordering-desc span.dt-column-order:before, -table.dataTable thead > tr > td.dt-ordering-desc span.dt-column-order:after { +table.dataTable thead > tr > th.dt-orderable-asc .dt-column-order:before, table.dataTable thead > tr > th.dt-orderable-asc .dt-column-order:after, table.dataTable thead > tr > th.dt-orderable-desc .dt-column-order:before, table.dataTable thead > tr > th.dt-orderable-desc .dt-column-order:after, table.dataTable thead > tr > th.dt-ordering-asc .dt-column-order:before, table.dataTable thead > tr > th.dt-ordering-asc .dt-column-order:after, table.dataTable thead > tr > th.dt-ordering-desc .dt-column-order:before, table.dataTable thead > tr > th.dt-ordering-desc .dt-column-order:after, +table.dataTable thead > tr > td.dt-orderable-asc .dt-column-order:before, +table.dataTable thead > tr > td.dt-orderable-asc .dt-column-order:after, +table.dataTable thead > tr > td.dt-orderable-desc .dt-column-order:before, +table.dataTable thead > tr > td.dt-orderable-desc .dt-column-order:after, +table.dataTable thead > tr > td.dt-ordering-asc .dt-column-order:before, +table.dataTable thead > tr > td.dt-ordering-asc .dt-column-order:after, +table.dataTable thead > tr > td.dt-ordering-desc .dt-column-order:before, +table.dataTable thead > tr > td.dt-ordering-desc .dt-column-order:after { left: 0; opacity: 0.125; line-height: 9px; @@ -140,15 +140,15 @@ table.dataTable thead > tr > td.dt-orderable-desc:hover { outline: 2px solid rgba(0, 0, 0, 0.05); outline-offset: -2px; } -table.dataTable thead > tr > th.dt-ordering-asc span.dt-column-order:before, table.dataTable thead > tr > th.dt-ordering-desc span.dt-column-order:after, -table.dataTable thead > tr > td.dt-ordering-asc span.dt-column-order:before, -table.dataTable thead > tr > td.dt-ordering-desc span.dt-column-order:after { +table.dataTable thead > tr > th.dt-ordering-asc .dt-column-order:before, table.dataTable thead > tr > th.dt-ordering-desc .dt-column-order:after, +table.dataTable thead > tr > td.dt-ordering-asc .dt-column-order:before, +table.dataTable thead > tr > td.dt-ordering-desc .dt-column-order:after { opacity: 0.6; } -table.dataTable thead > tr > th.dt-orderable-none:not(.dt-ordering-asc, .dt-ordering-desc) span.dt-column-order:empty, table.dataTable thead > tr > th.sorting_desc_disabled span.dt-column-order:after, table.dataTable thead > tr > th.sorting_asc_disabled span.dt-column-order:before, -table.dataTable thead > tr > td.dt-orderable-none:not(.dt-ordering-asc, .dt-ordering-desc) span.dt-column-order:empty, -table.dataTable thead > tr > td.sorting_desc_disabled span.dt-column-order:after, -table.dataTable thead > tr > td.sorting_asc_disabled span.dt-column-order:before { +table.dataTable thead > tr > th.dt-orderable-none:not(.dt-ordering-asc, .dt-ordering-desc) .dt-column-order:empty, table.dataTable thead > tr > th.sorting_desc_disabled .dt-column-order:after, table.dataTable thead > tr > th.sorting_asc_disabled .dt-column-order:before, +table.dataTable thead > tr > td.dt-orderable-none:not(.dt-ordering-asc, .dt-ordering-desc) .dt-column-order:empty, +table.dataTable thead > tr > td.sorting_desc_disabled .dt-column-order:after, +table.dataTable thead > tr > td.sorting_asc_disabled .dt-column-order:before { display: none; } table.dataTable thead > tr > th:active, @@ -169,24 +169,24 @@ table.dataTable tfoot > tr > td div.dt-column-footer { align-items: var(--dt-header-align-items); gap: 4px; } -table.dataTable thead > tr > th div.dt-column-header span.dt-column-title, -table.dataTable thead > tr > th div.dt-column-footer span.dt-column-title, -table.dataTable thead > tr > td div.dt-column-header span.dt-column-title, -table.dataTable thead > tr > td div.dt-column-footer span.dt-column-title, -table.dataTable tfoot > tr > th div.dt-column-header span.dt-column-title, -table.dataTable tfoot > tr > th div.dt-column-footer span.dt-column-title, -table.dataTable tfoot > tr > td div.dt-column-header span.dt-column-title, -table.dataTable tfoot > tr > td div.dt-column-footer span.dt-column-title { +table.dataTable thead > tr > th div.dt-column-header .dt-column-title, +table.dataTable thead > tr > th div.dt-column-footer .dt-column-title, +table.dataTable thead > tr > td div.dt-column-header .dt-column-title, +table.dataTable thead > tr > td div.dt-column-footer .dt-column-title, +table.dataTable tfoot > tr > th div.dt-column-header .dt-column-title, +table.dataTable tfoot > tr > th div.dt-column-footer .dt-column-title, +table.dataTable tfoot > tr > td div.dt-column-header .dt-column-title, +table.dataTable tfoot > tr > td div.dt-column-footer .dt-column-title { flex-grow: 1; } -table.dataTable thead > tr > th div.dt-column-header span.dt-column-title:empty, -table.dataTable thead > tr > th div.dt-column-footer span.dt-column-title:empty, -table.dataTable thead > tr > td div.dt-column-header span.dt-column-title:empty, -table.dataTable thead > tr > td div.dt-column-footer span.dt-column-title:empty, -table.dataTable tfoot > tr > th div.dt-column-header span.dt-column-title:empty, -table.dataTable tfoot > tr > th div.dt-column-footer span.dt-column-title:empty, -table.dataTable tfoot > tr > td div.dt-column-header span.dt-column-title:empty, -table.dataTable tfoot > tr > td div.dt-column-footer span.dt-column-title:empty { +table.dataTable thead > tr > th div.dt-column-header .dt-column-title:empty, +table.dataTable thead > tr > th div.dt-column-footer .dt-column-title:empty, +table.dataTable thead > tr > td div.dt-column-header .dt-column-title:empty, +table.dataTable thead > tr > td div.dt-column-footer .dt-column-title:empty, +table.dataTable tfoot > tr > th div.dt-column-header .dt-column-title:empty, +table.dataTable tfoot > tr > th div.dt-column-footer .dt-column-title:empty, +table.dataTable tfoot > tr > td div.dt-column-header .dt-column-title:empty, +table.dataTable tfoot > tr > td div.dt-column-footer .dt-column-title:empty { display: none; } @@ -588,16 +588,16 @@ table.dataTable.table-sm > thead > tr td.dt-ordering-asc, table.dataTable.table-sm > thead > tr td.dt-ordering-desc { padding-right: 0.25rem; } -table.dataTable.table-sm > thead > tr th.dt-orderable-asc span.dt-column-order, table.dataTable.table-sm > thead > tr th.dt-orderable-desc span.dt-column-order, table.dataTable.table-sm > thead > tr th.dt-ordering-asc span.dt-column-order, table.dataTable.table-sm > thead > tr th.dt-ordering-desc span.dt-column-order, -table.dataTable.table-sm > thead > tr td.dt-orderable-asc span.dt-column-order, -table.dataTable.table-sm > thead > tr td.dt-orderable-desc span.dt-column-order, -table.dataTable.table-sm > thead > tr td.dt-ordering-asc span.dt-column-order, -table.dataTable.table-sm > thead > tr td.dt-ordering-desc span.dt-column-order { +table.dataTable.table-sm > thead > tr th.dt-orderable-asc .dt-column-order, table.dataTable.table-sm > thead > tr th.dt-orderable-desc .dt-column-order, table.dataTable.table-sm > thead > tr th.dt-ordering-asc .dt-column-order, table.dataTable.table-sm > thead > tr th.dt-ordering-desc .dt-column-order, +table.dataTable.table-sm > thead > tr td.dt-orderable-asc .dt-column-order, +table.dataTable.table-sm > thead > tr td.dt-orderable-desc .dt-column-order, +table.dataTable.table-sm > thead > tr td.dt-ordering-asc .dt-column-order, +table.dataTable.table-sm > thead > tr td.dt-ordering-desc .dt-column-order { right: 0.25rem; } -table.dataTable.table-sm > thead > tr th.dt-type-date span.dt-column-order, table.dataTable.table-sm > thead > tr th.dt-type-numeric span.dt-column-order, -table.dataTable.table-sm > thead > tr td.dt-type-date span.dt-column-order, -table.dataTable.table-sm > thead > tr td.dt-type-numeric span.dt-column-order { +table.dataTable.table-sm > thead > tr th.dt-type-date .dt-column-order, table.dataTable.table-sm > thead > tr th.dt-type-numeric .dt-column-order, +table.dataTable.table-sm > thead > tr td.dt-type-date .dt-column-order, +table.dataTable.table-sm > thead > tr td.dt-type-numeric .dt-column-order { left: 0.25rem; } @@ -606,7 +606,8 @@ div.dt-scroll-head table.table-bordered { } div.table-responsive > div.dt-container > div.row { - margin: 0; + margin-left: 0; + margin-right: 0; } div.table-responsive > div.dt-container > div.row > div[class^=col-]:first-child { padding-left: 0; diff --git a/src/static/scripts/datatables.js b/src/static/scripts/datatables.js index 961af0b4..75f7965e 100644 --- a/src/static/scripts/datatables.js +++ b/src/static/scripts/datatables.js @@ -4,13 +4,13 @@ * * To rebuild or modify this file with the latest versions of the included * software please visit: - * https://datatables.net/download/#bs5/dt-2.3.5 + * https://datatables.net/download/#bs5/dt-2.3.6 * * Included libraries: - * DataTables 2.3.5 + * DataTables 2.3.6 */ -/*! DataTables 2.3.5 +/*! DataTables 2.3.6 * © SpryMedia Ltd - datatables.net/license */ @@ -186,7 +186,7 @@ "sDestroyWidth": $this[0].style.width, "sInstance": sId, "sTableId": sId, - colgroup: $('').prependTo(this), + colgroup: $(''), fastData: function (row, column, type) { return _fnGetCellData(oSettings, row, column, type); } @@ -259,6 +259,7 @@ "orderHandler", "titleRow", "typeDetect", + "columnTitleTag", [ "iCookieDuration", "iStateDuration" ], // backwards compat [ "oSearch", "oPreviousSearch" ], [ "aoSearchCols", "aoPreSearchCols" ], @@ -423,7 +424,7 @@ if ( oSettings.caption ) { if ( caption.length === 0 ) { - caption = $('').appendTo( $this ); + caption = $('').prependTo( $this ); } caption.html( oSettings.caption ); @@ -436,6 +437,14 @@ oSettings.captionNode = caption[0]; } + // Place the colgroup element in the correct location for the HTML structure + if (caption.length) { + oSettings.colgroup.insertAfter(caption); + } + else { + oSettings.colgroup.prependTo(oSettings.nTable); + } + if ( thead.length === 0 ) { thead = $('').appendTo($this); } @@ -451,7 +460,7 @@ if ( tfoot.length === 0 ) { // If we are a scrolling table, and no footer has been given, then we need to create // a tfoot element for the caption element to be appended to - tfoot = $('').appendTo($this); + tfoot = $('').insertAfter(thead); } oSettings.nTFoot = tfoot[0]; @@ -516,7 +525,7 @@ * * @type string */ - builder: "bs5/dt-2.3.5", + builder: "bs5/dt-2.3.6", /** * Buttons. For use with the Buttons extension for DataTables. This is @@ -1292,7 +1301,7 @@ }; // Replaceable function in api.util - var _stripHtml = function (input) { + var _stripHtml = function (input, replacement) { if (! input || typeof input !== 'string') { return input; } @@ -1304,7 +1313,7 @@ var previous; - input = input.replace(_re_html, ''); // Complete tags + input = input.replace(_re_html, replacement || ''); // Complete tags // Safety for incomplete script tag - use do / while to ensure that // we get all instances @@ -1769,7 +1778,7 @@ } }, - stripHtml: function (mixed) { + stripHtml: function (mixed, replacement) { var type = typeof mixed; if (type === 'function') { @@ -1777,7 +1786,7 @@ return; } else if (type === 'string') { - return _stripHtml(mixed); + return _stripHtml(mixed, replacement); } return mixed; }, @@ -3379,7 +3388,7 @@ colspan++; } - var titleSpan = $('span.dt-column-title', cell); + var titleSpan = $('.dt-column-title', cell); structure[row][column] = { cell: cell, @@ -4093,8 +4102,8 @@ } // Wrap the column title so we can write to it in future - if ( $('span.dt-column-title', cell).length === 0) { - $('') + if ( $('.dt-column-title', cell).length === 0) { + $(document.createElement(settings.columnTitleTag)) .addClass('dt-column-title') .append(cell.childNodes) .appendTo(cell); @@ -4105,9 +4114,9 @@ isHeader && jqCell.filter(':not([data-dt-order=disable])').length !== 0 && jqCell.parent(':not([data-dt-order=disable])').length !== 0 && - $('span.dt-column-order', cell).length === 0 + $('.dt-column-order', cell).length === 0 ) { - $('') + $(document.createElement(settings.columnTitleTag)) .addClass('dt-column-order') .appendTo(cell); } @@ -4116,7 +4125,7 @@ // layout for those elements var headerFooter = isHeader ? 'header' : 'footer'; - if ( $('span.dt-column-' + headerFooter, cell).length === 0) { + if ( $('div.dt-column-' + headerFooter, cell).length === 0) { $('
') .addClass('dt-column-' + headerFooter) .append(cell.childNodes) @@ -4273,6 +4282,10 @@ // Custom Ajax option to submit the parameters as a JSON string if (baseAjax.submitAs === 'json' && typeof data === 'object') { baseAjax.data = JSON.stringify(data); + + if (!baseAjax.contentType) { + baseAjax.contentType = 'application/json; charset=utf-8'; + } } if (typeof ajax === 'function') { @@ -5531,7 +5544,7 @@ var autoClass = _ext.type.className[column.sType]; var padding = column.sContentPadding || (scrollX ? '-' : ''); var text = longest + padding; - var insert = longest.indexOf('<') === -1 + var insert = longest.indexOf('<') === -1 && longest.indexOf('&') === -1 ? document.createTextNode(text) : text @@ -5719,15 +5732,20 @@ .replace(/id=".*?"/g, '') .replace(/name=".*?"/g, ''); - var s = _stripHtml(cellString) + // Don't want Javascript at all in these calculation cells. + cellString = cellString.replace(//gi, ' '); + + var noHtml = _stripHtml(cellString, ' ') .replace( / /g, ' ' ); + // The length is calculated on the text only, but we keep the HTML + // in the string so it can be used in the calculation table collection.push({ - str: s, - len: s.length + str: cellString, + len: noHtml.length }); - allStrings.push(s); + allStrings.push(noHtml); } // Order and then cut down to the size we need @@ -8782,7 +8800,7 @@ // Automatic - find the _last_ unique cell from the top that is not empty (last for // backwards compatibility) for (var i=0 ; i 6 ? h - 6 : h; - return decToHex(255 * ( - h < 1 ? m1 + (m2 - m1) * h : - h < 3 ? m2 : - h < 4 ? m1 + (m2 - m1) * (4 - h) : - m1)); -} - -/** - * @param {string} color Color value to parse. Currently hexadecimal strings on the format #rgb[a] and #rrggbb[aa] are supported. - * @returns {string} - */ -function parseColor(color) { - if (/^#[0-9a-f]{3,8}$/i.test(color)) { - var result; - var colorLength = color.length; - - if (colorLength < 6) { - var r = color[1], - g = color[2], - b = color[3], - a = color[4] || ""; - result = "#" + r + r + g + g + b + b + a + a; - } - if (colorLength == 7 || colorLength > 8) { - result = color; - } - - return result; - } -} - -/** - * Converts a hexadecimal color to a CSS3 compatible color. - * @param {string} hexColor Color on the format "#RRGGBB" or "#RRGGBBAA" - * @returns {string} - */ -function toCss3Color(hexColor) { - var a = parseHex(hexColor, 7, 2); - var result; - - if (isNaN(a)) { - result = hexColor; - } else { - var r = parseHex(hexColor, 1, 2), - g = parseHex(hexColor, 3, 2), - b = parseHex(hexColor, 5, 2); - result = "rgba(" + r + "," + g + "," + b + "," + (a / 255).toFixed(2) + ")"; - } - - return result; -} - -/** - * Converts an HSL color to a hexadecimal RGB color. - * @param {number} hue Hue in range [0, 1] - * @param {number} saturation Saturation in range [0, 1] - * @param {number} lightness Lightness in range [0, 1] - * @returns {string} - */ -function hsl(hue, saturation, lightness) { - // Based on http://www.w3.org/TR/2011/REC-css3-color-20110607/#hsl-color - var result; - - if (saturation == 0) { - var partialHex = decToHex(lightness * 255); - result = partialHex + partialHex + partialHex; - } - else { - var m2 = lightness <= 0.5 ? lightness * (saturation + 1) : lightness + saturation - lightness * saturation, - m1 = lightness * 2 - m2; - result = - hueToRgb(m1, m2, hue * 6 + 2) + - hueToRgb(m1, m2, hue * 6) + - hueToRgb(m1, m2, hue * 6 - 2); - } - - return "#" + result; -} - -/** - * Converts an HSL color to a hexadecimal RGB color. This function will correct the lightness for the "dark" hues - * @param {number} hue Hue in range [0, 1] - * @param {number} saturation Saturation in range [0, 1] - * @param {number} lightness Lightness in range [0, 1] - * @returns {string} - */ -function correctedHsl(hue, saturation, lightness) { - // The corrector specifies the perceived middle lightness for each hue - var correctors = [ 0.55, 0.5, 0.5, 0.46, 0.6, 0.55, 0.55 ], - corrector = correctors[(hue * 6 + 0.5) | 0]; - - // Adjust the input lightness relative to the corrector - lightness = lightness < 0.5 ? lightness * corrector * 2 : corrector + (lightness - 0.5) * (1 - corrector) * 2; - - return hsl(hue, saturation, lightness); +function decToHex(v) { + v |= 0; // Ensure integer value + return v < 0 ? "00" : + v < 16 ? "0" + v.toString(16) : + v < 256 ? v.toString(16) : + "ff"; } -/* global umdGlobal */ - -// In the future we can replace `GLOBAL` with `globalThis`, but for now use the old school global detection for -// backward compatibility. +function hueToRgb(m1, m2, h) { + h = h < 0 ? h + 6 : h > 6 ? h - 6 : h; + return decToHex(255 * ( + h < 1 ? m1 + (m2 - m1) * h : + h < 3 ? m2 : + h < 4 ? m1 + (m2 - m1) * (4 - h) : + m1)); +} + +/** + * @param {string} color Color value to parse. Currently hexadecimal strings on the format #rgb[a] and #rrggbb[aa] are supported. + * @returns {string} + */ +function parseColor(color) { + if (/^#[0-9a-f]{3,8}$/i.test(color)) { + var result; + var colorLength = color.length; + + if (colorLength < 6) { + var r = color[1], + g = color[2], + b = color[3], + a = color[4] || ""; + result = "#" + r + r + g + g + b + b + a + a; + } + if (colorLength == 7 || colorLength > 8) { + result = color; + } + + return result; + } +} + +/** + * Converts a hexadecimal color to a CSS3 compatible color. + * @param {string} hexColor Color on the format "#RRGGBB" or "#RRGGBBAA" + * @returns {string} + */ +function toCss3Color(hexColor) { + var a = parseHex(hexColor, 7, 2); + var result; + + if (isNaN(a)) { + result = hexColor; + } else { + var r = parseHex(hexColor, 1, 2), + g = parseHex(hexColor, 3, 2), + b = parseHex(hexColor, 5, 2); + result = "rgba(" + r + "," + g + "," + b + "," + (a / 255).toFixed(2) + ")"; + } + + return result; +} + +/** + * Converts an HSL color to a hexadecimal RGB color. + * @param {number} hue Hue in range [0, 1] + * @param {number} saturation Saturation in range [0, 1] + * @param {number} lightness Lightness in range [0, 1] + * @returns {string} + */ +function hsl(hue, saturation, lightness) { + // Based on http://www.w3.org/TR/2011/REC-css3-color-20110607/#hsl-color + var result; + + if (saturation == 0) { + var partialHex = decToHex(lightness * 255); + result = partialHex + partialHex + partialHex; + } + else { + var m2 = lightness <= 0.5 ? lightness * (saturation + 1) : lightness + saturation - lightness * saturation, + m1 = lightness * 2 - m2; + result = + hueToRgb(m1, m2, hue * 6 + 2) + + hueToRgb(m1, m2, hue * 6) + + hueToRgb(m1, m2, hue * 6 - 2); + } + + return "#" + result; +} + +/** + * Converts an HSL color to a hexadecimal RGB color. This function will correct the lightness for the "dark" hues + * @param {number} hue Hue in range [0, 1] + * @param {number} saturation Saturation in range [0, 1] + * @param {number} lightness Lightness in range [0, 1] + * @returns {string} + */ +function correctedHsl(hue, saturation, lightness) { + // The corrector specifies the perceived middle lightness for each hue + var correctors = [ 0.55, 0.5, 0.5, 0.46, 0.6, 0.55, 0.55 ], + corrector = correctors[(hue * 6 + 0.5) | 0]; + + // Adjust the input lightness relative to the corrector + lightness = lightness < 0.5 ? lightness * corrector * 2 : corrector + (lightness - 0.5) * (1 - corrector) * 2; + + return hsl(hue, saturation, lightness); +} + +/* global umdGlobal */ + +// In the future we can replace `GLOBAL` with `globalThis`, but for now use the old school global detection for +// backward compatibility. var GLOBAL = umdGlobal; -/** - * @typedef {Object} ParsedConfiguration - * @property {number} colorSaturation - * @property {number} grayscaleSaturation - * @property {string} backColor - * @property {number} iconPadding - * @property {function(number):number} hue - * @property {function(number):number} colorLightness - * @property {function(number):number} grayscaleLightness - */ - -var CONFIG_PROPERTIES = { - G/*GLOBAL*/: "jdenticon_config", - n/*MODULE*/: "config", -}; - -var rootConfigurationHolder = {}; - -/** - * Defines the deprecated `config` property on the root Jdenticon object without printing a warning in the console - * when it is being used. - * @param {!Object} rootObject - */ -function defineConfigProperty(rootObject) { - rootConfigurationHolder = rootObject; -} - -/** - * Sets a new icon style configuration. The new configuration is not merged with the previous one. * - * @param {Object} newConfiguration - New configuration object. - */ -function configure(newConfiguration) { - if (arguments.length) { - rootConfigurationHolder[CONFIG_PROPERTIES.n/*MODULE*/] = newConfiguration; - } - return rootConfigurationHolder[CONFIG_PROPERTIES.n/*MODULE*/]; -} - -/** - * Gets the normalized current Jdenticon color configuration. Missing fields have default values. - * @param {Object|number|undefined} paddingOrLocalConfig - Configuration passed to the called API method. A - * local configuration overrides the global configuration in it entirety. This parameter can for backward - * compatibility also contain a padding value. A padding value only overrides the global padding, not the - * entire global configuration. - * @param {number} defaultPadding - Padding used if no padding is specified in neither the configuration nor - * explicitly to the API method. - * @returns {ParsedConfiguration} - */ -function getConfiguration(paddingOrLocalConfig, defaultPadding) { - var configObject = - typeof paddingOrLocalConfig == "object" && paddingOrLocalConfig || - rootConfigurationHolder[CONFIG_PROPERTIES.n/*MODULE*/] || - GLOBAL[CONFIG_PROPERTIES.G/*GLOBAL*/] || - { }, - - lightnessConfig = configObject["lightness"] || { }, - - // In versions < 2.1.0 there was no grayscale saturation - - // saturation was the color saturation. - saturation = configObject["saturation"] || { }, - colorSaturation = "color" in saturation ? saturation["color"] : saturation, - grayscaleSaturation = saturation["grayscale"], - - backColor = configObject["backColor"], - padding = configObject["padding"]; - - /** - * Creates a lightness range. - */ - function lightness(configName, defaultRange) { - var range = lightnessConfig[configName]; - - // Check if the lightness range is an array-like object. This way we ensure the - // array contain two values at the same time. - if (!(range && range.length > 1)) { - range = defaultRange; - } - - /** - * Gets a lightness relative the specified value in the specified lightness range. - */ - return function (value) { - value = range[0] + value * (range[1] - range[0]); - return value < 0 ? 0 : value > 1 ? 1 : value; - }; - } - - /** - * Gets a hue allowed by the configured hue restriction, - * provided the originally computed hue. - */ - function hueFunction(originalHue) { - var hueConfig = configObject["hues"]; - var hue; - - // Check if 'hues' is an array-like object. This way we also ensure that - // the array is not empty, which would mean no hue restriction. - if (hueConfig && hueConfig.length > 0) { - // originalHue is in the range [0, 1] - // Multiply with 0.999 to change the range to [0, 1) and then truncate the index. - hue = hueConfig[0 | (0.999 * originalHue * hueConfig.length)]; - } - - return typeof hue == "number" ? - - // A hue was specified. We need to convert the hue from - // degrees on any turn - e.g. 746° is a perfectly valid hue - - // to turns in the range [0, 1). - ((((hue / 360) % 1) + 1) % 1) : - - // No hue configured => use original hue - originalHue; - } - - return { - X/*hue*/: hueFunction, - p/*colorSaturation*/: typeof colorSaturation == "number" ? colorSaturation : 0.5, - H/*grayscaleSaturation*/: typeof grayscaleSaturation == "number" ? grayscaleSaturation : 0, - q/*colorLightness*/: lightness("color", [0.4, 0.8]), - I/*grayscaleLightness*/: lightness("grayscale", [0.3, 0.9]), - J/*backColor*/: parseColor(backColor), - Y/*iconPadding*/: - typeof paddingOrLocalConfig == "number" ? paddingOrLocalConfig : - typeof padding == "number" ? padding : - defaultPadding - } +/** + * @typedef {Object} ParsedConfiguration + * @property {number} colorSaturation + * @property {number} grayscaleSaturation + * @property {string} backColor + * @property {number} iconPadding + * @property {function(number):number} hue + * @property {function(number):number} colorLightness + * @property {function(number):number} grayscaleLightness + */ + +var CONFIG_PROPERTIES = { + G/*GLOBAL*/: "jdenticon_config", + n/*MODULE*/: "config", +}; + +var rootConfigurationHolder = {}; + +/** + * Defines the deprecated `config` property on the root Jdenticon object without printing a warning in the console + * when it is being used. + * @param {!Object} rootObject + */ +function defineConfigProperty(rootObject) { + rootConfigurationHolder = rootObject; } -var ICON_TYPE_SVG = 1; - -var ICON_TYPE_CANVAS = 2; - -var ATTRIBUTES = { - t/*HASH*/: "data-jdenticon-hash", - o/*VALUE*/: "data-jdenticon-value" -}; - -var IS_RENDERED_PROPERTY = "jdenticonRendered"; - -var ICON_SELECTOR = "[" + ATTRIBUTES.t/*HASH*/ +"],[" + ATTRIBUTES.o/*VALUE*/ +"]"; - -var documentQuerySelectorAll = /** @type {!Function} */ ( - typeof document !== "undefined" && document.querySelectorAll.bind(document)); - -function getIdenticonType(el) { - if (el) { - var tagName = el["tagName"]; - - if (/^svg$/i.test(tagName)) { - return ICON_TYPE_SVG; - } - - if (/^canvas$/i.test(tagName) && "getContext" in el) { - return ICON_TYPE_CANVAS; - } - } -} - -function whenDocumentIsReady(/** @type {Function} */ callback) { - function loadedHandler() { - document.removeEventListener("DOMContentLoaded", loadedHandler); - window.removeEventListener("load", loadedHandler); - setTimeout(callback, 0); // Give scripts a chance to run - } - - if (typeof document !== "undefined" && - typeof window !== "undefined" && - typeof setTimeout !== "undefined" - ) { - if (document.readyState === "loading") { - document.addEventListener("DOMContentLoaded", loadedHandler); - window.addEventListener("load", loadedHandler); - } else { - // Document already loaded. The load events above likely won't be raised - setTimeout(callback, 0); - } - } +/** + * Sets a new icon style configuration. The new configuration is not merged with the previous one. * + * @param {Object} newConfiguration - New configuration object. + */ +function configure(newConfiguration) { + if (arguments.length) { + rootConfigurationHolder[CONFIG_PROPERTIES.n/*MODULE*/] = newConfiguration; + } + return rootConfigurationHolder[CONFIG_PROPERTIES.n/*MODULE*/]; } -function observer(updateCallback) { - if (typeof MutationObserver != "undefined") { - var mutationObserver = new MutationObserver(function onmutation(mutations) { - for (var mutationIndex = 0; mutationIndex < mutations.length; mutationIndex++) { - var mutation = mutations[mutationIndex]; - var addedNodes = mutation.addedNodes; - - for (var addedNodeIndex = 0; addedNodes && addedNodeIndex < addedNodes.length; addedNodeIndex++) { - var addedNode = addedNodes[addedNodeIndex]; - - // Skip other types of nodes than element nodes, since they might not support - // the querySelectorAll method => runtime error. - if (addedNode.nodeType == 1) { - if (getIdenticonType(addedNode)) { - updateCallback(addedNode); - } - else { - var icons = /** @type {Element} */(addedNode).querySelectorAll(ICON_SELECTOR); - for (var iconIndex = 0; iconIndex < icons.length; iconIndex++) { - updateCallback(icons[iconIndex]); - } - } - } - } - - if (mutation.type == "attributes" && getIdenticonType(mutation.target)) { - updateCallback(mutation.target); - } - } - }); - - mutationObserver.observe(document.body, { - "childList": true, - "attributes": true, - "attributeFilter": [ATTRIBUTES.o/*VALUE*/, ATTRIBUTES.t/*HASH*/, "width", "height"], - "subtree": true, - }); - } +/** + * Gets the normalized current Jdenticon color configuration. Missing fields have default values. + * @param {Object|number|undefined} paddingOrLocalConfig - Configuration passed to the called API method. A + * local configuration overrides the global configuration in it entirety. This parameter can for backward + * compatibility also contain a padding value. A padding value only overrides the global padding, not the + * entire global configuration. + * @param {number} defaultPadding - Padding used if no padding is specified in neither the configuration nor + * explicitly to the API method. + * @returns {ParsedConfiguration} + */ +function getConfiguration(paddingOrLocalConfig, defaultPadding) { + var configObject = + typeof paddingOrLocalConfig == "object" && paddingOrLocalConfig || + rootConfigurationHolder[CONFIG_PROPERTIES.n/*MODULE*/] || + GLOBAL[CONFIG_PROPERTIES.G/*GLOBAL*/] || + { }, + + lightnessConfig = configObject["lightness"] || { }, + + // In versions < 2.1.0 there was no grayscale saturation - + // saturation was the color saturation. + saturation = configObject["saturation"] || { }, + colorSaturation = "color" in saturation ? saturation["color"] : saturation, + grayscaleSaturation = saturation["grayscale"], + + backColor = configObject["backColor"], + padding = configObject["padding"]; + + /** + * Creates a lightness range. + */ + function lightness(configName, defaultRange) { + var range = lightnessConfig[configName]; + + // Check if the lightness range is an array-like object. This way we ensure the + // array contain two values at the same time. + if (!(range && range.length > 1)) { + range = defaultRange; + } + + /** + * Gets a lightness relative the specified value in the specified lightness range. + */ + return function (value) { + value = range[0] + value * (range[1] - range[0]); + return value < 0 ? 0 : value > 1 ? 1 : value; + }; + } + + /** + * Gets a hue allowed by the configured hue restriction, + * provided the originally computed hue. + */ + function hueFunction(originalHue) { + var hueConfig = configObject["hues"]; + var hue; + + // Check if 'hues' is an array-like object. This way we also ensure that + // the array is not empty, which would mean no hue restriction. + if (hueConfig && hueConfig.length > 0) { + // originalHue is in the range [0, 1] + // Multiply with 0.999 to change the range to [0, 1) and then truncate the index. + hue = hueConfig[0 | (0.999 * originalHue * hueConfig.length)]; + } + + return typeof hue == "number" ? + + // A hue was specified. We need to convert the hue from + // degrees on any turn - e.g. 746° is a perfectly valid hue - + // to turns in the range [0, 1). + ((((hue / 360) % 1) + 1) % 1) : + + // No hue configured => use original hue + originalHue; + } + + return { + X/*hue*/: hueFunction, + p/*colorSaturation*/: typeof colorSaturation == "number" ? colorSaturation : 0.5, + H/*grayscaleSaturation*/: typeof grayscaleSaturation == "number" ? grayscaleSaturation : 0, + q/*colorLightness*/: lightness("color", [0.4, 0.8]), + I/*grayscaleLightness*/: lightness("grayscale", [0.3, 0.9]), + J/*backColor*/: parseColor(backColor), + Y/*iconPadding*/: + typeof paddingOrLocalConfig == "number" ? paddingOrLocalConfig : + typeof padding == "number" ? padding : + defaultPadding + } } -/** - * Represents a point. - */ -function Point(x, y) { - this.x = x; - this.y = y; +var ICON_TYPE_SVG = 1; + +var ICON_TYPE_CANVAS = 2; + +var ATTRIBUTES = { + t/*HASH*/: "data-jdenticon-hash", + o/*VALUE*/: "data-jdenticon-value" +}; + +var IS_RENDERED_PROPERTY = "jdenticonRendered"; + +var ICON_SELECTOR = "[" + ATTRIBUTES.t/*HASH*/ +"],[" + ATTRIBUTES.o/*VALUE*/ +"]"; + +var documentQuerySelectorAll = /** @type {!Function} */ ( + typeof document !== "undefined" && document.querySelectorAll.bind(document)); + +function getIdenticonType(el) { + if (el) { + var tagName = el["tagName"]; + + if (/^svg$/i.test(tagName)) { + return ICON_TYPE_SVG; + } + + if (/^canvas$/i.test(tagName) && "getContext" in el) { + return ICON_TYPE_CANVAS; + } + } } -/** - * Translates and rotates a point before being passed on to the canvas context. This was previously done by the canvas context itself, - * but this caused a rendering issue in Chrome on sizes > 256 where the rotation transformation of inverted paths was not done properly. - */ -function Transform(x, y, size, rotation) { - this.u/*_x*/ = x; - this.v/*_y*/ = y; - this.K/*_size*/ = size; - this.Z/*_rotation*/ = rotation; -} - -/** - * Transforms the specified point based on the translation and rotation specification for this Transform. - * @param {number} x x-coordinate - * @param {number} y y-coordinate - * @param {number=} w The width of the transformed rectangle. If greater than 0, this will ensure the returned point is of the upper left corner of the transformed rectangle. - * @param {number=} h The height of the transformed rectangle. If greater than 0, this will ensure the returned point is of the upper left corner of the transformed rectangle. - */ -Transform.prototype.L/*transformIconPoint*/ = function transformIconPoint (x, y, w, h) { - var right = this.u/*_x*/ + this.K/*_size*/, - bottom = this.v/*_y*/ + this.K/*_size*/, - rotation = this.Z/*_rotation*/; - return rotation === 1 ? new Point(right - y - (h || 0), this.v/*_y*/ + x) : - rotation === 2 ? new Point(right - x - (w || 0), bottom - y - (h || 0)) : - rotation === 3 ? new Point(this.u/*_x*/ + y, bottom - x - (w || 0)) : - new Point(this.u/*_x*/ + x, this.v/*_y*/ + y); -}; - +function whenDocumentIsReady(/** @type {Function} */ callback) { + function loadedHandler() { + document.removeEventListener("DOMContentLoaded", loadedHandler); + window.removeEventListener("load", loadedHandler); + setTimeout(callback, 0); // Give scripts a chance to run + } + + if (typeof document !== "undefined" && + typeof window !== "undefined" && + typeof setTimeout !== "undefined" + ) { + if (document.readyState === "loading") { + document.addEventListener("DOMContentLoaded", loadedHandler); + window.addEventListener("load", loadedHandler); + } else { + // Document already loaded. The load events above likely won't be raised + setTimeout(callback, 0); + } + } +} + +function observer(updateCallback) { + if (typeof MutationObserver != "undefined") { + var mutationObserver = new MutationObserver(function onmutation(mutations) { + for (var mutationIndex = 0; mutationIndex < mutations.length; mutationIndex++) { + var mutation = mutations[mutationIndex]; + var addedNodes = mutation.addedNodes; + + for (var addedNodeIndex = 0; addedNodes && addedNodeIndex < addedNodes.length; addedNodeIndex++) { + var addedNode = addedNodes[addedNodeIndex]; + + // Skip other types of nodes than element nodes, since they might not support + // the querySelectorAll method => runtime error. + if (addedNode.nodeType == 1) { + if (getIdenticonType(addedNode)) { + updateCallback(addedNode); + } + else { + var icons = /** @type {Element} */(addedNode).querySelectorAll(ICON_SELECTOR); + for (var iconIndex = 0; iconIndex < icons.length; iconIndex++) { + updateCallback(icons[iconIndex]); + } + } + } + } + + if (mutation.type == "attributes" && getIdenticonType(mutation.target)) { + updateCallback(mutation.target); + } + } + }); + + mutationObserver.observe(document.body, { + "childList": true, + "attributes": true, + "attributeFilter": [ATTRIBUTES.o/*VALUE*/, ATTRIBUTES.t/*HASH*/, "width", "height"], + "subtree": true, + }); + } +} + +/** + * Represents a point. + */ +function Point(x, y) { + this.x = x; + this.y = y; +} + +/** + * Translates and rotates a point before being passed on to the canvas context. This was previously done by the canvas context itself, + * but this caused a rendering issue in Chrome on sizes > 256 where the rotation transformation of inverted paths was not done properly. + */ +function Transform(x, y, size, rotation) { + this.u/*_x*/ = x; + this.v/*_y*/ = y; + this.K/*_size*/ = size; + this.Z/*_rotation*/ = rotation; +} + +/** + * Transforms the specified point based on the translation and rotation specification for this Transform. + * @param {number} x x-coordinate + * @param {number} y y-coordinate + * @param {number=} w The width of the transformed rectangle. If greater than 0, this will ensure the returned point is of the upper left corner of the transformed rectangle. + * @param {number=} h The height of the transformed rectangle. If greater than 0, this will ensure the returned point is of the upper left corner of the transformed rectangle. + */ +Transform.prototype.L/*transformIconPoint*/ = function transformIconPoint (x, y, w, h) { + var right = this.u/*_x*/ + this.K/*_size*/, + bottom = this.v/*_y*/ + this.K/*_size*/, + rotation = this.Z/*_rotation*/; + return rotation === 1 ? new Point(right - y - (h || 0), this.v/*_y*/ + x) : + rotation === 2 ? new Point(right - x - (w || 0), bottom - y - (h || 0)) : + rotation === 3 ? new Point(this.u/*_x*/ + y, bottom - x - (w || 0)) : + new Point(this.u/*_x*/ + x, this.v/*_y*/ + y); +}; + var NO_TRANSFORM = new Transform(0, 0, 0, 0); - - -/** - * Provides helper functions for rendering common basic shapes. - */ -function Graphics(renderer) { - /** - * @type {Renderer} - * @private - */ - this.M/*_renderer*/ = renderer; - - /** - * @type {Transform} - */ - this.A/*currentTransform*/ = NO_TRANSFORM; -} -var Graphics__prototype = Graphics.prototype; - -/** - * Adds a polygon to the underlying renderer. - * @param {Array} points The points of the polygon clockwise on the format [ x0, y0, x1, y1, ..., xn, yn ] - * @param {boolean=} invert Specifies if the polygon will be inverted. - */ + + +/** + * Provides helper functions for rendering common basic shapes. + */ +function Graphics(renderer) { + /** + * @type {Renderer} + * @private + */ + this.M/*_renderer*/ = renderer; + + /** + * @type {Transform} + */ + this.A/*currentTransform*/ = NO_TRANSFORM; +} +var Graphics__prototype = Graphics.prototype; + +/** + * Adds a polygon to the underlying renderer. + * @param {Array} points The points of the polygon clockwise on the format [ x0, y0, x1, y1, ..., xn, yn ] + * @param {boolean=} invert Specifies if the polygon will be inverted. + */ Graphics__prototype.g/*addPolygon*/ = function addPolygon (points, invert) { var this$1 = this; - - var di = invert ? -2 : 2, - transformedPoints = []; - - for (var i = invert ? points.length - 2 : 0; i < points.length && i >= 0; i += di) { - transformedPoints.push(this$1.A/*currentTransform*/.L/*transformIconPoint*/(points[i], points[i + 1])); - } - - this.M/*_renderer*/.g/*addPolygon*/(transformedPoints); -}; - -/** - * Adds a polygon to the underlying renderer. - * Source: http://stackoverflow.com/a/2173084 - * @param {number} x The x-coordinate of the upper left corner of the rectangle holding the entire ellipse. - * @param {number} y The y-coordinate of the upper left corner of the rectangle holding the entire ellipse. - * @param {number} size The size of the ellipse. - * @param {boolean=} invert Specifies if the ellipse will be inverted. - */ -Graphics__prototype.h/*addCircle*/ = function addCircle (x, y, size, invert) { - var p = this.A/*currentTransform*/.L/*transformIconPoint*/(x, y, size, size); - this.M/*_renderer*/.h/*addCircle*/(p, size, invert); -}; - -/** - * Adds a rectangle to the underlying renderer. - * @param {number} x The x-coordinate of the upper left corner of the rectangle. - * @param {number} y The y-coordinate of the upper left corner of the rectangle. - * @param {number} w The width of the rectangle. - * @param {number} h The height of the rectangle. - * @param {boolean=} invert Specifies if the rectangle will be inverted. - */ -Graphics__prototype.i/*addRectangle*/ = function addRectangle (x, y, w, h, invert) { - this.g/*addPolygon*/([ - x, y, - x + w, y, - x + w, y + h, - x, y + h - ], invert); -}; - -/** - * Adds a right triangle to the underlying renderer. - * @param {number} x The x-coordinate of the upper left corner of the rectangle holding the triangle. - * @param {number} y The y-coordinate of the upper left corner of the rectangle holding the triangle. - * @param {number} w The width of the triangle. - * @param {number} h The height of the triangle. - * @param {number} r The rotation of the triangle (clockwise). 0 = right corner of the triangle in the lower left corner of the bounding rectangle. - * @param {boolean=} invert Specifies if the triangle will be inverted. - */ -Graphics__prototype.j/*addTriangle*/ = function addTriangle (x, y, w, h, r, invert) { - var points = [ - x + w, y, - x + w, y + h, - x, y + h, - x, y - ]; - points.splice(((r || 0) % 4) * 2, 2); - this.g/*addPolygon*/(points, invert); -}; - -/** - * Adds a rhombus to the underlying renderer. - * @param {number} x The x-coordinate of the upper left corner of the rectangle holding the rhombus. - * @param {number} y The y-coordinate of the upper left corner of the rectangle holding the rhombus. - * @param {number} w The width of the rhombus. - * @param {number} h The height of the rhombus. - * @param {boolean=} invert Specifies if the rhombus will be inverted. - */ -Graphics__prototype.N/*addRhombus*/ = function addRhombus (x, y, w, h, invert) { - this.g/*addPolygon*/([ - x + w / 2, y, - x + w, y + h / 2, - x + w / 2, y + h, - x, y + h / 2 - ], invert); + + var di = invert ? -2 : 2, + transformedPoints = []; + + for (var i = invert ? points.length - 2 : 0; i < points.length && i >= 0; i += di) { + transformedPoints.push(this$1.A/*currentTransform*/.L/*transformIconPoint*/(points[i], points[i + 1])); + } + + this.M/*_renderer*/.g/*addPolygon*/(transformedPoints); }; -/** - * @param {number} index - * @param {Graphics} g - * @param {number} cell - * @param {number} positionIndex - */ -function centerShape(index, g, cell, positionIndex) { - index = index % 14; - - var k, m, w, h, inner, outer; - - !index ? ( - k = cell * 0.42, - g.g/*addPolygon*/([ - 0, 0, - cell, 0, - cell, cell - k * 2, - cell - k, cell, - 0, cell - ])) : - - index == 1 ? ( - w = 0 | (cell * 0.5), - h = 0 | (cell * 0.8), - - g.j/*addTriangle*/(cell - w, 0, w, h, 2)) : - - index == 2 ? ( - w = 0 | (cell / 3), - g.i/*addRectangle*/(w, w, cell - w, cell - w)) : - - index == 3 ? ( - inner = cell * 0.1, - // Use fixed outer border widths in small icons to ensure the border is drawn - outer = - cell < 6 ? 1 : - cell < 8 ? 2 : - (0 | (cell * 0.25)), - - inner = - inner > 1 ? (0 | inner) : // large icon => truncate decimals - inner > 0.5 ? 1 : // medium size icon => fixed width - inner, // small icon => anti-aliased border - - g.i/*addRectangle*/(outer, outer, cell - inner - outer, cell - inner - outer)) : - - index == 4 ? ( - m = 0 | (cell * 0.15), - w = 0 | (cell * 0.5), - g.h/*addCircle*/(cell - w - m, cell - w - m, w)) : - - index == 5 ? ( - inner = cell * 0.1, - outer = inner * 4, - - // Align edge to nearest pixel in large icons - outer > 3 && (outer = 0 | outer), - - g.i/*addRectangle*/(0, 0, cell, cell), - g.g/*addPolygon*/([ - outer, outer, - cell - inner, outer, - outer + (cell - outer - inner) / 2, cell - inner - ], true)) : - - index == 6 ? - g.g/*addPolygon*/([ - 0, 0, - cell, 0, - cell, cell * 0.7, - cell * 0.4, cell * 0.4, - cell * 0.7, cell, - 0, cell - ]) : - - index == 7 ? - g.j/*addTriangle*/(cell / 2, cell / 2, cell / 2, cell / 2, 3) : - - index == 8 ? ( - g.i/*addRectangle*/(0, 0, cell, cell / 2), - g.i/*addRectangle*/(0, cell / 2, cell / 2, cell / 2), - g.j/*addTriangle*/(cell / 2, cell / 2, cell / 2, cell / 2, 1)) : - - index == 9 ? ( - inner = cell * 0.14, - // Use fixed outer border widths in small icons to ensure the border is drawn - outer = - cell < 4 ? 1 : - cell < 6 ? 2 : - (0 | (cell * 0.35)), - - inner = - cell < 8 ? inner : // small icon => anti-aliased border - (0 | inner), // large icon => truncate decimals - - g.i/*addRectangle*/(0, 0, cell, cell), - g.i/*addRectangle*/(outer, outer, cell - outer - inner, cell - outer - inner, true)) : - - index == 10 ? ( - inner = cell * 0.12, - outer = inner * 3, - - g.i/*addRectangle*/(0, 0, cell, cell), - g.h/*addCircle*/(outer, outer, cell - inner - outer, true)) : - - index == 11 ? - g.j/*addTriangle*/(cell / 2, cell / 2, cell / 2, cell / 2, 3) : - - index == 12 ? ( - m = cell * 0.25, - g.i/*addRectangle*/(0, 0, cell, cell), - g.N/*addRhombus*/(m, m, cell - m, cell - m, true)) : - - // 13 - ( - !positionIndex && ( - m = cell * 0.4, w = cell * 1.2, - g.h/*addCircle*/(m, m, w) - ) - ); -} - -/** - * @param {number} index - * @param {Graphics} g - * @param {number} cell - */ -function outerShape(index, g, cell) { - index = index % 4; - - var m; - - !index ? - g.j/*addTriangle*/(0, 0, cell, cell, 0) : - - index == 1 ? - g.j/*addTriangle*/(0, cell / 2, cell, cell / 2, 0) : - - index == 2 ? - g.N/*addRhombus*/(0, 0, cell, cell) : - - // 3 - ( - m = cell / 6, - g.h/*addCircle*/(m, m, cell - 2 * m) - ); -} - -/** - * Gets a set of identicon color candidates for a specified hue and config. - * @param {number} hue - * @param {ParsedConfiguration} config - */ -function colorTheme(hue, config) { - hue = config.X/*hue*/(hue); - return [ - // Dark gray - correctedHsl(hue, config.H/*grayscaleSaturation*/, config.I/*grayscaleLightness*/(0)), - // Mid color - correctedHsl(hue, config.p/*colorSaturation*/, config.q/*colorLightness*/(0.5)), - // Light gray - correctedHsl(hue, config.H/*grayscaleSaturation*/, config.I/*grayscaleLightness*/(1)), - // Light color - correctedHsl(hue, config.p/*colorSaturation*/, config.q/*colorLightness*/(1)), - // Dark color - correctedHsl(hue, config.p/*colorSaturation*/, config.q/*colorLightness*/(0)) - ]; -} - -/** - * Draws an identicon to a specified renderer. - * @param {Renderer} renderer - * @param {string} hash - * @param {Object|number=} config - */ -function iconGenerator(renderer, hash, config) { - var parsedConfig = getConfiguration(config, 0.08); - - // Set background color - if (parsedConfig.J/*backColor*/) { - renderer.m/*setBackground*/(parsedConfig.J/*backColor*/); - } - - // Calculate padding and round to nearest integer - var size = renderer.k/*iconSize*/; - var padding = (0.5 + size * parsedConfig.Y/*iconPadding*/) | 0; - size -= padding * 2; - - var graphics = new Graphics(renderer); - - // Calculate cell size and ensure it is an integer - var cell = 0 | (size / 4); - - // Since the cell size is integer based, the actual icon will be slightly smaller than specified => center icon - var x = 0 | (padding + size / 2 - cell * 2); - var y = 0 | (padding + size / 2 - cell * 2); - - function renderShape(colorIndex, shapes, index, rotationIndex, positions) { - var shapeIndex = parseHex(hash, index, 1); - var r = rotationIndex ? parseHex(hash, rotationIndex, 1) : 0; - - renderer.O/*beginShape*/(availableColors[selectedColorIndexes[colorIndex]]); - - for (var i = 0; i < positions.length; i++) { - graphics.A/*currentTransform*/ = new Transform(x + positions[i][0] * cell, y + positions[i][1] * cell, cell, r++ % 4); - shapes(shapeIndex, graphics, cell, i); - } - - renderer.P/*endShape*/(); - } - - // AVAILABLE COLORS - var hue = parseHex(hash, -7) / 0xfffffff, - - // Available colors for this icon - availableColors = colorTheme(hue, parsedConfig), - - // The index of the selected colors - selectedColorIndexes = []; - - var index; - - function isDuplicate(values) { - if (values.indexOf(index) >= 0) { - for (var i = 0; i < values.length; i++) { - if (selectedColorIndexes.indexOf(values[i]) >= 0) { - return true; - } - } - } - } - - for (var i = 0; i < 3; i++) { - index = parseHex(hash, 8 + i, 1) % availableColors.length; - if (isDuplicate([0, 4]) || // Disallow dark gray and dark color combo - isDuplicate([2, 3])) { // Disallow light gray and light color combo - index = 1; - } - selectedColorIndexes.push(index); - } - - // ACTUAL RENDERING - // Sides - renderShape(0, outerShape, 2, 3, [[1, 0], [2, 0], [2, 3], [1, 3], [0, 1], [3, 1], [3, 2], [0, 2]]); - // Corners - renderShape(1, outerShape, 4, 5, [[0, 0], [3, 0], [3, 3], [0, 3]]); - // Center - renderShape(2, centerShape, 1, null, [[1, 1], [2, 1], [2, 2], [1, 2]]); - - renderer.finish(); -} - -/** - * Computes a SHA1 hash for any value and returns it as a hexadecimal string. - * - * This function is optimized for minimal code size and rather short messages. - * - * @param {string} message - */ -function sha1(message) { - var HASH_SIZE_HALF_BYTES = 40; - var BLOCK_SIZE_WORDS = 16; - - // Variables - // `var` is used to be able to minimize the number of `var` keywords. - var i = 0, - f = 0, - - // Use `encodeURI` to UTF8 encode the message without any additional libraries - // We could use `unescape` + `encodeURI` to minimize the code, but that would be slightly risky - // since `unescape` is deprecated. - urlEncodedMessage = encodeURI(message) + "%80", // trailing '1' bit padding - - // This can be changed to a preallocated Uint32Array array for greater performance and larger code size - data = [], - dataSize, - - hashBuffer = [], - - a = 0x67452301, - b = 0xefcdab89, - c = ~a, - d = ~b, - e = 0xc3d2e1f0, - hash = [a, b, c, d, e], - - blockStartIndex = 0, - hexHash = ""; - - /** - * Rotates the value a specified number of bits to the left. - * @param {number} value Value to rotate - * @param {number} shift Bit count to shift. - */ - function rotl(value, shift) { - return (value << shift) | (value >>> (32 - shift)); - } - - // Message data - for ( ; i < urlEncodedMessage.length; f++) { - data[f >> 2] = data[f >> 2] | - ( - ( - urlEncodedMessage[i] == "%" - // Percent encoded byte - ? parseInt(urlEncodedMessage.substring(i + 1, i += 3), 16) - // Unencoded byte - : urlEncodedMessage.charCodeAt(i++) - ) - - // Read bytes in reverse order (big endian words) - << ((3 - (f & 3)) * 8) - ); - } - - // f is now the length of the utf8 encoded message - // 7 = 8 bytes (64 bit) for message size, -1 to round down - // >> 6 = integer division with block size - dataSize = (((f + 7) >> 6) + 1) * BLOCK_SIZE_WORDS; - - // Message size in bits. - // SHA1 uses a 64 bit integer to represent the size, but since we only support short messages only the least - // significant 32 bits are set. -8 is for the '1' bit padding byte. - data[dataSize - 1] = f * 8 - 8; - - // Compute hash - for ( ; blockStartIndex < dataSize; blockStartIndex += BLOCK_SIZE_WORDS) { - for (i = 0; i < 80; i++) { - f = rotl(a, 5) + e + ( - // Ch - i < 20 ? ((b & c) ^ ((~b) & d)) + 0x5a827999 : - - // Parity - i < 40 ? (b ^ c ^ d) + 0x6ed9eba1 : - - // Maj - i < 60 ? ((b & c) ^ (b & d) ^ (c & d)) + 0x8f1bbcdc : - - // Parity - (b ^ c ^ d) + 0xca62c1d6 - ) + ( - hashBuffer[i] = i < BLOCK_SIZE_WORDS - // Bitwise OR is used to coerse `undefined` to 0 - ? (data[blockStartIndex + i] | 0) - : rotl(hashBuffer[i - 3] ^ hashBuffer[i - 8] ^ hashBuffer[i - 14] ^ hashBuffer[i - 16], 1) - ); - - e = d; - d = c; - c = rotl(b, 30); - b = a; - a = f; - } - - hash[0] = a = ((hash[0] + a) | 0); - hash[1] = b = ((hash[1] + b) | 0); - hash[2] = c = ((hash[2] + c) | 0); - hash[3] = d = ((hash[3] + d) | 0); - hash[4] = e = ((hash[4] + e) | 0); - } - - // Format hex hash - for (i = 0; i < HASH_SIZE_HALF_BYTES; i++) { - hexHash += ( - ( - // Get word (2^3 half-bytes per word) - hash[i >> 3] >>> - - // Append half-bytes in reverse order - ((7 - (i & 7)) * 4) - ) - // Clamp to half-byte - & 0xf - ).toString(16); - } - - return hexHash; -} - -/** - * Inputs a value that might be a valid hash string for Jdenticon and returns it - * if it is determined valid, otherwise a falsy value is returned. - */ -function isValidHash(hashCandidate) { - return /^[0-9a-f]{11,}$/i.test(hashCandidate) && hashCandidate; -} - -/** - * Computes a hash for the specified value. Currently SHA1 is used. This function - * always returns a valid hash. - */ -function computeHash(value) { - return sha1(value == null ? "" : "" + value); -} - - - -/** - * Renderer redirecting drawing commands to a canvas context. - * @implements {Renderer} - */ -function CanvasRenderer(ctx, iconSize) { - var canvas = ctx.canvas; - var width = canvas.width; - var height = canvas.height; - - ctx.save(); - - if (!iconSize) { - iconSize = Math.min(width, height); - - ctx.translate( - ((width - iconSize) / 2) | 0, - ((height - iconSize) / 2) | 0); - } - - /** - * @private - */ - this.l/*_ctx*/ = ctx; - this.k/*iconSize*/ = iconSize; - - ctx.clearRect(0, 0, iconSize, iconSize); -} -var CanvasRenderer__prototype = CanvasRenderer.prototype; - -/** - * Fills the background with the specified color. - * @param {string} fillColor Fill color on the format #rrggbb[aa]. - */ -CanvasRenderer__prototype.m/*setBackground*/ = function setBackground (fillColor) { - var ctx = this.l/*_ctx*/; - var iconSize = this.k/*iconSize*/; - - ctx.fillStyle = toCss3Color(fillColor); - ctx.fillRect(0, 0, iconSize, iconSize); -}; - -/** - * Marks the beginning of a new shape of the specified color. Should be ended with a call to endShape. - * @param {string} fillColor Fill color on format #rrggbb[aa]. - */ -CanvasRenderer__prototype.O/*beginShape*/ = function beginShape (fillColor) { - var ctx = this.l/*_ctx*/; - ctx.fillStyle = toCss3Color(fillColor); - ctx.beginPath(); -}; - -/** - * Marks the end of the currently drawn shape. This causes the queued paths to be rendered on the canvas. - */ -CanvasRenderer__prototype.P/*endShape*/ = function endShape () { - this.l/*_ctx*/.fill(); -}; - -/** - * Adds a polygon to the rendering queue. - * @param points An array of Point objects. - */ -CanvasRenderer__prototype.g/*addPolygon*/ = function addPolygon (points) { - var ctx = this.l/*_ctx*/; - ctx.moveTo(points[0].x, points[0].y); - for (var i = 1; i < points.length; i++) { - ctx.lineTo(points[i].x, points[i].y); - } - ctx.closePath(); -}; - -/** - * Adds a circle to the rendering queue. - * @param {Point} point The upper left corner of the circle bounding box. - * @param {number} diameter The diameter of the circle. - * @param {boolean} counterClockwise True if the circle is drawn counter-clockwise (will result in a hole if rendered on a clockwise path). - */ -CanvasRenderer__prototype.h/*addCircle*/ = function addCircle (point, diameter, counterClockwise) { - var ctx = this.l/*_ctx*/, - radius = diameter / 2; - ctx.moveTo(point.x + radius, point.y + radius); - ctx.arc(point.x + radius, point.y + radius, radius, 0, Math.PI * 2, counterClockwise); - ctx.closePath(); -}; - -/** - * Called when the icon has been completely drawn. - */ -CanvasRenderer__prototype.finish = function finish () { - this.l/*_ctx*/.restore(); +/** + * Adds a polygon to the underlying renderer. + * Source: http://stackoverflow.com/a/2173084 + * @param {number} x The x-coordinate of the upper left corner of the rectangle holding the entire ellipse. + * @param {number} y The y-coordinate of the upper left corner of the rectangle holding the entire ellipse. + * @param {number} size The size of the ellipse. + * @param {boolean=} invert Specifies if the ellipse will be inverted. + */ +Graphics__prototype.h/*addCircle*/ = function addCircle (x, y, size, invert) { + var p = this.A/*currentTransform*/.L/*transformIconPoint*/(x, y, size, size); + this.M/*_renderer*/.h/*addCircle*/(p, size, invert); }; -/** - * Draws an identicon to a context. - * @param {CanvasRenderingContext2D} ctx - Canvas context on which the icon will be drawn at location (0, 0). - * @param {*} hashOrValue - A hexadecimal hash string or any value that will be hashed by Jdenticon. - * @param {number} size - Icon size in pixels. - * @param {Object|number=} config - Optional configuration. If specified, this configuration object overrides any - * global configuration in its entirety. For backward compatibility a padding value in the range [0.0, 0.5) can be - * specified in place of a configuration object. - */ -function drawIcon(ctx, hashOrValue, size, config) { - if (!ctx) { - throw new Error("No canvas specified."); - } - - iconGenerator(new CanvasRenderer(ctx, size), - isValidHash(hashOrValue) || computeHash(hashOrValue), - config); - - var canvas = ctx.canvas; - if (canvas) { - canvas[IS_RENDERED_PROPERTY] = true; - } -} - -/** - * Prepares a measure to be used as a measure in an SVG path, by - * rounding the measure to a single decimal. This reduces the file - * size of the generated SVG with more than 50% in some cases. - */ -function svgValue(value) { - return ((value * 10 + 0.5) | 0) / 10; -} - -/** - * Represents an SVG path element. - */ -function SvgPath() { - /** - * This property holds the data string (path.d) of the SVG path. - * @type {string} - */ - this.B/*dataString*/ = ""; -} -var SvgPath__prototype = SvgPath.prototype; - -/** - * Adds a polygon with the current fill color to the SVG path. - * @param points An array of Point objects. - */ -SvgPath__prototype.g/*addPolygon*/ = function addPolygon (points) { - var dataString = ""; - for (var i = 0; i < points.length; i++) { - dataString += (i ? "L" : "M") + svgValue(points[i].x) + " " + svgValue(points[i].y); - } - this.B/*dataString*/ += dataString + "Z"; -}; - -/** - * Adds a circle with the current fill color to the SVG path. - * @param {Point} point The upper left corner of the circle bounding box. - * @param {number} diameter The diameter of the circle. - * @param {boolean} counterClockwise True if the circle is drawn counter-clockwise (will result in a hole if rendered on a clockwise path). - */ -SvgPath__prototype.h/*addCircle*/ = function addCircle (point, diameter, counterClockwise) { - var sweepFlag = counterClockwise ? 0 : 1, - svgRadius = svgValue(diameter / 2), - svgDiameter = svgValue(diameter), - svgArc = "a" + svgRadius + "," + svgRadius + " 0 1," + sweepFlag + " "; - - this.B/*dataString*/ += - "M" + svgValue(point.x) + " " + svgValue(point.y + diameter / 2) + - svgArc + svgDiameter + ",0" + - svgArc + (-svgDiameter) + ",0"; +/** + * Adds a rectangle to the underlying renderer. + * @param {number} x The x-coordinate of the upper left corner of the rectangle. + * @param {number} y The y-coordinate of the upper left corner of the rectangle. + * @param {number} w The width of the rectangle. + * @param {number} h The height of the rectangle. + * @param {boolean=} invert Specifies if the rectangle will be inverted. + */ +Graphics__prototype.i/*addRectangle*/ = function addRectangle (x, y, w, h, invert) { + this.g/*addPolygon*/([ + x, y, + x + w, y, + x + w, y + h, + x, y + h + ], invert); }; - - -/** - * Renderer producing SVG output. - * @implements {Renderer} - */ -function SvgRenderer(target) { - /** - * @type {SvgPath} - * @private - */ - this.C/*_path*/; - - /** - * @type {Object.} - * @private - */ - this.D/*_pathsByColor*/ = { }; - - /** - * @type {SvgElement|SvgWriter} - * @private - */ - this.R/*_target*/ = target; - - /** - * @type {number} - */ - this.k/*iconSize*/ = target.k/*iconSize*/; -} -var SvgRenderer__prototype = SvgRenderer.prototype; - -/** - * Fills the background with the specified color. - * @param {string} fillColor Fill color on the format #rrggbb[aa]. - */ -SvgRenderer__prototype.m/*setBackground*/ = function setBackground (fillColor) { - var match = /^(#......)(..)?/.exec(fillColor), - opacity = match[2] ? parseHex(match[2], 0) / 255 : 1; - this.R/*_target*/.m/*setBackground*/(match[1], opacity); -}; - -/** - * Marks the beginning of a new shape of the specified color. Should be ended with a call to endShape. - * @param {string} color Fill color on format #xxxxxx. - */ -SvgRenderer__prototype.O/*beginShape*/ = function beginShape (color) { - this.C/*_path*/ = this.D/*_pathsByColor*/[color] || (this.D/*_pathsByColor*/[color] = new SvgPath()); -}; - -/** - * Marks the end of the currently drawn shape. - */ -SvgRenderer__prototype.P/*endShape*/ = function endShape () { }; - -/** - * Adds a polygon with the current fill color to the SVG. - * @param points An array of Point objects. - */ -SvgRenderer__prototype.g/*addPolygon*/ = function addPolygon (points) { - this.C/*_path*/.g/*addPolygon*/(points); -}; - -/** - * Adds a circle with the current fill color to the SVG. - * @param {Point} point The upper left corner of the circle bounding box. - * @param {number} diameter The diameter of the circle. - * @param {boolean} counterClockwise True if the circle is drawn counter-clockwise (will result in a hole if rendered on a clockwise path). - */ -SvgRenderer__prototype.h/*addCircle*/ = function addCircle (point, diameter, counterClockwise) { - this.C/*_path*/.h/*addCircle*/(point, diameter, counterClockwise); -}; - -/** - * Called when the icon has been completely drawn. - */ +/** + * Adds a right triangle to the underlying renderer. + * @param {number} x The x-coordinate of the upper left corner of the rectangle holding the triangle. + * @param {number} y The y-coordinate of the upper left corner of the rectangle holding the triangle. + * @param {number} w The width of the triangle. + * @param {number} h The height of the triangle. + * @param {number} r The rotation of the triangle (clockwise). 0 = right corner of the triangle in the lower left corner of the bounding rectangle. + * @param {boolean=} invert Specifies if the triangle will be inverted. + */ +Graphics__prototype.j/*addTriangle*/ = function addTriangle (x, y, w, h, r, invert) { + var points = [ + x + w, y, + x + w, y + h, + x, y + h, + x, y + ]; + points.splice(((r || 0) % 4) * 2, 2); + this.g/*addPolygon*/(points, invert); +}; + +/** + * Adds a rhombus to the underlying renderer. + * @param {number} x The x-coordinate of the upper left corner of the rectangle holding the rhombus. + * @param {number} y The y-coordinate of the upper left corner of the rectangle holding the rhombus. + * @param {number} w The width of the rhombus. + * @param {number} h The height of the rhombus. + * @param {boolean=} invert Specifies if the rhombus will be inverted. + */ +Graphics__prototype.N/*addRhombus*/ = function addRhombus (x, y, w, h, invert) { + this.g/*addPolygon*/([ + x + w / 2, y, + x + w, y + h / 2, + x + w / 2, y + h, + x, y + h / 2 + ], invert); +}; + +/** + * @param {number} index + * @param {Graphics} g + * @param {number} cell + * @param {number} positionIndex + */ +function centerShape(index, g, cell, positionIndex) { + index = index % 14; + + var k, m, w, h, inner, outer; + + !index ? ( + k = cell * 0.42, + g.g/*addPolygon*/([ + 0, 0, + cell, 0, + cell, cell - k * 2, + cell - k, cell, + 0, cell + ])) : + + index == 1 ? ( + w = 0 | (cell * 0.5), + h = 0 | (cell * 0.8), + + g.j/*addTriangle*/(cell - w, 0, w, h, 2)) : + + index == 2 ? ( + w = 0 | (cell / 3), + g.i/*addRectangle*/(w, w, cell - w, cell - w)) : + + index == 3 ? ( + inner = cell * 0.1, + // Use fixed outer border widths in small icons to ensure the border is drawn + outer = + cell < 6 ? 1 : + cell < 8 ? 2 : + (0 | (cell * 0.25)), + + inner = + inner > 1 ? (0 | inner) : // large icon => truncate decimals + inner > 0.5 ? 1 : // medium size icon => fixed width + inner, // small icon => anti-aliased border + + g.i/*addRectangle*/(outer, outer, cell - inner - outer, cell - inner - outer)) : + + index == 4 ? ( + m = 0 | (cell * 0.15), + w = 0 | (cell * 0.5), + g.h/*addCircle*/(cell - w - m, cell - w - m, w)) : + + index == 5 ? ( + inner = cell * 0.1, + outer = inner * 4, + + // Align edge to nearest pixel in large icons + outer > 3 && (outer = 0 | outer), + + g.i/*addRectangle*/(0, 0, cell, cell), + g.g/*addPolygon*/([ + outer, outer, + cell - inner, outer, + outer + (cell - outer - inner) / 2, cell - inner + ], true)) : + + index == 6 ? + g.g/*addPolygon*/([ + 0, 0, + cell, 0, + cell, cell * 0.7, + cell * 0.4, cell * 0.4, + cell * 0.7, cell, + 0, cell + ]) : + + index == 7 ? + g.j/*addTriangle*/(cell / 2, cell / 2, cell / 2, cell / 2, 3) : + + index == 8 ? ( + g.i/*addRectangle*/(0, 0, cell, cell / 2), + g.i/*addRectangle*/(0, cell / 2, cell / 2, cell / 2), + g.j/*addTriangle*/(cell / 2, cell / 2, cell / 2, cell / 2, 1)) : + + index == 9 ? ( + inner = cell * 0.14, + // Use fixed outer border widths in small icons to ensure the border is drawn + outer = + cell < 4 ? 1 : + cell < 6 ? 2 : + (0 | (cell * 0.35)), + + inner = + cell < 8 ? inner : // small icon => anti-aliased border + (0 | inner), // large icon => truncate decimals + + g.i/*addRectangle*/(0, 0, cell, cell), + g.i/*addRectangle*/(outer, outer, cell - outer - inner, cell - outer - inner, true)) : + + index == 10 ? ( + inner = cell * 0.12, + outer = inner * 3, + + g.i/*addRectangle*/(0, 0, cell, cell), + g.h/*addCircle*/(outer, outer, cell - inner - outer, true)) : + + index == 11 ? + g.j/*addTriangle*/(cell / 2, cell / 2, cell / 2, cell / 2, 3) : + + index == 12 ? ( + m = cell * 0.25, + g.i/*addRectangle*/(0, 0, cell, cell), + g.N/*addRhombus*/(m, m, cell - m, cell - m, true)) : + + // 13 + ( + !positionIndex && ( + m = cell * 0.4, w = cell * 1.2, + g.h/*addCircle*/(m, m, w) + ) + ); +} + +/** + * @param {number} index + * @param {Graphics} g + * @param {number} cell + */ +function outerShape(index, g, cell) { + index = index % 4; + + var m; + + !index ? + g.j/*addTriangle*/(0, 0, cell, cell, 0) : + + index == 1 ? + g.j/*addTriangle*/(0, cell / 2, cell, cell / 2, 0) : + + index == 2 ? + g.N/*addRhombus*/(0, 0, cell, cell) : + + // 3 + ( + m = cell / 6, + g.h/*addCircle*/(m, m, cell - 2 * m) + ); +} + +/** + * Gets a set of identicon color candidates for a specified hue and config. + * @param {number} hue + * @param {ParsedConfiguration} config + */ +function colorTheme(hue, config) { + hue = config.X/*hue*/(hue); + return [ + // Dark gray + correctedHsl(hue, config.H/*grayscaleSaturation*/, config.I/*grayscaleLightness*/(0)), + // Mid color + correctedHsl(hue, config.p/*colorSaturation*/, config.q/*colorLightness*/(0.5)), + // Light gray + correctedHsl(hue, config.H/*grayscaleSaturation*/, config.I/*grayscaleLightness*/(1)), + // Light color + correctedHsl(hue, config.p/*colorSaturation*/, config.q/*colorLightness*/(1)), + // Dark color + correctedHsl(hue, config.p/*colorSaturation*/, config.q/*colorLightness*/(0)) + ]; +} + +/** + * Draws an identicon to a specified renderer. + * @param {Renderer} renderer + * @param {string} hash + * @param {Object|number=} config + */ +function iconGenerator(renderer, hash, config) { + var parsedConfig = getConfiguration(config, 0.08); + + // Set background color + if (parsedConfig.J/*backColor*/) { + renderer.m/*setBackground*/(parsedConfig.J/*backColor*/); + } + + // Calculate padding and round to nearest integer + var size = renderer.k/*iconSize*/; + var padding = (0.5 + size * parsedConfig.Y/*iconPadding*/) | 0; + size -= padding * 2; + + var graphics = new Graphics(renderer); + + // Calculate cell size and ensure it is an integer + var cell = 0 | (size / 4); + + // Since the cell size is integer based, the actual icon will be slightly smaller than specified => center icon + var x = 0 | (padding + size / 2 - cell * 2); + var y = 0 | (padding + size / 2 - cell * 2); + + function renderShape(colorIndex, shapes, index, rotationIndex, positions) { + var shapeIndex = parseHex(hash, index, 1); + var r = rotationIndex ? parseHex(hash, rotationIndex, 1) : 0; + + renderer.O/*beginShape*/(availableColors[selectedColorIndexes[colorIndex]]); + + for (var i = 0; i < positions.length; i++) { + graphics.A/*currentTransform*/ = new Transform(x + positions[i][0] * cell, y + positions[i][1] * cell, cell, r++ % 4); + shapes(shapeIndex, graphics, cell, i); + } + + renderer.P/*endShape*/(); + } + + // AVAILABLE COLORS + var hue = parseHex(hash, -7) / 0xfffffff, + + // Available colors for this icon + availableColors = colorTheme(hue, parsedConfig), + + // The index of the selected colors + selectedColorIndexes = []; + + var index; + + function isDuplicate(values) { + if (values.indexOf(index) >= 0) { + for (var i = 0; i < values.length; i++) { + if (selectedColorIndexes.indexOf(values[i]) >= 0) { + return true; + } + } + } + } + + for (var i = 0; i < 3; i++) { + index = parseHex(hash, 8 + i, 1) % availableColors.length; + if (isDuplicate([0, 4]) || // Disallow dark gray and dark color combo + isDuplicate([2, 3])) { // Disallow light gray and light color combo + index = 1; + } + selectedColorIndexes.push(index); + } + + // ACTUAL RENDERING + // Sides + renderShape(0, outerShape, 2, 3, [[1, 0], [2, 0], [2, 3], [1, 3], [0, 1], [3, 1], [3, 2], [0, 2]]); + // Corners + renderShape(1, outerShape, 4, 5, [[0, 0], [3, 0], [3, 3], [0, 3]]); + // Center + renderShape(2, centerShape, 1, null, [[1, 1], [2, 1], [2, 2], [1, 2]]); + + renderer.finish(); +} + +/** + * Computes a SHA1 hash for any value and returns it as a hexadecimal string. + * + * This function is optimized for minimal code size and rather short messages. + * + * @param {string} message + */ +function sha1(message) { + var HASH_SIZE_HALF_BYTES = 40; + var BLOCK_SIZE_WORDS = 16; + + // Variables + // `var` is used to be able to minimize the number of `var` keywords. + var i = 0, + f = 0, + + // Use `encodeURI` to UTF8 encode the message without any additional libraries + // We could use `unescape` + `encodeURI` to minimize the code, but that would be slightly risky + // since `unescape` is deprecated. + urlEncodedMessage = encodeURI(message) + "%80", // trailing '1' bit padding + + // This can be changed to a preallocated Uint32Array array for greater performance and larger code size + data = [], + dataSize, + + hashBuffer = [], + + a = 0x67452301, + b = 0xefcdab89, + c = ~a, + d = ~b, + e = 0xc3d2e1f0, + hash = [a, b, c, d, e], + + blockStartIndex = 0, + hexHash = ""; + + /** + * Rotates the value a specified number of bits to the left. + * @param {number} value Value to rotate + * @param {number} shift Bit count to shift. + */ + function rotl(value, shift) { + return (value << shift) | (value >>> (32 - shift)); + } + + // Message data + for ( ; i < urlEncodedMessage.length; f++) { + data[f >> 2] = data[f >> 2] | + ( + ( + urlEncodedMessage[i] == "%" + // Percent encoded byte + ? parseInt(urlEncodedMessage.substring(i + 1, i += 3), 16) + // Unencoded byte + : urlEncodedMessage.charCodeAt(i++) + ) + + // Read bytes in reverse order (big endian words) + << ((3 - (f & 3)) * 8) + ); + } + + // f is now the length of the utf8 encoded message + // 7 = 8 bytes (64 bit) for message size, -1 to round down + // >> 6 = integer division with block size + dataSize = (((f + 7) >> 6) + 1) * BLOCK_SIZE_WORDS; + + // Message size in bits. + // SHA1 uses a 64 bit integer to represent the size, but since we only support short messages only the least + // significant 32 bits are set. -8 is for the '1' bit padding byte. + data[dataSize - 1] = f * 8 - 8; + + // Compute hash + for ( ; blockStartIndex < dataSize; blockStartIndex += BLOCK_SIZE_WORDS) { + for (i = 0; i < 80; i++) { + f = rotl(a, 5) + e + ( + // Ch + i < 20 ? ((b & c) ^ ((~b) & d)) + 0x5a827999 : + + // Parity + i < 40 ? (b ^ c ^ d) + 0x6ed9eba1 : + + // Maj + i < 60 ? ((b & c) ^ (b & d) ^ (c & d)) + 0x8f1bbcdc : + + // Parity + (b ^ c ^ d) + 0xca62c1d6 + ) + ( + hashBuffer[i] = i < BLOCK_SIZE_WORDS + // Bitwise OR is used to coerse `undefined` to 0 + ? (data[blockStartIndex + i] | 0) + : rotl(hashBuffer[i - 3] ^ hashBuffer[i - 8] ^ hashBuffer[i - 14] ^ hashBuffer[i - 16], 1) + ); + + e = d; + d = c; + c = rotl(b, 30); + b = a; + a = f; + } + + hash[0] = a = ((hash[0] + a) | 0); + hash[1] = b = ((hash[1] + b) | 0); + hash[2] = c = ((hash[2] + c) | 0); + hash[3] = d = ((hash[3] + d) | 0); + hash[4] = e = ((hash[4] + e) | 0); + } + + // Format hex hash + for (i = 0; i < HASH_SIZE_HALF_BYTES; i++) { + hexHash += ( + ( + // Get word (2^3 half-bytes per word) + hash[i >> 3] >>> + + // Append half-bytes in reverse order + ((7 - (i & 7)) * 4) + ) + // Clamp to half-byte + & 0xf + ).toString(16); + } + + return hexHash; +} + +/** + * Inputs a value that might be a valid hash string for Jdenticon and returns it + * if it is determined valid, otherwise a falsy value is returned. + */ +function isValidHash(hashCandidate) { + return /^[0-9a-f]{11,}$/i.test(hashCandidate) && hashCandidate; +} + +/** + * Computes a hash for the specified value. Currently SHA1 is used. This function + * always returns a valid hash. + */ +function computeHash(value) { + return sha1(value == null ? "" : "" + value); +} + + + +/** + * Renderer redirecting drawing commands to a canvas context. + * @implements {Renderer} + */ +function CanvasRenderer(ctx, iconSize) { + var canvas = ctx.canvas; + var width = canvas.width; + var height = canvas.height; + + ctx.save(); + + if (!iconSize) { + iconSize = Math.min(width, height); + + ctx.translate( + ((width - iconSize) / 2) | 0, + ((height - iconSize) / 2) | 0); + } + + /** + * @private + */ + this.l/*_ctx*/ = ctx; + this.k/*iconSize*/ = iconSize; + + ctx.clearRect(0, 0, iconSize, iconSize); +} +var CanvasRenderer__prototype = CanvasRenderer.prototype; + +/** + * Fills the background with the specified color. + * @param {string} fillColor Fill color on the format #rrggbb[aa]. + */ +CanvasRenderer__prototype.m/*setBackground*/ = function setBackground (fillColor) { + var ctx = this.l/*_ctx*/; + var iconSize = this.k/*iconSize*/; + + ctx.fillStyle = toCss3Color(fillColor); + ctx.fillRect(0, 0, iconSize, iconSize); +}; + +/** + * Marks the beginning of a new shape of the specified color. Should be ended with a call to endShape. + * @param {string} fillColor Fill color on format #rrggbb[aa]. + */ +CanvasRenderer__prototype.O/*beginShape*/ = function beginShape (fillColor) { + var ctx = this.l/*_ctx*/; + ctx.fillStyle = toCss3Color(fillColor); + ctx.beginPath(); +}; + +/** + * Marks the end of the currently drawn shape. This causes the queued paths to be rendered on the canvas. + */ +CanvasRenderer__prototype.P/*endShape*/ = function endShape () { + this.l/*_ctx*/.fill(); +}; + +/** + * Adds a polygon to the rendering queue. + * @param points An array of Point objects. + */ +CanvasRenderer__prototype.g/*addPolygon*/ = function addPolygon (points) { + var ctx = this.l/*_ctx*/; + ctx.moveTo(points[0].x, points[0].y); + for (var i = 1; i < points.length; i++) { + ctx.lineTo(points[i].x, points[i].y); + } + ctx.closePath(); +}; + +/** + * Adds a circle to the rendering queue. + * @param {Point} point The upper left corner of the circle bounding box. + * @param {number} diameter The diameter of the circle. + * @param {boolean} counterClockwise True if the circle is drawn counter-clockwise (will result in a hole if rendered on a clockwise path). + */ +CanvasRenderer__prototype.h/*addCircle*/ = function addCircle (point, diameter, counterClockwise) { + var ctx = this.l/*_ctx*/, + radius = diameter / 2; + ctx.moveTo(point.x + radius, point.y + radius); + ctx.arc(point.x + radius, point.y + radius, radius, 0, Math.PI * 2, counterClockwise); + ctx.closePath(); +}; + +/** + * Called when the icon has been completely drawn. + */ +CanvasRenderer__prototype.finish = function finish () { + this.l/*_ctx*/.restore(); +}; + +/** + * Draws an identicon to a context. + * @param {CanvasRenderingContext2D} ctx - Canvas context on which the icon will be drawn at location (0, 0). + * @param {*} hashOrValue - A hexadecimal hash string or any value that will be hashed by Jdenticon. + * @param {number} size - Icon size in pixels. + * @param {Object|number=} config - Optional configuration. If specified, this configuration object overrides any + * global configuration in its entirety. For backward compatibility a padding value in the range [0.0, 0.5) can be + * specified in place of a configuration object. + */ +function drawIcon(ctx, hashOrValue, size, config) { + if (!ctx) { + throw new Error("No canvas specified."); + } + + iconGenerator(new CanvasRenderer(ctx, size), + isValidHash(hashOrValue) || computeHash(hashOrValue), + config); + + var canvas = ctx.canvas; + if (canvas) { + canvas[IS_RENDERED_PROPERTY] = true; + } +} + +/** + * Prepares a measure to be used as a measure in an SVG path, by + * rounding the measure to a single decimal. This reduces the file + * size of the generated SVG with more than 50% in some cases. + */ +function svgValue(value) { + return ((value * 10 + 0.5) | 0) / 10; +} + +/** + * Represents an SVG path element. + */ +function SvgPath() { + /** + * This property holds the data string (path.d) of the SVG path. + * @type {string} + */ + this.B/*dataString*/ = ""; +} +var SvgPath__prototype = SvgPath.prototype; + +/** + * Adds a polygon with the current fill color to the SVG path. + * @param points An array of Point objects. + */ +SvgPath__prototype.g/*addPolygon*/ = function addPolygon (points) { + var dataString = ""; + for (var i = 0; i < points.length; i++) { + dataString += (i ? "L" : "M") + svgValue(points[i].x) + " " + svgValue(points[i].y); + } + this.B/*dataString*/ += dataString + "Z"; +}; + +/** + * Adds a circle with the current fill color to the SVG path. + * @param {Point} point The upper left corner of the circle bounding box. + * @param {number} diameter The diameter of the circle. + * @param {boolean} counterClockwise True if the circle is drawn counter-clockwise (will result in a hole if rendered on a clockwise path). + */ +SvgPath__prototype.h/*addCircle*/ = function addCircle (point, diameter, counterClockwise) { + var sweepFlag = counterClockwise ? 0 : 1, + svgRadius = svgValue(diameter / 2), + svgDiameter = svgValue(diameter), + svgArc = "a" + svgRadius + "," + svgRadius + " 0 1," + sweepFlag + " "; + + this.B/*dataString*/ += + "M" + svgValue(point.x) + " " + svgValue(point.y + diameter / 2) + + svgArc + svgDiameter + ",0" + + svgArc + (-svgDiameter) + ",0"; +}; + + + +/** + * Renderer producing SVG output. + * @implements {Renderer} + */ +function SvgRenderer(target) { + /** + * @type {SvgPath} + * @private + */ + this.C/*_path*/; + + /** + * @type {Object.} + * @private + */ + this.D/*_pathsByColor*/ = { }; + + /** + * @type {SvgElement|SvgWriter} + * @private + */ + this.R/*_target*/ = target; + + /** + * @type {number} + */ + this.k/*iconSize*/ = target.k/*iconSize*/; +} +var SvgRenderer__prototype = SvgRenderer.prototype; + +/** + * Fills the background with the specified color. + * @param {string} fillColor Fill color on the format #rrggbb[aa]. + */ +SvgRenderer__prototype.m/*setBackground*/ = function setBackground (fillColor) { + var match = /^(#......)(..)?/.exec(fillColor), + opacity = match[2] ? parseHex(match[2], 0) / 255 : 1; + this.R/*_target*/.m/*setBackground*/(match[1], opacity); +}; + +/** + * Marks the beginning of a new shape of the specified color. Should be ended with a call to endShape. + * @param {string} color Fill color on format #xxxxxx. + */ +SvgRenderer__prototype.O/*beginShape*/ = function beginShape (color) { + this.C/*_path*/ = this.D/*_pathsByColor*/[color] || (this.D/*_pathsByColor*/[color] = new SvgPath()); +}; + +/** + * Marks the end of the currently drawn shape. + */ +SvgRenderer__prototype.P/*endShape*/ = function endShape () { }; + +/** + * Adds a polygon with the current fill color to the SVG. + * @param points An array of Point objects. + */ +SvgRenderer__prototype.g/*addPolygon*/ = function addPolygon (points) { + this.C/*_path*/.g/*addPolygon*/(points); +}; + +/** + * Adds a circle with the current fill color to the SVG. + * @param {Point} point The upper left corner of the circle bounding box. + * @param {number} diameter The diameter of the circle. + * @param {boolean} counterClockwise True if the circle is drawn counter-clockwise (will result in a hole if rendered on a clockwise path). + */ +SvgRenderer__prototype.h/*addCircle*/ = function addCircle (point, diameter, counterClockwise) { + this.C/*_path*/.h/*addCircle*/(point, diameter, counterClockwise); +}; + +/** + * Called when the icon has been completely drawn. + */ SvgRenderer__prototype.finish = function finish () { var this$1 = this; - - var pathsByColor = this.D/*_pathsByColor*/; - for (var color in pathsByColor) { - // hasOwnProperty cannot be shadowed in pathsByColor - // eslint-disable-next-line no-prototype-builtins - if (pathsByColor.hasOwnProperty(color)) { - this$1.R/*_target*/.S/*appendPath*/(color, pathsByColor[color].B/*dataString*/); - } - } + + var pathsByColor = this.D/*_pathsByColor*/; + for (var color in pathsByColor) { + // hasOwnProperty cannot be shadowed in pathsByColor + // eslint-disable-next-line no-prototype-builtins + if (pathsByColor.hasOwnProperty(color)) { + this$1.R/*_target*/.S/*appendPath*/(color, pathsByColor[color].B/*dataString*/); + } + } }; -var SVG_CONSTANTS = { - T/*XMLNS*/: "http://www.w3.org/2000/svg", - U/*WIDTH*/: "width", - V/*HEIGHT*/: "height", +var SVG_CONSTANTS = { + T/*XMLNS*/: "http://www.w3.org/2000/svg", + U/*WIDTH*/: "width", + V/*HEIGHT*/: "height", }; -/** - * Renderer producing SVG output. - */ -function SvgWriter(iconSize) { - /** - * @type {number} - */ - this.k/*iconSize*/ = iconSize; - - /** - * @type {string} - * @private - */ - this.F/*_s*/ = - ''; -} -var SvgWriter__prototype = SvgWriter.prototype; - -/** - * Fills the background with the specified color. - * @param {string} fillColor Fill color on the format #rrggbb. - * @param {number} opacity Opacity in the range [0.0, 1.0]. - */ -SvgWriter__prototype.m/*setBackground*/ = function setBackground (fillColor, opacity) { - if (opacity) { - this.F/*_s*/ += ''; - } -}; - -/** - * Writes a path to the SVG string. - * @param {string} color Fill color on format #rrggbb. - * @param {string} dataString The SVG path data string. - */ -SvgWriter__prototype.S/*appendPath*/ = function appendPath (color, dataString) { - this.F/*_s*/ += ''; -}; - -/** - * Gets the rendered image as an SVG string. - */ -SvgWriter__prototype.toString = function toString () { - return this.F/*_s*/ + ""; +/** + * Renderer producing SVG output. + */ +function SvgWriter(iconSize) { + /** + * @type {number} + */ + this.k/*iconSize*/ = iconSize; + + /** + * @type {string} + * @private + */ + this.F/*_s*/ = + ''; +} +var SvgWriter__prototype = SvgWriter.prototype; + +/** + * Fills the background with the specified color. + * @param {string} fillColor Fill color on the format #rrggbb. + * @param {number} opacity Opacity in the range [0.0, 1.0]. + */ +SvgWriter__prototype.m/*setBackground*/ = function setBackground (fillColor, opacity) { + if (opacity) { + this.F/*_s*/ += ''; + } }; -/** - * Draws an identicon as an SVG string. - * @param {*} hashOrValue - A hexadecimal hash string or any value that will be hashed by Jdenticon. - * @param {number} size - Icon size in pixels. - * @param {Object|number=} config - Optional configuration. If specified, this configuration object overrides any - * global configuration in its entirety. For backward compatibility a padding value in the range [0.0, 0.5) can be - * specified in place of a configuration object. - * @returns {string} SVG string - */ -function toSvg(hashOrValue, size, config) { - var writer = new SvgWriter(size); - iconGenerator(new SvgRenderer(writer), - isValidHash(hashOrValue) || computeHash(hashOrValue), - config); - return writer.toString(); +/** + * Writes a path to the SVG string. + * @param {string} color Fill color on format #rrggbb. + * @param {string} dataString The SVG path data string. + */ +SvgWriter__prototype.S/*appendPath*/ = function appendPath (color, dataString) { + this.F/*_s*/ += ''; +}; + +/** + * Gets the rendered image as an SVG string. + */ +SvgWriter__prototype.toString = function toString () { + return this.F/*_s*/ + ""; +}; + +/** + * Draws an identicon as an SVG string. + * @param {*} hashOrValue - A hexadecimal hash string or any value that will be hashed by Jdenticon. + * @param {number} size - Icon size in pixels. + * @param {Object|number=} config - Optional configuration. If specified, this configuration object overrides any + * global configuration in its entirety. For backward compatibility a padding value in the range [0.0, 0.5) can be + * specified in place of a configuration object. + * @returns {string} SVG string + */ +function toSvg(hashOrValue, size, config) { + var writer = new SvgWriter(size); + iconGenerator(new SvgRenderer(writer), + isValidHash(hashOrValue) || computeHash(hashOrValue), + config); + return writer.toString(); } -/** - * Creates a new element and adds it to the specified parent. - * @param {Element} parentNode - * @param {string} name - * @param {...(string|number)} keyValuePairs - */ +/** + * Creates a new element and adds it to the specified parent. + * @param {Element} parentNode + * @param {string} name + * @param {...(string|number)} keyValuePairs + */ function SvgElement_append(parentNode, name) { var keyValuePairs = [], len = arguments.length - 2; while ( len-- > 0 ) keyValuePairs[ len ] = arguments[ len + 2 ]; - - var el = document.createElementNS(SVG_CONSTANTS.T/*XMLNS*/, name); - - for (var i = 0; i + 1 < keyValuePairs.length; i += 2) { - el.setAttribute( - /** @type {string} */(keyValuePairs[i]), - /** @type {string} */(keyValuePairs[i + 1]) - ); - } - - parentNode.appendChild(el); -} - - -/** - * Renderer producing SVG output. - */ -function SvgElement(element) { - // Don't use the clientWidth and clientHeight properties on SVG elements - // since Firefox won't serve a proper value of these properties on SVG - // elements (https://bugzilla.mozilla.org/show_bug.cgi?id=874811) - // Instead use 100px as a hardcoded size (the svg viewBox will rescale - // the icon to the correct dimensions) - var iconSize = this.k/*iconSize*/ = Math.min( - (Number(element.getAttribute(SVG_CONSTANTS.U/*WIDTH*/)) || 100), - (Number(element.getAttribute(SVG_CONSTANTS.V/*HEIGHT*/)) || 100) - ); - - /** - * @type {Element} - * @private - */ - this.W/*_el*/ = element; - - // Clear current SVG child elements - while (element.firstChild) { - element.removeChild(element.firstChild); - } - - // Set viewBox attribute to ensure the svg scales nicely. - element.setAttribute("viewBox", "0 0 " + iconSize + " " + iconSize); - element.setAttribute("preserveAspectRatio", "xMidYMid meet"); -} -var SvgElement__prototype = SvgElement.prototype; - -/** - * Fills the background with the specified color. - * @param {string} fillColor Fill color on the format #rrggbb. - * @param {number} opacity Opacity in the range [0.0, 1.0]. - */ -SvgElement__prototype.m/*setBackground*/ = function setBackground (fillColor, opacity) { - if (opacity) { - SvgElement_append(this.W/*_el*/, "rect", - SVG_CONSTANTS.U/*WIDTH*/, "100%", - SVG_CONSTANTS.V/*HEIGHT*/, "100%", - "fill", fillColor, - "opacity", opacity); - } -}; - -/** - * Appends a path to the SVG element. - * @param {string} color Fill color on format #xxxxxx. - * @param {string} dataString The SVG path data string. - */ -SvgElement__prototype.S/*appendPath*/ = function appendPath (color, dataString) { - SvgElement_append(this.W/*_el*/, "path", - "fill", color, - "d", dataString); + + var el = document.createElementNS(SVG_CONSTANTS.T/*XMLNS*/, name); + + for (var i = 0; i + 1 < keyValuePairs.length; i += 2) { + el.setAttribute( + /** @type {string} */(keyValuePairs[i]), + /** @type {string} */(keyValuePairs[i + 1]) + ); + } + + parentNode.appendChild(el); +} + + +/** + * Renderer producing SVG output. + */ +function SvgElement(element) { + // Don't use the clientWidth and clientHeight properties on SVG elements + // since Firefox won't serve a proper value of these properties on SVG + // elements (https://bugzilla.mozilla.org/show_bug.cgi?id=874811) + // Instead use 100px as a hardcoded size (the svg viewBox will rescale + // the icon to the correct dimensions) + var iconSize = this.k/*iconSize*/ = Math.min( + (Number(element.getAttribute(SVG_CONSTANTS.U/*WIDTH*/)) || 100), + (Number(element.getAttribute(SVG_CONSTANTS.V/*HEIGHT*/)) || 100) + ); + + /** + * @type {Element} + * @private + */ + this.W/*_el*/ = element; + + // Clear current SVG child elements + while (element.firstChild) { + element.removeChild(element.firstChild); + } + + // Set viewBox attribute to ensure the svg scales nicely. + element.setAttribute("viewBox", "0 0 " + iconSize + " " + iconSize); + element.setAttribute("preserveAspectRatio", "xMidYMid meet"); +} +var SvgElement__prototype = SvgElement.prototype; + +/** + * Fills the background with the specified color. + * @param {string} fillColor Fill color on the format #rrggbb. + * @param {number} opacity Opacity in the range [0.0, 1.0]. + */ +SvgElement__prototype.m/*setBackground*/ = function setBackground (fillColor, opacity) { + if (opacity) { + SvgElement_append(this.W/*_el*/, "rect", + SVG_CONSTANTS.U/*WIDTH*/, "100%", + SVG_CONSTANTS.V/*HEIGHT*/, "100%", + "fill", fillColor, + "opacity", opacity); + } }; -/** - * Updates all canvas elements with the `data-jdenticon-hash` or `data-jdenticon-value` attribute. - */ -function updateAll() { - if (documentQuerySelectorAll) { - update(ICON_SELECTOR); - } -} - -/** - * Updates all canvas elements with the `data-jdenticon-hash` or `data-jdenticon-value` attribute that have not already - * been rendered. - */ -function updateAllConditional() { - if (documentQuerySelectorAll) { - /** @type {NodeListOf} */ - var elements = documentQuerySelectorAll(ICON_SELECTOR); - - for (var i = 0; i < elements.length; i++) { - var el = elements[i]; - if (!el[IS_RENDERED_PROPERTY]) { - update(el); - } - } - } -} - -/** - * Updates the identicon in the specified `` or `` elements. - * @param {(string|Element)} el - Specifies the container in which the icon is rendered as a DOM element of the type - * `` or ``, or a CSS selector to such an element. - * @param {*=} hashOrValue - Optional hash or value to be rendered. If not specified, the `data-jdenticon-hash` or - * `data-jdenticon-value` attribute will be evaluated. - * @param {Object|number=} config - Optional configuration. If specified, this configuration object overrides any - * global configuration in its entirety. For backward compability a padding value in the range [0.0, 0.5) can be - * specified in place of a configuration object. - */ -function update(el, hashOrValue, config) { - renderDomElement(el, hashOrValue, config, function (el, iconType) { - if (iconType) { - return iconType == ICON_TYPE_SVG ? - new SvgRenderer(new SvgElement(el)) : - new CanvasRenderer(/** @type {HTMLCanvasElement} */(el).getContext("2d")); - } - }); -} - -/** - * Updates the identicon in the specified canvas or svg elements. - * @param {(string|Element)} el - Specifies the container in which the icon is rendered as a DOM element of the type - * `` or ``, or a CSS selector to such an element. - * @param {*} hashOrValue - Optional hash or value to be rendered. If not specified, the `data-jdenticon-hash` or - * `data-jdenticon-value` attribute will be evaluated. - * @param {Object|number|undefined} config - * @param {function(Element,number):Renderer} rendererFactory - Factory function for creating an icon renderer. - */ -function renderDomElement(el, hashOrValue, config, rendererFactory) { - if (typeof el === "string") { - if (documentQuerySelectorAll) { - var elements = documentQuerySelectorAll(el); - for (var i = 0; i < elements.length; i++) { - renderDomElement(elements[i], hashOrValue, config, rendererFactory); - } - } - return; - } - - // Hash selection. The result from getValidHash or computeHash is - // accepted as a valid hash. - var hash = - // 1. Explicit valid hash - isValidHash(hashOrValue) || - - // 2. Explicit value (`!= null` catches both null and undefined) - hashOrValue != null && computeHash(hashOrValue) || - - // 3. `data-jdenticon-hash` attribute - isValidHash(el.getAttribute(ATTRIBUTES.t/*HASH*/)) || - - // 4. `data-jdenticon-value` attribute. - // We want to treat an empty attribute as an empty value. - // Some browsers return empty string even if the attribute - // is not specified, so use hasAttribute to determine if - // the attribute is specified. - el.hasAttribute(ATTRIBUTES.o/*VALUE*/) && computeHash(el.getAttribute(ATTRIBUTES.o/*VALUE*/)); - - if (!hash) { - // No hash specified. Don't render an icon. - return; - } - - var renderer = rendererFactory(el, getIdenticonType(el)); - if (renderer) { - // Draw icon - iconGenerator(renderer, hash, config); - el[IS_RENDERED_PROPERTY] = true; - } +/** + * Appends a path to the SVG element. + * @param {string} color Fill color on format #xxxxxx. + * @param {string} dataString The SVG path data string. + */ +SvgElement__prototype.S/*appendPath*/ = function appendPath (color, dataString) { + SvgElement_append(this.W/*_el*/, "path", + "fill", color, + "d", dataString); +}; + +/** + * Updates all canvas elements with the `data-jdenticon-hash` or `data-jdenticon-value` attribute. + */ +function updateAll() { + if (documentQuerySelectorAll) { + update(ICON_SELECTOR); + } } -/** - * Renders an identicon for all matching supported elements. - * - * @param {*} hashOrValue - A hexadecimal hash string or any value that will be hashed by Jdenticon. If not - * specified the `data-jdenticon-hash` and `data-jdenticon-value` attributes of each element will be - * evaluated. - * @param {Object|number=} config - Optional configuration. If specified, this configuration object overrides any global - * configuration in its entirety. For backward compatibility a padding value in the range [0.0, 0.5) can be - * specified in place of a configuration object. - */ -function jdenticonJqueryPlugin(hashOrValue, config) { - this["each"](function (index, el) { - update(el, hashOrValue, config); - }); - return this; +/** + * Updates all canvas elements with the `data-jdenticon-hash` or `data-jdenticon-value` attribute that have not already + * been rendered. + */ +function updateAllConditional() { + if (documentQuerySelectorAll) { + /** @type {NodeListOf} */ + var elements = documentQuerySelectorAll(ICON_SELECTOR); + + for (var i = 0; i < elements.length; i++) { + var el = elements[i]; + if (!el[IS_RENDERED_PROPERTY]) { + update(el); + } + } + } } -// This file is compiled to dist/jdenticon.js and dist/jdenticon.min.js - -var jdenticon = updateAll; - -defineConfigProperty(jdenticon); - -// Export public API -jdenticon["configure"] = configure; -jdenticon["drawIcon"] = drawIcon; -jdenticon["toSvg"] = toSvg; -jdenticon["update"] = update; -jdenticon["updateCanvas"] = update; -jdenticon["updateSvg"] = update; - -/** - * Specifies the version of the Jdenticon package in use. - * @type {string} - */ -jdenticon["version"] = "3.3.0"; - -/** - * Specifies which bundle of Jdenticon that is used. - * @type {string} - */ -jdenticon["bundle"] = "browser-umd"; - -// Basic jQuery plugin -var jQuery = GLOBAL["jQuery"]; -if (jQuery) { - jQuery["fn"]["jdenticon"] = jdenticonJqueryPlugin; -} - -/** - * This function is called once upon page load. - */ -function jdenticonStartup() { - var replaceMode = ( - jdenticon[CONFIG_PROPERTIES.n/*MODULE*/] || - GLOBAL[CONFIG_PROPERTIES.G/*GLOBAL*/] || - { } - )["replaceMode"]; - - if (replaceMode != "never") { - updateAllConditional(); - - if (replaceMode == "observe") { - observer(update); - } - } -} - -// Schedule to render all identicons on the page once it has been loaded. -whenDocumentIsReady(jdenticonStartup); - +/** + * Updates the identicon in the specified `` or `` elements. + * @param {(string|Element)} el - Specifies the container in which the icon is rendered as a DOM element of the type + * `` or ``, or a CSS selector to such an element. + * @param {*=} hashOrValue - Optional hash or value to be rendered. If not specified, the `data-jdenticon-hash` or + * `data-jdenticon-value` attribute will be evaluated. + * @param {Object|number=} config - Optional configuration. If specified, this configuration object overrides any + * global configuration in its entirety. For backward compability a padding value in the range [0.0, 0.5) can be + * specified in place of a configuration object. + */ +function update(el, hashOrValue, config) { + renderDomElement(el, hashOrValue, config, function (el, iconType) { + if (iconType) { + return iconType == ICON_TYPE_SVG ? + new SvgRenderer(new SvgElement(el)) : + new CanvasRenderer(/** @type {HTMLCanvasElement} */(el).getContext("2d")); + } + }); +} + +/** + * Updates the identicon in the specified canvas or svg elements. + * @param {(string|Element)} el - Specifies the container in which the icon is rendered as a DOM element of the type + * `` or ``, or a CSS selector to such an element. + * @param {*} hashOrValue - Optional hash or value to be rendered. If not specified, the `data-jdenticon-hash` or + * `data-jdenticon-value` attribute will be evaluated. + * @param {Object|number|undefined} config + * @param {function(Element,number):Renderer} rendererFactory - Factory function for creating an icon renderer. + */ +function renderDomElement(el, hashOrValue, config, rendererFactory) { + if (typeof el === "string") { + if (documentQuerySelectorAll) { + var elements = documentQuerySelectorAll(el); + for (var i = 0; i < elements.length; i++) { + renderDomElement(elements[i], hashOrValue, config, rendererFactory); + } + } + return; + } + + // Hash selection. The result from getValidHash or computeHash is + // accepted as a valid hash. + var hash = + // 1. Explicit valid hash + isValidHash(hashOrValue) || + + // 2. Explicit value (`!= null` catches both null and undefined) + hashOrValue != null && computeHash(hashOrValue) || + + // 3. `data-jdenticon-hash` attribute + isValidHash(el.getAttribute(ATTRIBUTES.t/*HASH*/)) || + + // 4. `data-jdenticon-value` attribute. + // We want to treat an empty attribute as an empty value. + // Some browsers return empty string even if the attribute + // is not specified, so use hasAttribute to determine if + // the attribute is specified. + el.hasAttribute(ATTRIBUTES.o/*VALUE*/) && computeHash(el.getAttribute(ATTRIBUTES.o/*VALUE*/)); + + if (!hash) { + // No hash specified. Don't render an icon. + return; + } + + var renderer = rendererFactory(el, getIdenticonType(el)); + if (renderer) { + // Draw icon + iconGenerator(renderer, hash, config); + el[IS_RENDERED_PROPERTY] = true; + } +} + +/** + * Renders an identicon for all matching supported elements. + * + * @param {*} hashOrValue - A hexadecimal hash string or any value that will be hashed by Jdenticon. If not + * specified the `data-jdenticon-hash` and `data-jdenticon-value` attributes of each element will be + * evaluated. + * @param {Object|number=} config - Optional configuration. If specified, this configuration object overrides any global + * configuration in its entirety. For backward compatibility a padding value in the range [0.0, 0.5) can be + * specified in place of a configuration object. + */ +function jdenticonJqueryPlugin(hashOrValue, config) { + this["each"](function (index, el) { + update(el, hashOrValue, config); + }); + return this; +} + +// This file is compiled to dist/jdenticon.js and dist/jdenticon.min.js + +var jdenticon = updateAll; + +defineConfigProperty(jdenticon); + +// Export public API +jdenticon["configure"] = configure; +jdenticon["drawIcon"] = drawIcon; +jdenticon["toSvg"] = toSvg; +jdenticon["update"] = update; +jdenticon["updateCanvas"] = update; +jdenticon["updateSvg"] = update; + +/** + * Specifies the version of the Jdenticon package in use. + * @type {string} + */ +jdenticon["version"] = "3.3.0"; + +/** + * Specifies which bundle of Jdenticon that is used. + * @type {string} + */ +jdenticon["bundle"] = "browser-umd"; + +// Basic jQuery plugin +var jQuery = GLOBAL["jQuery"]; +if (jQuery) { + jQuery["fn"]["jdenticon"] = jdenticonJqueryPlugin; +} + +/** + * This function is called once upon page load. + */ +function jdenticonStartup() { + var replaceMode = ( + jdenticon[CONFIG_PROPERTIES.n/*MODULE*/] || + GLOBAL[CONFIG_PROPERTIES.G/*GLOBAL*/] || + { } + )["replaceMode"]; + + if (replaceMode != "never") { + updateAllConditional(); + + if (replaceMode == "observe") { + observer(update); + } + } +} + +// Schedule to render all identicons on the page once it has been loaded. +whenDocumentIsReady(jdenticonStartup); + return jdenticon; - + }); \ No newline at end of file diff --git a/src/static/scripts/jquery-3.7.1.slim.js b/src/static/scripts/jquery-4.0.0.slim.js similarity index 64% rename from src/static/scripts/jquery-3.7.1.slim.js rename to src/static/scripts/jquery-4.0.0.slim.js index f122b10d..a7bb40cf 100644 --- a/src/static/scripts/jquery-3.7.1.slim.js +++ b/src/static/scripts/jquery-4.0.0.slim.js @@ -1,12 +1,12 @@ /*! - * jQuery JavaScript Library v3.7.1 -ajax,-ajax/jsonp,-ajax/load,-ajax/script,-ajax/var/location,-ajax/var/nonce,-ajax/var/rquery,-ajax/xhr,-manipulation/_evalUrl,-deprecated/ajax-event-alias,-effects,-effects/animatedSelector,-effects/Tween + * jQuery JavaScript Library v4.0.0+slim * https://jquery.com/ * * Copyright OpenJS Foundation and other contributors * Released under the MIT license - * https://jquery.org/license + * https://jquery.com/license/ * - * Date: 2023-08-28T13:37Z + * Date: 2026-01-18T00:20Z */ ( function( global, factory ) { @@ -16,19 +16,7 @@ // For CommonJS and CommonJS-like environments where a proper `window` // is present, execute the factory and get jQuery. - // For environments that do not have a `window` with a `document` - // (such as Node.js), expose a factory as module.exports. - // This accentuates the need for the creation of a real `window`. - // e.g. var jQuery = require("jquery")(window); - // See ticket trac-14549 for more info. - module.exports = global.document ? - factory( global, true ) : - function( w ) { - if ( !w.document ) { - throw new Error( "jQuery requires a window with a document" ); - } - return factory( w ); - }; + module.exports = factory( global, true ); } else { factory( global ); } @@ -36,29 +24,31 @@ // Pass this if window is not defined yet } )( typeof window !== "undefined" ? window : this, function( window, noGlobal ) { -// Edge <= 12 - 13+, Firefox <=18 - 45+, IE 10 - 11, Safari 5.1 - 9+, iOS 6 - 9.1 -// throw exceptions when non-strict code (e.g., ASP.NET 4.5) accesses strict mode -// arguments.callee.caller (trac-13335). But as of jQuery 3.0 (2016), strict mode should be common -// enough that all such attempts are guarded in a try block. "use strict"; +if ( !window.document ) { + throw new Error( "jQuery requires a window with a document" ); +} + var arr = []; var getProto = Object.getPrototypeOf; var slice = arr.slice; +// Support: IE 11+ +// IE doesn't have Array#flat; provide a fallback. var flat = arr.flat ? function( array ) { return arr.flat.call( array ); } : function( array ) { return arr.concat.apply( [], array ); }; - var push = arr.push; var indexOf = arr.indexOf; +// [[Class]] -> type pairs var class2type = {}; var toString = class2type.toString; @@ -69,85 +59,64 @@ var fnToString = hasOwn.toString; var ObjectFunctionString = fnToString.call( Object ); +// All support tests are defined in their respective modules. var support = {}; -var isFunction = function isFunction( obj ) { - - // Support: Chrome <=57, Firefox <=52 - // In some browsers, typeof returns "function" for HTML elements - // (i.e., `typeof document.createElement( "object" ) === "function"`). - // We don't want to classify *any* DOM node as a function. - // Support: QtWeb <=3.8.5, WebKit <=534.34, wkhtmltopdf tool <=0.12.5 - // Plus for old WebKit, typeof returns "function" for HTML collections - // (e.g., `typeof document.getElementsByTagName("div") === "function"`). (gh-4756) - return typeof obj === "function" && typeof obj.nodeType !== "number" && - typeof obj.item !== "function"; - }; - - -var isWindow = function isWindow( obj ) { - return obj != null && obj === obj.window; - }; - - -var document = window.document; - - - - var preservedScriptAttributes = { - type: true, - src: true, - nonce: true, - noModule: true - }; - - function DOMEval( code, node, doc ) { - doc = doc || document; - - var i, val, - script = doc.createElement( "script" ); - - script.text = code; - if ( node ) { - for ( i in preservedScriptAttributes ) { - - // Support: Firefox 64+, Edge 18+ - // Some browsers don't support the "nonce" property on scripts. - // On the other hand, just using `getAttribute` is not enough as - // the `nonce` attribute is reset to an empty string whenever it - // becomes browsing-context connected. - // See https://github.com/whatwg/html/issues/2369 - // See https://html.spec.whatwg.org/#nonce-attributes - // The `node.getAttribute` check was added for the sake of - // `jQuery.globalEval` so that it can fake a nonce-containing node - // via an object. - val = node[ i ] || node.getAttribute && node.getAttribute( i ); - if ( val ) { - script.setAttribute( i, val ); - } - } - } - doc.head.appendChild( script ).parentNode.removeChild( script ); - } - - function toType( obj ) { if ( obj == null ) { return obj + ""; } - // Support: Android <=2.3 only (functionish RegExp) - return typeof obj === "object" || typeof obj === "function" ? + return typeof obj === "object" ? class2type[ toString.call( obj ) ] || "object" : typeof obj; } -/* global Symbol */ -// Defining this global in .eslintrc.json would create a danger of using the global -// unguarded in another place, it seems safer to define global only for this module +function isWindow( obj ) { + return obj != null && obj === obj.window; +} +function isArrayLike( obj ) { -var version = "3.7.1 -ajax,-ajax/jsonp,-ajax/load,-ajax/script,-ajax/var/location,-ajax/var/nonce,-ajax/var/rquery,-ajax/xhr,-manipulation/_evalUrl,-deprecated/ajax-event-alias,-effects,-effects/animatedSelector,-effects/Tween", + var length = !!obj && obj.length, + type = toType( obj ); + + if ( typeof obj === "function" || isWindow( obj ) ) { + return false; + } + + return type === "array" || length === 0 || + typeof length === "number" && length > 0 && ( length - 1 ) in obj; +} + +var document$1 = window.document; + +var preservedScriptAttributes = { + type: true, + src: true, + nonce: true, + noModule: true +}; + +function DOMEval( code, node, doc ) { + doc = doc || document$1; + + var i, + script = doc.createElement( "script" ); + + script.text = code; + for ( i in preservedScriptAttributes ) { + if ( node && node[ i ] ) { + script[ i ] = node[ i ]; + } + } + + if ( doc.head.appendChild( script ).parentNode ) { + script.parentNode.removeChild( script ); + } +} + +var version = "4.0.0+slim", rhtmlSuffix = /HTML$/i, @@ -243,13 +212,7 @@ jQuery.fn = jQuery.prototype = { end: function() { return this.prevObject || this.constructor(); - }, - - // For internal use only. - // Behaves like an Array's method, not like a jQuery method. - push: push, - sort: arr.sort, - splice: arr.splice + } }; jQuery.extend = jQuery.fn.extend = function() { @@ -269,7 +232,7 @@ jQuery.extend = jQuery.fn.extend = function() { } // Handle case when target is a string or something (possible in deep copy) - if ( typeof target !== "object" && !isFunction( target ) ) { + if ( typeof target !== "object" && typeof target !== "function" ) { target = {}; } @@ -427,6 +390,7 @@ jQuery.extend( { return ret; }, + // results is for internal usage only makeArray: function( arr, results ) { var ret = results || []; @@ -458,8 +422,20 @@ jQuery.extend( { return !rhtmlSuffix.test( namespace || docElem && docElem.nodeName || "HTML" ); }, - // Support: Android <=4.0 only, PhantomJS 1 only - // push.apply(_, arraylike) throws on ancient WebKit + // Note: an element does not contain itself + contains: function( a, b ) { + var bup = b && b.parentNode; + + return a === bup || !!( bup && bup.nodeType === 1 && ( + + // Support: IE 9 - 11+ + // IE doesn't have `contains` on SVG. + a.contains ? + a.contains( bup ) : + a.compareDocumentPosition && a.compareDocumentPosition( bup ) & 16 + ) ); + }, + merge: function( first, second ) { var len = +second.length, j = 0, @@ -543,65 +519,488 @@ jQuery.each( "Boolean Number String Function Array Date RegExp Object Error Symb class2type[ "[object " + name + "]" ] = name.toLowerCase(); } ); -function isArrayLike( obj ) { - - // Support: real iOS 8.2 only (not reproducible in simulator) - // `in` check used to prevent JIT error (gh-2145) - // hasOwn isn't used here due to false negatives - // regarding Nodelist length in IE - var length = !!obj && "length" in obj && obj.length, - type = toType( obj ); - - if ( isFunction( obj ) || isWindow( obj ) ) { - return false; - } - - return type === "array" || length === 0 || - typeof length === "number" && length > 0 && ( length - 1 ) in obj; -} - - function nodeName( elem, name ) { - return elem.nodeName && elem.nodeName.toLowerCase() === name.toLowerCase(); - } + var pop = arr.pop; - -var sort = arr.sort; - - -var splice = arr.splice; - - +// https://www.w3.org/TR/css3-selectors/#whitespace var whitespace = "[\\x20\\t\\r\\n\\f]"; +var isIE = document$1.documentMode; + +var rbuggyQSA = isIE && new RegExp( + + // Support: IE 9 - 11+ + // IE's :disabled selector does not pick up the children of disabled fieldsets + ":enabled|:disabled|" + + + // Support: IE 11+ + // IE 11 doesn't find elements on a `[name='']` query in some cases. + // Adding a temporary attribute to the document before the selection works + // around the issue. + "\\[" + whitespace + "*name" + whitespace + "*=" + + whitespace + "*(?:''|\"\")" + +); var rtrimCSS = new RegExp( "^" + whitespace + "+|((?:^|[^\\\\])(?:\\\\.)*)" + whitespace + "+$", "g" ); +// https://www.w3.org/TR/css-syntax-3/#ident-token-diagram +var identifier = "(?:\\\\[\\da-fA-F]{1,6}" + whitespace + + "?|\\\\[^\\r\\n\\f]|[\\w-]|[^\0-\\x7f])+"; +var rleadingCombinator = new RegExp( "^" + whitespace + "*([>+~]|" + + whitespace + ")" + whitespace + "*" ); +var rdescend = new RegExp( whitespace + "|>" ); -// Note: an element does not contain itself -jQuery.contains = function( a, b ) { - var bup = b && b.parentNode; +var rsibling = /[+~]/; - return a === bup || !!( bup && bup.nodeType === 1 && ( +var documentElement$1 = document$1.documentElement; - // Support: IE 9 - 11+ - // IE doesn't have `contains` on SVG. - a.contains ? - a.contains( bup ) : - a.compareDocumentPosition && a.compareDocumentPosition( bup ) & 16 - ) ); +// Support: IE 9 - 11+ +// IE requires a prefix. +var matches = documentElement$1.matches || documentElement$1.msMatchesSelector; + +/** + * Create key-value caches of limited size + * @returns {function(string, object)} Returns the Object data after storing it on itself with + * property name the (space-suffixed) string and (if the cache is larger than Expr.cacheLength) + * deleting the oldest entry + */ +function createCache() { + var keys = []; + + function cache( key, value ) { + + // Use (key + " ") to avoid collision with native prototype properties + // (see https://github.com/jquery/sizzle/issues/157) + if ( keys.push( key + " " ) > jQuery.expr.cacheLength ) { + + // Only keep the most recent entries + delete cache[ keys.shift() ]; + } + return ( cache[ key + " " ] = value ); + } + return cache; +} + +/** + * Checks a node for validity as a jQuery selector context + * @param {Element|Object=} context + * @returns {Element|Object|Boolean} The input node if acceptable, otherwise a falsy value + */ +function testContext( context ) { + return context && typeof context.getElementsByTagName !== "undefined" && context; +} + +// Attribute selectors: https://www.w3.org/TR/selectors/#attribute-selectors +var attributes = "\\[" + whitespace + "*(" + identifier + ")(?:" + whitespace + + + // Operator (capture 2) + "*([*^$|!~]?=)" + whitespace + + + // "Attribute values must be CSS identifiers [capture 5] or strings [capture 3 or capture 4]" + "*(?:'((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\"|(" + identifier + "))|)" + + whitespace + "*\\]"; + +var pseudos = ":(" + identifier + ")(?:\\((" + + + // To reduce the number of selectors needing tokenize in the preFilter, prefer arguments: + // 1. quoted (capture 3; capture 4 or capture 5) + "('((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\")|" + + + // 2. simple (capture 6) + "((?:\\\\.|[^\\\\()[\\]]|" + attributes + ")*)|" + + + // 3. anything else (capture 2) + ".*" + + ")\\)|)"; + +var filterMatchExpr = { + ID: new RegExp( "^#(" + identifier + ")" ), + CLASS: new RegExp( "^\\.(" + identifier + ")" ), + TAG: new RegExp( "^(" + identifier + "|[*])" ), + ATTR: new RegExp( "^" + attributes ), + PSEUDO: new RegExp( "^" + pseudos ), + CHILD: new RegExp( + "^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\(" + + whitespace + "*(even|odd|(([+-]|)(\\d*)n|)" + whitespace + "*(?:([+-]|)" + + whitespace + "*(\\d+)|))" + whitespace + "*\\)|)", "i" ) }; +var rpseudo = new RegExp( pseudos ); +// CSS escapes +// https://www.w3.org/TR/CSS21/syndata.html#escaped-characters +var runescape = new RegExp( "\\\\[\\da-fA-F]{1,6}" + whitespace + + "?|\\\\([^\\r\\n\\f])", "g" ), + funescape = function( escape, nonHex ) { + var high = "0x" + escape.slice( 1 ) - 0x10000; + + if ( nonHex ) { + + // Strip the backslash prefix from a non-hex escape sequence + return nonHex; + } + + // Replace a hexadecimal escape sequence with the encoded Unicode code point + // Support: IE <=11+ + // For values outside the Basic Multilingual Plane (BMP), manually construct a + // surrogate pair + return high < 0 ? + String.fromCharCode( high + 0x10000 ) : + String.fromCharCode( high >> 10 | 0xD800, high & 0x3FF | 0xDC00 ); + }; + +function unescapeSelector( sel ) { + return sel.replace( runescape, funescape ); +} + +function selectorError( msg ) { + jQuery.error( "Syntax error, unrecognized expression: " + msg ); +} + +var rcomma = new RegExp( "^" + whitespace + "*," + whitespace + "*" ); + +var tokenCache = createCache(); + +function tokenize( selector, parseOnly ) { + var matched, match, tokens, type, + soFar, groups, preFilters, + cached = tokenCache[ selector + " " ]; + + if ( cached ) { + return parseOnly ? 0 : cached.slice( 0 ); + } + + soFar = selector; + groups = []; + preFilters = jQuery.expr.preFilter; + + while ( soFar ) { + + // Comma and first run + if ( !matched || ( match = rcomma.exec( soFar ) ) ) { + if ( match ) { + + // Don't consume trailing commas as valid + soFar = soFar.slice( match[ 0 ].length ) || soFar; + } + groups.push( ( tokens = [] ) ); + } + + matched = false; + + // Combinators + if ( ( match = rleadingCombinator.exec( soFar ) ) ) { + matched = match.shift(); + tokens.push( { + value: matched, + + // Cast descendant combinators to space + type: match[ 0 ].replace( rtrimCSS, " " ) + } ); + soFar = soFar.slice( matched.length ); + } + + // Filters + for ( type in filterMatchExpr ) { + if ( ( match = jQuery.expr.match[ type ].exec( soFar ) ) && ( !preFilters[ type ] || + ( match = preFilters[ type ]( match ) ) ) ) { + matched = match.shift(); + tokens.push( { + value: matched, + type: type, + matches: match + } ); + soFar = soFar.slice( matched.length ); + } + } + + if ( !matched ) { + break; + } + } + + // Return the length of the invalid excess + // if we're just parsing + // Otherwise, throw an error or return tokens + if ( parseOnly ) { + return soFar.length; + } + + return soFar ? + selectorError( selector ) : + + // Cache the tokens + tokenCache( selector, groups ).slice( 0 ); +} + +var preFilter = { + ATTR: function( match ) { + match[ 1 ] = unescapeSelector( match[ 1 ] ); + + // Move the given value to match[3] whether quoted or unquoted + match[ 3 ] = unescapeSelector( match[ 3 ] || match[ 4 ] || match[ 5 ] || "" ); + + if ( match[ 2 ] === "~=" ) { + match[ 3 ] = " " + match[ 3 ] + " "; + } + + return match.slice( 0, 4 ); + }, + + CHILD: function( match ) { + + /* matches from filterMatchExpr["CHILD"] + 1 type (only|nth|...) + 2 what (child|of-type) + 3 argument (even|odd|\d*|\d*n([+-]\d+)?|...) + 4 xn-component of xn+y argument ([+-]?\d*n|) + 5 sign of xn-component + 6 x of xn-component + 7 sign of y-component + 8 y of y-component + */ + match[ 1 ] = match[ 1 ].toLowerCase(); + + if ( match[ 1 ].slice( 0, 3 ) === "nth" ) { + + // nth-* requires argument + if ( !match[ 3 ] ) { + selectorError( match[ 0 ] ); + } + + // numeric x and y parameters for jQuery.expr.filter.CHILD + // remember that false/true cast respectively to 0/1 + match[ 4 ] = +( match[ 4 ] ? + match[ 5 ] + ( match[ 6 ] || 1 ) : + 2 * ( match[ 3 ] === "even" || match[ 3 ] === "odd" ) + ); + match[ 5 ] = +( ( match[ 7 ] + match[ 8 ] ) || match[ 3 ] === "odd" ); + + // other types prohibit arguments + } else if ( match[ 3 ] ) { + selectorError( match[ 0 ] ); + } + + return match; + }, + + PSEUDO: function( match ) { + var excess, + unquoted = !match[ 6 ] && match[ 2 ]; + + if ( filterMatchExpr.CHILD.test( match[ 0 ] ) ) { + return null; + } + + // Accept quoted arguments as-is + if ( match[ 3 ] ) { + match[ 2 ] = match[ 4 ] || match[ 5 ] || ""; + + // Strip excess characters from unquoted arguments + } else if ( unquoted && rpseudo.test( unquoted ) && + + // Get excess from tokenize (recursively) + ( excess = tokenize( unquoted, true ) ) && + + // advance to the next closing parenthesis + ( excess = unquoted.indexOf( ")", unquoted.length - excess ) - + unquoted.length ) ) { + + // excess is a negative index + match[ 0 ] = match[ 0 ].slice( 0, excess ); + match[ 2 ] = unquoted.slice( 0, excess ); + } + + // Return only captures needed by the pseudo filter method (type and argument) + return match.slice( 0, 3 ); + } +}; + +function toSelector( tokens ) { + var i = 0, + len = tokens.length, + selector = ""; + for ( ; i < len; i++ ) { + selector += tokens[ i ].value; + } + return selector; +} + +// Multifunctional method to get and set values of a collection +// The value/s can optionally be executed if it's a function +function access( elems, fn, key, value, chainable, emptyGet, raw ) { + var i = 0, + len = elems.length, + bulk = key == null; + + // Sets many values + if ( toType( key ) === "object" ) { + chainable = true; + for ( i in key ) { + access( elems, fn, i, key[ i ], true, emptyGet, raw ); + } + + // Sets one value + } else if ( value !== undefined ) { + chainable = true; + + if ( typeof value !== "function" ) { + raw = true; + } + + if ( bulk ) { + + // Bulk operations run against the entire set + if ( raw ) { + fn.call( elems, value ); + fn = null; + + // ...except when executing function values + } else { + bulk = fn; + fn = function( elem, _key, value ) { + return bulk.call( jQuery( elem ), value ); + }; + } + } + + if ( fn ) { + for ( ; i < len; i++ ) { + fn( + elems[ i ], key, raw ? + value : + value.call( elems[ i ], i, fn( elems[ i ], key ) ) + ); + } + } + } + + if ( chainable ) { + return elems; + } + + // Gets + if ( bulk ) { + return fn.call( elems ); + } + + return len ? fn( elems[ 0 ], key ) : emptyGet; +} + +// Only count HTML whitespace +// Other whitespace should count in values +// https://infra.spec.whatwg.org/#ascii-whitespace +var rnothtmlwhite = /[^\x20\t\r\n\f]+/g; + +jQuery.fn.extend( { + attr: function( name, value ) { + return access( this, jQuery.attr, name, value, arguments.length > 1 ); + }, + + removeAttr: function( name ) { + return this.each( function() { + jQuery.removeAttr( this, name ); + } ); + } +} ); + +jQuery.extend( { + attr: function( elem, name, value ) { + var ret, hooks, + nType = elem.nodeType; + + // Don't get/set attributes on text, comment and attribute nodes + if ( nType === 3 || nType === 8 || nType === 2 ) { + return; + } + + // Fallback to prop when attributes are not supported + if ( typeof elem.getAttribute === "undefined" ) { + return jQuery.prop( elem, name, value ); + } + + // Attribute hooks are determined by the lowercase version + // Grab necessary hook if one is defined + if ( nType !== 1 || !jQuery.isXMLDoc( elem ) ) { + hooks = jQuery.attrHooks[ name.toLowerCase() ]; + } + + if ( value !== undefined ) { + if ( value === null || + + // For compat with previous handling of boolean attributes, + // remove when `false` passed. For ARIA attributes - + // many of which recognize a `"false"` value - continue to + // set the `"false"` value as jQuery <4 did. + ( value === false && name.toLowerCase().indexOf( "aria-" ) !== 0 ) ) { + + jQuery.removeAttr( elem, name ); + return; + } + + if ( hooks && "set" in hooks && + ( ret = hooks.set( elem, value, name ) ) !== undefined ) { + return ret; + } + + elem.setAttribute( name, value ); + return value; + } + + if ( hooks && "get" in hooks && ( ret = hooks.get( elem, name ) ) !== null ) { + return ret; + } + + ret = elem.getAttribute( name ); + + // Non-existent attributes return null, we normalize to undefined + return ret == null ? undefined : ret; + }, + + attrHooks: {}, + + removeAttr: function( elem, value ) { + var name, + i = 0, + + // Attribute names can contain non-HTML whitespace characters + // https://html.spec.whatwg.org/multipage/syntax.html#attributes-2 + attrNames = value && value.match( rnothtmlwhite ); + + if ( attrNames && elem.nodeType === 1 ) { + while ( ( name = attrNames[ i++ ] ) ) { + elem.removeAttribute( name ); + } + } + } +} ); + +// Support: IE <=11+ +// An input loses its value after becoming a radio +if ( isIE ) { + jQuery.attrHooks.type = { + set: function( elem, value ) { + if ( value === "radio" && nodeName( elem, "input" ) ) { + var val = elem.value; + elem.setAttribute( "type", value ); + if ( val ) { + elem.value = val; + } + return value; + } + } + }; +} // CSS string/identifier serialization // https://drafts.csswg.org/cssom/#common-serializing-idioms @@ -627,139 +1026,140 @@ jQuery.escapeSelector = function( sel ) { return ( sel + "" ).replace( rcssescape, fcssescape ); }; +var sort = arr.sort; +var splice = arr.splice; +var hasDuplicate; -var preferredDoc = document, - pushNative = push; +// Document order sorting +function sortOrder( a, b ) { -( function() { + // Flag for duplicate removal + if ( a === b ) { + hasDuplicate = true; + return 0; + } + + // Sort on method existence if only one input has compareDocumentPosition + var compare = !a.compareDocumentPosition - !b.compareDocumentPosition; + if ( compare ) { + return compare; + } + + // Calculate position if both inputs belong to the same document + // Support: IE 11+ + // IE sometimes throws a "Permission denied" error when strict-comparing + // two documents; shallow comparisons work. + // eslint-disable-next-line eqeqeq + compare = ( a.ownerDocument || a ) == ( b.ownerDocument || b ) ? + a.compareDocumentPosition( b ) : + + // Otherwise we know they are disconnected + 1; + + // Disconnected nodes + if ( compare & 1 ) { + + // Choose the first element that is related to the document + // Support: IE 11+ + // IE sometimes throws a "Permission denied" error when strict-comparing + // two documents; shallow comparisons work. + // eslint-disable-next-line eqeqeq + if ( a == document$1 || a.ownerDocument == document$1 && + jQuery.contains( document$1, a ) ) { + return -1; + } + + // Support: IE 11+ + // IE sometimes throws a "Permission denied" error when strict-comparing + // two documents; shallow comparisons work. + // eslint-disable-next-line eqeqeq + if ( b == document$1 || b.ownerDocument == document$1 && + jQuery.contains( document$1, b ) ) { + return 1; + } + + // Maintain original order + return 0; + } + + return compare & 4 ? -1 : 1; +} + +/** + * Document sorting and removing duplicates + * @param {ArrayLike} results + */ +jQuery.uniqueSort = function( results ) { + var elem, + duplicates = [], + j = 0, + i = 0; + + hasDuplicate = false; + + sort.call( results, sortOrder ); + + if ( hasDuplicate ) { + while ( ( elem = results[ i++ ] ) ) { + if ( elem === results[ i ] ) { + j = duplicates.push( i ); + } + } + while ( j-- ) { + splice.call( results, duplicates[ j ], 1 ); + } + } + + return results; +}; + +jQuery.fn.uniqueSort = function() { + return this.pushStack( jQuery.uniqueSort( slice.apply( this ) ) ); +}; var i, - Expr, outermostContext, - sortInput, - hasDuplicate, - push = pushNative, // Local document vars document, documentElement, documentIsHTML, - rbuggyQSA, - matches, // Instance-specific data - expando = jQuery.expando, dirruns = 0, done = 0, classCache = createCache(), - tokenCache = createCache(), compilerCache = createCache(), nonnativeSelectorCache = createCache(), - sortOrder = function( a, b ) { - if ( a === b ) { - hasDuplicate = true; - } - return 0; - }, - - booleans = "checked|selected|async|autofocus|autoplay|controls|defer|disabled|hidden|ismap|" + - "loop|multiple|open|readonly|required|scoped", // Regular expressions - // https://www.w3.org/TR/css-syntax-3/#ident-token-diagram - identifier = "(?:\\\\[\\da-fA-F]{1,6}" + whitespace + - "?|\\\\[^\\r\\n\\f]|[\\w-]|[^\0-\\x7f])+", - - // Attribute selectors: https://www.w3.org/TR/selectors/#attribute-selectors - attributes = "\\[" + whitespace + "*(" + identifier + ")(?:" + whitespace + - - // Operator (capture 2) - "*([*^$|!~]?=)" + whitespace + - - // "Attribute values must be CSS identifiers [capture 5] or strings [capture 3 or capture 4]" - "*(?:'((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\"|(" + identifier + "))|)" + - whitespace + "*\\]", - - pseudos = ":(" + identifier + ")(?:\\((" + - - // To reduce the number of selectors needing tokenize in the preFilter, prefer arguments: - // 1. quoted (capture 3; capture 4 or capture 5) - "('((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\")|" + - - // 2. simple (capture 6) - "((?:\\\\.|[^\\\\()[\\]]|" + attributes + ")*)|" + - - // 3. anything else (capture 2) - ".*" + - ")\\)|)", - // Leading and non-escaped trailing whitespace, capturing some non-whitespace characters preceding the latter rwhitespace = new RegExp( whitespace + "+", "g" ), - rcomma = new RegExp( "^" + whitespace + "*," + whitespace + "*" ), - rleadingCombinator = new RegExp( "^" + whitespace + "*([>+~]|" + whitespace + ")" + - whitespace + "*" ), - rdescend = new RegExp( whitespace + "|>" ), - - rpseudo = new RegExp( pseudos ), ridentifier = new RegExp( "^" + identifier + "$" ), - matchExpr = { - ID: new RegExp( "^#(" + identifier + ")" ), - CLASS: new RegExp( "^\\.(" + identifier + ")" ), - TAG: new RegExp( "^(" + identifier + "|[*])" ), - ATTR: new RegExp( "^" + attributes ), - PSEUDO: new RegExp( "^" + pseudos ), - CHILD: new RegExp( - "^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\(" + - whitespace + "*(even|odd|(([+-]|)(\\d*)n|)" + whitespace + "*(?:([+-]|)" + - whitespace + "*(\\d+)|))" + whitespace + "*\\)|)", "i" ), - bool: new RegExp( "^(?:" + booleans + ")$", "i" ), + matchExpr = jQuery.extend( { // For use in libraries implementing .is() // We use this for POS matching in `select` needsContext: new RegExp( "^" + whitespace + "*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\(" + whitespace + "*((?:-\\d)?\\d*)" + whitespace + "*\\)|)(?=[^-]|$)", "i" ) - }, + }, filterMatchExpr ), rinputs = /^(?:input|select|textarea|button)$/i, rheader = /^h\d$/i, // Easily-parseable/retrievable ID or TAG or CLASS selectors - rquickExpr = /^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/, - - rsibling = /[+~]/, - - // CSS escapes - // https://www.w3.org/TR/CSS21/syndata.html#escaped-characters - runescape = new RegExp( "\\\\[\\da-fA-F]{1,6}" + whitespace + - "?|\\\\([^\\r\\n\\f])", "g" ), - funescape = function( escape, nonHex ) { - var high = "0x" + escape.slice( 1 ) - 0x10000; - - if ( nonHex ) { - - // Strip the backslash prefix from a non-hex escape sequence - return nonHex; - } - - // Replace a hexadecimal escape sequence with the encoded Unicode code point - // Support: IE <=11+ - // For values outside the Basic Multilingual Plane (BMP), manually construct a - // surrogate pair - return high < 0 ? - String.fromCharCode( high + 0x10000 ) : - String.fromCharCode( high >> 10 | 0xD800, high & 0x3FF | 0xDC00 ); - }, + rquickExpr$1 = /^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/, // Used for iframes; see `setDocument`. - // Support: IE 9 - 11+, Edge 12 - 18+ + // Support: IE 9 - 11+ // Removing the function wrapper causes a "Permission Denied" - // error in IE/Edge. + // error in IE. unloadHandler = function() { setDocument(); }, @@ -771,37 +1171,6 @@ var i, { dir: "parentNode", next: "legend" } ); -// Support: IE <=9 only -// Accessing document.activeElement can throw unexpectedly -// https://bugs.jquery.com/ticket/13393 -function safeActiveElement() { - try { - return document.activeElement; - } catch ( err ) { } -} - -// Optimize for push.apply( _, NodeList ) -try { - push.apply( - ( arr = slice.call( preferredDoc.childNodes ) ), - preferredDoc.childNodes - ); - - // Support: Android <=4.0 - // Detect silently failing push.apply - // eslint-disable-next-line no-unused-expressions - arr[ preferredDoc.childNodes.length ].nodeType; -} catch ( e ) { - push = { - apply: function( target, els ) { - pushNative.apply( target, slice.call( els ) ); - }, - call: function( target ) { - pushNative.apply( target, slice.call( arguments, 1 ) ); - } - }; -} - function find( selector, context, results, seed ) { var m, i, elem, nid, match, groups, newSelector, newContext = context && context.ownerDocument, @@ -827,7 +1196,7 @@ function find( selector, context, results, seed ) { // If the selector is sufficiently simple, try using a "get*By*" DOM method // (excepting DocumentFragment context, where the methods don't exist) - if ( nodeType !== 11 && ( match = rquickExpr.exec( selector ) ) ) { + if ( nodeType !== 11 && ( match = rquickExpr$1.exec( selector ) ) ) { // ID selector if ( ( m = match[ 1 ] ) ) { @@ -835,25 +1204,14 @@ function find( selector, context, results, seed ) { // Document context if ( nodeType === 9 ) { if ( ( elem = context.getElementById( m ) ) ) { - - // Support: IE 9 only - // getElementById can match elements by name instead of ID - if ( elem.id === m ) { - push.call( results, elem ); - return results; - } - } else { - return results; + push.call( results, elem ); } + return results; // Element context } else { - - // Support: IE 9 only - // getElementById can match elements by name instead of ID if ( newContext && ( elem = newContext.getElementById( m ) ) && - find.contains( context, elem ) && - elem.id === m ) { + jQuery.contains( context, elem ) ) { push.call( results, elem ); return results; @@ -890,22 +1248,23 @@ function find( selector, context, results, seed ) { ( rdescend.test( selector ) || rleadingCombinator.test( selector ) ) ) { // Expand context for sibling selectors - newContext = rsibling.test( selector ) && testContext( context.parentNode ) || + newContext = rsibling.test( selector ) && + testContext( context.parentNode ) || context; - // We can use :scope instead of the ID hack if the browser - // supports it & if we're not changing the context. - // Support: IE 11+, Edge 17 - 18+ - // IE/Edge sometimes throw a "Permission denied" error when - // strict-comparing two documents; shallow comparisons work. + // Outside of IE, if we're not changing the context we can + // use :scope instead of an ID. + // Support: IE 11+ + // IE sometimes throws a "Permission denied" error when strict-comparing + // two documents; shallow comparisons work. // eslint-disable-next-line eqeqeq - if ( newContext != context || !support.scope ) { + if ( newContext != context || isIE ) { // Capture the context ID, setting it first if necessary if ( ( nid = context.getAttribute( "id" ) ) ) { nid = jQuery.escapeSelector( nid ); } else { - context.setAttribute( "id", ( nid = expando ) ); + context.setAttribute( "id", ( nid = jQuery.expando ) ); } } @@ -927,7 +1286,7 @@ function find( selector, context, results, seed ) { } catch ( qsaError ) { nonnativeSelectorCache( selector, true ); } finally { - if ( nid === expando ) { + if ( nid === jQuery.expando ) { context.removeAttribute( "id" ); } } @@ -939,61 +1298,15 @@ function find( selector, context, results, seed ) { return select( selector.replace( rtrimCSS, "$1" ), context, results, seed ); } -/** - * Create key-value caches of limited size - * @returns {function(string, object)} Returns the Object data after storing it on itself with - * property name the (space-suffixed) string and (if the cache is larger than Expr.cacheLength) - * deleting the oldest entry - */ -function createCache() { - var keys = []; - - function cache( key, value ) { - - // Use (key + " ") to avoid collision with native prototype properties - // (see https://github.com/jquery/sizzle/issues/157) - if ( keys.push( key + " " ) > Expr.cacheLength ) { - - // Only keep the most recent entries - delete cache[ keys.shift() ]; - } - return ( cache[ key + " " ] = value ); - } - return cache; -} - /** * Mark a function for special use by jQuery selector module * @param {Function} fn The function to mark */ function markFunction( fn ) { - fn[ expando ] = true; + fn[ jQuery.expando ] = true; return fn; } -/** - * Support testing using an element - * @param {Function} fn Passed the created element and returns a boolean result - */ -function assert( fn ) { - var el = document.createElement( "fieldset" ); - - try { - return !!fn( el ); - } catch ( e ) { - return false; - } finally { - - // Remove from its parent by default - if ( el.parentNode ) { - el.parentNode.removeChild( el ); - } - - // release memory in IE - el = null; - } -} - /** * Returns a function to use in pseudos for input types * @param {String} type @@ -1092,31 +1405,21 @@ function createPositionalPseudo( fn ) { } ); } -/** - * Checks a node for validity as a jQuery selector context - * @param {Element|Object=} context - * @returns {Element|Object|Boolean} The input node if acceptable, otherwise a falsy value - */ -function testContext( context ) { - return context && typeof context.getElementsByTagName !== "undefined" && context; -} - /** * Sets document-related variables once based on the current document * @param {Element|Object} [node] An element or document object to use to set the document - * @returns {Object} Returns the current document */ function setDocument( node ) { var subWindow, - doc = node ? node.ownerDocument || node : preferredDoc; + doc = node ? node.ownerDocument || node : document$1; // Return early if doc is invalid or already selected - // Support: IE 11+, Edge 17 - 18+ - // IE/Edge sometimes throw a "Permission denied" error when strict-comparing + // Support: IE 11+ + // IE sometimes throws a "Permission denied" error when strict-comparing // two documents; shallow comparisons work. // eslint-disable-next-line eqeqeq - if ( doc == document || doc.nodeType !== 9 || !doc.documentElement ) { - return document; + if ( doc == document || doc.nodeType !== 9 ) { + return; } // Update global variables @@ -1124,294 +1427,16 @@ function setDocument( node ) { documentElement = document.documentElement; documentIsHTML = !jQuery.isXMLDoc( document ); - // Support: iOS 7 only, IE 9 - 11+ - // Older browsers didn't support unprefixed `matches`. - matches = documentElement.matches || - documentElement.webkitMatchesSelector || - documentElement.msMatchesSelector; - - // Support: IE 9 - 11+, Edge 12 - 18+ - // Accessing iframe documents after unload throws "permission denied" errors - // (see trac-13936). - // Limit the fix to IE & Edge Legacy; despite Edge 15+ implementing `matches`, - // all IE 9+ and Edge Legacy versions implement `msMatchesSelector` as well. - if ( documentElement.msMatchesSelector && - - // Support: IE 11+, Edge 17 - 18+ - // IE/Edge sometimes throw a "Permission denied" error when strict-comparing - // two documents; shallow comparisons work. - // eslint-disable-next-line eqeqeq - preferredDoc != document && + // Support: IE 9 - 11+ + // Accessing iframe documents after unload throws "permission denied" errors (see trac-13936) + // Support: IE 11+ + // IE sometimes throws a "Permission denied" error when strict-comparing + // two documents; shallow comparisons work. + // eslint-disable-next-line eqeqeq + if ( isIE && document$1 != document && ( subWindow = document.defaultView ) && subWindow.top !== subWindow ) { - - // Support: IE 9 - 11+, Edge 12 - 18+ subWindow.addEventListener( "unload", unloadHandler ); } - - // Support: IE <10 - // Check if getElementById returns elements by name - // The broken getElementById methods don't pick up programmatically-set names, - // so use a roundabout getElementsByName test - support.getById = assert( function( el ) { - documentElement.appendChild( el ).id = jQuery.expando; - return !document.getElementsByName || - !document.getElementsByName( jQuery.expando ).length; - } ); - - // Support: IE 9 only - // Check to see if it's possible to do matchesSelector - // on a disconnected node. - support.disconnectedMatch = assert( function( el ) { - return matches.call( el, "*" ); - } ); - - // Support: IE 9 - 11+, Edge 12 - 18+ - // IE/Edge don't support the :scope pseudo-class. - support.scope = assert( function() { - return document.querySelectorAll( ":scope" ); - } ); - - // Support: Chrome 105 - 111 only, Safari 15.4 - 16.3 only - // Make sure the `:has()` argument is parsed unforgivingly. - // We include `*` in the test to detect buggy implementations that are - // _selectively_ forgiving (specifically when the list includes at least - // one valid selector). - // Note that we treat complete lack of support for `:has()` as if it were - // spec-compliant support, which is fine because use of `:has()` in such - // environments will fail in the qSA path and fall back to jQuery traversal - // anyway. - support.cssHas = assert( function() { - try { - document.querySelector( ":has(*,:jqfake)" ); - return false; - } catch ( e ) { - return true; - } - } ); - - // ID filter and find - if ( support.getById ) { - Expr.filter.ID = function( id ) { - var attrId = id.replace( runescape, funescape ); - return function( elem ) { - return elem.getAttribute( "id" ) === attrId; - }; - }; - Expr.find.ID = function( id, context ) { - if ( typeof context.getElementById !== "undefined" && documentIsHTML ) { - var elem = context.getElementById( id ); - return elem ? [ elem ] : []; - } - }; - } else { - Expr.filter.ID = function( id ) { - var attrId = id.replace( runescape, funescape ); - return function( elem ) { - var node = typeof elem.getAttributeNode !== "undefined" && - elem.getAttributeNode( "id" ); - return node && node.value === attrId; - }; - }; - - // Support: IE 6 - 7 only - // getElementById is not reliable as a find shortcut - Expr.find.ID = function( id, context ) { - if ( typeof context.getElementById !== "undefined" && documentIsHTML ) { - var node, i, elems, - elem = context.getElementById( id ); - - if ( elem ) { - - // Verify the id attribute - node = elem.getAttributeNode( "id" ); - if ( node && node.value === id ) { - return [ elem ]; - } - - // Fall back on getElementsByName - elems = context.getElementsByName( id ); - i = 0; - while ( ( elem = elems[ i++ ] ) ) { - node = elem.getAttributeNode( "id" ); - if ( node && node.value === id ) { - return [ elem ]; - } - } - } - - return []; - } - }; - } - - // Tag - Expr.find.TAG = function( tag, context ) { - if ( typeof context.getElementsByTagName !== "undefined" ) { - return context.getElementsByTagName( tag ); - - // DocumentFragment nodes don't have gEBTN - } else { - return context.querySelectorAll( tag ); - } - }; - - // Class - Expr.find.CLASS = function( className, context ) { - if ( typeof context.getElementsByClassName !== "undefined" && documentIsHTML ) { - return context.getElementsByClassName( className ); - } - }; - - /* QSA/matchesSelector - ---------------------------------------------------------------------- */ - - // QSA and matchesSelector support - - rbuggyQSA = []; - - // Build QSA regex - // Regex strategy adopted from Diego Perini - assert( function( el ) { - - var input; - - documentElement.appendChild( el ).innerHTML = - "" + - ""; - - // Support: iOS <=7 - 8 only - // Boolean attributes and "value" are not treated correctly in some XML documents - if ( !el.querySelectorAll( "[selected]" ).length ) { - rbuggyQSA.push( "\\[" + whitespace + "*(?:value|" + booleans + ")" ); - } - - // Support: iOS <=7 - 8 only - if ( !el.querySelectorAll( "[id~=" + expando + "-]" ).length ) { - rbuggyQSA.push( "~=" ); - } - - // Support: iOS 8 only - // https://bugs.webkit.org/show_bug.cgi?id=136851 - // In-page `selector#id sibling-combinator selector` fails - if ( !el.querySelectorAll( "a#" + expando + "+*" ).length ) { - rbuggyQSA.push( ".#.+[+~]" ); - } - - // Support: Chrome <=105+, Firefox <=104+, Safari <=15.4+ - // In some of the document kinds, these selectors wouldn't work natively. - // This is probably OK but for backwards compatibility we want to maintain - // handling them through jQuery traversal in jQuery 3.x. - if ( !el.querySelectorAll( ":checked" ).length ) { - rbuggyQSA.push( ":checked" ); - } - - // Support: Windows 8 Native Apps - // The type and name attributes are restricted during .innerHTML assignment - input = document.createElement( "input" ); - input.setAttribute( "type", "hidden" ); - el.appendChild( input ).setAttribute( "name", "D" ); - - // Support: IE 9 - 11+ - // IE's :disabled selector does not pick up the children of disabled fieldsets - // Support: Chrome <=105+, Firefox <=104+, Safari <=15.4+ - // In some of the document kinds, these selectors wouldn't work natively. - // This is probably OK but for backwards compatibility we want to maintain - // handling them through jQuery traversal in jQuery 3.x. - documentElement.appendChild( el ).disabled = true; - if ( el.querySelectorAll( ":disabled" ).length !== 2 ) { - rbuggyQSA.push( ":enabled", ":disabled" ); - } - - // Support: IE 11+, Edge 15 - 18+ - // IE 11/Edge don't find elements on a `[name='']` query in some cases. - // Adding a temporary attribute to the document before the selection works - // around the issue. - // Interestingly, IE 10 & older don't seem to have the issue. - input = document.createElement( "input" ); - input.setAttribute( "name", "" ); - el.appendChild( input ); - if ( !el.querySelectorAll( "[name='']" ).length ) { - rbuggyQSA.push( "\\[" + whitespace + "*name" + whitespace + "*=" + - whitespace + "*(?:''|\"\")" ); - } - } ); - - if ( !support.cssHas ) { - - // Support: Chrome 105 - 110+, Safari 15.4 - 16.3+ - // Our regular `try-catch` mechanism fails to detect natively-unsupported - // pseudo-classes inside `:has()` (such as `:has(:contains("Foo"))`) - // in browsers that parse the `:has()` argument as a forgiving selector list. - // https://drafts.csswg.org/selectors/#relational now requires the argument - // to be parsed unforgivingly, but browsers have not yet fully adjusted. - rbuggyQSA.push( ":has" ); - } - - rbuggyQSA = rbuggyQSA.length && new RegExp( rbuggyQSA.join( "|" ) ); - - /* Sorting - ---------------------------------------------------------------------- */ - - // Document order sorting - sortOrder = function( a, b ) { - - // Flag for duplicate removal - if ( a === b ) { - hasDuplicate = true; - return 0; - } - - // Sort on method existence if only one input has compareDocumentPosition - var compare = !a.compareDocumentPosition - !b.compareDocumentPosition; - if ( compare ) { - return compare; - } - - // Calculate position if both inputs belong to the same document - // Support: IE 11+, Edge 17 - 18+ - // IE/Edge sometimes throw a "Permission denied" error when strict-comparing - // two documents; shallow comparisons work. - // eslint-disable-next-line eqeqeq - compare = ( a.ownerDocument || a ) == ( b.ownerDocument || b ) ? - a.compareDocumentPosition( b ) : - - // Otherwise we know they are disconnected - 1; - - // Disconnected nodes - if ( compare & 1 || - ( !support.sortDetached && b.compareDocumentPosition( a ) === compare ) ) { - - // Choose the first element that is related to our preferred document - // Support: IE 11+, Edge 17 - 18+ - // IE/Edge sometimes throw a "Permission denied" error when strict-comparing - // two documents; shallow comparisons work. - // eslint-disable-next-line eqeqeq - if ( a === document || a.ownerDocument == preferredDoc && - find.contains( preferredDoc, a ) ) { - return -1; - } - - // Support: IE 11+, Edge 17 - 18+ - // IE/Edge sometimes throw a "Permission denied" error when strict-comparing - // two documents; shallow comparisons work. - // eslint-disable-next-line eqeqeq - if ( b === document || b.ownerDocument == preferredDoc && - find.contains( preferredDoc, b ) ) { - return 1; - } - - // Maintain original order - return sortInput ? - ( indexOf.call( sortInput, a ) - indexOf.call( sortInput, b ) ) : - 0; - } - - return compare & 4 ? -1 : 1; - }; - - return document; } find.matches = function( expr, elements ) { @@ -1426,16 +1451,7 @@ find.matchesSelector = function( elem, expr ) { ( !rbuggyQSA || !rbuggyQSA.test( expr ) ) ) { try { - var ret = matches.call( elem, expr ); - - // IE 9's matchesSelector returns false on disconnected nodes - if ( ret || support.disconnectedMatch || - - // As well, disconnected nodes are said to be in a document - // fragment in IE 9 - elem.document && elem.document.nodeType !== 11 ) { - return ret; - } + return matches.call( elem, expr ); } catch ( e ) { nonnativeSelectorCache( expr, true ); } @@ -1444,91 +1460,7 @@ find.matchesSelector = function( elem, expr ) { return find( expr, document, null, [ elem ] ).length > 0; }; -find.contains = function( context, elem ) { - - // Set document vars if needed - // Support: IE 11+, Edge 17 - 18+ - // IE/Edge sometimes throw a "Permission denied" error when strict-comparing - // two documents; shallow comparisons work. - // eslint-disable-next-line eqeqeq - if ( ( context.ownerDocument || context ) != document ) { - setDocument( context ); - } - return jQuery.contains( context, elem ); -}; - - -find.attr = function( elem, name ) { - - // Set document vars if needed - // Support: IE 11+, Edge 17 - 18+ - // IE/Edge sometimes throw a "Permission denied" error when strict-comparing - // two documents; shallow comparisons work. - // eslint-disable-next-line eqeqeq - if ( ( elem.ownerDocument || elem ) != document ) { - setDocument( elem ); - } - - var fn = Expr.attrHandle[ name.toLowerCase() ], - - // Don't get fooled by Object.prototype properties (see trac-13807) - val = fn && hasOwn.call( Expr.attrHandle, name.toLowerCase() ) ? - fn( elem, name, !documentIsHTML ) : - undefined; - - if ( val !== undefined ) { - return val; - } - - return elem.getAttribute( name ); -}; - -find.error = function( msg ) { - throw new Error( "Syntax error, unrecognized expression: " + msg ); -}; - -/** - * Document sorting and removing duplicates - * @param {ArrayLike} results - */ -jQuery.uniqueSort = function( results ) { - var elem, - duplicates = [], - j = 0, - i = 0; - - // Unless we *know* we can detect duplicates, assume their presence - // - // Support: Android <=4.0+ - // Testing for detecting duplicates is unpredictable so instead assume we can't - // depend on duplicate detection in all browsers without a stable sort. - hasDuplicate = !support.sortStable; - sortInput = !support.sortStable && slice.call( results, 0 ); - sort.call( results, sortOrder ); - - if ( hasDuplicate ) { - while ( ( elem = results[ i++ ] ) ) { - if ( elem === results[ i ] ) { - j = duplicates.push( i ); - } - } - while ( j-- ) { - splice.call( results, duplicates[ j ], 1 ); - } - } - - // Clear input after sorting to release objects - // See https://github.com/jquery/sizzle/pull/225 - sortInput = null; - - return results; -}; - -jQuery.fn.uniqueSort = function() { - return this.pushStack( jQuery.uniqueSort( slice.apply( this ) ) ); -}; - -Expr = jQuery.expr = { +jQuery.expr = { // Can be adjusted by the user cacheLength: 50, @@ -1537,9 +1469,30 @@ Expr = jQuery.expr = { match: matchExpr, - attrHandle: {}, + find: { + ID: function( id, context ) { + if ( typeof context.getElementById !== "undefined" && documentIsHTML ) { + var elem = context.getElementById( id ); + return elem ? [ elem ] : []; + } + }, - find: {}, + TAG: function( tag, context ) { + if ( typeof context.getElementsByTagName !== "undefined" ) { + return context.getElementsByTagName( tag ); + + // DocumentFragment nodes don't have gEBTN + } else { + return context.querySelectorAll( tag ); + } + }, + + CLASS: function( className, context ) { + if ( typeof context.getElementsByClassName !== "undefined" && documentIsHTML ) { + return context.getElementsByClassName( className ); + } + } + }, relative: { ">": { dir: "parentNode", first: true }, @@ -1548,97 +1501,24 @@ Expr = jQuery.expr = { "~": { dir: "previousSibling" } }, - preFilter: { - ATTR: function( match ) { - match[ 1 ] = match[ 1 ].replace( runescape, funescape ); - - // Move the given value to match[3] whether quoted or unquoted - match[ 3 ] = ( match[ 3 ] || match[ 4 ] || match[ 5 ] || "" ) - .replace( runescape, funescape ); - - if ( match[ 2 ] === "~=" ) { - match[ 3 ] = " " + match[ 3 ] + " "; - } - - return match.slice( 0, 4 ); - }, - - CHILD: function( match ) { - - /* matches from matchExpr["CHILD"] - 1 type (only|nth|...) - 2 what (child|of-type) - 3 argument (even|odd|\d*|\d*n([+-]\d+)?|...) - 4 xn-component of xn+y argument ([+-]?\d*n|) - 5 sign of xn-component - 6 x of xn-component - 7 sign of y-component - 8 y of y-component - */ - match[ 1 ] = match[ 1 ].toLowerCase(); - - if ( match[ 1 ].slice( 0, 3 ) === "nth" ) { - - // nth-* requires argument - if ( !match[ 3 ] ) { - find.error( match[ 0 ] ); - } - - // numeric x and y parameters for Expr.filter.CHILD - // remember that false/true cast respectively to 0/1 - match[ 4 ] = +( match[ 4 ] ? - match[ 5 ] + ( match[ 6 ] || 1 ) : - 2 * ( match[ 3 ] === "even" || match[ 3 ] === "odd" ) - ); - match[ 5 ] = +( ( match[ 7 ] + match[ 8 ] ) || match[ 3 ] === "odd" ); - - // other types prohibit arguments - } else if ( match[ 3 ] ) { - find.error( match[ 0 ] ); - } - - return match; - }, - - PSEUDO: function( match ) { - var excess, - unquoted = !match[ 6 ] && match[ 2 ]; - - if ( matchExpr.CHILD.test( match[ 0 ] ) ) { - return null; - } - - // Accept quoted arguments as-is - if ( match[ 3 ] ) { - match[ 2 ] = match[ 4 ] || match[ 5 ] || ""; - - // Strip excess characters from unquoted arguments - } else if ( unquoted && rpseudo.test( unquoted ) && - - // Get excess from tokenize (recursively) - ( excess = tokenize( unquoted, true ) ) && - - // advance to the next closing parenthesis - ( excess = unquoted.indexOf( ")", unquoted.length - excess ) - unquoted.length ) ) { - - // excess is a negative index - match[ 0 ] = match[ 0 ].slice( 0, excess ); - match[ 2 ] = unquoted.slice( 0, excess ); - } - - // Return only captures needed by the pseudo filter method (type and argument) - return match.slice( 0, 3 ); - } - }, + preFilter: preFilter, filter: { + ID: function( id ) { + var attrId = unescapeSelector( id ); + return function( elem ) { + return elem.getAttribute( "id" ) === attrId; + }; + }, TAG: function( nodeNameSelector ) { - var expectedNodeName = nodeNameSelector.replace( runescape, funescape ).toLowerCase(); + var expectedNodeName = unescapeSelector( nodeNameSelector ).toLowerCase(); return nodeNameSelector === "*" ? + function() { return true; } : + function( elem ) { return nodeName( elem, expectedNodeName ); }; @@ -1662,7 +1542,7 @@ Expr = jQuery.expr = { ATTR: function( name, operator, check ) { return function( elem ) { - var result = find.attr( elem, name ); + var result = jQuery.attr( elem, name ); if ( result == null ) { return operator === "!="; @@ -1747,7 +1627,8 @@ Expr = jQuery.expr = { if ( forward && useCache ) { // Seek `elem` from a previously-cached index - outerCache = parent[ expando ] || ( parent[ expando ] = {} ); + outerCache = parent[ jQuery.expando ] || + ( parent[ jQuery.expando ] = {} ); cache = outerCache[ type ] || []; nodeIndex = cache[ 0 ] === dirruns && cache[ 1 ]; diff = nodeIndex && cache[ 2 ]; @@ -1769,7 +1650,8 @@ Expr = jQuery.expr = { // Use previously-cached element index if available if ( useCache ) { - outerCache = elem[ expando ] || ( elem[ expando ] = {} ); + outerCache = elem[ jQuery.expando ] || + ( elem[ jQuery.expando ] = {} ); cache = outerCache[ type ] || []; nodeIndex = cache[ 0 ] === dirruns && cache[ 1 ]; diff = nodeIndex; @@ -1790,8 +1672,8 @@ Expr = jQuery.expr = { // Cache the index of each encountered element if ( useCache ) { - outerCache = node[ expando ] || - ( node[ expando ] = {} ); + outerCache = node[ jQuery.expando ] || + ( node[ jQuery.expando ] = {} ); outerCache[ type ] = [ dirruns, diff ]; } @@ -1816,35 +1698,17 @@ Expr = jQuery.expr = { // https://www.w3.org/TR/selectors/#pseudo-classes // Prioritize by case sensitivity in case custom pseudos are added with uppercase letters // Remember that setFilters inherits from pseudos - var args, - fn = Expr.pseudos[ pseudo ] || Expr.setFilters[ pseudo.toLowerCase() ] || - find.error( "unsupported pseudo: " + pseudo ); + var fn = jQuery.expr.pseudos[ pseudo ] || + jQuery.expr.setFilters[ pseudo.toLowerCase() ] || + selectorError( "unsupported pseudo: " + pseudo ); // The user may use createPseudo to indicate that // arguments are needed to create the filter function // just as jQuery does - if ( fn[ expando ] ) { + if ( fn[ jQuery.expando ] ) { return fn( argument ); } - // But maintain support for old signatures - if ( fn.length > 1 ) { - args = [ pseudo, pseudo, "", argument ]; - return Expr.setFilters.hasOwnProperty( pseudo.toLowerCase() ) ? - markFunction( function( seed, matches ) { - var idx, - matched = fn( seed, argument ), - i = matched.length; - while ( i-- ) { - idx = indexOf.call( seed, matched[ i ] ); - seed[ idx ] = !( matches[ idx ] = matched[ i ] ); - } - } ) : - function( elem ) { - return fn( elem, 0, args ); - }; - } - return fn; } }, @@ -1861,7 +1725,7 @@ Expr = jQuery.expr = { results = [], matcher = compile( selector.replace( rtrimCSS, "$1" ) ); - return matcher[ expando ] ? + return matcher[ jQuery.expando ] ? markFunction( function( seed, matches, _context, xml ) { var elem, unmatched = matcher( seed, null, xml, [] ), @@ -1892,7 +1756,7 @@ Expr = jQuery.expr = { } ), contains: markFunction( function( text ) { - text = text.replace( runescape, funescape ); + text = unescapeSelector( text ); return function( elem ) { return ( elem.textContent || jQuery.text( elem ) ).indexOf( text ) > -1; }; @@ -1909,9 +1773,9 @@ Expr = jQuery.expr = { // lang value must be a valid identifier if ( !ridentifier.test( lang || "" ) ) { - find.error( "unsupported lang: " + lang ); + selectorError( "unsupported lang: " + lang ); } - lang = lang.replace( runescape, funescape ).toLowerCase(); + lang = unescapeSelector( lang ).toLowerCase(); return function( elem ) { var elemLang; do { @@ -1938,7 +1802,7 @@ Expr = jQuery.expr = { }, focus: function( elem ) { - return elem === safeActiveElement() && + return elem === document.activeElement && document.hasFocus() && !!( elem.type || elem.href || ~elem.tabIndex ); }, @@ -1961,7 +1825,7 @@ Expr = jQuery.expr = { // Accessing the selectedIndex property // forces the browser to treat the default option as // selected when in an optgroup. - if ( elem.parentNode ) { + if ( isIE && elem.parentNode ) { // eslint-disable-next-line no-unused-expressions elem.parentNode.selectedIndex; } @@ -1985,7 +1849,7 @@ Expr = jQuery.expr = { }, parent: function( elem ) { - return !Expr.pseudos.empty( elem ); + return !jQuery.expr.pseudos.empty( elem ); }, // Element/input types @@ -2003,14 +1867,7 @@ Expr = jQuery.expr = { }, text: function( elem ) { - var attr; - return nodeName( elem, "input" ) && elem.type === "text" && - - // Support: IE <10 only - // New HTML5 attribute values (e.g., "search") appear - // with elem.type === "text" - ( ( attr = elem.getAttribute( "type" ) ) == null || - attr.toLowerCase() === "text" ); + return nodeName( elem, "input" ) && elem.type === "text"; }, // Position-in-collection @@ -2069,102 +1926,20 @@ Expr = jQuery.expr = { } }; -Expr.pseudos.nth = Expr.pseudos.eq; +jQuery.expr.pseudos.nth = jQuery.expr.pseudos.eq; // Add button/input type pseudos for ( i in { radio: true, checkbox: true, file: true, password: true, image: true } ) { - Expr.pseudos[ i ] = createInputPseudo( i ); + jQuery.expr.pseudos[ i ] = createInputPseudo( i ); } for ( i in { submit: true, reset: true } ) { - Expr.pseudos[ i ] = createButtonPseudo( i ); + jQuery.expr.pseudos[ i ] = createButtonPseudo( i ); } // Easy API for creating new setFilters function setFilters() {} -setFilters.prototype = Expr.filters = Expr.pseudos; -Expr.setFilters = new setFilters(); - -function tokenize( selector, parseOnly ) { - var matched, match, tokens, type, - soFar, groups, preFilters, - cached = tokenCache[ selector + " " ]; - - if ( cached ) { - return parseOnly ? 0 : cached.slice( 0 ); - } - - soFar = selector; - groups = []; - preFilters = Expr.preFilter; - - while ( soFar ) { - - // Comma and first run - if ( !matched || ( match = rcomma.exec( soFar ) ) ) { - if ( match ) { - - // Don't consume trailing commas as valid - soFar = soFar.slice( match[ 0 ].length ) || soFar; - } - groups.push( ( tokens = [] ) ); - } - - matched = false; - - // Combinators - if ( ( match = rleadingCombinator.exec( soFar ) ) ) { - matched = match.shift(); - tokens.push( { - value: matched, - - // Cast descendant combinators to space - type: match[ 0 ].replace( rtrimCSS, " " ) - } ); - soFar = soFar.slice( matched.length ); - } - - // Filters - for ( type in Expr.filter ) { - if ( ( match = matchExpr[ type ].exec( soFar ) ) && ( !preFilters[ type ] || - ( match = preFilters[ type ]( match ) ) ) ) { - matched = match.shift(); - tokens.push( { - value: matched, - type: type, - matches: match - } ); - soFar = soFar.slice( matched.length ); - } - } - - if ( !matched ) { - break; - } - } - - // Return the length of the invalid excess - // if we're just parsing - // Otherwise, throw an error or return tokens - if ( parseOnly ) { - return soFar.length; - } - - return soFar ? - find.error( selector ) : - - // Cache the tokens - tokenCache( selector, groups ).slice( 0 ); -} - -function toSelector( tokens ) { - var i = 0, - len = tokens.length, - selector = ""; - for ( ; i < len; i++ ) { - selector += tokens[ i ].value; - } - return selector; -} +setFilters.prototype = jQuery.expr.pseudos; +jQuery.expr.setFilters = new setFilters(); function addCombinator( matcher, combinator, base ) { var dir = combinator.dir, @@ -2202,7 +1977,7 @@ function addCombinator( matcher, combinator, base ) { } else { while ( ( elem = elem[ dir ] ) ) { if ( elem.nodeType === 1 || checkNonElements ) { - outerCache = elem[ expando ] || ( elem[ expando ] = {} ); + outerCache = elem[ jQuery.expando ] || ( elem[ jQuery.expando ] = {} ); if ( skip && nodeName( elem, skip ) ) { elem = elem[ dir ] || elem; @@ -2273,10 +2048,10 @@ function condense( unmatched, map, filter, context, xml ) { } function setMatcher( preFilter, selector, matcher, postFilter, postFinder, postSelector ) { - if ( postFilter && !postFilter[ expando ] ) { + if ( postFilter && !postFilter[ jQuery.expando ] ) { postFilter = setMatcher( postFilter ); } - if ( postFinder && !postFinder[ expando ] ) { + if ( postFinder && !postFinder[ jQuery.expando ] ) { postFinder = setMatcher( postFinder, postSelector ); } return markFunction( function( seed, results, context, xml ) { @@ -2374,8 +2149,8 @@ function setMatcher( preFilter, selector, matcher, postFilter, postFinder, postS function matcherFromTokens( tokens ) { var checkContext, matcher, j, len = tokens.length, - leadingRelative = Expr.relative[ tokens[ 0 ].type ], - implicitRelative = leadingRelative || Expr.relative[ " " ], + leadingRelative = jQuery.expr.relative[ tokens[ 0 ].type ], + implicitRelative = leadingRelative || jQuery.expr.relative[ " " ], i = leadingRelative ? 1 : 0, // The foundational matcher ensures that elements are reachable from top-level context(s) @@ -2387,8 +2162,8 @@ function matcherFromTokens( tokens ) { }, implicitRelative, true ), matchers = [ function( elem, context, xml ) { - // Support: IE 11+, Edge 17 - 18+ - // IE/Edge sometimes throw a "Permission denied" error when strict-comparing + // Support: IE 11+ + // IE sometimes throws a "Permission denied" error when strict-comparing // two documents; shallow comparisons work. // eslint-disable-next-line eqeqeq var ret = ( !leadingRelative && ( xml || context != outermostContext ) ) || ( @@ -2403,18 +2178,18 @@ function matcherFromTokens( tokens ) { } ]; for ( ; i < len; i++ ) { - if ( ( matcher = Expr.relative[ tokens[ i ].type ] ) ) { + if ( ( matcher = jQuery.expr.relative[ tokens[ i ].type ] ) ) { matchers = [ addCombinator( elementMatcher( matchers ), matcher ) ]; } else { - matcher = Expr.filter[ tokens[ i ].type ].apply( null, tokens[ i ].matches ); + matcher = jQuery.expr.filter[ tokens[ i ].type ].apply( null, tokens[ i ].matches ); // Return special upon seeing a positional matcher - if ( matcher[ expando ] ) { + if ( matcher[ jQuery.expando ] ) { // Find the next relative operator (if any) for proper handling j = ++i; for ( ; j < len; j++ ) { - if ( Expr.relative[ tokens[ j ].type ] ) { + if ( jQuery.expr.relative[ tokens[ j ].type ] ) { break; } } @@ -2451,31 +2226,27 @@ function matcherFromGroupMatchers( elementMatchers, setMatchers ) { contextBackup = outermostContext, // We must always have either seed elements or outermost context - elems = seed || byElement && Expr.find.TAG( "*", outermost ), + elems = seed || byElement && jQuery.expr.find.TAG( "*", outermost ), // Use integer dirruns iff this is the outermost matcher - dirrunsUnique = ( dirruns += contextBackup == null ? 1 : Math.random() || 0.1 ), - len = elems.length; + dirrunsUnique = ( dirruns += contextBackup == null ? 1 : Math.random() || 0.1 ); if ( outermost ) { - // Support: IE 11+, Edge 17 - 18+ - // IE/Edge sometimes throw a "Permission denied" error when strict-comparing + // Support: IE 11+ + // IE sometimes throws a "Permission denied" error when strict-comparing // two documents; shallow comparisons work. // eslint-disable-next-line eqeqeq outermostContext = context == document || context || outermost; } // Add elements passing elementMatchers directly to results - // Support: iOS <=7 - 9 only - // Tolerate NodeList properties (IE: "length"; Safari: ) matching - // elements by id. (see trac-14142) - for ( ; i !== len && ( elem = elems[ i ] ) != null; i++ ) { + for ( ; ( elem = elems[ i ] ) != null; i++ ) { if ( byElement && elem ) { j = 0; - // Support: IE 11+, Edge 17 - 18+ - // IE/Edge sometimes throw a "Permission denied" error when strict-comparing + // Support: IE 11+ + // IE sometimes throws a "Permission denied" error when strict-comparing // two documents; shallow comparisons work. // eslint-disable-next-line eqeqeq if ( !context && elem.ownerDocument != document ) { @@ -2580,7 +2351,7 @@ function compile( selector, match /* Internal Use Only */ ) { i = match.length; while ( i-- ) { cached = matcherFromTokens( match[ i ] ); - if ( cached[ expando ] ) { + if ( cached[ jQuery.expando ] ) { setMatchers.push( cached ); } else { elementMatchers.push( cached ); @@ -2620,10 +2391,11 @@ function select( selector, context, results, seed ) { // Reduce context if the leading compound selector is an ID tokens = match[ 0 ] = match[ 0 ].slice( 0 ); if ( tokens.length > 2 && ( token = tokens[ 0 ] ).type === "ID" && - context.nodeType === 9 && documentIsHTML && Expr.relative[ tokens[ 1 ].type ] ) { + context.nodeType === 9 && documentIsHTML && + jQuery.expr.relative[ tokens[ 1 ].type ] ) { - context = ( Expr.find.ID( - token.matches[ 0 ].replace( runescape, funescape ), + context = ( jQuery.expr.find.ID( + unescapeSelector( token.matches[ 0 ] ), context ) || [] )[ 0 ]; if ( !context ) { @@ -2643,14 +2415,14 @@ function select( selector, context, results, seed ) { token = tokens[ i ]; // Abort if we hit a combinator - if ( Expr.relative[ ( type = token.type ) ] ) { + if ( jQuery.expr.relative[ ( type = token.type ) ] ) { break; } - if ( ( find = Expr.find[ type ] ) ) { + if ( ( find = jQuery.expr.find[ type ] ) ) { // Search, expanding context for leading sibling combinators if ( ( seed = find( - token.matches[ 0 ].replace( runescape, funescape ), + unescapeSelector( token.matches[ 0 ] ), rsibling.test( tokens[ 0 ].type ) && testContext( context.parentNode ) || context ) ) ) { @@ -2681,29 +2453,11 @@ function select( selector, context, results, seed ) { return results; } -// One-time assignments - -// Support: Android <=4.0 - 4.1+ -// Sort stability -support.sortStable = expando.split( "" ).sort( sortOrder ).join( "" ) === expando; - // Initialize against the default document setDocument(); -// Support: Android <=4.0 - 4.1+ -// Detached nodes confoundingly follow *each other* -support.sortDetached = assert( function( el ) { - - // Should return 1, but returns 4 (following) - return el.compareDocumentPosition( document.createElement( "fieldset" ) ) & 1; -} ); - jQuery.find = find; -// Deprecated -jQuery.expr[ ":" ] = jQuery.expr.pseudos; -jQuery.unique = jQuery.uniqueSort; - // These have always been private, but they used to be documented as part of // Sizzle so let's maintain them for now for backwards compatibility purposes. find.compile = compile; @@ -2711,19 +2465,7 @@ find.select = select; find.setDocument = setDocument; find.tokenize = tokenize; -find.escape = jQuery.escapeSelector; -find.getText = jQuery.text; -find.isXML = jQuery.isXMLDoc; -find.selectors = jQuery.expr; -find.support = jQuery.support; -find.uniqueSort = jQuery.uniqueSort; - - /* eslint-enable */ - -} )(); - - -var dir = function( elem, dir, until ) { +function dir( elem, dir, until ) { var matched = [], truncate = until !== undefined; @@ -2736,10 +2478,9 @@ var dir = function( elem, dir, until ) { } } return matched; -}; +} - -var siblings = function( n, elem ) { +function siblings( n, elem ) { var matched = []; for ( ; n; n = n.nextSibling ) { @@ -2749,18 +2490,23 @@ var siblings = function( n, elem ) { } return matched; -}; - +} var rneedsContext = jQuery.expr.match.needsContext; -var rsingleTag = ( /^<([a-z][^\/\0>:\x20\t\r\n\f]*)[\x20\t\r\n\f]*\/?>(?:<\/\1>|)$/i ); - +// rsingleTag matches a string consisting of a single HTML element with no attributes +// and captures the element's name +var rsingleTag = /^<([a-z][^\/\0>:\x20\t\r\n\f]*)[\x20\t\r\n\f]*\/?>(?:<\/\1>|)$/i; +function isObviousHtml( input ) { + return input[ 0 ] === "<" && + input[ input.length - 1 ] === ">" && + input.length >= 3; +} // Implement the identical functionality for filter and not function winnow( elements, qualifier, not ) { - if ( isFunction( qualifier ) ) { + if ( typeof qualifier === "function" ) { return jQuery.grep( elements, function( elem, i ) { return !!qualifier.call( elem, i, elem ) !== not; } ); @@ -2844,10 +2590,8 @@ jQuery.fn.extend( { } } ); - // Initialize a jQuery object - // A central reference to the root jQuery(document) var rootjQuery, @@ -2857,7 +2601,7 @@ var rootjQuery, // Shortcut simple #id case for speed rquickExpr = /^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]+))$/, - init = jQuery.fn.init = function( selector, context, root ) { + init = jQuery.fn.init = function( selector, context ) { var match, elem; // HANDLE: $(""), $(null), $(undefined), $(false) @@ -2865,24 +2609,41 @@ var rootjQuery, return this; } - // Method init() accepts an alternate rootjQuery - // so migrate can support jQuery.sub (gh-2101) - root = root || rootjQuery; + // HANDLE: $(DOMElement) + if ( selector.nodeType ) { + this[ 0 ] = selector; + this.length = 1; + return this; - // Handle HTML strings - if ( typeof selector === "string" ) { - if ( selector[ 0 ] === "<" && - selector[ selector.length - 1 ] === ">" && - selector.length >= 3 ) { + // HANDLE: $(function) + // Shortcut for document ready + } else if ( typeof selector === "function" ) { + return rootjQuery.ready !== undefined ? + rootjQuery.ready( selector ) : - // Assume that strings that start and end with <> are HTML and skip the regex check + // Execute immediately if ready is not present + selector( jQuery ); + + } else { + + // Handle obvious HTML strings + match = selector + ""; + if ( isObviousHtml( match ) ) { + + // Assume that strings that start and end with <> are HTML and skip + // the regex check. This also handles browser-supported HTML wrappers + // like TrustedHTML. match = [ null, selector, null ]; - } else { + // Handle HTML strings or selectors + } else if ( typeof selector === "string" ) { match = rquickExpr.exec( selector ); + } else { + return jQuery.makeArray( selector, this ); } // Match html or make sure no context is specified for #id + // Note: match[1] may be a string or a TrustedHTML wrapper if ( match && ( match[ 1 ] || !context ) ) { // HANDLE: $(html) -> $(array) @@ -2893,7 +2654,7 @@ var rootjQuery, // Intentionally let the error be thrown if parseHTML is not present jQuery.merge( this, jQuery.parseHTML( match[ 1 ], - context && context.nodeType ? context.ownerDocument || context : document, + context && context.nodeType ? context.ownerDocument || context : document$1, true ) ); @@ -2902,7 +2663,7 @@ var rootjQuery, for ( match in context ) { // Properties of context are called as methods if possible - if ( isFunction( this[ match ] ) ) { + if ( typeof this[ match ] === "function" ) { this[ match ]( context[ match ] ); // ...and otherwise set as attributes @@ -2916,7 +2677,7 @@ var rootjQuery, // HANDLE: $(#id) } else { - elem = document.getElementById( match[ 2 ] ); + elem = document$1.getElementById( match[ 2 ] ); if ( elem ) { @@ -2927,41 +2688,24 @@ var rootjQuery, return this; } - // HANDLE: $(expr, $(...)) + // HANDLE: $(expr) & $(expr, $(...)) } else if ( !context || context.jquery ) { - return ( context || root ).find( selector ); + return ( context || rootjQuery ).find( selector ); // HANDLE: $(expr, context) // (which is just equivalent to: $(context).find(expr) } else { return this.constructor( context ).find( selector ); } - - // HANDLE: $(DOMElement) - } else if ( selector.nodeType ) { - this[ 0 ] = selector; - this.length = 1; - return this; - - // HANDLE: $(function) - // Shortcut for document ready - } else if ( isFunction( selector ) ) { - return root.ready !== undefined ? - root.ready( selector ) : - - // Execute immediately if ready is not present - selector( jQuery ); } - return jQuery.makeArray( selector, this ); }; // Give the init function the jQuery prototype for later instantiation init.prototype = jQuery.fn; // Initialize central reference -rootjQuery = jQuery( document ); - +rootjQuery = jQuery( document$1 ); var rparentsprev = /^(?:parents|prev(?:Until|All))/, @@ -3105,7 +2849,7 @@ jQuery.each( { return elem.contentDocument; } - // Support: IE 9 - 11 only, iOS 7 only, Android Browser <=4.3 only + // Support: IE 9 - 11+ // Treat the template element as a regular one in browsers that // don't support it. if ( nodeName( elem, "template" ) ) { @@ -3142,818 +2886,24 @@ jQuery.each( { return this.pushStack( matched ); }; } ); -var rnothtmlwhite = ( /[^\x20\t\r\n\f]+/g ); - - - -// Convert String-formatted options into Object-formatted ones -function createOptions( options ) { - var object = {}; - jQuery.each( options.match( rnothtmlwhite ) || [], function( _, flag ) { - object[ flag ] = true; - } ); - return object; -} - -/* - * Create a callback list using the following parameters: - * - * options: an optional list of space-separated options that will change how - * the callback list behaves or a more traditional option object - * - * By default a callback list will act like an event callback list and can be - * "fired" multiple times. - * - * Possible options: - * - * once: will ensure the callback list can only be fired once (like a Deferred) - * - * memory: will keep track of previous values and will call any callback added - * after the list has been fired right away with the latest "memorized" - * values (like a Deferred) - * - * unique: will ensure a callback can only be added once (no duplicate in the list) - * - * stopOnFalse: interrupt callings when a callback returns false - * - */ -jQuery.Callbacks = function( options ) { - - // Convert options from String-formatted to Object-formatted if needed - // (we check in cache first) - options = typeof options === "string" ? - createOptions( options ) : - jQuery.extend( {}, options ); - - var // Flag to know if list is currently firing - firing, - - // Last fire value for non-forgettable lists - memory, - - // Flag to know if list was already fired - fired, - - // Flag to prevent firing - locked, - - // Actual callback list - list = [], - - // Queue of execution data for repeatable lists - queue = [], - - // Index of currently firing callback (modified by add/remove as needed) - firingIndex = -1, - - // Fire callbacks - fire = function() { - - // Enforce single-firing - locked = locked || options.once; - - // Execute callbacks for all pending executions, - // respecting firingIndex overrides and runtime changes - fired = firing = true; - for ( ; queue.length; firingIndex = -1 ) { - memory = queue.shift(); - while ( ++firingIndex < list.length ) { - - // Run callback and check for early termination - if ( list[ firingIndex ].apply( memory[ 0 ], memory[ 1 ] ) === false && - options.stopOnFalse ) { - - // Jump to end and forget the data so .add doesn't re-fire - firingIndex = list.length; - memory = false; - } - } - } - - // Forget the data if we're done with it - if ( !options.memory ) { - memory = false; - } - - firing = false; - - // Clean up if we're done firing for good - if ( locked ) { - - // Keep an empty list if we have data for future add calls - if ( memory ) { - list = []; - - // Otherwise, this object is spent - } else { - list = ""; - } - } - }, - - // Actual Callbacks object - self = { - - // Add a callback or a collection of callbacks to the list - add: function() { - if ( list ) { - - // If we have memory from a past run, we should fire after adding - if ( memory && !firing ) { - firingIndex = list.length - 1; - queue.push( memory ); - } - - ( function add( args ) { - jQuery.each( args, function( _, arg ) { - if ( isFunction( arg ) ) { - if ( !options.unique || !self.has( arg ) ) { - list.push( arg ); - } - } else if ( arg && arg.length && toType( arg ) !== "string" ) { - - // Inspect recursively - add( arg ); - } - } ); - } )( arguments ); - - if ( memory && !firing ) { - fire(); - } - } - return this; - }, - - // Remove a callback from the list - remove: function() { - jQuery.each( arguments, function( _, arg ) { - var index; - while ( ( index = jQuery.inArray( arg, list, index ) ) > -1 ) { - list.splice( index, 1 ); - - // Handle firing indexes - if ( index <= firingIndex ) { - firingIndex--; - } - } - } ); - return this; - }, - - // Check if a given callback is in the list. - // If no argument is given, return whether or not list has callbacks attached. - has: function( fn ) { - return fn ? - jQuery.inArray( fn, list ) > -1 : - list.length > 0; - }, - - // Remove all callbacks from the list - empty: function() { - if ( list ) { - list = []; - } - return this; - }, - - // Disable .fire and .add - // Abort any current/pending executions - // Clear all callbacks and values - disable: function() { - locked = queue = []; - list = memory = ""; - return this; - }, - disabled: function() { - return !list; - }, - - // Disable .fire - // Also disable .add unless we have memory (since it would have no effect) - // Abort any pending executions - lock: function() { - locked = queue = []; - if ( !memory && !firing ) { - list = memory = ""; - } - return this; - }, - locked: function() { - return !!locked; - }, - - // Call all callbacks with the given context and arguments - fireWith: function( context, args ) { - if ( !locked ) { - args = args || []; - args = [ context, args.slice ? args.slice() : args ]; - queue.push( args ); - if ( !firing ) { - fire(); - } - } - return this; - }, - - // Call all the callbacks with the given arguments - fire: function() { - self.fireWith( this, arguments ); - return this; - }, - - // To know if the callbacks have already been called at least once - fired: function() { - return !!fired; - } - }; - - return self; -}; - - -function Identity( v ) { - return v; -} -function Thrower( ex ) { - throw ex; -} - -function adoptValue( value, resolve, reject, noValue ) { - var method; - - try { - - // Check for promise aspect first to privilege synchronous behavior - if ( value && isFunction( ( method = value.promise ) ) ) { - method.call( value ).done( resolve ).fail( reject ); - - // Other thenables - } else if ( value && isFunction( ( method = value.then ) ) ) { - method.call( value, resolve, reject ); - - // Other non-thenables - } else { - - // Control `resolve` arguments by letting Array#slice cast boolean `noValue` to integer: - // * false: [ value ].slice( 0 ) => resolve( value ) - // * true: [ value ].slice( 1 ) => resolve() - resolve.apply( undefined, [ value ].slice( noValue ) ); - } - - // For Promises/A+, convert exceptions into rejections - // Since jQuery.when doesn't unwrap thenables, we can skip the extra checks appearing in - // Deferred#then to conditionally suppress rejection. - } catch ( value ) { - - // Support: Android 4.0 only - // Strict mode functions invoked without .call/.apply get global-object context - reject.apply( undefined, [ value ] ); - } -} - -jQuery.extend( { - - Deferred: function( func ) { - var tuples = [ - - // action, add listener, callbacks, - // ... .then handlers, argument index, [final state] - [ "notify", "progress", jQuery.Callbacks( "memory" ), - jQuery.Callbacks( "memory" ), 2 ], - [ "resolve", "done", jQuery.Callbacks( "once memory" ), - jQuery.Callbacks( "once memory" ), 0, "resolved" ], - [ "reject", "fail", jQuery.Callbacks( "once memory" ), - jQuery.Callbacks( "once memory" ), 1, "rejected" ] - ], - state = "pending", - promise = { - state: function() { - return state; - }, - always: function() { - deferred.done( arguments ).fail( arguments ); - return this; - }, - "catch": function( fn ) { - return promise.then( null, fn ); - }, - - // Keep pipe for back-compat - pipe: function( /* fnDone, fnFail, fnProgress */ ) { - var fns = arguments; - - return jQuery.Deferred( function( newDefer ) { - jQuery.each( tuples, function( _i, tuple ) { - - // Map tuples (progress, done, fail) to arguments (done, fail, progress) - var fn = isFunction( fns[ tuple[ 4 ] ] ) && fns[ tuple[ 4 ] ]; - - // deferred.progress(function() { bind to newDefer or newDefer.notify }) - // deferred.done(function() { bind to newDefer or newDefer.resolve }) - // deferred.fail(function() { bind to newDefer or newDefer.reject }) - deferred[ tuple[ 1 ] ]( function() { - var returned = fn && fn.apply( this, arguments ); - if ( returned && isFunction( returned.promise ) ) { - returned.promise() - .progress( newDefer.notify ) - .done( newDefer.resolve ) - .fail( newDefer.reject ); - } else { - newDefer[ tuple[ 0 ] + "With" ]( - this, - fn ? [ returned ] : arguments - ); - } - } ); - } ); - fns = null; - } ).promise(); - }, - then: function( onFulfilled, onRejected, onProgress ) { - var maxDepth = 0; - function resolve( depth, deferred, handler, special ) { - return function() { - var that = this, - args = arguments, - mightThrow = function() { - var returned, then; - - // Support: Promises/A+ section 2.3.3.3.3 - // https://promisesaplus.com/#point-59 - // Ignore double-resolution attempts - if ( depth < maxDepth ) { - return; - } - - returned = handler.apply( that, args ); - - // Support: Promises/A+ section 2.3.1 - // https://promisesaplus.com/#point-48 - if ( returned === deferred.promise() ) { - throw new TypeError( "Thenable self-resolution" ); - } - - // Support: Promises/A+ sections 2.3.3.1, 3.5 - // https://promisesaplus.com/#point-54 - // https://promisesaplus.com/#point-75 - // Retrieve `then` only once - then = returned && - - // Support: Promises/A+ section 2.3.4 - // https://promisesaplus.com/#point-64 - // Only check objects and functions for thenability - ( typeof returned === "object" || - typeof returned === "function" ) && - returned.then; - - // Handle a returned thenable - if ( isFunction( then ) ) { - - // Special processors (notify) just wait for resolution - if ( special ) { - then.call( - returned, - resolve( maxDepth, deferred, Identity, special ), - resolve( maxDepth, deferred, Thrower, special ) - ); - - // Normal processors (resolve) also hook into progress - } else { - - // ...and disregard older resolution values - maxDepth++; - - then.call( - returned, - resolve( maxDepth, deferred, Identity, special ), - resolve( maxDepth, deferred, Thrower, special ), - resolve( maxDepth, deferred, Identity, - deferred.notifyWith ) - ); - } - - // Handle all other returned values - } else { - - // Only substitute handlers pass on context - // and multiple values (non-spec behavior) - if ( handler !== Identity ) { - that = undefined; - args = [ returned ]; - } - - // Process the value(s) - // Default process is resolve - ( special || deferred.resolveWith )( that, args ); - } - }, - - // Only normal processors (resolve) catch and reject exceptions - process = special ? - mightThrow : - function() { - try { - mightThrow(); - } catch ( e ) { - - if ( jQuery.Deferred.exceptionHook ) { - jQuery.Deferred.exceptionHook( e, - process.error ); - } - - // Support: Promises/A+ section 2.3.3.3.4.1 - // https://promisesaplus.com/#point-61 - // Ignore post-resolution exceptions - if ( depth + 1 >= maxDepth ) { - - // Only substitute handlers pass on context - // and multiple values (non-spec behavior) - if ( handler !== Thrower ) { - that = undefined; - args = [ e ]; - } - - deferred.rejectWith( that, args ); - } - } - }; - - // Support: Promises/A+ section 2.3.3.3.1 - // https://promisesaplus.com/#point-57 - // Re-resolve promises immediately to dodge false rejection from - // subsequent errors - if ( depth ) { - process(); - } else { - - // Call an optional hook to record the error, in case of exception - // since it's otherwise lost when execution goes async - if ( jQuery.Deferred.getErrorHook ) { - process.error = jQuery.Deferred.getErrorHook(); - - // The deprecated alias of the above. While the name suggests - // returning the stack, not an error instance, jQuery just passes - // it directly to `console.warn` so both will work; an instance - // just better cooperates with source maps. - } else if ( jQuery.Deferred.getStackHook ) { - process.error = jQuery.Deferred.getStackHook(); - } - window.setTimeout( process ); - } - }; - } - - return jQuery.Deferred( function( newDefer ) { - - // progress_handlers.add( ... ) - tuples[ 0 ][ 3 ].add( - resolve( - 0, - newDefer, - isFunction( onProgress ) ? - onProgress : - Identity, - newDefer.notifyWith - ) - ); - - // fulfilled_handlers.add( ... ) - tuples[ 1 ][ 3 ].add( - resolve( - 0, - newDefer, - isFunction( onFulfilled ) ? - onFulfilled : - Identity - ) - ); - - // rejected_handlers.add( ... ) - tuples[ 2 ][ 3 ].add( - resolve( - 0, - newDefer, - isFunction( onRejected ) ? - onRejected : - Thrower - ) - ); - } ).promise(); - }, - - // Get a promise for this deferred - // If obj is provided, the promise aspect is added to the object - promise: function( obj ) { - return obj != null ? jQuery.extend( obj, promise ) : promise; - } - }, - deferred = {}; - - // Add list-specific methods - jQuery.each( tuples, function( i, tuple ) { - var list = tuple[ 2 ], - stateString = tuple[ 5 ]; - - // promise.progress = list.add - // promise.done = list.add - // promise.fail = list.add - promise[ tuple[ 1 ] ] = list.add; - - // Handle state - if ( stateString ) { - list.add( - function() { - - // state = "resolved" (i.e., fulfilled) - // state = "rejected" - state = stateString; - }, - - // rejected_callbacks.disable - // fulfilled_callbacks.disable - tuples[ 3 - i ][ 2 ].disable, - - // rejected_handlers.disable - // fulfilled_handlers.disable - tuples[ 3 - i ][ 3 ].disable, - - // progress_callbacks.lock - tuples[ 0 ][ 2 ].lock, - - // progress_handlers.lock - tuples[ 0 ][ 3 ].lock - ); - } - - // progress_handlers.fire - // fulfilled_handlers.fire - // rejected_handlers.fire - list.add( tuple[ 3 ].fire ); - - // deferred.notify = function() { deferred.notifyWith(...) } - // deferred.resolve = function() { deferred.resolveWith(...) } - // deferred.reject = function() { deferred.rejectWith(...) } - deferred[ tuple[ 0 ] ] = function() { - deferred[ tuple[ 0 ] + "With" ]( this === deferred ? undefined : this, arguments ); - return this; - }; - - // deferred.notifyWith = list.fireWith - // deferred.resolveWith = list.fireWith - // deferred.rejectWith = list.fireWith - deferred[ tuple[ 0 ] + "With" ] = list.fireWith; - } ); - - // Make the deferred a promise - promise.promise( deferred ); - - // Call given func if any - if ( func ) { - func.call( deferred, deferred ); - } - - // All done! - return deferred; - }, - - // Deferred helper - when: function( singleValue ) { - var - - // count of uncompleted subordinates - remaining = arguments.length, - - // count of unprocessed arguments - i = remaining, - - // subordinate fulfillment data - resolveContexts = Array( i ), - resolveValues = slice.call( arguments ), - - // the primary Deferred - primary = jQuery.Deferred(), - - // subordinate callback factory - updateFunc = function( i ) { - return function( value ) { - resolveContexts[ i ] = this; - resolveValues[ i ] = arguments.length > 1 ? slice.call( arguments ) : value; - if ( !( --remaining ) ) { - primary.resolveWith( resolveContexts, resolveValues ); - } - }; - }; - - // Single- and empty arguments are adopted like Promise.resolve - if ( remaining <= 1 ) { - adoptValue( singleValue, primary.done( updateFunc( i ) ).resolve, primary.reject, - !remaining ); - - // Use .then() to unwrap secondary thenables (cf. gh-3000) - if ( primary.state() === "pending" || - isFunction( resolveValues[ i ] && resolveValues[ i ].then ) ) { - - return primary.then(); - } - } - - // Multiple arguments are aggregated like Promise.all array elements - while ( i-- ) { - adoptValue( resolveValues[ i ], updateFunc( i ), primary.reject ); - } - - return primary.promise(); - } -} ); - - -// These usually indicate a programmer mistake during development, -// warn about them ASAP rather than swallowing them by default. -var rerrorNames = /^(Eval|Internal|Range|Reference|Syntax|Type|URI)Error$/; - -// If `jQuery.Deferred.getErrorHook` is defined, `asyncError` is an error -// captured before the async barrier to get the original error cause -// which may otherwise be hidden. -jQuery.Deferred.exceptionHook = function( error, asyncError ) { - - // Support: IE 8 - 9 only - // Console exists when dev tools are open, which can happen at any time - if ( window.console && window.console.warn && error && rerrorNames.test( error.name ) ) { - window.console.warn( "jQuery.Deferred exception: " + error.message, - error.stack, asyncError ); - } -}; - - - - -jQuery.readyException = function( error ) { - window.setTimeout( function() { - throw error; - } ); -}; - - - - -// The deferred used on DOM ready -var readyList = jQuery.Deferred(); - -jQuery.fn.ready = function( fn ) { - - readyList - .then( fn ) - - // Wrap jQuery.readyException in a function so that the lookup - // happens at the time of error handling instead of callback - // registration. - .catch( function( error ) { - jQuery.readyException( error ); - } ); - - return this; -}; - -jQuery.extend( { - - // Is the DOM ready to be used? Set to true once it occurs. - isReady: false, - - // A counter to track how many items to wait for before - // the ready event fires. See trac-6781 - readyWait: 1, - - // Handle when the DOM is ready - ready: function( wait ) { - - // Abort if there are pending holds or we're already ready - if ( wait === true ? --jQuery.readyWait : jQuery.isReady ) { - return; - } - - // Remember that the DOM is ready - jQuery.isReady = true; - - // If a normal DOM Ready event fired, decrement, and wait if need be - if ( wait !== true && --jQuery.readyWait > 0 ) { - return; - } - - // If there are functions bound, to execute - readyList.resolveWith( document, [ jQuery ] ); - } -} ); - -jQuery.ready.then = readyList.then; - -// The ready event handler and self cleanup method -function completed() { - document.removeEventListener( "DOMContentLoaded", completed ); - window.removeEventListener( "load", completed ); - jQuery.ready(); -} - -// Catch cases where $(document).ready() is called -// after the browser event has already occurred. -// Support: IE <=9 - 10 only -// Older IE sometimes signals "interactive" too soon -if ( document.readyState === "complete" || - ( document.readyState !== "loading" && !document.documentElement.doScroll ) ) { - - // Handle it asynchronously to allow scripts the opportunity to delay ready - window.setTimeout( jQuery.ready ); - -} else { - - // Use the handy event callback - document.addEventListener( "DOMContentLoaded", completed ); - - // A fallback to window.onload, that will always work - window.addEventListener( "load", completed ); -} - - - - -// Multifunctional method to get and set values of a collection -// The value/s can optionally be executed if it's a function -var access = function( elems, fn, key, value, chainable, emptyGet, raw ) { - var i = 0, - len = elems.length, - bulk = key == null; - - // Sets many values - if ( toType( key ) === "object" ) { - chainable = true; - for ( i in key ) { - access( elems, fn, i, key[ i ], true, emptyGet, raw ); - } - - // Sets one value - } else if ( value !== undefined ) { - chainable = true; - - if ( !isFunction( value ) ) { - raw = true; - } - - if ( bulk ) { - - // Bulk operations run against the entire set - if ( raw ) { - fn.call( elems, value ); - fn = null; - - // ...except when executing function values - } else { - bulk = fn; - fn = function( elem, _key, value ) { - return bulk.call( jQuery( elem ), value ); - }; - } - } - - if ( fn ) { - for ( ; i < len; i++ ) { - fn( - elems[ i ], key, raw ? - value : - value.call( elems[ i ], i, fn( elems[ i ], key ) ) - ); - } - } - } - - if ( chainable ) { - return elems; - } - - // Gets - if ( bulk ) { - return fn.call( elems ); - } - - return len ? fn( elems[ 0 ], key ) : emptyGet; -}; - // Matches dashed string for camelizing -var rmsPrefix = /^-ms-/, - rdashAlpha = /-([a-z])/g; +var rdashAlpha = /-([a-z])/g; // Used by camelCase as callback to replace() function fcamelCase( _all, letter ) { return letter.toUpperCase(); } -// Convert dashed to camelCase; used by the css and data modules -// Support: IE <=9 - 11, Edge 12 - 15 -// Microsoft forgot to hump their vendor prefix (trac-9572) +// Convert dashed to camelCase function camelCase( string ) { - return string.replace( rmsPrefix, "ms-" ).replace( rdashAlpha, fcamelCase ); + return string.replace( rdashAlpha, fcamelCase ); } -var acceptData = function( owner ) { + +/** + * Determines whether an object can have data + */ +function acceptData( owner ) { // Accepts only: // - Node @@ -3962,10 +2912,7 @@ var acceptData = function( owner ) { // - Object // - Any return owner.nodeType === 1 || owner.nodeType === 9 || !( +owner.nodeType ); -}; - - - +} function Data() { this.expando = jQuery.expando + Data.uid++; @@ -3982,7 +2929,7 @@ Data.prototype = { // If not, create one if ( !value ) { - value = {}; + value = Object.create( null ); // We can accept data for non-element nodes in modern browsers, // but we should not, see trac-8335. @@ -4025,7 +2972,7 @@ Data.prototype = { cache[ camelCase( prop ) ] = data[ prop ]; } } - return cache; + return value; }, get: function( owner, key ) { return key === undefined ? @@ -4101,7 +3048,7 @@ Data.prototype = { // Remove the expando if there's no more data if ( key === undefined || jQuery.isEmptyObject( cache ) ) { - // Support: Chrome <=35 - 45 + // Support: Chrome <=35 - 45+ // Webkit & Blink performance suffers when deleting properties // from DOM nodes, so set to undefined instead // https://bugs.chromium.org/p/chromium/issues/detail?id=378607 (bug restricted) @@ -4117,12 +3064,11 @@ Data.prototype = { return cache !== undefined && !jQuery.isEmptyObject( cache ); } }; + var dataPriv = new Data(); var dataUser = new Data(); - - // Implementation Summary // // 1. Enforce API surface and semantic compatibility with 1.9.x branch @@ -4223,7 +3169,7 @@ jQuery.fn.extend( { i = attrs.length; while ( i-- ) { - // Support: IE 11 only + // Support: IE 11+ // The attrs elements can be null (trac-14894) if ( attrs[ i ] ) { name = attrs[ i ].name; @@ -4291,541 +3237,459 @@ jQuery.fn.extend( { } } ); +var rfocusable = /^(?:input|select|textarea|button)$/i, + rclickable = /^(?:a|area)$/i; + +jQuery.fn.extend( { + prop: function( name, value ) { + return access( this, jQuery.prop, name, value, arguments.length > 1 ); + }, + + removeProp: function( name ) { + return this.each( function() { + delete this[ jQuery.propFix[ name ] || name ]; + } ); + } +} ); jQuery.extend( { - queue: function( elem, type, data ) { - var queue; + prop: function( elem, name, value ) { + var ret, hooks, + nType = elem.nodeType; - if ( elem ) { - type = ( type || "fx" ) + "queue"; - queue = dataPriv.get( elem, type ); + // Don't get/set properties on text, comment and attribute nodes + if ( nType === 3 || nType === 8 || nType === 2 ) { + return; + } - // Speed up dequeue by getting out quickly if this is just a lookup - if ( data ) { - if ( !queue || Array.isArray( data ) ) { - queue = dataPriv.access( elem, type, jQuery.makeArray( data ) ); - } else { - queue.push( data ); + if ( nType !== 1 || !jQuery.isXMLDoc( elem ) ) { + + // Fix name and attach hooks + name = jQuery.propFix[ name ] || name; + hooks = jQuery.propHooks[ name ]; + } + + if ( value !== undefined ) { + if ( hooks && "set" in hooks && + ( ret = hooks.set( elem, value, name ) ) !== undefined ) { + return ret; + } + + return ( elem[ name ] = value ); + } + + if ( hooks && "get" in hooks && ( ret = hooks.get( elem, name ) ) !== null ) { + return ret; + } + + return elem[ name ]; + }, + + propHooks: { + tabIndex: { + get: function( elem ) { + + // Support: IE <=9 - 11+ + // elem.tabIndex doesn't always return the + // correct value when it hasn't been explicitly set + // Use proper attribute retrieval (trac-12072) + var tabindex = elem.getAttribute( "tabindex" ); + + if ( tabindex ) { + return parseInt( tabindex, 10 ); } + + if ( + rfocusable.test( elem.nodeName ) || + + // href-less anchor's `tabIndex` property value is `0` and + // the `tabindex` attribute value: `null`. We want `-1`. + rclickable.test( elem.nodeName ) && elem.href + ) { + return 0; + } + + return -1; } - return queue || []; } }, - dequeue: function( elem, type ) { - type = type || "fx"; - - var queue = jQuery.queue( elem, type ), - startLength = queue.length, - fn = queue.shift(), - hooks = jQuery._queueHooks( elem, type ), - next = function() { - jQuery.dequeue( elem, type ); - }; - - // If the fx queue is dequeued, always remove the progress sentinel - if ( fn === "inprogress" ) { - fn = queue.shift(); - startLength--; - } - - if ( fn ) { - - // Add a progress sentinel to prevent the fx queue from being - // automatically dequeued - if ( type === "fx" ) { - queue.unshift( "inprogress" ); - } - - // Clear up the last queue stop function - delete hooks.stop; - fn.call( elem, next, hooks ); - } - - if ( !startLength && hooks ) { - hooks.empty.fire(); - } - }, - - // Not public - generate a queueHooks object, or return the current one - _queueHooks: function( elem, type ) { - var key = type + "queueHooks"; - return dataPriv.get( elem, key ) || dataPriv.access( elem, key, { - empty: jQuery.Callbacks( "once memory" ).add( function() { - dataPriv.remove( elem, [ type + "queue", key ] ); - } ) - } ); + propFix: { + "for": "htmlFor", + "class": "className" } } ); +// Support: IE <=11+ +// Accessing the selectedIndex property forces the browser to respect +// setting selected on the option. The getter ensures a default option +// is selected when in an optgroup. ESLint rule "no-unused-expressions" +// is disabled for this code since it considers such accessions noop. +if ( isIE ) { + jQuery.propHooks.selected = { + get: function( elem ) { + + var parent = elem.parentNode; + if ( parent && parent.parentNode ) { + // eslint-disable-next-line no-unused-expressions + parent.parentNode.selectedIndex; + } + return null; + }, + set: function( elem ) { + + + var parent = elem.parentNode; + if ( parent ) { + // eslint-disable-next-line no-unused-expressions + parent.selectedIndex; + + if ( parent.parentNode ) { + // eslint-disable-next-line no-unused-expressions + parent.parentNode.selectedIndex; + } + } + } + }; +} + +jQuery.each( [ + "tabIndex", + "readOnly", + "maxLength", + "cellSpacing", + "cellPadding", + "rowSpan", + "colSpan", + "useMap", + "frameBorder", + "contentEditable" +], function() { + jQuery.propFix[ this.toLowerCase() ] = this; +} ); + +// Strip and collapse whitespace according to HTML spec +// https://infra.spec.whatwg.org/#strip-and-collapse-ascii-whitespace +function stripAndCollapse( value ) { + var tokens = value.match( rnothtmlwhite ) || []; + return tokens.join( " " ); +} + +function getClass( elem ) { + return elem.getAttribute && elem.getAttribute( "class" ) || ""; +} + +function classesToArray( value ) { + if ( Array.isArray( value ) ) { + return value; + } + if ( typeof value === "string" ) { + return value.match( rnothtmlwhite ) || []; + } + return []; +} + jQuery.fn.extend( { - queue: function( type, data ) { - var setter = 2; + addClass: function( value ) { + var classNames, cur, curValue, className, i, finalValue; - if ( typeof type !== "string" ) { - data = type; - type = "fx"; - setter--; + if ( typeof value === "function" ) { + return this.each( function( j ) { + jQuery( this ).addClass( value.call( this, j, getClass( this ) ) ); + } ); } - if ( arguments.length < setter ) { - return jQuery.queue( this[ 0 ], type ); - } + classNames = classesToArray( value ); - return data === undefined ? - this : - this.each( function() { - var queue = jQuery.queue( this, type, data ); + if ( classNames.length ) { + return this.each( function() { + curValue = getClass( this ); + cur = this.nodeType === 1 && ( " " + stripAndCollapse( curValue ) + " " ); - // Ensure a hooks for this queue - jQuery._queueHooks( this, type ); + if ( cur ) { + for ( i = 0; i < classNames.length; i++ ) { + className = classNames[ i ]; + if ( cur.indexOf( " " + className + " " ) < 0 ) { + cur += className + " "; + } + } - if ( type === "fx" && queue[ 0 ] !== "inprogress" ) { - jQuery.dequeue( this, type ); + // Only assign if different to avoid unneeded rendering. + finalValue = stripAndCollapse( cur ); + if ( curValue !== finalValue ) { + this.setAttribute( "class", finalValue ); + } } } ); - }, - dequeue: function( type ) { - return this.each( function() { - jQuery.dequeue( this, type ); - } ); - }, - clearQueue: function( type ) { - return this.queue( type || "fx", [] ); - }, - - // Get a promise resolved when queues of a certain type - // are emptied (fx is the type by default) - promise: function( type, obj ) { - var tmp, - count = 1, - defer = jQuery.Deferred(), - elements = this, - i = this.length, - resolve = function() { - if ( !( --count ) ) { - defer.resolveWith( elements, [ elements ] ); - } - }; - - if ( typeof type !== "string" ) { - obj = type; - type = undefined; } - type = type || "fx"; - while ( i-- ) { - tmp = dataPriv.get( elements[ i ], type + "queueHooks" ); - if ( tmp && tmp.empty ) { - count++; - tmp.empty.add( resolve ); + return this; + }, + + removeClass: function( value ) { + var classNames, cur, curValue, className, i, finalValue; + + if ( typeof value === "function" ) { + return this.each( function( j ) { + jQuery( this ).removeClass( value.call( this, j, getClass( this ) ) ); + } ); + } + + if ( !arguments.length ) { + return this.attr( "class", "" ); + } + + classNames = classesToArray( value ); + + if ( classNames.length ) { + return this.each( function() { + curValue = getClass( this ); + + // This expression is here for better compressibility (see addClass) + cur = this.nodeType === 1 && ( " " + stripAndCollapse( curValue ) + " " ); + + if ( cur ) { + for ( i = 0; i < classNames.length; i++ ) { + className = classNames[ i ]; + + // Remove *all* instances + while ( cur.indexOf( " " + className + " " ) > -1 ) { + cur = cur.replace( " " + className + " ", " " ); + } + } + + // Only assign if different to avoid unneeded rendering. + finalValue = stripAndCollapse( cur ); + if ( curValue !== finalValue ) { + this.setAttribute( "class", finalValue ); + } + } + } ); + } + + return this; + }, + + toggleClass: function( value, stateVal ) { + var classNames, className, i, self; + + if ( typeof value === "function" ) { + return this.each( function( i ) { + jQuery( this ).toggleClass( + value.call( this, i, getClass( this ), stateVal ), + stateVal + ); + } ); + } + + if ( typeof stateVal === "boolean" ) { + return stateVal ? this.addClass( value ) : this.removeClass( value ); + } + + classNames = classesToArray( value ); + + if ( classNames.length ) { + return this.each( function() { + + // Toggle individual class names + self = jQuery( this ); + + for ( i = 0; i < classNames.length; i++ ) { + className = classNames[ i ]; + + // Check each className given, space separated list + if ( self.hasClass( className ) ) { + self.removeClass( className ); + } else { + self.addClass( className ); + } + } + } ); + } + + return this; + }, + + hasClass: function( selector ) { + var className, elem, + i = 0; + + className = " " + selector + " "; + while ( ( elem = this[ i++ ] ) ) { + if ( elem.nodeType === 1 && + ( " " + stripAndCollapse( getClass( elem ) ) + " " ).indexOf( className ) > -1 ) { + return true; } } - resolve(); - return defer.promise( obj ); + + return false; } } ); -var pnum = ( /[+-]?(?:\d*\.|)\d+(?:[eE][+-]?\d+|)/ ).source; - -var rcssNum = new RegExp( "^(?:([+-])=|)(" + pnum + ")([a-z%]*)$", "i" ); - - -var cssExpand = [ "Top", "Right", "Bottom", "Left" ]; - -var documentElement = document.documentElement; - - - - var isAttached = function( elem ) { - return jQuery.contains( elem.ownerDocument, elem ); - }, - composed = { composed: true }; - - // Support: IE 9 - 11+, Edge 12 - 18+, iOS 10.0 - 10.2 only - // Check attachment across shadow DOM boundaries when possible (gh-3504) - // Support: iOS 10.0-10.2 only - // Early iOS 10 versions support `attachShadow` but not `getRootNode`, - // leading to errors. We need to check for `getRootNode`. - if ( documentElement.getRootNode ) { - isAttached = function( elem ) { - return jQuery.contains( elem.ownerDocument, elem ) || - elem.getRootNode( composed ) === elem.ownerDocument; - }; - } -var isHiddenWithinTree = function( elem, el ) { - - // isHiddenWithinTree might be called from jQuery#filter function; - // in that case, element will be second argument - elem = el || elem; - - // Inline style trumps all - return elem.style.display === "none" || - elem.style.display === "" && - - // Otherwise, check computed style - // Support: Firefox <=43 - 45 - // Disconnected elements can have computed display: none, so first confirm that elem is - // in the document. - isAttached( elem ) && - - jQuery.css( elem, "display" ) === "none"; - }; - - - -function adjustCSS( elem, prop, valueParts, tween ) { - var adjusted, scale, - maxIterations = 20, - currentValue = tween ? - function() { - return tween.cur(); - } : - function() { - return jQuery.css( elem, prop, "" ); - }, - initial = currentValue(), - unit = valueParts && valueParts[ 3 ] || ( jQuery.cssNumber[ prop ] ? "" : "px" ), - - // Starting value computation is required for potential unit mismatches - initialInUnit = elem.nodeType && - ( jQuery.cssNumber[ prop ] || unit !== "px" && +initial ) && - rcssNum.exec( jQuery.css( elem, prop ) ); - - if ( initialInUnit && initialInUnit[ 3 ] !== unit ) { - - // Support: Firefox <=54 - // Halve the iteration target value to prevent interference from CSS upper bounds (gh-2144) - initial = initial / 2; - - // Trust units reported by jQuery.css - unit = unit || initialInUnit[ 3 ]; - - // Iteratively approximate from a nonzero starting point - initialInUnit = +initial || 1; - - while ( maxIterations-- ) { - - // Evaluate and update our best guess (doubling guesses that zero out). - // Finish if the scale equals or crosses 1 (making the old*new product non-positive). - jQuery.style( elem, prop, initialInUnit + unit ); - if ( ( 1 - scale ) * ( 1 - ( scale = currentValue() / initial || 0.5 ) ) <= 0 ) { - maxIterations = 0; - } - initialInUnit = initialInUnit / scale; - - } - - initialInUnit = initialInUnit * 2; - jQuery.style( elem, prop, initialInUnit + unit ); - - // Make sure we update the tween properties later on - valueParts = valueParts || []; - } - - if ( valueParts ) { - initialInUnit = +initialInUnit || +initial || 0; - - // Apply relative offset (+=/-=) if specified - adjusted = valueParts[ 1 ] ? - initialInUnit + ( valueParts[ 1 ] + 1 ) * valueParts[ 2 ] : - +valueParts[ 2 ]; - if ( tween ) { - tween.unit = unit; - tween.start = initialInUnit; - tween.end = adjusted; - } - } - return adjusted; -} - - -var defaultDisplayMap = {}; - -function getDefaultDisplay( elem ) { - var temp, - doc = elem.ownerDocument, - nodeName = elem.nodeName, - display = defaultDisplayMap[ nodeName ]; - - if ( display ) { - return display; - } - - temp = doc.body.appendChild( doc.createElement( nodeName ) ); - display = jQuery.css( temp, "display" ); - - temp.parentNode.removeChild( temp ); - - if ( display === "none" ) { - display = "block"; - } - defaultDisplayMap[ nodeName ] = display; - - return display; -} - -function showHide( elements, show ) { - var display, elem, - values = [], - index = 0, - length = elements.length; - - // Determine new display value for elements that need to change - for ( ; index < length; index++ ) { - elem = elements[ index ]; - if ( !elem.style ) { - continue; - } - - display = elem.style.display; - if ( show ) { - - // Since we force visibility upon cascade-hidden elements, an immediate (and slow) - // check is required in this first loop unless we have a nonempty display value (either - // inline or about-to-be-restored) - if ( display === "none" ) { - values[ index ] = dataPriv.get( elem, "display" ) || null; - if ( !values[ index ] ) { - elem.style.display = ""; - } - } - if ( elem.style.display === "" && isHiddenWithinTree( elem ) ) { - values[ index ] = getDefaultDisplay( elem ); - } - } else { - if ( display !== "none" ) { - values[ index ] = "none"; - - // Remember what we're overwriting - dataPriv.set( elem, "display", display ); - } - } - } - - // Set the display of the elements in a second loop to avoid constant reflow - for ( index = 0; index < length; index++ ) { - if ( values[ index ] != null ) { - elements[ index ].style.display = values[ index ]; - } - } - - return elements; -} jQuery.fn.extend( { - show: function() { - return showHide( this, true ); - }, - hide: function() { - return showHide( this ); - }, - toggle: function( state ) { - if ( typeof state === "boolean" ) { - return state ? this.show() : this.hide(); + val: function( value ) { + var hooks, ret, valueIsFunction, + elem = this[ 0 ]; + + if ( !arguments.length ) { + if ( elem ) { + hooks = jQuery.valHooks[ elem.type ] || + jQuery.valHooks[ elem.nodeName.toLowerCase() ]; + + if ( hooks && + "get" in hooks && + ( ret = hooks.get( elem, "value" ) ) !== undefined + ) { + return ret; + } + + ret = elem.value; + + // Handle cases where value is null/undef or number + return ret == null ? "" : ret; + } + + return; } - return this.each( function() { - if ( isHiddenWithinTree( this ) ) { - jQuery( this ).show(); + valueIsFunction = typeof value === "function"; + + return this.each( function( i ) { + var val; + + if ( this.nodeType !== 1 ) { + return; + } + + if ( valueIsFunction ) { + val = value.call( this, i, jQuery( this ).val() ); } else { - jQuery( this ).hide(); + val = value; + } + + // Treat null/undefined as ""; convert numbers to string + if ( val == null ) { + val = ""; + + } else if ( typeof val === "number" ) { + val += ""; + + } else if ( Array.isArray( val ) ) { + val = jQuery.map( val, function( value ) { + return value == null ? "" : value + ""; + } ); + } + + hooks = jQuery.valHooks[ this.type ] || jQuery.valHooks[ this.nodeName.toLowerCase() ]; + + // If set returns undefined, fall back to normal setting + if ( !hooks || !( "set" in hooks ) || hooks.set( this, val, "value" ) === undefined ) { + this.value = val; } } ); } } ); -var rcheckableType = ( /^(?:checkbox|radio)$/i ); -var rtagName = ( /<([a-z][^\/\0>\x20\t\r\n\f]*)/i ); +jQuery.extend( { + valHooks: { + select: { + get: function( elem ) { + var value, option, i, + options = elem.options, + index = elem.selectedIndex, + one = elem.type === "select-one", + values = one ? null : [], + max = one ? index + 1 : options.length; -var rscriptType = ( /^$|^module$|\/(?:java|ecma)script/i ); + if ( index < 0 ) { + i = max; - - -( function() { - var fragment = document.createDocumentFragment(), - div = fragment.appendChild( document.createElement( "div" ) ), - input = document.createElement( "input" ); - - // Support: Android 4.0 - 4.3 only - // Check state lost if the name is set (trac-11217) - // Support: Windows Web Apps (WWA) - // `name` and `type` must use .setAttribute for WWA (trac-14901) - input.setAttribute( "type", "radio" ); - input.setAttribute( "checked", "checked" ); - input.setAttribute( "name", "t" ); - - div.appendChild( input ); - - // Support: Android <=4.1 only - // Older WebKit doesn't clone checked state correctly in fragments - support.checkClone = div.cloneNode( true ).cloneNode( true ).lastChild.checked; - - // Support: IE <=11 only - // Make sure textarea (and checkbox) defaultValue is properly cloned - div.innerHTML = ""; - support.noCloneChecked = !!div.cloneNode( true ).lastChild.defaultValue; - - // Support: IE <=9 only - // IE <=9 replaces ", "
" ], - col: [ 2, "", "
" ], - tr: [ 2, "", "
" ], - td: [ 3, "", "
" ], - - _default: [ 0, "", "" ] -}; - -wrapMap.tbody = wrapMap.tfoot = wrapMap.colgroup = wrapMap.caption = wrapMap.thead; -wrapMap.th = wrapMap.td; - -// Support: IE <=9 only -if ( !support.option ) { - wrapMap.optgroup = wrapMap.option = [ 1, "" ]; -} - - -function getAll( context, tag ) { - - // Support: IE <=9 - 11 only - // Use typeof to avoid zero-argument method invocation on host objects (trac-15151) - var ret; - - if ( typeof context.getElementsByTagName !== "undefined" ) { - ret = context.getElementsByTagName( tag || "*" ); - - } else if ( typeof context.querySelectorAll !== "undefined" ) { - ret = context.querySelectorAll( tag || "*" ); - - } else { - ret = []; - } - - if ( tag === undefined || tag && nodeName( context, tag ) ) { - return jQuery.merge( [ context ], ret ); - } - - return ret; -} - - -// Mark scripts as having already been evaluated -function setGlobalEval( elems, refElements ) { - var i = 0, - l = elems.length; - - for ( ; i < l; i++ ) { - dataPriv.set( - elems[ i ], - "globalEval", - !refElements || dataPriv.get( refElements[ i ], "globalEval" ) - ); - } -} - - -var rhtml = /<|&#?\w+;/; - -function buildFragment( elems, context, scripts, selection, ignored ) { - var elem, tmp, tag, wrap, attached, j, - fragment = context.createDocumentFragment(), - nodes = [], - i = 0, - l = elems.length; - - for ( ; i < l; i++ ) { - elem = elems[ i ]; - - if ( elem || elem === 0 ) { - - // Add nodes directly - if ( toType( elem ) === "object" ) { - - // Support: Android <=4.0 only, PhantomJS 1 only - // push.apply(_, arraylike) throws on ancient WebKit - jQuery.merge( nodes, elem.nodeType ? [ elem ] : elem ); - - // Convert non-html into a text node - } else if ( !rhtml.test( elem ) ) { - nodes.push( context.createTextNode( elem ) ); - - // Convert html into DOM nodes - } else { - tmp = tmp || fragment.appendChild( context.createElement( "div" ) ); - - // Deserialize a standard representation - tag = ( rtagName.exec( elem ) || [ "", "" ] )[ 1 ].toLowerCase(); - wrap = wrapMap[ tag ] || wrapMap._default; - tmp.innerHTML = wrap[ 1 ] + jQuery.htmlPrefilter( elem ) + wrap[ 2 ]; - - // Descend through wrappers to the right content - j = wrap[ 0 ]; - while ( j-- ) { - tmp = tmp.lastChild; + } else { + i = one ? index : 0; } - // Support: Android <=4.0 only, PhantomJS 1 only - // push.apply(_, arraylike) throws on ancient WebKit - jQuery.merge( nodes, tmp.childNodes ); + // Loop through all the selected options + for ( ; i < max; i++ ) { + option = options[ i ]; - // Remember the top-level container - tmp = fragment.firstChild; + if ( option.selected && - // Ensure the created nodes are orphaned (trac-12392) - tmp.textContent = ""; - } - } - } + // Don't return options that are disabled or in a disabled optgroup + !option.disabled && + ( !option.parentNode.disabled || + !nodeName( option.parentNode, "optgroup" ) ) ) { - // Remove wrapper from fragment - fragment.textContent = ""; + // Get the specific value for the option + value = jQuery( option ).val(); - i = 0; - while ( ( elem = nodes[ i++ ] ) ) { + // We don't need an array for one selects + if ( one ) { + return value; + } - // Skip elements already in the context collection (trac-4087) - if ( selection && jQuery.inArray( elem, selection ) > -1 ) { - if ( ignored ) { - ignored.push( elem ); - } - continue; - } - - attached = isAttached( elem ); - - // Append to fragment - tmp = getAll( fragment.appendChild( elem ), "script" ); - - // Preserve script evaluation history - if ( attached ) { - setGlobalEval( tmp ); - } - - // Capture executables - if ( scripts ) { - j = 0; - while ( ( elem = tmp[ j++ ] ) ) { - if ( rscriptType.test( elem.type || "" ) ) { - scripts.push( elem ); + // Multi-Selects return an array + values.push( value ); + } } + + return values; + }, + + set: function( elem, value ) { + var optionSet, option, + options = elem.options, + values = jQuery.makeArray( value ), + i = options.length; + + while ( i-- ) { + option = options[ i ]; + + if ( ( option.selected = + jQuery.inArray( jQuery( option ).val(), values ) > -1 + ) ) { + optionSet = true; + } + } + + // Force browsers to behave consistently when non-matching value is set + if ( !optionSet ) { + elem.selectedIndex = -1; + } + return values; } } } +} ); - return fragment; +if ( isIE ) { + jQuery.valHooks.option = { + get: function( elem ) { + + var val = elem.getAttribute( "value" ); + return val != null ? + val : + + // Support: IE <=10 - 11+ + // option.text throws exceptions (trac-14686, trac-14858) + // Strip and collapse whitespace + // https://html.spec.whatwg.org/#strip-and-collapse-whitespace + stripAndCollapse( jQuery.text( elem ) ); + } + }; } +// Radios and checkboxes getter/setter +jQuery.each( [ "radio", "checkbox" ], function() { + jQuery.valHooks[ this ] = { + set: function( elem, value ) { + if ( Array.isArray( value ) ) { + return ( elem.checked = jQuery.inArray( jQuery( elem ).val(), value ) > -1 ); + } + } + }; +} ); + +var rcheckableType = /^(?:checkbox|radio)$/i; var rtypenamespace = /^([^.]*)(?:\.(.+)|)/; @@ -4904,8 +3768,6 @@ function on( elem, types, selector, data, fn, one ) { */ jQuery.event = { - global: {}, - add: function( elem, types, handler, data, selector ) { var handleObjIn, eventHandle, tmp, @@ -4928,7 +3790,7 @@ jQuery.event = { // Ensure that invalid selectors throw exceptions at attach time // Evaluate against documentElement in case elem is a non-element node (e.g., document) if ( selector ) { - jQuery.find.matchesSelector( documentElement, selector ); + jQuery.find.matchesSelector( documentElement$1, selector ); } // Make sure that the handler has a unique ID, used to find/remove it later @@ -5013,9 +3875,6 @@ jQuery.event = { } else { handlers.push( handleObj ); } - - // Keep track of which events have ever been used, for event optimization - jQuery.event.global[ type ] = true; } }, @@ -5171,14 +4030,10 @@ jQuery.event = { // Find delegate handlers if ( delegateCount && - // Support: IE <=9 - // Black-hole SVG instance trees (trac-13180) - cur.nodeType && - - // Support: Firefox <=42 + // Support: Firefox <=42 - 66+ // Suppress spec-violating clicks indicating a non-primary pointer button (trac-3861) // https://www.w3.org/TR/DOM-Level-3-Events/#event-type-click - // Support: IE 11 only + // Support: IE 11+ // ...but not arrow key "clicks" of radio inputs, which can have `button` -1 (gh-2343) !( event.type === "click" && event.button >= 1 ) ) { @@ -5225,7 +4080,7 @@ jQuery.event = { enumerable: true, configurable: true, - get: isFunction( hook ) ? + get: typeof hook === "function" ? function() { if ( this.originalEvent ) { return hook( this.originalEvent ); @@ -5254,7 +4109,7 @@ jQuery.event = { new jQuery.Event( originalEvent ); }, - special: { + special: jQuery.extend( Object.create( null ), { load: { // Prevent triggered image.load events from bubbling to window.load @@ -5310,15 +4165,21 @@ jQuery.event = { beforeunload: { postDispatch: function( event ) { + if ( event.result !== undefined ) { - // Support: Firefox 20+ - // Firefox doesn't alert if the returnValue field is not set. - if ( event.result !== undefined && event.originalEvent ) { - event.originalEvent.returnValue = event.result; + // Setting `event.originalEvent.returnValue` in modern + // browsers does the same as just calling `preventDefault()`, + // the browsers ignore the value anyway. + // Incidentally, IE 11 is the only browser from our supported + // ones which respects the value returned from a `beforeunload` + // handler attached by `addEventListener`; other browsers do + // so only for inline handlers, so not setting the value + // directly shouldn't reduce any functionality. + event.preventDefault(); } } } - } + } ) }; // Ensure the presence of an event listener that handles manually-triggered @@ -5343,14 +4204,29 @@ function leverageNative( el, type, isSetup ) { var result, saved = dataPriv.get( this, type ); + // This controller function is invoked under multiple circumstances, + // differentiated by the stored value in `saved`: + // 1. For an outer synthetic `.trigger()`ed event (detected by + // `event.isTrigger & 1` and non-array `saved`), it records arguments + // as an array and fires an [inner] native event to prompt state + // changes that should be observed by registered listeners (such as + // checkbox toggling and focus updating), then clears the stored value. + // 2. For an [inner] native event (detected by `saved` being + // an array), it triggers an inner synthetic event, records the + // result, and preempts propagation to further jQuery listeners. + // 3. For an inner synthetic event (detected by `event.isTrigger & 1` and + // array `saved`), it prevents double-propagation of surrogate events + // but otherwise allows everything to proceed (particularly including + // further listeners). + // Possible `saved` data shapes: `[...], `{ value }`, `false`. if ( ( event.isTrigger & 1 ) && this[ type ] ) { // Interrupt processing of the outer synthetic .trigger()ed event - if ( !saved ) { + if ( !saved.length ) { // Store arguments for use when handling the inner native event - // There will always be at least one argument (an event object), so this array - // will not be confused with a leftover capture object. + // There will always be at least one argument (an event object), + // so this array will not be confused with a leftover capture object. saved = slice.call( arguments ); dataPriv.set( this, type, saved ); @@ -5365,29 +4241,35 @@ function leverageNative( el, type, isSetup ) { event.stopImmediatePropagation(); event.preventDefault(); - return result; + // Support: Chrome 86+ + // In Chrome, if an element having a focusout handler is + // blurred by clicking outside of it, it invokes the handler + // synchronously. If that handler calls `.remove()` on + // the element, the data is cleared, leaving `result` + // undefined. We need to guard against this. + return result && result.value; } - // If this is an inner synthetic event for an event with a bubbling surrogate - // (focus or blur), assume that the surrogate already propagated from triggering - // the native event and prevent that from happening again here. - // This technically gets the ordering wrong w.r.t. to `.trigger()` (in which the - // bubbling surrogate propagates *after* the non-bubbling base), but that seems - // less bad than duplication. + // If this is an inner synthetic event for an event with a bubbling + // surrogate (focus or blur), assume that the surrogate already + // propagated from triggering the native event and prevent that + // from happening again here. } else if ( ( jQuery.event.special[ type ] || {} ).delegateType ) { event.stopPropagation(); } - // If this is a native event triggered above, everything is now in order - // Fire an inner synthetic event with the original arguments - } else if ( saved ) { + // If this is a native event triggered above, everything is now in order. + // Fire an inner synthetic event with the original arguments. + } else if ( saved.length ) { // ...and capture the result - dataPriv.set( this, type, jQuery.event.trigger( - saved[ 0 ], - saved.slice( 1 ), - this - ) ); + dataPriv.set( this, type, { + value: jQuery.event.trigger( + saved[ 0 ], + saved.slice( 1 ), + this + ) + } ); // Abort handling of the native event by all jQuery handlers while allowing // native handlers on the same element to run. On target, this is achieved @@ -5426,21 +4308,12 @@ jQuery.Event = function( src, props ) { // Events bubbling up the document may have been marked as prevented // by a handler lower down the tree; reflect the correct value. - this.isDefaultPrevented = src.defaultPrevented || - src.defaultPrevented === undefined && - - // Support: Android <=2.3 only - src.returnValue === false ? + this.isDefaultPrevented = src.defaultPrevented ? returnTrue : returnFalse; // Create target properties - // Support: Safari <=6 - 7 only - // Target should not be a text node (trac-504, trac-13143) - this.target = ( src.target && src.target.nodeType === 3 ) ? - src.target.parentNode : - src.target; - + this.target = src.target; this.currentTarget = src.currentTarget; this.relatedTarget = src.relatedTarget; @@ -5538,41 +4411,25 @@ jQuery.each( { jQuery.each( { focus: "focusin", blur: "focusout" }, function( type, delegateType ) { + // Support: IE 11+ + // Attach a single focusin/focusout handler on the document while someone wants focus/blur. + // This is because the former are synchronous in IE while the latter are async. In other + // browsers, all those handlers are invoked synchronously. function focusMappedHandler( nativeEvent ) { - if ( document.documentMode ) { - // Support: IE 11+ - // Attach a single focusin/focusout handler on the document while someone wants - // focus/blur. This is because the former are synchronous in IE while the latter - // are async. In other browsers, all those handlers are invoked synchronously. + // `eventHandle` would already wrap the event, but we need to change the `type` here. + var event = jQuery.event.fix( nativeEvent ); + event.type = nativeEvent.type === "focusin" ? "focus" : "blur"; + event.isSimulated = true; - // `handle` from private data would already wrap the event, but we need - // to change the `type` here. - var handle = dataPriv.get( this, "handle" ), - event = jQuery.event.fix( nativeEvent ); - event.type = nativeEvent.type === "focusin" ? "focus" : "blur"; - event.isSimulated = true; + // focus/blur don't bubble while focusin/focusout do; simulate the former by only + // invoking the handler at the lower level. + if ( event.target === event.currentTarget ) { - // First, handle focusin/focusout - handle( nativeEvent ); - - // ...then, handle focus/blur - // - // focus/blur don't bubble while focusin/focusout do; simulate the former by only - // invoking the handler at the lower level. - if ( event.target === event.currentTarget ) { - - // The setup part calls `leverageNative`, which, in turn, calls - // `jQuery.event.add`, so event handle will already have been set - // by this point. - handle( event ); - } - } else { - - // For non-IE browsers, attach a single capturing handler on the document - // while someone wants focusin/focusout. - jQuery.event.simulate( delegateType, nativeEvent.target, - jQuery.event.fix( nativeEvent ) ); + // The setup part calls `leverageNative`, which, in turn, calls + // `jQuery.event.add`, so event handle will already have been set + // by this point. + dataPriv.get( this, "handle" )( event ); } } @@ -5581,24 +4438,13 @@ jQuery.each( { focus: "focusin", blur: "focusout" }, function( type, delegateTyp // Utilize native event if possible so blur/focus sequence is correct setup: function() { - var attaches; - // Claim the first handler // dataPriv.set( this, "focus", ... ) // dataPriv.set( this, "blur", ... ) leverageNative( this, type, true ); - if ( document.documentMode ) { - - // Support: IE 9 - 11+ - // We use the same native handler for focusin & focus (and focusout & blur) - // so we need to coordinate setup & teardown parts between those events. - // Use `delegateType` as the key as `type` is already used by `leverageNative`. - attaches = dataPriv.get( this, delegateType ); - if ( !attaches ) { - this.addEventListener( delegateType, focusMappedHandler ); - } - dataPriv.set( this, delegateType, ( attaches || 0 ) + 1 ); + if ( isIE ) { + this.addEventListener( delegateType, focusMappedHandler ); } else { // Return false to allow normal processing in the caller @@ -5615,16 +4461,8 @@ jQuery.each( { focus: "focusin", blur: "focusout" }, function( type, delegateTyp }, teardown: function() { - var attaches; - - if ( document.documentMode ) { - attaches = dataPriv.get( this, delegateType ) - 1; - if ( !attaches ) { - this.removeEventListener( delegateType, focusMappedHandler ); - dataPriv.remove( this, delegateType ); - } else { - dataPriv.set( this, delegateType, attaches ); - } + if ( isIE ) { + this.removeEventListener( delegateType, focusMappedHandler ); } else { // Return false to indicate standard teardown should be applied @@ -5640,68 +4478,11 @@ jQuery.each( { focus: "focusin", blur: "focusout" }, function( type, delegateTyp delegateType: delegateType }; - - // Support: Firefox <=44 - // Firefox doesn't have focus(in | out) events - // Related ticket - https://bugzilla.mozilla.org/show_bug.cgi?id=687787 - // - // Support: Chrome <=48 - 49, Safari <=9.0 - 9.1 - // focus(in | out) events fire after focus & blur events, - // which is spec violation - http://www.w3.org/TR/DOM-Level-3-Events/#events-focusevent-event-order - // Related ticket - https://bugs.chromium.org/p/chromium/issues/detail?id=449857 - // - // Support: IE 9 - 11+ - // To preserve relative focusin/focus & focusout/blur event order guaranteed on the 3.x branch, - // attach a single handler for both events in IE. - jQuery.event.special[ delegateType ] = { - setup: function() { - - // Handle: regular nodes (via `this.ownerDocument`), window - // (via `this.document`) & document (via `this`). - var doc = this.ownerDocument || this.document || this, - dataHolder = document.documentMode ? this : doc, - attaches = dataPriv.get( dataHolder, delegateType ); - - // Support: IE 9 - 11+ - // We use the same native handler for focusin & focus (and focusout & blur) - // so we need to coordinate setup & teardown parts between those events. - // Use `delegateType` as the key as `type` is already used by `leverageNative`. - if ( !attaches ) { - if ( document.documentMode ) { - this.addEventListener( delegateType, focusMappedHandler ); - } else { - doc.addEventListener( type, focusMappedHandler, true ); - } - } - dataPriv.set( dataHolder, delegateType, ( attaches || 0 ) + 1 ); - }, - teardown: function() { - var doc = this.ownerDocument || this.document || this, - dataHolder = document.documentMode ? this : doc, - attaches = dataPriv.get( dataHolder, delegateType ) - 1; - - if ( !attaches ) { - if ( document.documentMode ) { - this.removeEventListener( delegateType, focusMappedHandler ); - } else { - doc.removeEventListener( type, focusMappedHandler, true ); - } - dataPriv.remove( dataHolder, delegateType ); - } else { - dataPriv.set( dataHolder, delegateType, attaches ); - } - } - }; } ); // Create mouseenter/leave events using mouseover/out and event-time checks // so that event delegation works in jQuery. // Do the same for pointerenter/pointerleave and pointerover/pointerout -// -// Support: Safari 7 only -// Safari sends mouseenter too often; see: -// https://bugs.chromium.org/p/chromium/issues/detail?id=470258 -// for the description of the bug (it existed in older Chrome versions as well). jQuery.each( { mouseenter: "mouseover", mouseleave: "mouseout", @@ -5776,28 +4557,352 @@ jQuery.fn.extend( { } } ); +var rfocusMorph = /^(?:focusinfocus|focusoutblur)$/, + stopPropagationCallback = function( e ) { + e.stopPropagation(); + }; -var +jQuery.extend( jQuery.event, { - // Support: IE <=10 - 11, Edge 12 - 13 only - // In IE/Edge using regex groups here causes severe slowdowns. - // See https://connect.microsoft.com/IE/feedback/details/1736512/ - rnoInnerhtml = /\s*$/g; + cur = lastElement = tmp = elem = elem || document$1; -// Prefer a tbody over its parent table for containing new rows -function manipulationTarget( elem, content ) { - if ( nodeName( elem, "table" ) && - nodeName( content.nodeType !== 11 ? content : content.firstChild, "tr" ) ) { + // Don't do events on text and comment nodes + if ( elem.nodeType === 3 || elem.nodeType === 8 ) { + return; + } - return jQuery( elem ).children( "tbody" )[ 0 ] || elem; + // focus/blur morphs to focusin/out; ensure we're not firing them right now + if ( rfocusMorph.test( type + jQuery.event.triggered ) ) { + return; + } + + if ( type.indexOf( "." ) > -1 ) { + + // Namespaced trigger; create a regexp to match event type in handle() + namespaces = type.split( "." ); + type = namespaces.shift(); + namespaces.sort(); + } + ontype = type.indexOf( ":" ) < 0 && "on" + type; + + // Caller can pass in a jQuery.Event object, Object, or just an event type string + event = event[ jQuery.expando ] ? + event : + new jQuery.Event( type, typeof event === "object" && event ); + + // Trigger bitmask: & 1 for native handlers; & 2 for jQuery (always true) + event.isTrigger = onlyHandlers ? 2 : 3; + event.namespace = namespaces.join( "." ); + event.rnamespace = event.namespace ? + new RegExp( "(^|\\.)" + namespaces.join( "\\.(?:.*\\.|)" ) + "(\\.|$)" ) : + null; + + // Clean up the event in case it is being reused + event.result = undefined; + if ( !event.target ) { + event.target = elem; + } + + // Clone any incoming data and prepend the event, creating the handler arg list + data = data == null ? + [ event ] : + jQuery.makeArray( data, [ event ] ); + + // Allow special events to draw outside the lines + special = jQuery.event.special[ type ] || {}; + if ( !onlyHandlers && special.trigger && special.trigger.apply( elem, data ) === false ) { + return; + } + + // Determine event propagation path in advance, per W3C events spec (trac-9951) + // Bubble up to document, then to window; watch for a global ownerDocument var (trac-9724) + if ( !onlyHandlers && !special.noBubble && !isWindow( elem ) ) { + + bubbleType = special.delegateType || type; + if ( !rfocusMorph.test( bubbleType + type ) ) { + cur = cur.parentNode; + } + for ( ; cur; cur = cur.parentNode ) { + eventPath.push( cur ); + tmp = cur; + } + + // Only add window if we got to document (e.g., not plain obj or detached DOM) + if ( tmp === ( elem.ownerDocument || document$1 ) ) { + eventPath.push( tmp.defaultView || tmp.parentWindow || window ); + } + } + + // Fire handlers on the event path + i = 0; + while ( ( cur = eventPath[ i++ ] ) && !event.isPropagationStopped() ) { + lastElement = cur; + event.type = i > 1 ? + bubbleType : + special.bindType || type; + + // jQuery handler + handle = ( dataPriv.get( cur, "events" ) || Object.create( null ) )[ event.type ] && + dataPriv.get( cur, "handle" ); + if ( handle ) { + handle.apply( cur, data ); + } + + // Native handler + handle = ontype && cur[ ontype ]; + if ( handle && handle.apply && acceptData( cur ) ) { + event.result = handle.apply( cur, data ); + if ( event.result === false ) { + event.preventDefault(); + } + } + } + event.type = type; + + // If nobody prevented the default action, do it now + if ( !onlyHandlers && !event.isDefaultPrevented() ) { + + if ( ( !special._default || + special._default.apply( eventPath.pop(), data ) === false ) && + acceptData( elem ) ) { + + // Call a native DOM method on the target with the same name as the event. + // Don't do default actions on window, that's where global variables be (trac-6170) + if ( ontype && typeof elem[ type ] === "function" && !isWindow( elem ) ) { + + // Don't re-trigger an onFOO event when we call its FOO() method + tmp = elem[ ontype ]; + + if ( tmp ) { + elem[ ontype ] = null; + } + + // Prevent re-triggering of the same event, since we already bubbled it above + jQuery.event.triggered = type; + + if ( event.isPropagationStopped() ) { + lastElement.addEventListener( type, stopPropagationCallback ); + } + + elem[ type ](); + + if ( event.isPropagationStopped() ) { + lastElement.removeEventListener( type, stopPropagationCallback ); + } + + jQuery.event.triggered = undefined; + + if ( tmp ) { + elem[ ontype ] = tmp; + } + } + } + } + + return event.result; + }, + + // Piggyback on a donor event to simulate a different one + // Used only for `focus(in | out)` events + simulate: function( type, elem, event ) { + var e = jQuery.extend( + new jQuery.Event(), + event, + { + type: type, + isSimulated: true + } + ); + + jQuery.event.trigger( e, null, elem ); } - return elem; +} ); + +jQuery.fn.extend( { + + trigger: function( type, data ) { + return this.each( function() { + jQuery.event.trigger( type, data, this ); + } ); + }, + triggerHandler: function( type, data ) { + var elem = this[ 0 ]; + if ( elem ) { + return jQuery.event.trigger( type, data, elem, true ); + } + } +} ); + +var isAttached = function( elem ) { + return jQuery.contains( elem.ownerDocument, elem ) || + elem.getRootNode( composed ) === elem.ownerDocument; + }, + composed = { composed: true }; + +// Support: IE 9 - 11+ +// Check attachment across shadow DOM boundaries when possible (gh-3504). +// Provide a fallback for browsers without Shadow DOM v1 support. +if ( !documentElement$1.getRootNode ) { + isAttached = function( elem ) { + return jQuery.contains( elem.ownerDocument, elem ); + }; +} + +// rtagName captures the name from the first start tag in a string of HTML +// https://html.spec.whatwg.org/multipage/syntax.html#tag-open-state +// https://html.spec.whatwg.org/multipage/syntax.html#tag-name-state +var rtagName = /<([a-z][^\/\0>\x20\t\r\n\f]*)/i; + +var wrapMap = { + + // Table parts need to be wrapped with `` or they're + // stripped to their contents when put in a div. + // XHTML parsers do not magically insert elements in the + // same way that tag soup parsers do, so we cannot shorten + // this by omitting or other required elements. + thead: [ "table" ], + col: [ "colgroup", "table" ], + tr: [ "tbody", "table" ], + td: [ "tr", "tbody", "table" ] +}; + +wrapMap.tbody = wrapMap.tfoot = wrapMap.colgroup = wrapMap.caption = wrapMap.thead; +wrapMap.th = wrapMap.td; + +function getAll( context, tag ) { + + // Support: IE <=9 - 11+ + // Use typeof to avoid zero-argument method invocation on host objects (trac-15151) + var ret; + + if ( typeof context.getElementsByTagName !== "undefined" ) { + + // Use slice to snapshot the live collection from gEBTN + ret = arr.slice.call( context.getElementsByTagName( tag || "*" ) ); + + } else if ( typeof context.querySelectorAll !== "undefined" ) { + ret = context.querySelectorAll( tag || "*" ); + + } else { + ret = []; + } + + if ( tag === undefined || tag && nodeName( context, tag ) ) { + return jQuery.merge( [ context ], ret ); + } + + return ret; +} + +var rscriptType = /^$|^module$|\/(?:java|ecma)script/i; + +// Mark scripts as having already been evaluated +function setGlobalEval( elems, refElements ) { + var i = 0, + l = elems.length; + + for ( ; i < l; i++ ) { + dataPriv.set( + elems[ i ], + "globalEval", + !refElements || dataPriv.get( refElements[ i ], "globalEval" ) + ); + } +} + +var rhtml = /<|&#?\w+;/; + +function buildFragment( elems, context, scripts, selection, ignored ) { + var elem, tmp, tag, wrap, attached, j, + fragment = context.createDocumentFragment(), + nodes = [], + i = 0, + l = elems.length; + + for ( ; i < l; i++ ) { + elem = elems[ i ]; + + if ( elem || elem === 0 ) { + + // Add nodes directly + if ( toType( elem ) === "object" && ( elem.nodeType || isArrayLike( elem ) ) ) { + jQuery.merge( nodes, elem.nodeType ? [ elem ] : elem ); + + // Convert non-html into a text node + } else if ( !rhtml.test( elem ) ) { + nodes.push( context.createTextNode( elem ) ); + + // Convert html into DOM nodes + } else { + tmp = tmp || fragment.appendChild( context.createElement( "div" ) ); + + // Deserialize a standard representation + tag = ( rtagName.exec( elem ) || [ "", "" ] )[ 1 ].toLowerCase(); + wrap = wrapMap[ tag ] || arr; + + // Create wrappers & descend into them. + j = wrap.length; + while ( --j > -1 ) { + tmp = tmp.appendChild( context.createElement( wrap[ j ] ) ); + } + + tmp.innerHTML = jQuery.htmlPrefilter( elem ); + + jQuery.merge( nodes, tmp.childNodes ); + + // Remember the top-level container + tmp = fragment.firstChild; + + // Ensure the created nodes are orphaned (trac-12392) + tmp.textContent = ""; + } + } + } + + // Remove wrapper from fragment + fragment.textContent = ""; + + i = 0; + while ( ( elem = nodes[ i++ ] ) ) { + + // Skip elements already in the context collection (trac-4087) + if ( selection && jQuery.inArray( elem, selection ) > -1 ) { + if ( ignored ) { + ignored.push( elem ); + } + continue; + } + + attached = isAttached( elem ); + + // Append to fragment + tmp = getAll( fragment.appendChild( elem ), "script" ); + + // Preserve script evaluation history + if ( attached ) { + setGlobalEval( tmp ); + } + + // Capture executables + if ( scripts ) { + j = 0; + while ( ( elem = tmp[ j++ ] ) ) { + if ( rscriptType.test( elem.type || "" ) ) { + scripts.push( elem ); + } + } + } + } + + return fragment; } // Replace/restore the type attribute of script elements for safe DOM manipulation @@ -5815,52 +4920,6 @@ function restoreScript( elem ) { return elem; } -function cloneCopyEvent( src, dest ) { - var i, l, type, pdataOld, udataOld, udataCur, events; - - if ( dest.nodeType !== 1 ) { - return; - } - - // 1. Copy private data: events, handlers, etc. - if ( dataPriv.hasData( src ) ) { - pdataOld = dataPriv.get( src ); - events = pdataOld.events; - - if ( events ) { - dataPriv.remove( dest, "handle events" ); - - for ( type in events ) { - for ( i = 0, l = events[ type ].length; i < l; i++ ) { - jQuery.event.add( dest, type, events[ type ][ i ] ); - } - } - } - } - - // 2. Copy user data - if ( dataUser.hasData( src ) ) { - udataOld = dataUser.access( src ); - udataCur = jQuery.extend( {}, udataOld ); - - dataUser.set( dest, udataCur ); - } -} - -// Fix IE bugs, see support tests -function fixInput( src, dest ) { - var nodeName = dest.nodeName.toLowerCase(); - - // Fails to persist the checked state of a cloned checkbox or radio button. - if ( nodeName === "input" && rcheckableType.test( src.type ) ) { - dest.checked = src.checked; - - // Fails to return the selected option to the default selected state when cloning options - } else if ( nodeName === "input" || nodeName === "textarea" ) { - dest.defaultValue = src.defaultValue; - } -} - function domManip( collection, args, callback, ignored ) { // Flatten any nested arrays @@ -5871,17 +4930,12 @@ function domManip( collection, args, callback, ignored ) { l = collection.length, iNoClone = l - 1, value = args[ 0 ], - valueIsFunction = isFunction( value ); + valueIsFunction = typeof value === "function"; - // We can't cloneNode fragments that contain checked, in WebKit - if ( valueIsFunction || - ( l > 1 && typeof value === "string" && - !support.checkClone && rchecked.test( value ) ) ) { + if ( valueIsFunction ) { return collection.each( function( index ) { var self = collection.eq( index ); - if ( valueIsFunction ) { - args[ 0 ] = value.call( this, index, self.html() ); - } + args[ 0 ] = value.call( this, index, self.html() ); domManip( self, args, callback, ignored ); } ); } @@ -5910,9 +4964,6 @@ function domManip( collection, args, callback, ignored ) { // Keep references to cloned scripts for later restoration if ( hasScripts ) { - - // Support: Android <=4.0 only, PhantomJS 1 only - // push.apply(_, arraylike) throws on ancient WebKit jQuery.merge( scripts, getAll( node, "script" ) ); } } @@ -5930,7 +4981,7 @@ function domManip( collection, args, callback, ignored ) { for ( i = 0; i < hasScripts; i++ ) { node = scripts[ i ]; if ( rscriptType.test( node.type || "" ) && - !dataPriv.access( node, "globalEval" ) && + !dataPriv.get( node, "globalEval" ) && jQuery.contains( doc, node ) ) { if ( node.src && ( node.type || "" ).toLowerCase() !== "module" ) { @@ -5938,17 +4989,12 @@ function domManip( collection, args, callback, ignored ) { // Optional AJAX dependency, but won't run scripts if not present if ( jQuery._evalUrl && !node.noModule ) { jQuery._evalUrl( node.src, { - nonce: node.nonce || node.getAttribute( "nonce" ) + nonce: node.nonce, + crossOrigin: node.crossOrigin }, doc ); } } else { - - // Unwrap a CDATA section containing script contents. This shouldn't be - // needed as in XML documents they're already not visible when - // inspecting element contents and in HTML documents they have no - // meaning but we're preserving that logic for backwards compatibility. - // This will be removed completely in 4.0. See gh-4904. - DOMEval( node.textContent.replace( rcleanScript, "" ), node, doc ); + DOMEval( node.textContent, node, doc ); } } } @@ -5959,6 +5005,47 @@ function domManip( collection, args, callback, ignored ) { return collection; } +var + + // Support: IE <=10 - 11+ + // In IE using regex groups here causes severe slowdowns. + rnoInnerhtml = /` computed width is `"auto"` unless `width` is set + // explicitly via CSS so measurements there remain incorrect. Because of + // the lack of a proper workaround, we accept this limitation, treating + // IE as passing the test. + reliableColDimensionsVal = isIE || Math.round( parseFloat( + window.getComputedStyle( col ).width ) + ) === 18; + + // Support: IE 10 - 11+ + // IE misreports `getComputedStyle` of table rows with width/height + // set in CSS while `offset*` properties report correct values. + // Support: Firefox 70 - 135+ + // Only Firefox includes border widths + // in computed dimensions for table rows. (gh-4529) + reliableTrDimensionsVal = Math.round( parseFloat( trStyle.height ) + + parseFloat( trStyle.borderTopWidth ) + + parseFloat( trStyle.borderBottomWidth ) ) === tr.offsetHeight; + + documentElement$1.removeChild( table ); + + // Nullify the table so it wouldn't be stored in the memory; + // it will also be a sign that checks were already performed. + table = null; +} + +jQuery.extend( support, { + reliableTrDimensions: function() { + computeTableStyleTests(); + return reliableTrDimensionsVal; + }, + + reliableColDimensions: function() { + computeTableStyleTests(); + return reliableColDimensionsVal; + } +} ); + +var cssShow = { position: "absolute", visibility: "hidden", display: "block" }, cssNormalTransform = { letterSpacing: "0", fontWeight: "400" @@ -6654,7 +5802,7 @@ function getWidthOrHeight( elem, dimension, extra ) { // To avoid forcing a reflow, only fetch boxSizing if we need it (gh-4322). // Fake content-box until we know it's needed to know the true value. - boxSizingNeeded = !support.boxSizingReliable() || extra, + boxSizingNeeded = isIE || extra, isBorderBox = boxSizingNeeded && jQuery.css( elem, "boxSizing", false, styles ) === "border-box", valueIsBorderBox = isBorderBox, @@ -6662,7 +5810,6 @@ function getWidthOrHeight( elem, dimension, extra ) { val = curCSS( elem, dimension, styles ), offsetProp = "offset" + dimension[ 0 ].toUpperCase() + dimension.slice( 1 ); - // Support: Firefox <=54 // Return a confounding non-pixel value or feign ignorance, as appropriate. if ( rnumnonpx.test( val ) ) { if ( !extra ) { @@ -6672,24 +5819,22 @@ function getWidthOrHeight( elem, dimension, extra ) { } - // Support: IE 9 - 11 only - // Use offsetWidth/offsetHeight for when box sizing is unreliable. - // In those cases, the computed value can be trusted to be border-box. - if ( ( !support.boxSizingReliable() && isBorderBox || + if ( + ( - // Support: IE 10 - 11+, Edge 15 - 18+ - // IE/Edge misreport `getComputedStyle` of table rows with width/height - // set in CSS while `offset*` properties report correct values. - // Interestingly, in some cases IE 9 doesn't suffer from this issue. - !support.reliableTrDimensions() && nodeName( elem, "tr" ) || + // Fall back to offsetWidth/offsetHeight when value is "auto" + // This happens for inline elements with no explicit setting (gh-3571) + val === "auto" || - // Fall back to offsetWidth/offsetHeight when value is "auto" - // This happens for inline elements with no explicit setting (gh-3571) - val === "auto" || + // Support: IE 9 - 11+ + // Use offsetWidth/offsetHeight for when box sizing is unreliable. + // In those cases, the computed value can be trusted to be border-box. + ( isIE && isBorderBox ) || - // Support: Android <=4.1 - 4.3 only - // Also use offsetWidth/offsetHeight for misreported inline dimensions (gh-3602) - !parseFloat( val ) && jQuery.css( elem, "display", false, styles ) === "inline" ) && + ( !support.reliableColDimensions() && nodeName( elem, "col" ) ) || + + ( !support.reliableTrDimensions() && nodeName( elem, "tr" ) ) + ) && // Make sure the element is visible & connected elem.getClientRects().length ) { @@ -6727,55 +5872,7 @@ jQuery.extend( { // Add in style property hooks for overriding the default // behavior of getting and setting a style property - cssHooks: { - opacity: { - get: function( elem, computed ) { - if ( computed ) { - - // We should always get a number back from opacity - var ret = curCSS( elem, "opacity" ); - return ret === "" ? "1" : ret; - } - } - } - }, - - // Don't automatically add "px" to these possibly-unitless properties - cssNumber: { - animationIterationCount: true, - aspectRatio: true, - borderImageSlice: true, - columnCount: true, - flexGrow: true, - flexShrink: true, - fontWeight: true, - gridArea: true, - gridColumn: true, - gridColumnEnd: true, - gridColumnStart: true, - gridRow: true, - gridRowEnd: true, - gridRowStart: true, - lineHeight: true, - opacity: true, - order: true, - orphans: true, - scale: true, - widows: true, - zIndex: true, - zoom: true, - - // SVG-related - fillOpacity: true, - floodOpacity: true, - stopOpacity: true, - strokeMiterlimit: true, - strokeOpacity: true - }, - - // Add in properties whose names you wish to fix before - // setting or getting the value - cssProps: {}, + cssHooks: {}, // Get and set the style property on a DOM Node style: function( elem, name, value, extra ) { @@ -6787,7 +5884,7 @@ jQuery.extend( { // Make sure that we're working with the right name var ret, type, hooks, - origName = camelCase( name ), + origName = cssCamelCase( name ), isCustomProp = rcustomProp.test( name ), style = elem.style; @@ -6818,15 +5915,14 @@ jQuery.extend( { return; } - // If a number was passed in, add the unit (except for certain CSS properties) - // The isCustomProp check can be removed in jQuery 4.0 when we only auto-append - // "px" to a few hardcoded values. - if ( type === "number" && !isCustomProp ) { - value += ret && ret[ 3 ] || ( jQuery.cssNumber[ origName ] ? "" : "px" ); + // If the value is a number, add `px` for certain CSS properties + if ( type === "number" ) { + value += ret && ret[ 3 ] || ( isAutoPx( origName ) ? "px" : "" ); } - // background-* props affect original clone's values - if ( !support.clearCloneStyle && value === "" && name.indexOf( "background" ) === 0 ) { + // Support: IE <=9 - 11+ + // background-* props of a cloned element affect the source element (trac-8908) + if ( isIE && value === "" && name.indexOf( "background" ) === 0 ) { style[ name ] = "inherit"; } @@ -6857,7 +5953,7 @@ jQuery.extend( { css: function( elem, name, extra, styles ) { var val, num, hooks, - origName = camelCase( name ), + origName = cssCamelCase( name ), isCustomProp = rcustomProp.test( name ); // Make sure that we're working with the right name. We don't @@ -6900,17 +5996,9 @@ jQuery.each( [ "height", "width" ], function( _i, dimension ) { get: function( elem, computed, extra ) { if ( computed ) { - // Certain elements can have dimension info if we invisibly show them - // but it must have a current display style that would benefit - return rdisplayswap.test( jQuery.css( elem, "display" ) ) && - - // Support: Safari 8+ - // Table columns in Safari have non-zero offsetWidth & zero - // getBoundingClientRect().width unless display is changed. - // Support: IE <=11 only - // Running getBoundingClientRect on a disconnected node - // in IE throws an error. - ( !elem.getClientRects().length || !elem.getBoundingClientRect().width ) ? + // Elements with `display: none` can have dimension info if + // we invisibly show them. + return jQuery.css( elem, "display" ) === "none" ? swap( elem, cssShow, function() { return getWidthOrHeight( elem, dimension, extra ); } ) : @@ -6922,14 +6010,8 @@ jQuery.each( [ "height", "width" ], function( _i, dimension ) { var matches, styles = getStyles( elem ), - // Only read styles.position if the test has a chance to fail - // to avoid forcing a reflow. - scrollboxSizeBuggy = !support.scrollboxSize() && - styles.position === "absolute", - // To avoid forcing a reflow, only fetch boxSizing if we need it (gh-3991) - boxSizingNeeded = scrollboxSizeBuggy || extra, - isBorderBox = boxSizingNeeded && + isBorderBox = extra && jQuery.css( elem, "boxSizing", false, styles ) === "border-box", subtract = extra ? boxModelAdjustment( @@ -6941,17 +6023,6 @@ jQuery.each( [ "height", "width" ], function( _i, dimension ) { ) : 0; - // Account for unreliable border-box dimensions by comparing offset* to computed and - // faking a content-box to get border and padding (gh-3699) - if ( isBorderBox && scrollboxSizeBuggy ) { - subtract -= Math.ceil( - elem[ "offset" + dimension[ 0 ].toUpperCase() + dimension.slice( 1 ) ] - - parseFloat( styles[ dimension ] ) - - boxModelAdjustment( elem, dimension, "border", false, styles ) - - 0.5 - ); - } - // Convert to pixels if value adjustment is needed if ( subtract && ( matches = rcssNum.exec( value ) ) && ( matches[ 3 ] || "px" ) !== "px" ) { @@ -6965,19 +6036,6 @@ jQuery.each( [ "height", "width" ], function( _i, dimension ) { }; } ); -jQuery.cssHooks.marginLeft = addGetHookIf( support.reliableMarginLeft, - function( elem, computed ) { - if ( computed ) { - return ( parseFloat( curCSS( elem, "marginLeft" ) ) || - elem.getBoundingClientRect().left - - swap( elem, { marginLeft: 0 }, function() { - return elem.getBoundingClientRect().left; - } ) - ) + "px"; - } - } -); - // These hooks are used by animate to expand properties jQuery.each( { margin: "", @@ -7031,891 +6089,127 @@ jQuery.fn.extend( { } } ); - -// Based off of the plugin by Clint Helfers, with permission. -jQuery.fn.delay = function( time, type ) { - time = jQuery.fx ? jQuery.fx.speeds[ time ] || time : time; - type = type || "fx"; - - return this.queue( type, function( next, hooks ) { - var timeout = window.setTimeout( next, time ); - hooks.stop = function() { - window.clearTimeout( timeout ); - }; - } ); +jQuery.expr.pseudos.hidden = function( elem ) { + return !jQuery.expr.pseudos.visible( elem ); +}; +jQuery.expr.pseudos.visible = function( elem ) { + return !!( elem.offsetWidth || elem.offsetHeight || elem.getClientRects().length ); }; +// isHiddenWithinTree reports if an element has a non-"none" display style (inline and/or +// through the CSS cascade), which is useful in deciding whether or not to make it visible. +// It differs from the :hidden selector (jQuery.expr.pseudos.hidden) in two important ways: +// * A hidden ancestor does not force an element to be classified as hidden. +// * Being disconnected from the document does not force an element to be classified as hidden. +// These differences improve the behavior of .toggle() et al. when applied to elements that are +// detached or contained within hidden ancestors (gh-2404, gh-2863). +function isHiddenWithinTree( elem, el ) { -( function() { - var input = document.createElement( "input" ), - select = document.createElement( "select" ), - opt = select.appendChild( document.createElement( "option" ) ); + // isHiddenWithinTree might be called from jQuery#filter function; + // in that case, element will be second argument + elem = elem; - input.type = "checkbox"; + // Inline style trumps all + return elem.style.display === "none" || + elem.style.display === "" && + jQuery.css( elem, "display" ) === "none"; +} - // Support: Android <=4.3 only - // Default value for a checkbox should be "on" - support.checkOn = input.value !== ""; +var defaultDisplayMap = {}; - // Support: IE <=11 only - // Must access selectedIndex to make default options select - support.optSelected = opt.selected; +function getDefaultDisplay( elem ) { + var temp, + doc = elem.ownerDocument, + nodeName = elem.nodeName, + display = defaultDisplayMap[ nodeName ]; - // Support: IE <=11 only - // An input loses its value after becoming a radio - input = document.createElement( "input" ); - input.value = "t"; - input.type = "radio"; - support.radioValue = input.value === "t"; -} )(); - - -var boolHook, - attrHandle = jQuery.expr.attrHandle; - -jQuery.fn.extend( { - attr: function( name, value ) { - return access( this, jQuery.attr, name, value, arguments.length > 1 ); - }, - - removeAttr: function( name ) { - return this.each( function() { - jQuery.removeAttr( this, name ); - } ); + if ( display ) { + return display; } -} ); -jQuery.extend( { - attr: function( elem, name, value ) { - var ret, hooks, - nType = elem.nodeType; + temp = doc.body.appendChild( doc.createElement( nodeName ) ); + display = jQuery.css( temp, "display" ); - // Don't get/set attributes on text, comment and attribute nodes - if ( nType === 3 || nType === 8 || nType === 2 ) { - return; + temp.parentNode.removeChild( temp ); + + if ( display === "none" ) { + display = "block"; + } + defaultDisplayMap[ nodeName ] = display; + + return display; +} + +function showHide( elements, show ) { + var display, elem, + values = [], + index = 0, + length = elements.length; + + // Determine new display value for elements that need to change + for ( ; index < length; index++ ) { + elem = elements[ index ]; + if ( !elem.style ) { + continue; } - // Fallback to prop when attributes are not supported - if ( typeof elem.getAttribute === "undefined" ) { - return jQuery.prop( elem, name, value ); - } + display = elem.style.display; + if ( show ) { - // Attribute hooks are determined by the lowercase version - // Grab necessary hook if one is defined - if ( nType !== 1 || !jQuery.isXMLDoc( elem ) ) { - hooks = jQuery.attrHooks[ name.toLowerCase() ] || - ( jQuery.expr.match.bool.test( name ) ? boolHook : undefined ); - } - - if ( value !== undefined ) { - if ( value === null ) { - jQuery.removeAttr( elem, name ); - return; - } - - if ( hooks && "set" in hooks && - ( ret = hooks.set( elem, value, name ) ) !== undefined ) { - return ret; - } - - elem.setAttribute( name, value + "" ); - return value; - } - - if ( hooks && "get" in hooks && ( ret = hooks.get( elem, name ) ) !== null ) { - return ret; - } - - ret = jQuery.find.attr( elem, name ); - - // Non-existent attributes return null, we normalize to undefined - return ret == null ? undefined : ret; - }, - - attrHooks: { - type: { - set: function( elem, value ) { - if ( !support.radioValue && value === "radio" && - nodeName( elem, "input" ) ) { - var val = elem.value; - elem.setAttribute( "type", value ); - if ( val ) { - elem.value = val; - } - return value; + // Since we force visibility upon cascade-hidden elements, an immediate (and slow) + // check is required in this first loop unless we have a nonempty display value (either + // inline or about-to-be-restored) + if ( display === "none" ) { + values[ index ] = dataPriv.get( elem, "display" ) || null; + if ( !values[ index ] ) { + elem.style.display = ""; } } - } - }, - - removeAttr: function( elem, value ) { - var name, - i = 0, - - // Attribute names can contain non-HTML whitespace characters - // https://html.spec.whatwg.org/multipage/syntax.html#attributes-2 - attrNames = value && value.match( rnothtmlwhite ); - - if ( attrNames && elem.nodeType === 1 ) { - while ( ( name = attrNames[ i++ ] ) ) { - elem.removeAttribute( name ); + if ( elem.style.display === "" && isHiddenWithinTree( elem ) ) { + values[ index ] = getDefaultDisplay( elem ); } - } - } -} ); - -// Hooks for boolean attributes -boolHook = { - set: function( elem, value, name ) { - if ( value === false ) { - - // Remove boolean attributes when set to false - jQuery.removeAttr( elem, name ); } else { - elem.setAttribute( name, name ); - } - return name; - } -}; + if ( display !== "none" ) { + values[ index ] = "none"; -jQuery.each( jQuery.expr.match.bool.source.match( /\w+/g ), function( _i, name ) { - var getter = attrHandle[ name ] || jQuery.find.attr; - - attrHandle[ name ] = function( elem, name, isXML ) { - var ret, handle, - lowercaseName = name.toLowerCase(); - - if ( !isXML ) { - - // Avoid an infinite loop by temporarily removing this function from the getter - handle = attrHandle[ lowercaseName ]; - attrHandle[ lowercaseName ] = ret; - ret = getter( elem, name, isXML ) != null ? - lowercaseName : - null; - attrHandle[ lowercaseName ] = handle; - } - return ret; - }; -} ); - - - - -var rfocusable = /^(?:input|select|textarea|button)$/i, - rclickable = /^(?:a|area)$/i; - -jQuery.fn.extend( { - prop: function( name, value ) { - return access( this, jQuery.prop, name, value, arguments.length > 1 ); - }, - - removeProp: function( name ) { - return this.each( function() { - delete this[ jQuery.propFix[ name ] || name ]; - } ); - } -} ); - -jQuery.extend( { - prop: function( elem, name, value ) { - var ret, hooks, - nType = elem.nodeType; - - // Don't get/set properties on text, comment and attribute nodes - if ( nType === 3 || nType === 8 || nType === 2 ) { - return; - } - - if ( nType !== 1 || !jQuery.isXMLDoc( elem ) ) { - - // Fix name and attach hooks - name = jQuery.propFix[ name ] || name; - hooks = jQuery.propHooks[ name ]; - } - - if ( value !== undefined ) { - if ( hooks && "set" in hooks && - ( ret = hooks.set( elem, value, name ) ) !== undefined ) { - return ret; - } - - return ( elem[ name ] = value ); - } - - if ( hooks && "get" in hooks && ( ret = hooks.get( elem, name ) ) !== null ) { - return ret; - } - - return elem[ name ]; - }, - - propHooks: { - tabIndex: { - get: function( elem ) { - - // Support: IE <=9 - 11 only - // elem.tabIndex doesn't always return the - // correct value when it hasn't been explicitly set - // Use proper attribute retrieval (trac-12072) - var tabindex = jQuery.find.attr( elem, "tabindex" ); - - if ( tabindex ) { - return parseInt( tabindex, 10 ); - } - - if ( - rfocusable.test( elem.nodeName ) || - rclickable.test( elem.nodeName ) && - elem.href - ) { - return 0; - } - - return -1; + // Remember what we're overwriting + dataPriv.set( elem, "display", display ); } } - }, - - propFix: { - "for": "htmlFor", - "class": "className" } -} ); -// Support: IE <=11 only -// Accessing the selectedIndex property -// forces the browser to respect setting selected -// on the option -// The getter ensures a default option is selected -// when in an optgroup -// eslint rule "no-unused-expressions" is disabled for this code -// since it considers such accessions noop -if ( !support.optSelected ) { - jQuery.propHooks.selected = { - get: function( elem ) { - - /* eslint no-unused-expressions: "off" */ - - var parent = elem.parentNode; - if ( parent && parent.parentNode ) { - parent.parentNode.selectedIndex; - } - return null; - }, - set: function( elem ) { - - /* eslint no-unused-expressions: "off" */ - - var parent = elem.parentNode; - if ( parent ) { - parent.selectedIndex; - - if ( parent.parentNode ) { - parent.parentNode.selectedIndex; - } - } + // Set the display of the elements in a second loop to avoid constant reflow + for ( index = 0; index < length; index++ ) { + if ( values[ index ] != null ) { + elements[ index ].style.display = values[ index ]; } - }; -} - -jQuery.each( [ - "tabIndex", - "readOnly", - "maxLength", - "cellSpacing", - "cellPadding", - "rowSpan", - "colSpan", - "useMap", - "frameBorder", - "contentEditable" -], function() { - jQuery.propFix[ this.toLowerCase() ] = this; -} ); - - - - - // Strip and collapse whitespace according to HTML spec - // https://infra.spec.whatwg.org/#strip-and-collapse-ascii-whitespace - function stripAndCollapse( value ) { - var tokens = value.match( rnothtmlwhite ) || []; - return tokens.join( " " ); } - -function getClass( elem ) { - return elem.getAttribute && elem.getAttribute( "class" ) || ""; -} - -function classesToArray( value ) { - if ( Array.isArray( value ) ) { - return value; - } - if ( typeof value === "string" ) { - return value.match( rnothtmlwhite ) || []; - } - return []; + return elements; } jQuery.fn.extend( { - addClass: function( value ) { - var classNames, cur, curValue, className, i, finalValue; - - if ( isFunction( value ) ) { - return this.each( function( j ) { - jQuery( this ).addClass( value.call( this, j, getClass( this ) ) ); - } ); - } - - classNames = classesToArray( value ); - - if ( classNames.length ) { - return this.each( function() { - curValue = getClass( this ); - cur = this.nodeType === 1 && ( " " + stripAndCollapse( curValue ) + " " ); - - if ( cur ) { - for ( i = 0; i < classNames.length; i++ ) { - className = classNames[ i ]; - if ( cur.indexOf( " " + className + " " ) < 0 ) { - cur += className + " "; - } - } - - // Only assign if different to avoid unneeded rendering. - finalValue = stripAndCollapse( cur ); - if ( curValue !== finalValue ) { - this.setAttribute( "class", finalValue ); - } - } - } ); - } - - return this; + show: function() { + return showHide( this, true ); }, - - removeClass: function( value ) { - var classNames, cur, curValue, className, i, finalValue; - - if ( isFunction( value ) ) { - return this.each( function( j ) { - jQuery( this ).removeClass( value.call( this, j, getClass( this ) ) ); - } ); - } - - if ( !arguments.length ) { - return this.attr( "class", "" ); - } - - classNames = classesToArray( value ); - - if ( classNames.length ) { - return this.each( function() { - curValue = getClass( this ); - - // This expression is here for better compressibility (see addClass) - cur = this.nodeType === 1 && ( " " + stripAndCollapse( curValue ) + " " ); - - if ( cur ) { - for ( i = 0; i < classNames.length; i++ ) { - className = classNames[ i ]; - - // Remove *all* instances - while ( cur.indexOf( " " + className + " " ) > -1 ) { - cur = cur.replace( " " + className + " ", " " ); - } - } - - // Only assign if different to avoid unneeded rendering. - finalValue = stripAndCollapse( cur ); - if ( curValue !== finalValue ) { - this.setAttribute( "class", finalValue ); - } - } - } ); - } - - return this; + hide: function() { + return showHide( this ); }, - - toggleClass: function( value, stateVal ) { - var classNames, className, i, self, - type = typeof value, - isValidValue = type === "string" || Array.isArray( value ); - - if ( isFunction( value ) ) { - return this.each( function( i ) { - jQuery( this ).toggleClass( - value.call( this, i, getClass( this ), stateVal ), - stateVal - ); - } ); + toggle: function( state ) { + if ( typeof state === "boolean" ) { + return state ? this.show() : this.hide(); } - if ( typeof stateVal === "boolean" && isValidValue ) { - return stateVal ? this.addClass( value ) : this.removeClass( value ); - } - - classNames = classesToArray( value ); - return this.each( function() { - if ( isValidValue ) { - - // Toggle individual class names - self = jQuery( this ); - - for ( i = 0; i < classNames.length; i++ ) { - className = classNames[ i ]; - - // Check each className given, space separated list - if ( self.hasClass( className ) ) { - self.removeClass( className ); - } else { - self.addClass( className ); - } - } - - // Toggle whole class name - } else if ( value === undefined || type === "boolean" ) { - className = getClass( this ); - if ( className ) { - - // Store className if set - dataPriv.set( this, "__className__", className ); - } - - // If the element has a class name or if we're passed `false`, - // then remove the whole classname (if there was one, the above saved it). - // Otherwise bring back whatever was previously saved (if anything), - // falling back to the empty string if nothing was stored. - if ( this.setAttribute ) { - this.setAttribute( "class", - className || value === false ? - "" : - dataPriv.get( this, "__className__" ) || "" - ); - } - } - } ); - }, - - hasClass: function( selector ) { - var className, elem, - i = 0; - - className = " " + selector + " "; - while ( ( elem = this[ i++ ] ) ) { - if ( elem.nodeType === 1 && - ( " " + stripAndCollapse( getClass( elem ) ) + " " ).indexOf( className ) > -1 ) { - return true; - } - } - - return false; - } -} ); - - - - -var rreturn = /\r/g; - -jQuery.fn.extend( { - val: function( value ) { - var hooks, ret, valueIsFunction, - elem = this[ 0 ]; - - if ( !arguments.length ) { - if ( elem ) { - hooks = jQuery.valHooks[ elem.type ] || - jQuery.valHooks[ elem.nodeName.toLowerCase() ]; - - if ( hooks && - "get" in hooks && - ( ret = hooks.get( elem, "value" ) ) !== undefined - ) { - return ret; - } - - ret = elem.value; - - // Handle most common string cases - if ( typeof ret === "string" ) { - return ret.replace( rreturn, "" ); - } - - // Handle cases where value is null/undef or number - return ret == null ? "" : ret; - } - - return; - } - - valueIsFunction = isFunction( value ); - - return this.each( function( i ) { - var val; - - if ( this.nodeType !== 1 ) { - return; - } - - if ( valueIsFunction ) { - val = value.call( this, i, jQuery( this ).val() ); + if ( isHiddenWithinTree( this ) ) { + jQuery( this ).show(); } else { - val = value; - } - - // Treat null/undefined as ""; convert numbers to string - if ( val == null ) { - val = ""; - - } else if ( typeof val === "number" ) { - val += ""; - - } else if ( Array.isArray( val ) ) { - val = jQuery.map( val, function( value ) { - return value == null ? "" : value + ""; - } ); - } - - hooks = jQuery.valHooks[ this.type ] || jQuery.valHooks[ this.nodeName.toLowerCase() ]; - - // If set returns undefined, fall back to normal setting - if ( !hooks || !( "set" in hooks ) || hooks.set( this, val, "value" ) === undefined ) { - this.value = val; + jQuery( this ).hide(); } } ); } } ); -jQuery.extend( { - valHooks: { - option: { - get: function( elem ) { - - var val = jQuery.find.attr( elem, "value" ); - return val != null ? - val : - - // Support: IE <=10 - 11 only - // option.text throws exceptions (trac-14686, trac-14858) - // Strip and collapse whitespace - // https://html.spec.whatwg.org/#strip-and-collapse-whitespace - stripAndCollapse( jQuery.text( elem ) ); - } - }, - select: { - get: function( elem ) { - var value, option, i, - options = elem.options, - index = elem.selectedIndex, - one = elem.type === "select-one", - values = one ? null : [], - max = one ? index + 1 : options.length; - - if ( index < 0 ) { - i = max; - - } else { - i = one ? index : 0; - } - - // Loop through all the selected options - for ( ; i < max; i++ ) { - option = options[ i ]; - - // Support: IE <=9 only - // IE8-9 doesn't update selected after form reset (trac-2551) - if ( ( option.selected || i === index ) && - - // Don't return options that are disabled or in a disabled optgroup - !option.disabled && - ( !option.parentNode.disabled || - !nodeName( option.parentNode, "optgroup" ) ) ) { - - // Get the specific value for the option - value = jQuery( option ).val(); - - // We don't need an array for one selects - if ( one ) { - return value; - } - - // Multi-Selects return an array - values.push( value ); - } - } - - return values; - }, - - set: function( elem, value ) { - var optionSet, option, - options = elem.options, - values = jQuery.makeArray( value ), - i = options.length; - - while ( i-- ) { - option = options[ i ]; - - /* eslint-disable no-cond-assign */ - - if ( option.selected = - jQuery.inArray( jQuery.valHooks.option.get( option ), values ) > -1 - ) { - optionSet = true; - } - - /* eslint-enable no-cond-assign */ - } - - // Force browsers to behave consistently when non-matching value is set - if ( !optionSet ) { - elem.selectedIndex = -1; - } - return values; - } - } - } -} ); - -// Radios and checkboxes getter/setter -jQuery.each( [ "radio", "checkbox" ], function() { - jQuery.valHooks[ this ] = { - set: function( elem, value ) { - if ( Array.isArray( value ) ) { - return ( elem.checked = jQuery.inArray( jQuery( elem ).val(), value ) > -1 ); - } - } - }; - if ( !support.checkOn ) { - jQuery.valHooks[ this ].get = function( elem ) { - return elem.getAttribute( "value" ) === null ? "on" : elem.value; - }; - } -} ); - - - - -// Return jQuery for attributes-only inclusion - - -// Cross-browser xml parsing -jQuery.parseXML = function( data ) { - var xml, parserErrorElem; - if ( !data || typeof data !== "string" ) { - return null; - } - - // Support: IE 9 - 11 only - // IE throws on parseFromString with invalid input. - try { - xml = ( new window.DOMParser() ).parseFromString( data, "text/xml" ); - } catch ( e ) {} - - parserErrorElem = xml && xml.getElementsByTagName( "parsererror" )[ 0 ]; - if ( !xml || parserErrorElem ) { - jQuery.error( "Invalid XML: " + ( - parserErrorElem ? - jQuery.map( parserErrorElem.childNodes, function( el ) { - return el.textContent; - } ).join( "\n" ) : - data - ) ); - } - return xml; -}; - - -var rfocusMorph = /^(?:focusinfocus|focusoutblur)$/, - stopPropagationCallback = function( e ) { - e.stopPropagation(); - }; - -jQuery.extend( jQuery.event, { - - trigger: function( event, data, elem, onlyHandlers ) { - - var i, cur, tmp, bubbleType, ontype, handle, special, lastElement, - eventPath = [ elem || document ], - type = hasOwn.call( event, "type" ) ? event.type : event, - namespaces = hasOwn.call( event, "namespace" ) ? event.namespace.split( "." ) : []; - - cur = lastElement = tmp = elem = elem || document; - - // Don't do events on text and comment nodes - if ( elem.nodeType === 3 || elem.nodeType === 8 ) { - return; - } - - // focus/blur morphs to focusin/out; ensure we're not firing them right now - if ( rfocusMorph.test( type + jQuery.event.triggered ) ) { - return; - } - - if ( type.indexOf( "." ) > -1 ) { - - // Namespaced trigger; create a regexp to match event type in handle() - namespaces = type.split( "." ); - type = namespaces.shift(); - namespaces.sort(); - } - ontype = type.indexOf( ":" ) < 0 && "on" + type; - - // Caller can pass in a jQuery.Event object, Object, or just an event type string - event = event[ jQuery.expando ] ? - event : - new jQuery.Event( type, typeof event === "object" && event ); - - // Trigger bitmask: & 1 for native handlers; & 2 for jQuery (always true) - event.isTrigger = onlyHandlers ? 2 : 3; - event.namespace = namespaces.join( "." ); - event.rnamespace = event.namespace ? - new RegExp( "(^|\\.)" + namespaces.join( "\\.(?:.*\\.|)" ) + "(\\.|$)" ) : - null; - - // Clean up the event in case it is being reused - event.result = undefined; - if ( !event.target ) { - event.target = elem; - } - - // Clone any incoming data and prepend the event, creating the handler arg list - data = data == null ? - [ event ] : - jQuery.makeArray( data, [ event ] ); - - // Allow special events to draw outside the lines - special = jQuery.event.special[ type ] || {}; - if ( !onlyHandlers && special.trigger && special.trigger.apply( elem, data ) === false ) { - return; - } - - // Determine event propagation path in advance, per W3C events spec (trac-9951) - // Bubble up to document, then to window; watch for a global ownerDocument var (trac-9724) - if ( !onlyHandlers && !special.noBubble && !isWindow( elem ) ) { - - bubbleType = special.delegateType || type; - if ( !rfocusMorph.test( bubbleType + type ) ) { - cur = cur.parentNode; - } - for ( ; cur; cur = cur.parentNode ) { - eventPath.push( cur ); - tmp = cur; - } - - // Only add window if we got to document (e.g., not plain obj or detached DOM) - if ( tmp === ( elem.ownerDocument || document ) ) { - eventPath.push( tmp.defaultView || tmp.parentWindow || window ); - } - } - - // Fire handlers on the event path - i = 0; - while ( ( cur = eventPath[ i++ ] ) && !event.isPropagationStopped() ) { - lastElement = cur; - event.type = i > 1 ? - bubbleType : - special.bindType || type; - - // jQuery handler - handle = ( dataPriv.get( cur, "events" ) || Object.create( null ) )[ event.type ] && - dataPriv.get( cur, "handle" ); - if ( handle ) { - handle.apply( cur, data ); - } - - // Native handler - handle = ontype && cur[ ontype ]; - if ( handle && handle.apply && acceptData( cur ) ) { - event.result = handle.apply( cur, data ); - if ( event.result === false ) { - event.preventDefault(); - } - } - } - event.type = type; - - // If nobody prevented the default action, do it now - if ( !onlyHandlers && !event.isDefaultPrevented() ) { - - if ( ( !special._default || - special._default.apply( eventPath.pop(), data ) === false ) && - acceptData( elem ) ) { - - // Call a native DOM method on the target with the same name as the event. - // Don't do default actions on window, that's where global variables be (trac-6170) - if ( ontype && isFunction( elem[ type ] ) && !isWindow( elem ) ) { - - // Don't re-trigger an onFOO event when we call its FOO() method - tmp = elem[ ontype ]; - - if ( tmp ) { - elem[ ontype ] = null; - } - - // Prevent re-triggering of the same event, since we already bubbled it above - jQuery.event.triggered = type; - - if ( event.isPropagationStopped() ) { - lastElement.addEventListener( type, stopPropagationCallback ); - } - - elem[ type ](); - - if ( event.isPropagationStopped() ) { - lastElement.removeEventListener( type, stopPropagationCallback ); - } - - jQuery.event.triggered = undefined; - - if ( tmp ) { - elem[ ontype ] = tmp; - } - } - } - } - - return event.result; - }, - - // Piggyback on a donor event to simulate a different one - // Used only for `focus(in | out)` events - simulate: function( type, elem, event ) { - var e = jQuery.extend( - new jQuery.Event(), - event, - { - type: type, - isSimulated: true - } - ); - - jQuery.event.trigger( e, null, elem ); - } - -} ); - -jQuery.fn.extend( { - - trigger: function( type, data ) { - return this.each( function() { - jQuery.event.trigger( type, data, this ); - } ); - }, - triggerHandler: function( type, data ) { - var elem = this[ 0 ]; - if ( elem ) { - return jQuery.event.trigger( type, data, elem, true ); - } - } -} ); - - var rbracket = /\[\]$/, rCRLF = /\r?\n/g, @@ -7968,7 +6262,7 @@ jQuery.param = function( a, traditional ) { add = function( key, valueOrFunction ) { // If value is a function, invoke it and use its return value - var value = isFunction( valueOrFunction ) ? + var value = typeof valueOrFunction === "function" ? valueOrFunction() : valueOrFunction; @@ -8036,102 +6330,38 @@ jQuery.fn.extend( { } } ); - -jQuery.fn.extend( { - wrapAll: function( html ) { - var wrap; - - if ( this[ 0 ] ) { - if ( isFunction( html ) ) { - html = html.call( this[ 0 ] ); - } - - // The elements to wrap the target around - wrap = jQuery( html, this[ 0 ].ownerDocument ).eq( 0 ).clone( true ); - - if ( this[ 0 ].parentNode ) { - wrap.insertBefore( this[ 0 ] ); - } - - wrap.map( function() { - var elem = this; - - while ( elem.firstElementChild ) { - elem = elem.firstElementChild; - } - - return elem; - } ).append( this ); - } - - return this; - }, - - wrapInner: function( html ) { - if ( isFunction( html ) ) { - return this.each( function( i ) { - jQuery( this ).wrapInner( html.call( this, i ) ); - } ); - } - - return this.each( function() { - var self = jQuery( this ), - contents = self.contents(); - - if ( contents.length ) { - contents.wrapAll( html ); - - } else { - self.append( html ); - } - } ); - }, - - wrap: function( html ) { - var htmlIsFunction = isFunction( html ); - - return this.each( function( i ) { - jQuery( this ).wrapAll( htmlIsFunction ? html.call( this, i ) : html ); - } ); - }, - - unwrap: function( selector ) { - this.parent( selector ).not( "body" ).each( function() { - jQuery( this ).replaceWith( this.childNodes ); - } ); - return this; +// Cross-browser xml parsing +jQuery.parseXML = function( data ) { + var xml, parserErrorElem; + if ( !data || typeof data !== "string" ) { + return null; } -} ); + // Support: IE 9 - 11+ + // IE throws on parseFromString with invalid input. + try { + xml = ( new window.DOMParser() ).parseFromString( data, "text/xml" ); + } catch ( e ) {} -jQuery.expr.pseudos.hidden = function( elem ) { - return !jQuery.expr.pseudos.visible( elem ); -}; -jQuery.expr.pseudos.visible = function( elem ) { - return !!( elem.offsetWidth || elem.offsetHeight || elem.getClientRects().length ); + parserErrorElem = xml && xml.getElementsByTagName( "parsererror" )[ 0 ]; + if ( !xml || parserErrorElem ) { + jQuery.error( "Invalid XML: " + ( + parserErrorElem ? + jQuery.map( parserErrorElem.childNodes, function( el ) { + return el.textContent; + } ).join( "\n" ) : + data + ) ); + } + return xml; }; - - - -// Support: Safari 8 only -// In Safari 8 documents created via document.implementation.createHTMLDocument -// collapse sibling forms: the second one becomes a child of the first one. -// Because of that, this security measure has to be disabled in Safari 8. -// https://bugs.webkit.org/show_bug.cgi?id=137337 -support.createHTMLDocument = ( function() { - var body = document.implementation.createHTMLDocument( "" ).body; - body.innerHTML = "
"; - return body.childNodes.length === 2; -} )(); - - -// Argument "data" should be string of html +// Argument "data" should be string of html or a TrustedHTML wrapper of obvious HTML // context (optional): If specified, the fragment will be created in this context, // defaults to document // keepScripts (optional): If true, will include scripts passed in the html string jQuery.parseHTML = function( data, context, keepScripts ) { - if ( typeof data !== "string" ) { + if ( typeof data !== "string" && !isObviousHtml( data + "" ) ) { return []; } if ( typeof context === "boolean" ) { @@ -8139,24 +6369,14 @@ jQuery.parseHTML = function( data, context, keepScripts ) { context = false; } - var base, parsed, scripts; + var parsed, scripts; if ( !context ) { // Stop scripts or inline event handlers from being executed immediately - // by using document.implementation - if ( support.createHTMLDocument ) { - context = document.implementation.createHTMLDocument( "" ); - - // Set the base href for the created document - // so any parsed elements with URLs - // are based on the document's URL (gh-2965) - base = context.createElement( "base" ); - base.href = document.location.href; - context.head.appendChild( base ); - } else { - context = document; - } + // by using DOMParser + context = ( new window.DOMParser() ) + .parseFromString( "", "text/html" ); } parsed = rsingleTag.exec( data ); @@ -8176,7 +6396,6 @@ jQuery.parseHTML = function( data, context, keepScripts ) { return jQuery.merge( [], parsed.childNodes ); }; - jQuery.offset = { setOffset: function( elem, options, i ) { var curPosition, curLeft, curCSSTop, curTop, curOffset, curCSSLeft, calculatePosition, @@ -8207,7 +6426,7 @@ jQuery.offset = { curLeft = parseFloat( curCSSLeft ) || 0; } - if ( isFunction( options ) ) { + if ( typeof options === "function" ) { // Use jQuery.extend here to allow modification of coordinates argument (gh-1848) options = options.call( elem, i, jQuery.extend( {}, curOffset ) ); @@ -8251,7 +6470,7 @@ jQuery.fn.extend( { } // Return zeros for disconnected and hidden (display: none) elements (gh-2310) - // Support: IE <=11 only + // Support: IE <=11+ // Running getBoundingClientRect on a // disconnected node in IE throws an error if ( !elem.getClientRects().length ) { @@ -8292,12 +6511,13 @@ jQuery.fn.extend( { doc = elem.ownerDocument; offsetParent = elem.offsetParent || doc.documentElement; while ( offsetParent && - ( offsetParent === doc.body || offsetParent === doc.documentElement ) && + offsetParent !== doc.documentElement && jQuery.css( offsetParent, "position" ) === "static" ) { - offsetParent = offsetParent.parentNode; + offsetParent = offsetParent.offsetParent || doc.documentElement; } - if ( offsetParent && offsetParent !== elem && offsetParent.nodeType === 1 ) { + if ( offsetParent && offsetParent !== elem && offsetParent.nodeType === 1 && + jQuery.css( offsetParent, "position" ) !== "static" ) { // Incorporate borders into its offset, since they are outside its content origin parentOffset = jQuery( offsetParent ).offset(); @@ -8331,7 +6551,7 @@ jQuery.fn.extend( { offsetParent = offsetParent.offsetParent; } - return offsetParent || documentElement; + return offsetParent || documentElement$1; } ); } } ); @@ -8368,28 +6588,6 @@ jQuery.each( { scrollLeft: "pageXOffset", scrollTop: "pageYOffset" }, function( }; } ); -// Support: Safari <=7 - 9.1, Chrome <=37 - 49 -// Add the top/left cssHooks using jQuery.fn.position -// Webkit bug: https://bugs.webkit.org/show_bug.cgi?id=29084 -// Blink bug: https://bugs.chromium.org/p/chromium/issues/detail?id=589347 -// getComputedStyle returns percent when specified for top/left/bottom/right; -// rather than make the css module depend on the offset module, just check for it here -jQuery.each( [ "top", "left" ], function( _i, prop ) { - jQuery.cssHooks[ prop ] = addGetHookIf( support.pixelPosition, - function( elem, computed ) { - if ( computed ) { - computed = curCSS( elem, prop ); - - // If curCSS returns percentage, fallback to offset - return rnumnonpx.test( computed ) ? - jQuery( elem ).position()[ prop ] + "px" : - computed; - } - } - ); -} ); - - // Create innerHeight, innerWidth, height, width, outerHeight and outerWidth methods jQuery.each( { Height: "height", Width: "width" }, function( name, type ) { jQuery.each( { @@ -8439,7 +6637,6 @@ jQuery.each( { Height: "height", Width: "width" }, function( name, type ) { } ); } ); - jQuery.fn.extend( { bind: function( types, data, fn ) { @@ -8482,15 +6679,6 @@ jQuery.each( } ); - - - -// Support: Android <=4.0 only -// Make sure we trim BOM and NBSP -// Require that the "whitespace run" starts from a non-whitespace -// to avoid O(N^2) behavior when the engine would try matching "\s+$" at each space position. -var rtrim = /^[\s\uFEFF\xA0]+|([^\s\uFEFF\xA0])[\s\uFEFF\xA0]+$/g; - // Bind a function to a context, optionally partially applying any // arguments. // jQuery.proxy is deprecated to promote standards (specifically Function#bind) @@ -8506,7 +6694,7 @@ jQuery.proxy = function( fn, context ) { // Quick check to determine if target is callable, in the spec // this throws a TypeError, but we will just return undefined. - if ( !isFunction( fn ) ) { + if ( typeof fn !== "function" ) { return undefined; } @@ -8529,37 +6717,8 @@ jQuery.holdReady = function( hold ) { jQuery.ready( true ); } }; -jQuery.isArray = Array.isArray; -jQuery.parseJSON = JSON.parse; -jQuery.nodeName = nodeName; -jQuery.isFunction = isFunction; -jQuery.isWindow = isWindow; -jQuery.camelCase = camelCase; -jQuery.type = toType; - -jQuery.now = Date.now; - -jQuery.isNumeric = function( obj ) { - - // As of jQuery 3.0, isNumeric is limited to - // strings and numbers (primitives or objects) - // that can be coerced to finite numbers (gh-2662) - var type = jQuery.type( obj ); - return ( type === "number" || type === "string" ) && - - // parseFloat NaNs numeric-cast false positives ("") - // ...but misinterprets leading-number strings, particularly hex literals ("0x...") - // subtraction forces infinities to NaN - !isNaN( obj - parseFloat( obj ) ); -}; - -jQuery.trim = function( text ) { - return text == null ? - "" : - ( text + "" ).replace( rtrim, "$1" ); -}; - +jQuery.expr[ ":" ] = jQuery.expr.filters = jQuery.expr.pseudos; // Register as a named AMD module, since jQuery can be concatenated with other // files that may use define, but not via a proper concatenation script that @@ -8580,9 +6739,6 @@ if ( typeof define === "function" && define.amd ) { } ); } - - - var // Map over jQuery in case of overwrite @@ -8604,14 +6760,97 @@ jQuery.noConflict = function( deep ) { }; // Expose jQuery and $ identifiers, even in AMD -// (trac-7102#comment:10, https://github.com/jquery/jquery/pull/557) +// (trac-7102#comment:10, gh-557) // and CommonJS for browser emulators (trac-13566) if ( typeof noGlobal === "undefined" ) { window.jQuery = window.$ = jQuery; } +var readyCallbacks = [], + whenReady = function( fn ) { + readyCallbacks.push( fn ); + }, + executeReady = function( fn ) { + // Prevent errors from freezing future callback execution (gh-1823) + // Not backwards-compatible as this does not execute sync + window.setTimeout( function() { + fn.call( document$1, jQuery ); + } ); + }; +jQuery.fn.ready = function( fn ) { + whenReady( fn ); + return this; +}; + +jQuery.extend( { + + // Is the DOM ready to be used? Set to true once it occurs. + isReady: false, + + // A counter to track how many items to wait for before + // the ready event fires. See trac-6781 + readyWait: 1, + + ready: function( wait ) { + + // Abort if there are pending holds or we're already ready + if ( wait === true ? --jQuery.readyWait : jQuery.isReady ) { + return; + } + + // Remember that the DOM is ready + jQuery.isReady = true; + + // If a normal DOM Ready event fired, decrement, and wait if need be + if ( wait !== true && --jQuery.readyWait > 0 ) { + return; + } + + whenReady = function( fn ) { + readyCallbacks.push( fn ); + + while ( readyCallbacks.length ) { + fn = readyCallbacks.shift(); + if ( typeof fn === "function" ) { + executeReady( fn ); + } + } + }; + + whenReady(); + } +} ); + +// Make jQuery.ready Promise consumable (gh-1778) +jQuery.ready.then = jQuery.fn.ready; + +/** + * The ready event handler and self cleanup method + */ +function completed() { + document$1.removeEventListener( "DOMContentLoaded", completed ); + window.removeEventListener( "load", completed ); + jQuery.ready(); +} + +// Catch cases where $(document).ready() is called +// after the browser event has already occurred. +if ( document$1.readyState !== "loading" ) { + + // Handle it asynchronously to allow scripts the opportunity to delay ready + window.setTimeout( jQuery.ready ); + +} else { + + // Use the handy event callback + document$1.addEventListener( "DOMContentLoaded", completed ); + + // A fallback to window.onload, that will always work + window.addEventListener( "load", completed ); +} return jQuery; + } ); diff --git a/src/static/templates/admin/organizations.hbs b/src/static/templates/admin/organizations.hbs index 130bb14b..14f06e34 100644 --- a/src/static/templates/admin/organizations.hbs +++ b/src/static/templates/admin/organizations.hbs @@ -59,7 +59,7 @@ - + diff --git a/src/static/templates/admin/users.hbs b/src/static/templates/admin/users.hbs index 52458012..73f0cbc8 100644 --- a/src/static/templates/admin/users.hbs +++ b/src/static/templates/admin/users.hbs @@ -153,7 +153,7 @@ - + From 7f65a254b3a7439fc4dd882706727f5b7e505cee Mon Sep 17 00:00:00 2001 From: "Helmut K. C. Tessarek" Date: Sun, 1 Feb 2026 16:35:03 -0500 Subject: [PATCH 15/89] refactor: improve tooltips in diagnostics page (#6765) The term "seems to" is used too loosely in many of the tooltips, but in these 2 instances it is wrong wording. An update is either available or not. If there is no update, one could argue that "seems to" is valid, since the Internet could be down to check for a new version. But in this situation the update is availble. It is impossible that an update seems to be available. --- src/static/templates/admin/diagnostics.hbs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/static/templates/admin/diagnostics.hbs b/src/static/templates/admin/diagnostics.hbs index f8edabb2..77f2c95b 100644 --- a/src/static/templates/admin/diagnostics.hbs +++ b/src/static/templates/admin/diagnostics.hbs @@ -8,7 +8,7 @@
Server Installed Ok - Update + Update Branched
@@ -23,8 +23,8 @@ {{#if page_data.web_vault_enabled}}
Web Installed Ok - Update - Pre-Release + Update + Pre-Release
{{page_data.active_web_release}} From 347279a12c2ad3b99faf01e71a8dc20dbf521ad7 Mon Sep 17 00:00:00 2001 From: Timshel Date: Mon, 2 Feb 2026 05:35:22 +0800 Subject: [PATCH 16/89] Empty AccountKeys when no private key (#6761) Co-authored-by: Timshel --- src/api/identity.rs | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/src/api/identity.rs b/src/api/identity.rs index 9eaa6b36..f5f2afd6 100644 --- a/src/api/identity.rs +++ b/src/api/identity.rs @@ -482,14 +482,18 @@ async fn authenticated_response( Value::Null }; - let account_keys = json!({ - "publicKeyEncryptionKeyPair": { - "wrappedPrivateKey": user.private_key, - "publicKey": user.public_key, - "Object": "publicKeyEncryptionKeyPair" - }, - "Object": "privateKeys" - }); + let account_keys = if user.private_key.is_some() { + json!({ + "publicKeyEncryptionKeyPair": { + "wrappedPrivateKey": user.private_key, + "publicKey": user.public_key, + "Object": "publicKeyEncryptionKeyPair" + }, + "Object": "privateKeys" + }) + } else { + Value::Null + }; let mut result = json!({ "access_token": auth_tokens.access_token(), From feecfb20daeee7c61af84a904fb1bf40a8fa056f Mon Sep 17 00:00:00 2001 From: Stefan Melmuk <509385+stefan0xC@users.noreply.github.com> Date: Sun, 1 Feb 2026 22:35:55 +0100 Subject: [PATCH 17/89] fix error message for purging auth requests (#6776) --- src/api/core/accounts.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/api/core/accounts.rs b/src/api/core/accounts.rs index f5c32acb..0e01c1c4 100644 --- a/src/api/core/accounts.rs +++ b/src/api/core/accounts.rs @@ -1704,6 +1704,6 @@ pub async fn purge_auth_requests(pool: DbPool) { if let Ok(conn) = pool.get().await { AuthRequest::purge_expired_auth_requests(&conn).await; } else { - error!("Failed to get DB connection while purging trashed ciphers") + error!("Failed to get DB connection while purging auth requests") } } From d09c45bb63f734e0b3da32a5cbef09e91c27e8c0 Mon Sep 17 00:00:00 2001 From: Mathijs van Veluw Date: Sun, 8 Feb 2026 19:24:20 +0100 Subject: [PATCH 18/89] Misc updates, crates, rust, js, gha, vault (#6799) --- .github/workflows/release.yml | 20 +-- .github/workflows/trivy.yml | 2 +- .github/workflows/typos.yml | 2 +- .github/workflows/zizmor.yml | 2 +- .pre-commit-config.yaml | 2 +- Cargo.lock | 275 +++++++++++++++--------------- Cargo.toml | 20 +-- docker/DockerSettings.yaml | 6 +- docker/Dockerfile.alpine | 20 +-- docker/Dockerfile.debian | 14 +- macros/Cargo.toml | 2 +- rust-toolchain.toml | 2 +- src/static/scripts/datatables.css | 4 +- src/static/scripts/datatables.js | 16 +- 14 files changed, 197 insertions(+), 190 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 48cca0bb..fc117e30 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -102,7 +102,7 @@ jobs: # Login to Docker Hub - name: Login to Docker Hub - uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0 + uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} @@ -117,7 +117,7 @@ jobs: # Login to GitHub Container Registry - name: Login to GitHub Container Registry - uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0 + uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0 with: registry: ghcr.io username: ${{ github.repository_owner }} @@ -133,7 +133,7 @@ jobs: # Login to Quay.io - name: Login to Quay.io - uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0 + uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0 with: registry: quay.io username: ${{ secrets.QUAY_USERNAME }} @@ -233,7 +233,7 @@ jobs: # Upload artifacts to Github Actions and Attest the binaries - name: Attest binaries - uses: actions/attest-build-provenance@00014ed6ed5efc5b1ab7f7f34a39eb55d41aa4f8 # v3.1.0 + uses: actions/attest-build-provenance@96278af6caaf10aea03fd8d33a09a777ca52d62f # v3.2.0 with: subject-path: vaultwarden-${{ env.NORMALIZED_ARCH }} @@ -265,7 +265,7 @@ jobs: # Login to Docker Hub - name: Login to Docker Hub - uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0 + uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} @@ -280,7 +280,7 @@ jobs: # Login to GitHub Container Registry - name: Login to GitHub Container Registry - uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0 + uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0 with: registry: ghcr.io username: ${{ github.repository_owner }} @@ -296,7 +296,7 @@ jobs: # Login to Quay.io - name: Login to Quay.io - uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0 + uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0 with: registry: quay.io username: ${{ secrets.QUAY_USERNAME }} @@ -358,7 +358,7 @@ jobs: # Attest container images - name: Attest - docker.io - ${{ matrix.base_image }} if: ${{ env.HAVE_DOCKERHUB_LOGIN == 'true' && env.DIGEST_SHA != ''}} - uses: actions/attest-build-provenance@00014ed6ed5efc5b1ab7f7f34a39eb55d41aa4f8 # v3.1.0 + uses: actions/attest-build-provenance@96278af6caaf10aea03fd8d33a09a777ca52d62f # v3.2.0 with: subject-name: ${{ vars.DOCKERHUB_REPO }} subject-digest: ${{ env.DIGEST_SHA }} @@ -366,7 +366,7 @@ jobs: - name: Attest - ghcr.io - ${{ matrix.base_image }} if: ${{ env.HAVE_GHCR_LOGIN == 'true' && env.DIGEST_SHA != ''}} - uses: actions/attest-build-provenance@00014ed6ed5efc5b1ab7f7f34a39eb55d41aa4f8 # v3.1.0 + uses: actions/attest-build-provenance@96278af6caaf10aea03fd8d33a09a777ca52d62f # v3.2.0 with: subject-name: ${{ vars.GHCR_REPO }} subject-digest: ${{ env.DIGEST_SHA }} @@ -374,7 +374,7 @@ jobs: - name: Attest - quay.io - ${{ matrix.base_image }} if: ${{ env.HAVE_QUAY_LOGIN == 'true' && env.DIGEST_SHA != ''}} - uses: actions/attest-build-provenance@00014ed6ed5efc5b1ab7f7f34a39eb55d41aa4f8 # v3.1.0 + uses: actions/attest-build-provenance@96278af6caaf10aea03fd8d33a09a777ca52d62f # v3.2.0 with: subject-name: ${{ vars.QUAY_REPO }} subject-digest: ${{ env.DIGEST_SHA }} diff --git a/.github/workflows/trivy.yml b/.github/workflows/trivy.yml index 4aeb43b1..5ef08998 100644 --- a/.github/workflows/trivy.yml +++ b/.github/workflows/trivy.yml @@ -50,6 +50,6 @@ jobs: severity: CRITICAL,HIGH - name: Upload Trivy scan results to GitHub Security tab - uses: github/codeql-action/upload-sarif@cdefb33c0f6224e58673d9004f47f7cb3e328b89 # v4.31.10 + uses: github/codeql-action/upload-sarif@45cbd0c69e560cd9e7cd7f8c32362050c9b7ded2 # v4.32.2 with: sarif_file: 'trivy-results.sarif' diff --git a/.github/workflows/typos.yml b/.github/workflows/typos.yml index 45a596ce..99e2eacf 100644 --- a/.github/workflows/typos.yml +++ b/.github/workflows/typos.yml @@ -23,4 +23,4 @@ jobs: # When this version is updated, do not forget to update this in `.pre-commit-config.yaml` too - name: Spell Check Repo - uses: crate-ci/typos@65120634e79d8374d1aa2f27e54baa0c364fff5a # v1.42.1 + uses: crate-ci/typos@9066e9940a8a05b98fb4733c62a726f83c9e57f8 # v1.43.3 diff --git a/.github/workflows/zizmor.yml b/.github/workflows/zizmor.yml index 6083ef95..4051a8b2 100644 --- a/.github/workflows/zizmor.yml +++ b/.github/workflows/zizmor.yml @@ -24,7 +24,7 @@ jobs: persist-credentials: false - name: Run zizmor - uses: zizmorcore/zizmor-action@135698455da5c3b3e55f73f4419e481ab68cdd95 # v0.4.1 + uses: zizmorcore/zizmor-action@0dce2577a4760a2749d8cfb7a84b7d5585ebcb7d # v0.5.0 with: # intentionally not scanning the entire repository, # since it contains integration tests. diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 6da57526..771eb042 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -53,6 +53,6 @@ repos: - "cd docker && make" # When this version is updated, do not forget to update this in `.github/workflows/typos.yaml` too - repo: https://github.com/crate-ci/typos - rev: 65120634e79d8374d1aa2f27e54baa0c364fff5a # v1.42.1 + rev: 9066e9940a8a05b98fb4733c62a726f83c9e57f8 # v1.43.3 hooks: - id: typos diff --git a/Cargo.lock b/Cargo.lock index a4697493..d9bf20c6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -72,15 +72,15 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.100" +version = "1.0.101" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" +checksum = "5f0e0fee31ef5ed1ba1316088939cea399010ed7731dba877ed44aeb407a75ea" [[package]] name = "ar_archive_writer" -version = "0.2.0" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0c269894b6fe5e9d7ada0cf69b5bf847ff35bc25fc271f08e1d080fce80339a" +checksum = "7eb93bbb63b9c227414f6eb3a0adfddca591a8ce1e9b60661bb08969b87e340b" dependencies = [ "object", ] @@ -161,9 +161,9 @@ dependencies = [ [[package]] name = "async-compression" -version = "0.4.37" +version = "0.4.39" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d10e4f991a553474232bc0a31799f6d24b034a84c0971d80d2e2f78b2e576e40" +checksum = "68650b7df54f0293fd061972a0fb05aaf4fc0879d3b3d21a638a182c5c543b9f" dependencies = [ "compression-codecs", "compression-core", @@ -360,9 +360,9 @@ checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" [[package]] name = "aws-config" -version = "1.8.12" +version = "1.8.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96571e6996817bf3d58f6b569e4b9fd2e9d2fcf9f7424eed07b2ce9bb87535e5" +checksum = "c456581cb3c77fafcc8c67204a70680d40b61112d6da78c77bd31d945b65f1b5" dependencies = [ "aws-credential-types", "aws-runtime", @@ -402,9 +402,9 @@ dependencies = [ [[package]] name = "aws-runtime" -version = "1.5.18" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "959dab27ce613e6c9658eb3621064d0e2027e5f2acb65bc526a43577facea557" +checksum = "c635c2dc792cb4a11ce1a4f392a925340d1bdf499289b5ec1ec6810954eb43f5" dependencies = [ "aws-credential-types", "aws-sigv4", @@ -416,8 +416,8 @@ dependencies = [ "aws-types", "bytes", "fastrand", - "http 0.2.12", - "http-body 0.4.6", + "http 1.4.0", + "http-body 1.0.1", "percent-encoding", "pin-project-lite", "tracing", @@ -426,9 +426,9 @@ dependencies = [ [[package]] name = "aws-sdk-sso" -version = "1.92.0" +version = "1.93.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7d63bd2bdeeb49aa3f9b00c15e18583503b778b2e792fc06284d54e7d5b6566" +checksum = "9dcb38bb33fc0a11f1ffc3e3e85669e0a11a37690b86f77e75306d8f369146a0" dependencies = [ "aws-credential-types", "aws-runtime", @@ -443,15 +443,16 @@ dependencies = [ "bytes", "fastrand", "http 0.2.12", + "http 1.4.0", "regex-lite", "tracing", ] [[package]] name = "aws-sdk-ssooidc" -version = "1.94.0" +version = "1.95.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "532d93574bf731f311bafb761366f9ece345a0416dbcc273d81d6d1a1205239b" +checksum = "2ada8ffbea7bd1be1f53df1dadb0f8fdb04badb13185b3321b929d1ee3caad09" dependencies = [ "aws-credential-types", "aws-runtime", @@ -466,15 +467,16 @@ dependencies = [ "bytes", "fastrand", "http 0.2.12", + "http 1.4.0", "regex-lite", "tracing", ] [[package]] name = "aws-sdk-sts" -version = "1.96.0" +version = "1.97.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "357e9a029c7524db6a0099cd77fbd5da165540339e7296cca603531bc783b56c" +checksum = "e6443ccadc777095d5ed13e21f5c364878c9f5bad4e35187a6cdbd863b0afcad" dependencies = [ "aws-credential-types", "aws-runtime", @@ -490,15 +492,16 @@ dependencies = [ "aws-types", "fastrand", "http 0.2.12", + "http 1.4.0", "regex-lite", "tracing", ] [[package]] name = "aws-sigv4" -version = "1.3.7" +version = "1.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69e523e1c4e8e7e8ff219d732988e22bfeae8a1cafdbe6d9eca1546fa080be7c" +checksum = "efa49f3c607b92daae0c078d48a4571f599f966dce3caee5f1ea55c4d9073f99" dependencies = [ "aws-credential-types", "aws-smithy-http", @@ -518,9 +521,9 @@ dependencies = [ [[package]] name = "aws-smithy-async" -version = "1.2.7" +version = "1.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ee19095c7c4dda59f1697d028ce704c24b2d33c6718790c7f1d5a3015b4107c" +checksum = "52eec3db979d18cb807fc1070961cc51d87d069abe9ab57917769687368a8c6c" dependencies = [ "futures-util", "pin-project-lite", @@ -529,9 +532,9 @@ dependencies = [ [[package]] name = "aws-smithy-http" -version = "0.62.6" +version = "0.63.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "826141069295752372f8203c17f28e30c464d22899a43a0c9fd9c458d469c88b" +checksum = "630e67f2a31094ffa51b210ae030855cb8f3b7ee1329bdd8d085aaf61e8b97fc" dependencies = [ "aws-smithy-runtime-api", "aws-smithy-types", @@ -539,9 +542,9 @@ dependencies = [ "bytes-utils", "futures-core", "futures-util", - "http 0.2.12", "http 1.4.0", - "http-body 0.4.6", + "http-body 1.0.1", + "http-body-util", "percent-encoding", "pin-project-lite", "pin-utils", @@ -550,27 +553,27 @@ dependencies = [ [[package]] name = "aws-smithy-json" -version = "0.61.9" +version = "0.62.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49fa1213db31ac95288d981476f78d05d9cbb0353d22cdf3472cc05bb02f6551" +checksum = "3cb96aa208d62ee94104645f7b2ecaf77bf27edf161590b6224bfbac2832f979" dependencies = [ "aws-smithy-types", ] [[package]] name = "aws-smithy-observability" -version = "0.2.0" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef1fcbefc7ece1d70dcce29e490f269695dfca2d2bacdeaf9e5c3f799e4e6a42" +checksum = "c0a46543fbc94621080b3cf553eb4cbbdc41dd9780a30c4756400f0139440a1d" dependencies = [ "aws-smithy-runtime-api", ] [[package]] name = "aws-smithy-query" -version = "0.60.9" +version = "0.60.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae5d689cf437eae90460e944a58b5668530d433b4ff85789e69d2f2a556e057d" +checksum = "0cebbddb6f3a5bd81553643e9c7daf3cc3dc5b0b5f398ac668630e8a84e6fff0" dependencies = [ "aws-smithy-types", "urlencoding", @@ -578,9 +581,9 @@ dependencies = [ [[package]] name = "aws-smithy-runtime" -version = "1.9.8" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb5b6167fcdf47399024e81ac08e795180c576a20e4d4ce67949f9a88ae37dc1" +checksum = "f3df87c14f0127a0d77eb261c3bc45d5b4833e2a1f63583ebfb728e4852134ee" dependencies = [ "aws-smithy-async", "aws-smithy-http", @@ -593,6 +596,7 @@ dependencies = [ "http 1.4.0", "http-body 0.4.6", "http-body 1.0.1", + "http-body-util", "pin-project-lite", "pin-utils", "tokio", @@ -601,9 +605,9 @@ dependencies = [ [[package]] name = "aws-smithy-runtime-api" -version = "1.10.0" +version = "1.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "efce7aaaf59ad53c5412f14fc19b2d5c6ab2c3ec688d272fd31f76ec12f44fb0" +checksum = "49952c52f7eebb72ce2a754d3866cc0f87b97d2a46146b79f80f3a93fb2b3716" dependencies = [ "aws-smithy-async", "aws-smithy-types", @@ -618,9 +622,9 @@ dependencies = [ [[package]] name = "aws-smithy-types" -version = "1.3.6" +version = "1.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65f172bcb02424eb94425db8aed1b6d583b5104d4d5ddddf22402c661a320048" +checksum = "3b3a26048eeab0ddeba4b4f9d51654c79af8c3b32357dc5f336cee85ab331c33" dependencies = [ "base64-simd", "bytes", @@ -818,9 +822,9 @@ checksum = "175812e0be2bccb6abe50bb8d566126198344f707e304f45c648fd8f2cc0365e" [[package]] name = "bytemuck" -version = "1.24.0" +version = "1.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fbdf580320f38b612e485521afda1ee26d10cc9884efaaa750d383e13e3c5f4" +checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec" [[package]] name = "byteorder" @@ -830,9 +834,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.11.0" +version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" [[package]] name = "bytes-utils" @@ -922,9 +926,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.53" +version = "1.2.55" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "755d2fce177175ffca841e9a06afdb2c4ab0f593d53b4dee48147dfaade85932" +checksum = "47b26a0954ae34af09b50f0de26458fa95369a0d478d8236d3f93082b219bd29" dependencies = [ "find-msvc-tools", "jobserver", @@ -1469,9 +1473,9 @@ dependencies = [ [[package]] name = "diesel" -version = "2.3.5" +version = "2.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e130c806dccc85428c564f2dc5a96e05b6615a27c9a28776bd7761a9af4bb552" +checksum = "d9b6c2fc184a6fb6ebcf5f9a5e3bbfa84d8fd268cdfcce4ed508979a6259494d" dependencies = [ "bigdecimal", "bitflags", @@ -1506,9 +1510,9 @@ dependencies = [ [[package]] name = "diesel_derives" -version = "2.3.6" +version = "2.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c30b2969f923fa1f73744b92bb7df60b858df8832742d9a3aceb79236c0be1d2" +checksum = "47618bf0fac06bb670c036e48404c26a865e6a71af4114dfd97dfe89936e404e" dependencies = [ "diesel_table_macro_syntax", "dsl_auto_type", @@ -1823,15 +1827,15 @@ dependencies = [ [[package]] name = "find-msvc-tools" -version = "0.1.8" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8591b0bcc8a98a64310a2fae1bb3e9b8564dd10e381e6e28010fde8e8e8568db" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" [[package]] name = "flate2" -version = "1.1.8" +version = "1.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b375d6465b98090a5f25b1c7703f3859783755aa9a80433b36e0379a3ec2f369" +checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" dependencies = [ "crc32fast", "miniz_oxide", @@ -2447,14 +2451,13 @@ dependencies = [ [[package]] name = "hyper-util" -version = "0.1.19" +version = "0.1.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "727805d60e7938b76b826a6ef209eb70eaa1812794f9424d4a4e2d740662df5f" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" dependencies = [ "base64 0.22.1", "bytes", "futures-channel", - "futures-core", "futures-util", "http 1.4.0", "http-body 1.0.1", @@ -2463,7 +2466,7 @@ dependencies = [ "libc", "percent-encoding", "pin-project-lite", - "socket2 0.6.1", + "socket2 0.6.2", "system-configuration", "tokio", "tower-service", @@ -2473,9 +2476,9 @@ dependencies = [ [[package]] name = "iana-time-zone" -version = "0.1.64" +version = "0.1.65" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" dependencies = [ "android_system_properties", "core-foundation-sys", @@ -2704,9 +2707,9 @@ checksum = "47f142fe24a9c9944451e8349de0a56af5f3e7226dc46f3ed4d4ecc0b85af75e" [[package]] name = "jiff" -version = "0.2.18" +version = "0.2.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e67e8da4c49d6d9909fe03361f9b620f58898859f5c7aded68351e85e71ecf50" +checksum = "d89a5b5e10d5a9ad6e5d1f4bd58225f655d6fe9767575a5e8ac5a6fe64e04495" dependencies = [ "jiff-static", "jiff-tzdb-platform", @@ -2719,9 +2722,9 @@ dependencies = [ [[package]] name = "jiff-static" -version = "0.2.18" +version = "0.2.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0c84ee7f197eca9a86c6fd6cb771e55eb991632f15f2bc3ca6ec838929e6e78" +checksum = "ff7a39c8862fc1369215ccf0a8f12dd4598c7f6484704359f0351bd617034dbf" dependencies = [ "proc-macro2", "quote", @@ -2791,9 +2794,9 @@ dependencies = [ [[package]] name = "jsonwebtoken" -version = "10.2.0" +version = "10.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c76e1c7d7df3e34443b3621b459b066a7b79644f059fc8b2db7070c825fd417e" +checksum = "0529410abe238729a60b108898784df8984c87f6054c9c4fcacc47e4803c1ce1" dependencies = [ "base64 0.22.1", "ed25519-dalek", @@ -2864,7 +2867,7 @@ dependencies = [ "rustls 0.23.36", "rustls-native-certs", "serde", - "socket2 0.6.1", + "socket2 0.6.2", "tokio", "tokio-rustls 0.26.4", "tracing", @@ -2879,9 +2882,9 @@ checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc" [[package]] name = "libm" -version = "0.2.15" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" [[package]] name = "libmimalloc-sys" @@ -2990,9 +2993,9 @@ dependencies = [ [[package]] name = "memchr" -version = "2.7.6" +version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" [[package]] name = "migrations_internals" @@ -3074,9 +3077,9 @@ dependencies = [ [[package]] name = "moka" -version = "0.12.12" +version = "0.12.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3dec6bd31b08944e08b58fd99373893a6c17054d6f3ea5006cc894f4f4eee2a" +checksum = "b4ac832c50ced444ef6be0767a008b02c106a909ba79d1d830501e94b96f6b7e" dependencies = [ "crossbeam-channel", "crossbeam-epoch", @@ -3110,9 +3113,9 @@ dependencies = [ [[package]] name = "mysqlclient-sys" -version = "0.4.7" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86a34a2bdec189f1060343ba712983e14cad7e87515cfd9ac4653e207535b6b1" +checksum = "92ed7312f0cfc4032aea6f8ea2abb4d288e4413e33bf0c80ad30eef8aa8fb9d8" dependencies = [ "pkg-config", "semver", @@ -3198,9 +3201,9 @@ dependencies = [ [[package]] name = "num-conv" -version = "0.1.0" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" +checksum = "cf97ec579c3c42f953ef76dbf8d55ac91fb219dde70e49aa4a6b7d74e9919050" [[package]] name = "num-derive" @@ -3299,9 +3302,9 @@ dependencies = [ [[package]] name = "object" -version = "0.32.2" +version = "0.37.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6a622008b6e321afc04970976f62ee297fdbaa6f95318ca343e3eebb9648441" +checksum = "ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe" dependencies = [ "memchr", ] @@ -3425,9 +3428,9 @@ checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" [[package]] name = "openssl-src" -version = "300.5.4+3.5.4" +version = "300.5.5+3.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a507b3792995dae9b0df8a1c1e3771e8418b7c2d9f0baeba32e6fe8b06c7cb72" +checksum = "3f1787d533e03597a7934fd0a765f0d28e94ecc5fb7789f8053b1e699a56f709" dependencies = [ "cc", ] @@ -3606,9 +3609,9 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "pest" -version = "2.8.5" +version = "2.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c9eb05c21a464ea704b53158d358a31e6425db2f63a1a7312268b05fe2b75f7" +checksum = "e0848c601009d37dfa3430c4666e147e49cdcf1b92ecd3e63657d8a5f19da662" dependencies = [ "memchr", "ucd-trie", @@ -3616,9 +3619,9 @@ dependencies = [ [[package]] name = "pest_derive" -version = "2.8.5" +version = "2.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68f9dbced329c441fa79d80472764b1a2c7e57123553b8519b36663a2fb234ed" +checksum = "11f486f1ea21e6c10ed15d5a7c77165d0ee443402f0780849d1768e7d9d6fe77" dependencies = [ "pest", "pest_generator", @@ -3626,9 +3629,9 @@ dependencies = [ [[package]] name = "pest_generator" -version = "2.8.5" +version = "2.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3bb96d5051a78f44f43c8f712d8e810adb0ebf923fc9ed2655a7f66f63ba8ee5" +checksum = "8040c4647b13b210a963c1ed407c1ff4fdfa01c31d6d2a098218702e6664f94f" dependencies = [ "pest", "pest_meta", @@ -3639,9 +3642,9 @@ dependencies = [ [[package]] name = "pest_meta" -version = "2.8.5" +version = "2.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "602113b5b5e8621770cfd490cfd90b9f84ab29bd2b0e49ad83eb6d186cef2365" +checksum = "89815c69d36021a140146f26659a81d6c2afa33d216d736dd4be5381a7362220" dependencies = [ "pest", "sha2", @@ -3796,15 +3799,15 @@ dependencies = [ [[package]] name = "portable-atomic" -version = "1.13.0" +version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f89776e4d69bb58bc6993e99ffa1d11f228b839984854c7daeb5d37f87cbe950" +checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" [[package]] name = "portable-atomic-util" -version = "0.2.4" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8a2f0d8d040d7848a709caf78912debcc3f33ee4b3cac47d73d1e1069e83507" +checksum = "7a9db96d7fa8782dd8c15ce32ffe8680bbd1e978a43bf51a34d39483540495f5" dependencies = [ "portable-atomic", ] @@ -3883,9 +3886,9 @@ checksum = "33cb294fe86a74cbcf50d4445b37da762029549ebeea341421c7c70370f86cac" [[package]] name = "psm" -version = "0.1.28" +version = "0.1.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d11f2fedc3b7dafdc2851bc52f277377c5473d378859be234bc7ebb593144d01" +checksum = "3852766467df634d74f0b2d7819bf8dc483a0eb2e3b0f50f756f9cfe8b0d18d8" dependencies = [ "ar_archive_writer", "cc", @@ -3966,7 +3969,7 @@ dependencies = [ "quinn-udp", "rustc-hash", "rustls 0.23.36", - "socket2 0.6.1", + "socket2 0.6.2", "thiserror 2.0.18", "tokio", "tracing", @@ -4003,16 +4006,16 @@ dependencies = [ "cfg_aliases", "libc", "once_cell", - "socket2 0.6.1", + "socket2 0.6.2", "tracing", "windows-sys 0.60.2", ] [[package]] name = "quote" -version = "1.0.43" +version = "1.0.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc74d9a594b72ae6656596548f56f667211f8a97b3d4c3d467150794690dc40a" +checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4" dependencies = [ "proc-macro2", ] @@ -4139,9 +4142,9 @@ dependencies = [ [[package]] name = "regex" -version = "1.12.2" +version = "1.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4" +checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" dependencies = [ "aho-corasick", "memchr", @@ -4151,9 +4154,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.13" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" dependencies = [ "aho-corasick", "memchr", @@ -4162,15 +4165,15 @@ dependencies = [ [[package]] name = "regex-lite" -version = "0.1.8" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d942b98df5e658f56f20d592c7f868833fe38115e65c33003d8cd224b0155da" +checksum = "cab834c73d247e67f4fae452806d17d3c7501756d98c8808d7c9c7aa7d18f973" [[package]] name = "regex-syntax" -version = "0.8.8" +version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" +checksum = "a96887878f22d7bad8a3b6dc5b7440e0ada9a245242924394987b21cf2210a4c" [[package]] name = "reopen" @@ -4650,9 +4653,9 @@ dependencies = [ [[package]] name = "schemars" -version = "1.2.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54e910108742c57a770f492731f99be216a52fadd361b06c8fb59d74ccc267d2" +checksum = "a2b42f36aa1cd011945615b92222f6bf73c599a102a300334cd7f8dbeec726cc" dependencies = [ "dyn-clone", "ref-cast", @@ -4878,7 +4881,7 @@ dependencies = [ "indexmap 1.9.3", "indexmap 2.13.0", "schemars 0.9.0", - "schemars 1.2.0", + "schemars 1.2.1", "serde_core", "serde_json", "serde_with_macros", @@ -4984,9 +4987,9 @@ dependencies = [ [[package]] name = "siphasher" -version = "1.0.1" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d" +checksum = "b2aa850e253778c88a04c3d7323b043aeda9d3e30d5971937c1855769763678e" [[package]] name = "skeptic" @@ -5005,9 +5008,9 @@ dependencies = [ [[package]] name = "slab" -version = "0.4.11" +version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" [[package]] name = "smallvec" @@ -5027,9 +5030,9 @@ dependencies = [ [[package]] name = "socket2" -version = "0.6.1" +version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17129e116933cf371d018bb80ae557e889637989d8638274fb25622827b03881" +checksum = "86f4aa3ad99f2088c990dfa82d367e19cb29268ed67c574d10d0a4bfe71f07e0" dependencies = [ "libc", "windows-sys 0.60.2", @@ -5089,9 +5092,9 @@ checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" [[package]] name = "stacker" -version = "0.1.22" +version = "0.1.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1f8b29fb42aafcea4edeeb6b2f2d7ecd0d969c48b4cf0d2e64aafc471dd6e59" +checksum = "08d74a23609d509411d10e2176dc2a4346e3b4aea2e7b1869f19fdedbc71c013" dependencies = [ "cc", "cfg-if", @@ -5180,9 +5183,9 @@ dependencies = [ [[package]] name = "system-configuration" -version = "0.6.1" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" +checksum = "a13f3d0daba03132c0aa9767f98351b3488edc2c100cda2d2ec2b04f3d8d3c8b" dependencies = [ "bitflags", "core-foundation 0.9.4", @@ -5278,9 +5281,9 @@ dependencies = [ [[package]] name = "time" -version = "0.3.45" +version = "0.3.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9e442fc33d7fdb45aa9bfeb312c095964abdf596f7567261062b2a7107aaabd" +checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" dependencies = [ "deranged", "itoa", @@ -5295,15 +5298,15 @@ dependencies = [ [[package]] name = "time-core" -version = "0.1.7" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b36ee98fd31ec7426d599183e8fe26932a8dc1fb76ddb6214d05493377d34ca" +checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" [[package]] name = "time-macros" -version = "0.2.25" +version = "0.2.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "71e552d1249bf61ac2a52db88179fd0673def1e1ad8243a00d9ec9ed71fee3dd" +checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215" dependencies = [ "num-conv", "time-core", @@ -5355,7 +5358,7 @@ dependencies = [ "parking_lot", "pin-project-lite", "signal-hook-registry", - "socket2 0.6.1", + "socket2 0.6.2", "tokio-macros", "windows-sys 0.61.2", ] @@ -5759,9 +5762,9 @@ checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" [[package]] name = "uuid" -version = "1.19.0" +version = "1.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2e054861b4bd027cd373e18e8d8d8e6548085000e41290d95ce0c373a654b4a" +checksum = "ee48d38b119b0cd71fe4141b30f5ba9c7c5d9f4e7a3a8b4a674e4b6ef789976f" dependencies = [ "getrandom 0.3.4", "js-sys", @@ -5815,7 +5818,7 @@ dependencies = [ "html5gum", "http 1.4.0", "job_scheduler_ng", - "jsonwebtoken 10.2.0", + "jsonwebtoken 10.3.0", "lettre", "libsqlite3-sys", "log", @@ -6073,9 +6076,9 @@ dependencies = [ [[package]] name = "webpki-roots" -version = "1.0.5" +version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12bed680863276c63889429bfd6cab3b99943659923822de1c8a39c49e4d722c" +checksum = "22cfaf3c063993ff62e73cb4311efde4db1efb31ab78a3e5c457939ad5cc0bed" dependencies = [ "rustls-pki-types", ] @@ -6563,18 +6566,18 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.8.33" +version = "0.8.39" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "668f5168d10b9ee831de31933dc111a459c97ec93225beb307aed970d1372dfd" +checksum = "db6d35d663eadb6c932438e763b262fe1a70987f9ae936e60158176d710cae4a" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.33" +version = "0.8.39" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c7962b26b0a8685668b671ee4b54d007a67d4eaf05fda79ac0ecf41e32270f1" +checksum = "4122cd3169e94605190e77839c9a40d40ed048d305bfdc146e7df40ab0f3e517" dependencies = [ "proc-macro2", "quote", @@ -6643,9 +6646,9 @@ dependencies = [ [[package]] name = "zmij" -version = "1.0.16" +version = "1.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfcd145825aace48cff44a8844de64bf75feec3080e0aa5cdbde72961ae51a65" +checksum = "3ff05f8caa9038894637571ae6b9e29466c1f4f829d26c9b28f869a29cbe3445" [[package]] name = "zstd" diff --git a/Cargo.toml b/Cargo.toml index 9d54590e..88d8b3c4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [workspace.package] edition = "2021" -rust-version = "1.90.0" +rust-version = "1.91.0" license = "AGPL-3.0-only" repository = "https://github.com/dani-garcia/vaultwarden" publish = false @@ -88,7 +88,7 @@ serde_json = "1.0.149" # A safe, extensible ORM and Query builder # Currently pinned diesel to v2.3.3 as newer version break MySQL/MariaDB compatibility -diesel = { version = "2.3.5", features = ["chrono", "r2d2", "numeric"] } +diesel = { version = "2.3.6", features = ["chrono", "r2d2", "numeric"] } diesel_migrations = "2.3.1" derive_more = { version = "2.1.1", features = ["from", "into", "as_ref", "deref", "display"] } @@ -103,12 +103,12 @@ ring = "0.17.14" subtle = "2.6.1" # UUID generation -uuid = { version = "1.19.0", features = ["v4"] } +uuid = { version = "1.20.0", features = ["v4"] } # Date and time libraries chrono = { version = "0.4.43", features = ["clock", "serde"], default-features = false } chrono-tz = "0.10.4" -time = "0.3.45" +time = "0.3.47" # Job scheduler job_scheduler_ng = "2.4.0" @@ -117,7 +117,7 @@ job_scheduler_ng = "2.4.0" data-encoding = "2.10.0" # JWT library -jsonwebtoken = { version = "10.2.0", features = ["use_pem", "rust_crypto"], default-features = false } +jsonwebtoken = { version = "10.3.0", features = ["use_pem", "rust_crypto"], default-features = false } # TOTP library totp-lite = "2.0.1" @@ -149,9 +149,9 @@ hickory-resolver = "0.25.2" # Favicon extraction libraries html5gum = "0.8.3" -regex = { version = "1.12.2", features = ["std", "perf", "unicode-perl"], default-features = false } +regex = { version = "1.12.3", features = ["std", "perf", "unicode-perl"], default-features = false } data-url = "0.3.2" -bytes = "1.11.0" +bytes = "1.11.1" svg-hush = "0.9.5" # Cache function results (Used for version check and favicon fetching) @@ -197,10 +197,10 @@ grass_compiler = { version = "0.13.4", default-features = false } opendal = { version = "0.55.0", features = ["services-fs"], default-features = false } # For retrieving AWS credentials, including temporary SSO credentials -anyhow = { version = "1.0.100", optional = true } -aws-config = { version = "1.8.12", features = ["behavior-version-latest", "rt-tokio", "credentials-process", "sso"], default-features = false, optional = true } +anyhow = { version = "1.0.101", optional = true } +aws-config = { version = "1.8.13", features = ["behavior-version-latest", "rt-tokio", "credentials-process", "sso"], default-features = false, optional = true } aws-credential-types = { version = "1.2.11", optional = true } -aws-smithy-runtime-api = { version = "1.10.0", optional = true } +aws-smithy-runtime-api = { version = "1.11.3", optional = true } http = { version = "1.4.0", optional = true } reqsign = { version = "0.16.5", optional = true } diff --git a/docker/DockerSettings.yaml b/docker/DockerSettings.yaml index 02cd166b..a0f3c80a 100644 --- a/docker/DockerSettings.yaml +++ b/docker/DockerSettings.yaml @@ -1,11 +1,11 @@ --- -vault_version: "v2025.12.2" -vault_image_digest: "sha256:3c9aec4924c4f529af5e48888cfddd559e553ee62ce3e81372b50103bbaf20f7" +vault_version: "v2026.1.0" +vault_image_digest: "sha256:3537918638c8bd4f62f3f7c626a8959e4e48d35e3e0d3a567f98569d8a97eced" # Cross Compile Docker Helper Scripts v1.9.0 # We use the linux/amd64 platform shell scripts since there is no difference between the different platform scripts # https://github.com/tonistiigi/xx | https://hub.docker.com/r/tonistiigi/xx/tags xx_image_digest: "sha256:c64defb9ed5a91eacb37f96ccc3d4cd72521c4bd18d5442905b95e2226b0e707" -rust_version: 1.92.0 # Rust version to be used +rust_version: 1.93.0 # Rust version to be used debian_version: trixie # Debian release name to be used alpine_version: "3.23" # Alpine version to be used # For which platforms/architectures will we try to build images diff --git a/docker/Dockerfile.alpine b/docker/Dockerfile.alpine index 95aae642..9886b6c1 100644 --- a/docker/Dockerfile.alpine +++ b/docker/Dockerfile.alpine @@ -19,23 +19,23 @@ # - From https://hub.docker.com/r/vaultwarden/web-vault/tags, # click the tag name to view the digest of the image it currently points to. # - From the command line: -# $ docker pull docker.io/vaultwarden/web-vault:v2025.12.2 -# $ docker image inspect --format "{{.RepoDigests}}" docker.io/vaultwarden/web-vault:v2025.12.2 -# [docker.io/vaultwarden/web-vault@sha256:3c9aec4924c4f529af5e48888cfddd559e553ee62ce3e81372b50103bbaf20f7] +# $ docker pull docker.io/vaultwarden/web-vault:v2026.1.0 +# $ docker image inspect --format "{{.RepoDigests}}" docker.io/vaultwarden/web-vault:v2026.1.0 +# [docker.io/vaultwarden/web-vault@sha256:3537918638c8bd4f62f3f7c626a8959e4e48d35e3e0d3a567f98569d8a97eced] # # - Conversely, to get the tag name from the digest: -# $ docker image inspect --format "{{.RepoTags}}" docker.io/vaultwarden/web-vault@sha256:3c9aec4924c4f529af5e48888cfddd559e553ee62ce3e81372b50103bbaf20f7 -# [docker.io/vaultwarden/web-vault:v2025.12.2] +# $ docker image inspect --format "{{.RepoTags}}" docker.io/vaultwarden/web-vault@sha256:3537918638c8bd4f62f3f7c626a8959e4e48d35e3e0d3a567f98569d8a97eced +# [docker.io/vaultwarden/web-vault:v2026.1.0] # -FROM --platform=linux/amd64 docker.io/vaultwarden/web-vault@sha256:3c9aec4924c4f529af5e48888cfddd559e553ee62ce3e81372b50103bbaf20f7 AS vault +FROM --platform=linux/amd64 docker.io/vaultwarden/web-vault@sha256:3537918638c8bd4f62f3f7c626a8959e4e48d35e3e0d3a567f98569d8a97eced AS vault ########################## ALPINE BUILD IMAGES ########################## ## NOTE: The Alpine Base Images do not support other platforms then linux/amd64 and linux/arm64 ## And for Alpine we define all build images here, they will only be loaded when actually used -FROM --platform=$BUILDPLATFORM ghcr.io/blackdex/rust-musl:x86_64-musl-stable-1.92.0 AS build_amd64 -FROM --platform=$BUILDPLATFORM ghcr.io/blackdex/rust-musl:aarch64-musl-stable-1.92.0 AS build_arm64 -FROM --platform=$BUILDPLATFORM ghcr.io/blackdex/rust-musl:armv7-musleabihf-stable-1.92.0 AS build_armv7 -FROM --platform=$BUILDPLATFORM ghcr.io/blackdex/rust-musl:arm-musleabi-stable-1.92.0 AS build_armv6 +FROM --platform=$BUILDPLATFORM ghcr.io/blackdex/rust-musl:x86_64-musl-stable-1.93.0 AS build_amd64 +FROM --platform=$BUILDPLATFORM ghcr.io/blackdex/rust-musl:aarch64-musl-stable-1.93.0 AS build_arm64 +FROM --platform=$BUILDPLATFORM ghcr.io/blackdex/rust-musl:armv7-musleabihf-stable-1.93.0 AS build_armv7 +FROM --platform=$BUILDPLATFORM ghcr.io/blackdex/rust-musl:arm-musleabi-stable-1.93.0 AS build_armv6 ########################## BUILD IMAGE ########################## # hadolint ignore=DL3006 diff --git a/docker/Dockerfile.debian b/docker/Dockerfile.debian index 113304b8..4ca1b009 100644 --- a/docker/Dockerfile.debian +++ b/docker/Dockerfile.debian @@ -19,15 +19,15 @@ # - From https://hub.docker.com/r/vaultwarden/web-vault/tags, # click the tag name to view the digest of the image it currently points to. # - From the command line: -# $ docker pull docker.io/vaultwarden/web-vault:v2025.12.2 -# $ docker image inspect --format "{{.RepoDigests}}" docker.io/vaultwarden/web-vault:v2025.12.2 -# [docker.io/vaultwarden/web-vault@sha256:3c9aec4924c4f529af5e48888cfddd559e553ee62ce3e81372b50103bbaf20f7] +# $ docker pull docker.io/vaultwarden/web-vault:v2026.1.0 +# $ docker image inspect --format "{{.RepoDigests}}" docker.io/vaultwarden/web-vault:v2026.1.0 +# [docker.io/vaultwarden/web-vault@sha256:3537918638c8bd4f62f3f7c626a8959e4e48d35e3e0d3a567f98569d8a97eced] # # - Conversely, to get the tag name from the digest: -# $ docker image inspect --format "{{.RepoTags}}" docker.io/vaultwarden/web-vault@sha256:3c9aec4924c4f529af5e48888cfddd559e553ee62ce3e81372b50103bbaf20f7 -# [docker.io/vaultwarden/web-vault:v2025.12.2] +# $ docker image inspect --format "{{.RepoTags}}" docker.io/vaultwarden/web-vault@sha256:3537918638c8bd4f62f3f7c626a8959e4e48d35e3e0d3a567f98569d8a97eced +# [docker.io/vaultwarden/web-vault:v2026.1.0] # -FROM --platform=linux/amd64 docker.io/vaultwarden/web-vault@sha256:3c9aec4924c4f529af5e48888cfddd559e553ee62ce3e81372b50103bbaf20f7 AS vault +FROM --platform=linux/amd64 docker.io/vaultwarden/web-vault@sha256:3537918638c8bd4f62f3f7c626a8959e4e48d35e3e0d3a567f98569d8a97eced AS vault ########################## Cross Compile Docker Helper Scripts ########################## ## We use the linux/amd64 no matter which Build Platform, since these are all bash scripts @@ -36,7 +36,7 @@ FROM --platform=linux/amd64 docker.io/tonistiigi/xx@sha256:c64defb9ed5a91eacb37f ########################## BUILD IMAGE ########################## # hadolint ignore=DL3006 -FROM --platform=$BUILDPLATFORM docker.io/library/rust:1.92.0-slim-trixie AS build +FROM --platform=$BUILDPLATFORM docker.io/library/rust:1.93.0-slim-trixie AS build COPY --from=xx / / ARG TARGETARCH ARG TARGETVARIANT diff --git a/macros/Cargo.toml b/macros/Cargo.toml index 933d46d3..0a560a74 100644 --- a/macros/Cargo.toml +++ b/macros/Cargo.toml @@ -13,7 +13,7 @@ path = "src/lib.rs" proc-macro = true [dependencies] -quote = "1.0.43" +quote = "1.0.44" syn = "2.0.114" [lints] diff --git a/rust-toolchain.toml b/rust-toolchain.toml index 568d0faa..57c529a1 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -1,4 +1,4 @@ [toolchain] -channel = "1.92.0" +channel = "1.93.0" components = [ "rustfmt", "clippy" ] profile = "minimal" diff --git a/src/static/scripts/datatables.css b/src/static/scripts/datatables.css index fe087655..d91ea601 100644 --- a/src/static/scripts/datatables.css +++ b/src/static/scripts/datatables.css @@ -4,10 +4,10 @@ * * To rebuild or modify this file with the latest versions of the included * software please visit: - * https://datatables.net/download/#bs5/dt-2.3.6 + * https://datatables.net/download/#bs5/dt-2.3.7 * * Included libraries: - * DataTables 2.3.6 + * DataTables 2.3.7 */ :root { diff --git a/src/static/scripts/datatables.js b/src/static/scripts/datatables.js index 75f7965e..9c7fa042 100644 --- a/src/static/scripts/datatables.js +++ b/src/static/scripts/datatables.js @@ -4,13 +4,13 @@ * * To rebuild or modify this file with the latest versions of the included * software please visit: - * https://datatables.net/download/#bs5/dt-2.3.6 + * https://datatables.net/download/#bs5/dt-2.3.7 * * Included libraries: - * DataTables 2.3.6 + * DataTables 2.3.7 */ -/*! DataTables 2.3.6 +/*! DataTables 2.3.7 * © SpryMedia Ltd - datatables.net/license */ @@ -460,7 +460,7 @@ if ( tfoot.length === 0 ) { // If we are a scrolling table, and no footer has been given, then we need to create // a tfoot element for the caption element to be appended to - tfoot = $('
').insertAfter(thead); + tfoot = $('').appendTo($this); } oSettings.nTFoot = tfoot[0]; @@ -525,7 +525,7 @@ * * @type string */ - builder: "bs5/dt-2.3.6", + builder: "bs5/dt-2.3.7", /** * Buttons. For use with the Buttons extension for DataTables. This is @@ -8896,6 +8896,10 @@ return null; } + if (col.responsiveVisible === false) { + return null; + } + // Selector if (match[1]) { return $(nodes[idx]).filter(match[1]).length > 0 ? idx : null; @@ -10300,7 +10304,7 @@ * @type string * @default Version number */ - DataTable.version = "2.3.6"; + DataTable.version = "2.3.7"; /** * Private data store, containing all of the settings objects that are From 3cd2d4afe70046aaef97d8f9d71d58304bc091bc Mon Sep 17 00:00:00 2001 From: Mathijs van Veluw Date: Tue, 10 Feb 2026 20:24:35 +0100 Subject: [PATCH 19/89] Update crates and web-vault (#6810) Signed-off-by: BlackDex --- Cargo.lock | 182 ++++++++++++++++++++++++++++++++++--- docker/DockerSettings.yaml | 4 +- docker/Dockerfile.alpine | 12 +-- docker/Dockerfile.debian | 12 +-- 4 files changed, 185 insertions(+), 25 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d9bf20c6..fb952dd1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2042,6 +2042,19 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "getrandom" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "139ef39800118c7683f2fd3c98c1b23c09ae076556b435f8e9064ae108aaeeec" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasip2", + "wasip3", +] + [[package]] name = "glob" version = "0.3.3" @@ -2579,6 +2592,12 @@ dependencies = [ "zerovec", ] +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + [[package]] name = "ident_case" version = "1.0.1" @@ -2842,6 +2861,12 @@ dependencies = [ "spin", ] +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + [[package]] name = "lettre" version = "0.11.19" @@ -2876,9 +2901,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.180" +version = "0.2.181" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc" +checksum = "459427e2af2b9c839b132acb702a1c654d95e10f8c326bfc2ad11310e458b1c5" [[package]] name = "libm" @@ -3847,6 +3872,16 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn", +] + [[package]] name = "primeorder" version = "0.13.6" @@ -4599,9 +4634,9 @@ checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" [[package]] name = "ryu" -version = "1.0.22" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a50f4cf475b65d88e057964e0e9bb1f0aa9bbb2036dc65c64596b42932536984" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" [[package]] name = "salsa20" @@ -5210,12 +5245,12 @@ checksum = "7b2093cf4c8eb1e67749a6762251bc9cd836b6fc171623bd0a9d324d37af2417" [[package]] name = "tempfile" -version = "3.24.0" +version = "3.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "655da9c7eb6305c55742045d5a8d2037996d61d8de95806335c7c86ce0f82e9c" +checksum = "0136791f7c95b1f6dd99f9cc786b91bb81c3800b639b3478e561ddb7be95e5f1" dependencies = [ "fastrand", - "getrandom 0.3.4", + "getrandom 0.4.1", "once_cell", "rustix", "windows-sys 0.61.2", @@ -5707,9 +5742,9 @@ checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142" [[package]] name = "unicode-ident" -version = "1.0.22" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" +checksum = "537dd038a89878be9b64dd4bd1b260315c1bb94f4d784956b81e27a088d9a09e" [[package]] name = "unicode-segmentation" @@ -5914,6 +5949,15 @@ dependencies = [ "wit-bindgen", ] +[[package]] +name = "wasip3" +version = "0.4.0+wasi-0.3.0-rc-2026-01-06" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" +dependencies = [ + "wit-bindgen", +] + [[package]] name = "wasm-bindgen" version = "0.2.108" @@ -5973,6 +6017,28 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "wasm-encoder" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" +dependencies = [ + "anyhow", + "indexmap 2.13.0", + "wasm-encoder", + "wasmparser", +] + [[package]] name = "wasm-streams" version = "0.4.2" @@ -5986,6 +6052,18 @@ dependencies = [ "web-sys", ] +[[package]] +name = "wasmparser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +dependencies = [ + "bitflags", + "hashbrown 0.15.5", + "indexmap 2.13.0", + "semver", +] + [[package]] name = "web-sys" version = "0.3.85" @@ -6480,6 +6558,88 @@ name = "wit-bindgen" version = "0.51.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" +dependencies = [ + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen-core" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" +dependencies = [ + "anyhow", + "heck", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" +dependencies = [ + "anyhow", + "heck", + "indexmap 2.13.0", + "prettyplease", + "syn", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" +dependencies = [ + "anyhow", + "bitflags", + "indexmap 2.13.0", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" +dependencies = [ + "anyhow", + "id-arena", + "indexmap 2.13.0", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] [[package]] name = "writeable" @@ -6646,9 +6806,9 @@ dependencies = [ [[package]] name = "zmij" -version = "1.0.19" +version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ff05f8caa9038894637571ae6b9e29466c1f4f829d26c9b28f869a29cbe3445" +checksum = "4de98dfa5d5b7fef4ee834d0073d560c9ca7b6c46a71d058c48db7960f8cfaf7" [[package]] name = "zstd" diff --git a/docker/DockerSettings.yaml b/docker/DockerSettings.yaml index a0f3c80a..5380b3df 100644 --- a/docker/DockerSettings.yaml +++ b/docker/DockerSettings.yaml @@ -1,6 +1,6 @@ --- -vault_version: "v2026.1.0" -vault_image_digest: "sha256:3537918638c8bd4f62f3f7c626a8959e4e48d35e3e0d3a567f98569d8a97eced" +vault_version: "v2026.1.1" +vault_image_digest: "sha256:062fcf0d5dc37247dae61b0ee1ba5d20f9296e290d7ad1f6114ea5888f5738a7" # Cross Compile Docker Helper Scripts v1.9.0 # We use the linux/amd64 platform shell scripts since there is no difference between the different platform scripts # https://github.com/tonistiigi/xx | https://hub.docker.com/r/tonistiigi/xx/tags diff --git a/docker/Dockerfile.alpine b/docker/Dockerfile.alpine index 9886b6c1..f006f5b4 100644 --- a/docker/Dockerfile.alpine +++ b/docker/Dockerfile.alpine @@ -19,15 +19,15 @@ # - From https://hub.docker.com/r/vaultwarden/web-vault/tags, # click the tag name to view the digest of the image it currently points to. # - From the command line: -# $ docker pull docker.io/vaultwarden/web-vault:v2026.1.0 -# $ docker image inspect --format "{{.RepoDigests}}" docker.io/vaultwarden/web-vault:v2026.1.0 -# [docker.io/vaultwarden/web-vault@sha256:3537918638c8bd4f62f3f7c626a8959e4e48d35e3e0d3a567f98569d8a97eced] +# $ docker pull docker.io/vaultwarden/web-vault:v2026.1.1 +# $ docker image inspect --format "{{.RepoDigests}}" docker.io/vaultwarden/web-vault:v2026.1.1 +# [docker.io/vaultwarden/web-vault@sha256:062fcf0d5dc37247dae61b0ee1ba5d20f9296e290d7ad1f6114ea5888f5738a7] # # - Conversely, to get the tag name from the digest: -# $ docker image inspect --format "{{.RepoTags}}" docker.io/vaultwarden/web-vault@sha256:3537918638c8bd4f62f3f7c626a8959e4e48d35e3e0d3a567f98569d8a97eced -# [docker.io/vaultwarden/web-vault:v2026.1.0] +# $ docker image inspect --format "{{.RepoTags}}" docker.io/vaultwarden/web-vault@sha256:062fcf0d5dc37247dae61b0ee1ba5d20f9296e290d7ad1f6114ea5888f5738a7 +# [docker.io/vaultwarden/web-vault:v2026.1.1] # -FROM --platform=linux/amd64 docker.io/vaultwarden/web-vault@sha256:3537918638c8bd4f62f3f7c626a8959e4e48d35e3e0d3a567f98569d8a97eced AS vault +FROM --platform=linux/amd64 docker.io/vaultwarden/web-vault@sha256:062fcf0d5dc37247dae61b0ee1ba5d20f9296e290d7ad1f6114ea5888f5738a7 AS vault ########################## ALPINE BUILD IMAGES ########################## ## NOTE: The Alpine Base Images do not support other platforms then linux/amd64 and linux/arm64 diff --git a/docker/Dockerfile.debian b/docker/Dockerfile.debian index 4ca1b009..449bbcfd 100644 --- a/docker/Dockerfile.debian +++ b/docker/Dockerfile.debian @@ -19,15 +19,15 @@ # - From https://hub.docker.com/r/vaultwarden/web-vault/tags, # click the tag name to view the digest of the image it currently points to. # - From the command line: -# $ docker pull docker.io/vaultwarden/web-vault:v2026.1.0 -# $ docker image inspect --format "{{.RepoDigests}}" docker.io/vaultwarden/web-vault:v2026.1.0 -# [docker.io/vaultwarden/web-vault@sha256:3537918638c8bd4f62f3f7c626a8959e4e48d35e3e0d3a567f98569d8a97eced] +# $ docker pull docker.io/vaultwarden/web-vault:v2026.1.1 +# $ docker image inspect --format "{{.RepoDigests}}" docker.io/vaultwarden/web-vault:v2026.1.1 +# [docker.io/vaultwarden/web-vault@sha256:062fcf0d5dc37247dae61b0ee1ba5d20f9296e290d7ad1f6114ea5888f5738a7] # # - Conversely, to get the tag name from the digest: -# $ docker image inspect --format "{{.RepoTags}}" docker.io/vaultwarden/web-vault@sha256:3537918638c8bd4f62f3f7c626a8959e4e48d35e3e0d3a567f98569d8a97eced -# [docker.io/vaultwarden/web-vault:v2026.1.0] +# $ docker image inspect --format "{{.RepoTags}}" docker.io/vaultwarden/web-vault@sha256:062fcf0d5dc37247dae61b0ee1ba5d20f9296e290d7ad1f6114ea5888f5738a7 +# [docker.io/vaultwarden/web-vault:v2026.1.1] # -FROM --platform=linux/amd64 docker.io/vaultwarden/web-vault@sha256:3537918638c8bd4f62f3f7c626a8959e4e48d35e3e0d3a567f98569d8a97eced AS vault +FROM --platform=linux/amd64 docker.io/vaultwarden/web-vault@sha256:062fcf0d5dc37247dae61b0ee1ba5d20f9296e290d7ad1f6114ea5888f5738a7 AS vault ########################## Cross Compile Docker Helper Scripts ########################## ## We use the linux/amd64 no matter which Build Platform, since these are all bash scripts From 36f0620fd19af0816a7ee2ed882d368b1b298ddc Mon Sep 17 00:00:00 2001 From: Mathijs van Veluw Date: Tue, 10 Feb 2026 20:34:30 +0100 Subject: [PATCH 20/89] Fix org-details issue (#6811) Fix an issue where it was possible for users who were not eligible to access all org ciphers to be able to download and extract the encrypted contents. Only Managers with full access and Admins and Owners should be able to access this endpoint. This change will block and prevent access for other users. Signed-off-by: BlackDex --- src/api/core/organizations.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/api/core/organizations.rs b/src/api/core/organizations.rs index 356d7786..f173f90f 100644 --- a/src/api/core/organizations.rs +++ b/src/api/core/organizations.rs @@ -929,11 +929,15 @@ struct OrgIdData { } #[get("/ciphers/organization-details?")] -async fn get_org_details(data: OrgIdData, headers: OrgMemberHeaders, conn: DbConn) -> JsonResult { +async fn get_org_details(data: OrgIdData, headers: ManagerHeadersLoose, conn: DbConn) -> JsonResult { if data.organization_id != headers.membership.org_uuid { err_code!("Resource not found.", "Organization id's do not match", rocket::http::Status::NotFound.code); } + if !headers.membership.has_full_access() { + err_code!("Resource not found.", "User does not have full access", rocket::http::Status::NotFound.code); + } + Ok(Json(json!({ "data": _get_org_details(&data.organization_id, &headers.host, &headers.user.uuid, &conn).await?, "object": "list", From 1583fe4af3b3ce98e221172d649567630b899a36 Mon Sep 17 00:00:00 2001 From: Mathijs van Veluw Date: Wed, 18 Feb 2026 00:17:20 +0100 Subject: [PATCH 21/89] Update Rust and Crates and GHA (#6843) - Update Rust to v1.93.1 - Updated all the crates Adjust changes needed for the newer `rand` crate - Updated GitHub Actions Signed-off-by: BlackDex --- .github/workflows/build.yml | 4 +- .github/workflows/trivy.yml | 4 +- .github/workflows/typos.yml | 2 +- .pre-commit-config.yaml | 2 +- Cargo.lock | 310 ++++++++++++++++-------------------- Cargo.toml | 18 +-- docker/DockerSettings.yaml | 2 +- docker/Dockerfile.alpine | 8 +- docker/Dockerfile.debian | 2 +- rust-toolchain.toml | 2 +- src/api/core/accounts.rs | 7 +- src/api/identity.rs | 11 +- src/crypto.rs | 4 +- 13 files changed, 173 insertions(+), 203 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 3e7818ec..2e3468f4 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -87,7 +87,7 @@ jobs: # Only install the clippy and rustfmt components on the default rust-toolchain - name: "Install rust-toolchain version" - uses: dtolnay/rust-toolchain@f7ccc83f9ed1e5b9c81d8a67d7ad1a747e22a561 # master @ Dec 16, 2025, 6:11 PM GMT+1 + uses: dtolnay/rust-toolchain@efa25f7f19611383d5b0ccf2d1c8914531636bf9 # master @ Feb 13, 2026, 3:46 AM GMT+1 if: ${{ matrix.channel == 'rust-toolchain' }} with: toolchain: "${{steps.toolchain.outputs.RUST_TOOLCHAIN}}" @@ -97,7 +97,7 @@ jobs: # Install the any other channel to be used for which we do not execute clippy and rustfmt - name: "Install MSRV version" - uses: dtolnay/rust-toolchain@f7ccc83f9ed1e5b9c81d8a67d7ad1a747e22a561 # master @ Dec 16, 2025, 6:11 PM GMT+1 + uses: dtolnay/rust-toolchain@efa25f7f19611383d5b0ccf2d1c8914531636bf9 # master @ Feb 13, 2026, 3:46 AM GMT+1 if: ${{ matrix.channel != 'rust-toolchain' }} with: toolchain: "${{steps.toolchain.outputs.RUST_TOOLCHAIN}}" diff --git a/.github/workflows/trivy.yml b/.github/workflows/trivy.yml index 5ef08998..13cd2e24 100644 --- a/.github/workflows/trivy.yml +++ b/.github/workflows/trivy.yml @@ -38,7 +38,7 @@ jobs: persist-credentials: false - name: Run Trivy vulnerability scanner - uses: aquasecurity/trivy-action@b6643a29fecd7f34b3597bc6acb0a98b03d33ff8 # 0.33.1 + uses: aquasecurity/trivy-action@c1824fd6edce30d7ab345a9989de00bbd46ef284 # 0.34.0 env: TRIVY_DB_REPOSITORY: docker.io/aquasec/trivy-db:2,public.ecr.aws/aquasecurity/trivy-db:2,ghcr.io/aquasecurity/trivy-db:2 TRIVY_JAVA_DB_REPOSITORY: docker.io/aquasec/trivy-java-db:1,public.ecr.aws/aquasecurity/trivy-java-db:1,ghcr.io/aquasecurity/trivy-java-db:1 @@ -50,6 +50,6 @@ jobs: severity: CRITICAL,HIGH - name: Upload Trivy scan results to GitHub Security tab - uses: github/codeql-action/upload-sarif@45cbd0c69e560cd9e7cd7f8c32362050c9b7ded2 # v4.32.2 + uses: github/codeql-action/upload-sarif@9e907b5e64f6b83e7804b09294d44122997950d6 # v4.32.3 with: sarif_file: 'trivy-results.sarif' diff --git a/.github/workflows/typos.yml b/.github/workflows/typos.yml index 99e2eacf..5726a6fc 100644 --- a/.github/workflows/typos.yml +++ b/.github/workflows/typos.yml @@ -23,4 +23,4 @@ jobs: # When this version is updated, do not forget to update this in `.pre-commit-config.yaml` too - name: Spell Check Repo - uses: crate-ci/typos@9066e9940a8a05b98fb4733c62a726f83c9e57f8 # v1.43.3 + uses: crate-ci/typos@57b11c6b7e54c402ccd9cda953f1072ec4f78e33 # v1.43.5 diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 771eb042..54f09b8b 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -53,6 +53,6 @@ repos: - "cd docker && make" # When this version is updated, do not forget to update this in `.github/workflows/typos.yaml` too - repo: https://github.com/crate-ci/typos - rev: 9066e9940a8a05b98fb4733c62a726f83c9e57f8 # v1.43.3 + rev: 57b11c6b7e54c402ccd9cda953f1072ec4f78e33 # v1.43.5 hooks: - id: typos diff --git a/Cargo.lock b/Cargo.lock index fb952dd1..da9e0d79 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -16,7 +16,7 @@ checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" dependencies = [ "cfg-if", "cipher", - "cpufeatures", + "cpufeatures 0.2.17", ] [[package]] @@ -93,7 +93,7 @@ checksum = "3c3610892ee6e0cbce8ae2700349fcf8f98adb0dbfbee85aec3c9179d29cc072" dependencies = [ "base64ct", "blake2", - "cpufeatures", + "cpufeatures 0.2.17", "password-hash", ] @@ -173,9 +173,9 @@ dependencies = [ [[package]] name = "async-executor" -version = "1.13.3" +version = "1.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "497c00e0fd83a72a79a39fcbd8e3e2f055d6f6c7e025f3b3d91f4f8e76527fb8" +checksum = "c96bf972d85afc50bf5ab8fe2d54d1586b4e0b46c97c50a0c9e71e2f7bcd812a" dependencies = [ "async-task", "concurrent-queue", @@ -360,9 +360,9 @@ checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" [[package]] name = "aws-config" -version = "1.8.13" +version = "1.8.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c456581cb3c77fafcc8c67204a70680d40b61112d6da78c77bd31d945b65f1b5" +checksum = "8a8fc176d53d6fe85017f230405e3255cedb4a02221cb55ed6d76dccbbb099b2" dependencies = [ "aws-credential-types", "aws-runtime", @@ -390,9 +390,9 @@ dependencies = [ [[package]] name = "aws-credential-types" -version = "1.2.11" +version = "1.2.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3cd362783681b15d136480ad555a099e82ecd8e2d10a841e14dfd0078d67fee3" +checksum = "6d203b0bf2626dcba8665f5cd0871d7c2c0930223d6b6be9097592fea21242d0" dependencies = [ "aws-smithy-async", "aws-smithy-runtime-api", @@ -402,9 +402,9 @@ dependencies = [ [[package]] name = "aws-runtime" -version = "1.6.0" +version = "1.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c635c2dc792cb4a11ce1a4f392a925340d1bdf499289b5ec1ec6810954eb43f5" +checksum = "ede2ddc593e6c8acc6ce3358c28d6677a6dc49b65ba4b37a2befe14a11297e75" dependencies = [ "aws-credential-types", "aws-sigv4", @@ -415,6 +415,7 @@ dependencies = [ "aws-smithy-types", "aws-types", "bytes", + "bytes-utils", "fastrand", "http 1.4.0", "http-body 1.0.1", @@ -426,9 +427,9 @@ dependencies = [ [[package]] name = "aws-sdk-sso" -version = "1.93.0" +version = "1.95.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9dcb38bb33fc0a11f1ffc3e3e85669e0a11a37690b86f77e75306d8f369146a0" +checksum = "00c5ff27c6ba2cbd95e6e26e2e736676fdf6bcf96495b187733f521cfe4ce448" dependencies = [ "aws-credential-types", "aws-runtime", @@ -450,9 +451,9 @@ dependencies = [ [[package]] name = "aws-sdk-ssooidc" -version = "1.95.0" +version = "1.97.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ada8ffbea7bd1be1f53df1dadb0f8fdb04badb13185b3321b929d1ee3caad09" +checksum = "4d186f1e5a3694a188e5a0640b3115ccc6e084d104e16fd6ba968dca072ffef8" dependencies = [ "aws-credential-types", "aws-runtime", @@ -474,9 +475,9 @@ dependencies = [ [[package]] name = "aws-sdk-sts" -version = "1.97.0" +version = "1.99.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6443ccadc777095d5ed13e21f5c364878c9f5bad4e35187a6cdbd863b0afcad" +checksum = "9acba7c62f3d4e2408fa998a3a8caacd8b9a5b5549cf36e2372fbdae329d5449" dependencies = [ "aws-credential-types", "aws-runtime", @@ -499,9 +500,9 @@ dependencies = [ [[package]] name = "aws-sigv4" -version = "1.3.8" +version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "efa49f3c607b92daae0c078d48a4571f599f966dce3caee5f1ea55c4d9073f99" +checksum = "37411f8e0f4bea0c3ca0958ce7f18f6439db24d555dbd809787262cd00926aa9" dependencies = [ "aws-credential-types", "aws-smithy-http", @@ -521,9 +522,9 @@ dependencies = [ [[package]] name = "aws-smithy-async" -version = "1.2.11" +version = "1.2.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52eec3db979d18cb807fc1070961cc51d87d069abe9ab57917769687368a8c6c" +checksum = "5cc50d0f63e714784b84223abd7abbc8577de8c35d699e0edd19f0a88a08ae13" dependencies = [ "futures-util", "pin-project-lite", @@ -532,9 +533,9 @@ dependencies = [ [[package]] name = "aws-smithy-http" -version = "0.63.3" +version = "0.63.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "630e67f2a31094ffa51b210ae030855cb8f3b7ee1329bdd8d085aaf61e8b97fc" +checksum = "d619373d490ad70966994801bc126846afaa0d1ee920697a031f0cf63f2568e7" dependencies = [ "aws-smithy-runtime-api", "aws-smithy-types", @@ -553,27 +554,27 @@ dependencies = [ [[package]] name = "aws-smithy-json" -version = "0.62.3" +version = "0.62.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3cb96aa208d62ee94104645f7b2ecaf77bf27edf161590b6224bfbac2832f979" +checksum = "27b3a779093e18cad88bbae08dc4261e1d95018c4c5b9356a52bcae7c0b6e9bb" dependencies = [ "aws-smithy-types", ] [[package]] name = "aws-smithy-observability" -version = "0.2.4" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0a46543fbc94621080b3cf553eb4cbbdc41dd9780a30c4756400f0139440a1d" +checksum = "4d3f39d5bb871aaf461d59144557f16d5927a5248a983a40654d9cf3b9ba183b" dependencies = [ "aws-smithy-runtime-api", ] [[package]] name = "aws-smithy-query" -version = "0.60.13" +version = "0.60.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0cebbddb6f3a5bd81553643e9c7daf3cc3dc5b0b5f398ac668630e8a84e6fff0" +checksum = "05f76a580e3d8f8961e5d48763214025a2af65c2fa4cd1fb7f270a0e107a71b0" dependencies = [ "aws-smithy-types", "urlencoding", @@ -581,9 +582,9 @@ dependencies = [ [[package]] name = "aws-smithy-runtime" -version = "1.10.0" +version = "1.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3df87c14f0127a0d77eb261c3bc45d5b4833e2a1f63583ebfb728e4852134ee" +checksum = "22ccf7f6eba8b2dcf8ce9b74806c6c185659c311665c4bf8d6e71ebd454db6bf" dependencies = [ "aws-smithy-async", "aws-smithy-http", @@ -605,9 +606,9 @@ dependencies = [ [[package]] name = "aws-smithy-runtime-api" -version = "1.11.3" +version = "1.11.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49952c52f7eebb72ce2a754d3866cc0f87b97d2a46146b79f80f3a93fb2b3716" +checksum = "b4af6e5def28be846479bbeac55aa4603d6f7986fc5da4601ba324dd5d377516" dependencies = [ "aws-smithy-async", "aws-smithy-types", @@ -622,9 +623,9 @@ dependencies = [ [[package]] name = "aws-smithy-types" -version = "1.4.3" +version = "1.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b3a26048eeab0ddeba4b4f9d51654c79af8c3b32357dc5f336cee85ab331c33" +checksum = "8ca2734c16913a45343b37313605d84e7d8b34a4611598ce1d25b35860a2bed3" dependencies = [ "base64-simd", "bytes", @@ -645,18 +646,18 @@ dependencies = [ [[package]] name = "aws-smithy-xml" -version = "0.60.13" +version = "0.60.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11b2f670422ff42bf7065031e72b45bc52a3508bd089f743ea90731ca2b6ea57" +checksum = "b53543b4b86ed43f051644f704a98c7291b3618b67adf057ee77a366fa52fcaa" dependencies = [ "xmlparser", ] [[package]] name = "aws-types" -version = "1.3.11" +version = "1.3.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d980627d2dd7bfc32a3c025685a033eeab8d365cc840c631ef59d1b8f428164" +checksum = "0470cc047657c6e286346bdf10a8719d26efd6a91626992e0e64481e44323e96" dependencies = [ "aws-credential-types", "aws-smithy-async", @@ -743,9 +744,9 @@ checksum = "383d29d513d8764dcdc42ea295d979eb99c3c9f00607b3692cf68a431f7dca72" [[package]] name = "bitflags" -version = "2.10.0" +version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" +checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" [[package]] name = "blake2" @@ -926,9 +927,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.55" +version = "1.2.56" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47b26a0954ae34af09b50f0de26458fa95369a0d478d8236d3f93082b219bd29" +checksum = "aebf35691d1bfb0ac386a69bac2fde4dd276fb618cf8bf4f5318fe285e821bb2" dependencies = [ "find-msvc-tools", "jobserver", @@ -948,6 +949,17 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" +[[package]] +name = "chacha20" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f8d983286843e49675a4b7a2d174efe136dc93a18d69130dd18198a6c167601" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.0", + "rand_core 0.10.0", +] + [[package]] name = "chrono" version = "0.4.43" @@ -1075,9 +1087,9 @@ dependencies = [ [[package]] name = "cookie_store" -version = "0.22.0" +version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fc4bff745c9b4c7fb1e97b25d13153da2bc7796260141df62378998d070207f" +checksum = "15b2c103cf610ec6cae3da84a766285b42fd16aad564758459e6ecf128c75206" dependencies = [ "cookie", "document-features", @@ -1126,6 +1138,15 @@ dependencies = [ "libc", ] +[[package]] +name = "cpufeatures" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" +dependencies = [ + "libc", +] + [[package]] name = "crc32c" version = "0.6.8" @@ -1220,7 +1241,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" dependencies = [ "cfg-if", - "cpufeatures", + "cpufeatures 0.2.17", "curve25519-dalek-derive", "digest", "fiat-crypto", @@ -1376,9 +1397,9 @@ dependencies = [ [[package]] name = "deranged" -version = "0.5.5" +version = "0.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ececcb659e7ba858fb4f10388c250a7252eb0a27373f1a72b8748afdd248e587" +checksum = "cc3dc5ad92c2e2d1c193bbbbdf2ea477cb81331de4f3103f267ca18368b988c4" dependencies = [ "powerfmt", "serde_core", @@ -1885,9 +1906,9 @@ dependencies = [ [[package]] name = "futures" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" +checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d" dependencies = [ "futures-channel", "futures-core", @@ -1900,9 +1921,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" dependencies = [ "futures-core", "futures-sink", @@ -1910,15 +1931,15 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" [[package]] name = "futures-executor" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" +checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" dependencies = [ "futures-core", "futures-task", @@ -1927,9 +1948,9 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" [[package]] name = "futures-lite" @@ -1946,9 +1967,9 @@ dependencies = [ [[package]] name = "futures-macro" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" dependencies = [ "proc-macro2", "quote", @@ -1957,15 +1978,15 @@ dependencies = [ [[package]] name = "futures-sink" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" [[package]] name = "futures-task" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" [[package]] name = "futures-timer" @@ -1975,9 +1996,9 @@ checksum = "f288b0a4f20f9a56b5d1da57e2227c661b7b16168e2f72365f57b63326e29b24" [[package]] name = "futures-util" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" dependencies = [ "futures-channel", "futures-core", @@ -1987,7 +2008,6 @@ dependencies = [ "futures-task", "memchr", "pin-project-lite", - "pin-utils", "slab", ] @@ -2051,6 +2071,7 @@ dependencies = [ "cfg-if", "libc", "r-efi", + "rand_core 0.10.0", "wasip2", "wasip3", ] @@ -2446,22 +2467,6 @@ dependencies = [ "webpki-roots", ] -[[package]] -name = "hyper-tls" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" -dependencies = [ - "bytes", - "http-body-util", - "hyper 1.8.1", - "hyper-util", - "native-tls", - "tokio", - "tokio-native-tls", - "tower-service", -] - [[package]] name = "hyper-util" version = "0.1.20" @@ -2726,9 +2731,9 @@ checksum = "47f142fe24a9c9944451e8349de0a56af5f3e7226dc46f3ed4d4ecc0b85af75e" [[package]] name = "jiff" -version = "0.2.19" +version = "0.2.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d89a5b5e10d5a9ad6e5d1f4bd58225f655d6fe9767575a5e8ac5a6fe64e04495" +checksum = "c867c356cc096b33f4981825ab281ecba3db0acefe60329f044c1789d94c6543" dependencies = [ "jiff-static", "jiff-tzdb-platform", @@ -2741,9 +2746,9 @@ dependencies = [ [[package]] name = "jiff-static" -version = "0.2.19" +version = "0.2.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff7a39c8862fc1369215ccf0a8f12dd4598c7f6484704359f0351bd617034dbf" +checksum = "f7946b4325269738f270bb55b3c19ab5c5040525f83fd625259422a9d25d9be5" dependencies = [ "proc-macro2", "quote", @@ -2901,9 +2906,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.181" +version = "0.2.182" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "459427e2af2b9c839b132acb702a1c654d95e10f8c326bfc2ad11310e458b1c5" +checksum = "6800badb6cb2082ffd7b6a67e6125bb39f18782f793520caee8cb8846be06112" [[package]] name = "libm" @@ -3029,7 +3034,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "36c791ecdf977c99f45f23280405d7723727470f6689a5e6dbf513ac547ae10d" dependencies = [ "serde", - "toml 0.9.11+spec-1.1.0", + "toml 0.9.12+spec-1.1.0", ] [[package]] @@ -3147,23 +3152,6 @@ dependencies = [ "vcpkg", ] -[[package]] -name = "native-tls" -version = "0.2.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87de3442987e9dbec73158d5c715e7ad9072fda936bb03d19d7fa10e00520f0e" -dependencies = [ - "libc", - "log", - "openssl", - "openssl-probe 0.1.6", - "openssl-sys", - "schannel", - "security-framework 2.11.1", - "security-framework-sys", - "tempfile", -] - [[package]] name = "nom" version = "7.1.3" @@ -3439,12 +3427,6 @@ dependencies = [ "syn", ] -[[package]] -name = "openssl-probe" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" - [[package]] name = "openssl-probe" version = "0.2.1" @@ -4099,6 +4081,17 @@ dependencies = [ "rand_core 0.9.5", ] +[[package]] +name = "rand" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc266eb313df6c5c09c1c7b1fbe2510961e5bcd3add930c1e31f7ed9da0feff8" +dependencies = [ + "chacha20", + "getrandom 0.4.1", + "rand_core 0.10.0", +] + [[package]] name = "rand_chacha" version = "0.3.1" @@ -4137,6 +4130,12 @@ dependencies = [ "getrandom 0.3.4", ] +[[package]] +name = "rand_core" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c8d0fd677905edcbeedbf2edb6494d676f0e98d54d5cf9bda0b061cb8fb8aba" + [[package]] name = "raw-cpuid" version = "11.6.0" @@ -4274,12 +4273,10 @@ dependencies = [ "http-body-util", "hyper 1.8.1", "hyper-rustls", - "hyper-tls", "hyper-util", "js-sys", "log", "mime", - "native-tls", "percent-encoding", "pin-project-lite", "quinn", @@ -4291,7 +4288,6 @@ dependencies = [ "serde_urlencoded", "sync_wrapper", "tokio", - "tokio-native-tls", "tokio-rustls 0.26.4", "tokio-util", "tower", @@ -4580,10 +4576,10 @@ version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "612460d5f7bea540c490b2b6395d8e34a953e52b491accd6c86c8164c5932a63" dependencies = [ - "openssl-probe 0.2.1", + "openssl-probe", "rustls-pki-types", "schannel", - "security-framework 3.5.1", + "security-framework", ] [[package]] @@ -4747,22 +4743,9 @@ dependencies = [ [[package]] name = "security-framework" -version = "2.11.1" +version = "3.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" -dependencies = [ - "bitflags", - "core-foundation 0.9.4", - "core-foundation-sys", - "libc", - "security-framework-sys", -] - -[[package]] -name = "security-framework" -version = "3.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3297343eaf830f66ede390ea39da1d462b6b0c1b000f420d0a83f898bbbe6ef" +checksum = "d17b898a6d6948c3a8ee4372c17cb384f90d2e6e912ef00895b14fd7ab54ec38" dependencies = [ "bitflags", "core-foundation 0.10.1", @@ -4773,9 +4756,9 @@ dependencies = [ [[package]] name = "security-framework-sys" -version = "2.15.0" +version = "2.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc1f0cbffaac4852523ce30d8bd3c5cdc873501d96ff467ca09b6767bb8cd5c0" +checksum = "321c8673b092a9a42605034a9879d73cb79101ed5fd117bc9a597b89b4e9e61a" dependencies = [ "core-foundation-sys", "libc", @@ -4942,7 +4925,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" dependencies = [ "cfg-if", - "cpufeatures", + "cpufeatures 0.2.17", "digest", ] @@ -4953,7 +4936,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" dependencies = [ "cfg-if", - "cpufeatures", + "cpufeatures 0.2.17", "digest", ] @@ -5010,9 +4993,9 @@ checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2" [[package]] name = "simple_asn1" -version = "0.6.3" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "297f631f50729c8c99b84667867963997ec0b50f32b2a7dbcab828ef0541e8bb" +checksum = "0d585997b0ac10be3c5ee635f1bab02d512760d14b7c468801ac8a01d9ae5f1d" dependencies = [ "num-bigint", "num-traits", @@ -5161,23 +5144,22 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "svg-hush" -version = "0.9.5" +version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d647e9386e34dd750ba80bdb7dae2a2c50b78338515ffeb9fa7bdd3ef803bf2" +checksum = "929223e80cdcec0482207576ea09692dd71b2b559057fc172e292ecec9a97559" dependencies = [ "base64 0.22.1", "data-url", - "once_cell", "quick-error", "url", - "xml-rs", + "xml", ] [[package]] name = "syn" -version = "2.0.114" +version = "2.0.116" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4d107df263a3013ef9b1879b0df87d706ff80f65a86ea879bd9c31f9b307c2a" +checksum = "3df424c70518695237746f84cede799c9c58fcb37450d7b23716568cc8bc69cb" dependencies = [ "proc-macro2", "quote", @@ -5409,16 +5391,6 @@ dependencies = [ "syn", ] -[[package]] -name = "tokio-native-tls" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" -dependencies = [ - "native-tls", - "tokio", -] - [[package]] name = "tokio-rustls" version = "0.24.1" @@ -5490,9 +5462,9 @@ dependencies = [ [[package]] name = "toml" -version = "0.9.11+spec-1.1.0" +version = "0.9.12+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3afc9a848309fe1aaffaed6e1546a7a14de1f935dc9d89d32afd9a44bab7c46" +checksum = "cf92845e79fc2e2def6a5d828f0801e29a2f8acc037becc5ab08595c7d5e9863" dependencies = [ "serde_core", "serde_spanned 1.0.4", @@ -5535,9 +5507,9 @@ dependencies = [ [[package]] name = "toml_parser" -version = "1.0.6+spec-1.1.0" +version = "1.0.9+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3198b4b0a8e11f09dd03e133c0280504d0801269e9afa46362ffde1cbeebf44" +checksum = "702d4415e08923e7e1ef96cd5727c0dfed80b4d2fa25db9647fe5eb6f7c5a4c4" dependencies = [ "winnow 0.7.14", ] @@ -5742,9 +5714,9 @@ checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142" [[package]] name = "unicode-ident" -version = "1.0.23" +version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "537dd038a89878be9b64dd4bd1b260315c1bb94f4d784956b81e27a088d9a09e" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" [[package]] name = "unicode-segmentation" @@ -5797,11 +5769,11 @@ checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" [[package]] name = "uuid" -version = "1.20.0" +version = "1.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee48d38b119b0cd71fe4141b30f5ba9c7c5d9f4e7a3a8b4a674e4b6ef789976f" +checksum = "b672338555252d43fd2240c714dc444b8c6fb0a5c5335e65a07bba7742735ddb" dependencies = [ - "getrandom 0.3.4", + "getrandom 0.4.1", "js-sys", "serde_core", "wasm-bindgen", @@ -5868,7 +5840,7 @@ dependencies = [ "pastey 0.2.1", "percent-encoding", "pico-args", - "rand 0.9.2", + "rand 0.10.0", "regex", "reqsign", "reqwest", @@ -6665,10 +6637,10 @@ dependencies = [ ] [[package]] -name = "xml-rs" -version = "0.8.28" +name = "xml" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ae8337f8a065cfc972643663ea4279e04e7256de865aa66fe25cec5fb912d3f" +checksum = "b8aa498d22c9bbaf482329839bc5620c46be275a19a812e9a22a2b07529a642a" [[package]] name = "xmlparser" @@ -6806,9 +6778,9 @@ dependencies = [ [[package]] name = "zmij" -version = "1.0.20" +version = "1.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4de98dfa5d5b7fef4ee834d0073d560c9ca7b6c46a71d058c48db7960f8cfaf7" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" [[package]] name = "zstd" diff --git a/Cargo.toml b/Cargo.toml index 88d8b3c4..fd910852 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -78,7 +78,7 @@ rmpv = "1.3.1" # MessagePack library dashmap = "6.1.0" # Async futures -futures = "0.3.31" +futures = "0.3.32" tokio = { version = "1.49.0", features = ["rt-multi-thread", "fs", "io-util", "parking_lot", "time", "signal", "net"] } tokio-util = { version = "0.7.18", features = ["compat"]} @@ -98,12 +98,12 @@ diesel-derive-newtype = "2.1.2" libsqlite3-sys = { version = "0.35.0", features = ["bundled"], optional = true } # Crypto-related libraries -rand = "0.9.2" +rand = "0.10.0" ring = "0.17.14" subtle = "2.6.1" # UUID generation -uuid = { version = "1.20.0", features = ["v4"] } +uuid = { version = "1.21.0", features = ["v4"] } # Date and time libraries chrono = { version = "0.4.43", features = ["clock", "serde"], default-features = false } @@ -152,14 +152,14 @@ html5gum = "0.8.3" regex = { version = "1.12.3", features = ["std", "perf", "unicode-perl"], default-features = false } data-url = "0.3.2" bytes = "1.11.1" -svg-hush = "0.9.5" +svg-hush = "0.9.6" # Cache function results (Used for version check and favicon fetching) cached = { version = "0.56.0", features = ["async"] } # Used for custom short lived cookie jar during favicon extraction cookie = "0.18.1" -cookie_store = "0.22.0" +cookie_store = "0.22.1" # Used by U2F, JWT and PostgreSQL openssl = "0.10.75" @@ -172,7 +172,7 @@ pastey = "0.2.1" governor = "0.10.4" # OIDC for SSO -openidconnect = { version = "4.0.1", features = ["reqwest", "native-tls"] } +openidconnect = { version = "4.0.1", features = ["reqwest", "rustls-tls"] } mini-moka = "0.10.3" # Check client versions for specific features. @@ -198,9 +198,9 @@ opendal = { version = "0.55.0", features = ["services-fs"], default-features = f # For retrieving AWS credentials, including temporary SSO credentials anyhow = { version = "1.0.101", optional = true } -aws-config = { version = "1.8.13", features = ["behavior-version-latest", "rt-tokio", "credentials-process", "sso"], default-features = false, optional = true } -aws-credential-types = { version = "1.2.11", optional = true } -aws-smithy-runtime-api = { version = "1.11.3", optional = true } +aws-config = { version = "1.8.14", features = ["behavior-version-latest", "rt-tokio", "credentials-process", "sso"], default-features = false, optional = true } +aws-credential-types = { version = "1.2.13", optional = true } +aws-smithy-runtime-api = { version = "1.11.5", optional = true } http = { version = "1.4.0", optional = true } reqsign = { version = "0.16.5", optional = true } diff --git a/docker/DockerSettings.yaml b/docker/DockerSettings.yaml index 5380b3df..7b4a9af7 100644 --- a/docker/DockerSettings.yaml +++ b/docker/DockerSettings.yaml @@ -5,7 +5,7 @@ vault_image_digest: "sha256:062fcf0d5dc37247dae61b0ee1ba5d20f9296e290d7ad1f6114e # We use the linux/amd64 platform shell scripts since there is no difference between the different platform scripts # https://github.com/tonistiigi/xx | https://hub.docker.com/r/tonistiigi/xx/tags xx_image_digest: "sha256:c64defb9ed5a91eacb37f96ccc3d4cd72521c4bd18d5442905b95e2226b0e707" -rust_version: 1.93.0 # Rust version to be used +rust_version: 1.93.1 # Rust version to be used debian_version: trixie # Debian release name to be used alpine_version: "3.23" # Alpine version to be used # For which platforms/architectures will we try to build images diff --git a/docker/Dockerfile.alpine b/docker/Dockerfile.alpine index f006f5b4..b3df2f4e 100644 --- a/docker/Dockerfile.alpine +++ b/docker/Dockerfile.alpine @@ -32,10 +32,10 @@ FROM --platform=linux/amd64 docker.io/vaultwarden/web-vault@sha256:062fcf0d5dc37 ########################## ALPINE BUILD IMAGES ########################## ## NOTE: The Alpine Base Images do not support other platforms then linux/amd64 and linux/arm64 ## And for Alpine we define all build images here, they will only be loaded when actually used -FROM --platform=$BUILDPLATFORM ghcr.io/blackdex/rust-musl:x86_64-musl-stable-1.93.0 AS build_amd64 -FROM --platform=$BUILDPLATFORM ghcr.io/blackdex/rust-musl:aarch64-musl-stable-1.93.0 AS build_arm64 -FROM --platform=$BUILDPLATFORM ghcr.io/blackdex/rust-musl:armv7-musleabihf-stable-1.93.0 AS build_armv7 -FROM --platform=$BUILDPLATFORM ghcr.io/blackdex/rust-musl:arm-musleabi-stable-1.93.0 AS build_armv6 +FROM --platform=$BUILDPLATFORM ghcr.io/blackdex/rust-musl:x86_64-musl-stable-1.93.1 AS build_amd64 +FROM --platform=$BUILDPLATFORM ghcr.io/blackdex/rust-musl:aarch64-musl-stable-1.93.1 AS build_arm64 +FROM --platform=$BUILDPLATFORM ghcr.io/blackdex/rust-musl:armv7-musleabihf-stable-1.93.1 AS build_armv7 +FROM --platform=$BUILDPLATFORM ghcr.io/blackdex/rust-musl:arm-musleabi-stable-1.93.1 AS build_armv6 ########################## BUILD IMAGE ########################## # hadolint ignore=DL3006 diff --git a/docker/Dockerfile.debian b/docker/Dockerfile.debian index 449bbcfd..8796dedb 100644 --- a/docker/Dockerfile.debian +++ b/docker/Dockerfile.debian @@ -36,7 +36,7 @@ FROM --platform=linux/amd64 docker.io/tonistiigi/xx@sha256:c64defb9ed5a91eacb37f ########################## BUILD IMAGE ########################## # hadolint ignore=DL3006 -FROM --platform=$BUILDPLATFORM docker.io/library/rust:1.93.0-slim-trixie AS build +FROM --platform=$BUILDPLATFORM docker.io/library/rust:1.93.1-slim-trixie AS build COPY --from=xx / / ARG TARGETARCH ARG TARGETVARIANT diff --git a/rust-toolchain.toml b/rust-toolchain.toml index 57c529a1..585747e0 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -1,4 +1,4 @@ [toolchain] -channel = "1.93.0" +channel = "1.93.1" components = [ "rustfmt", "clippy" ] profile = "minimal" diff --git a/src/api/core/accounts.rs b/src/api/core/accounts.rs index 0e01c1c4..51ebbf03 100644 --- a/src/api/core/accounts.rs +++ b/src/api/core/accounts.rs @@ -1199,10 +1199,9 @@ async fn password_hint(data: Json, conn: DbConn) -> EmptyResul // There is still a timing side channel here in that the code // paths that send mail take noticeably longer than ones that // don't. Add a randomized sleep to mitigate this somewhat. - use rand::{rngs::SmallRng, Rng, SeedableRng}; - let mut rng = SmallRng::from_os_rng(); - let delta: i32 = 100; - let sleep_ms = (1_000 + rng.random_range(-delta..=delta)) as u64; + use rand::{rngs::SmallRng, RngExt}; + let mut rng: SmallRng = rand::make_rng(); + let sleep_ms = rng.random_range(900..=1100) as u64; tokio::time::sleep(tokio::time::Duration::from_millis(sleep_ms)).await; Ok(()) } else { diff --git a/src/api/identity.rs b/src/api/identity.rs index f5f2afd6..0ac0a730 100644 --- a/src/api/identity.rs +++ b/src/api/identity.rs @@ -975,12 +975,11 @@ async fn register_verification_email( let user = User::find_by_mail(&data.email, &conn).await; if user.filter(|u| u.private_key.is_some()).is_some() { // There is still a timing side channel here in that the code - // paths that send mail take noticeably longer than ones that - // don't. Add a randomized sleep to mitigate this somewhat. - use rand::{rngs::SmallRng, Rng, SeedableRng}; - let mut rng = SmallRng::from_os_rng(); - let delta: i32 = 100; - let sleep_ms = (1_000 + rng.random_range(-delta..=delta)) as u64; + // paths that send mail take noticeably longer than ones that don't. + // Add a randomized sleep to mitigate this somewhat. + use rand::{rngs::SmallRng, RngExt}; + let mut rng: SmallRng = rand::make_rng(); + let sleep_ms = rng.random_range(900..=1100) as u64; tokio::time::sleep(tokio::time::Duration::from_millis(sleep_ms)).await; } else { mail::send_register_verify_email(&data.email, &token).await?; diff --git a/src/crypto.rs b/src/crypto.rs index e2add1c6..1930f380 100644 --- a/src/crypto.rs +++ b/src/crypto.rs @@ -55,13 +55,13 @@ pub fn encode_random_bytes(e: &Encoding) -> String { /// Generates a random string over a specified alphabet. pub fn get_random_string(alphabet: &[u8], num_chars: usize) -> String { // Ref: https://rust-lang-nursery.github.io/rust-cookbook/algorithms/randomness.html - use rand::Rng; + use rand::RngExt; let mut rng = rand::rng(); (0..num_chars) .map(|_| { let i = rng.random_range(0..alphabet.len()); - alphabet[i] as char + char::from(alphabet[i]) }) .collect() } From da2af3d3629abec3bdfd91008f0f8805cb1d20f6 Mon Sep 17 00:00:00 2001 From: Stefan Melmuk <509385+stefan0xC@users.noreply.github.com> Date: Mon, 23 Feb 2026 20:27:40 +0100 Subject: [PATCH 22/89] hide remember 2fa token (#6852) --- src/api/web.rs | 1 + src/static/templates/scss/vaultwarden.scss.hbs | 7 +++++++ 2 files changed, 8 insertions(+) diff --git a/src/api/web.rs b/src/api/web.rs index d1ca0db4..0ae9c7db 100644 --- a/src/api/web.rs +++ b/src/api/web.rs @@ -60,6 +60,7 @@ fn vaultwarden_css() -> Cached> { "mail_2fa_enabled": CONFIG._enable_email_2fa(), "mail_enabled": CONFIG.mail_enabled(), "sends_allowed": CONFIG.sends_allowed(), + "remember_2fa_disabled": CONFIG.disable_2fa_remember(), "password_hints_allowed": CONFIG.password_hints_allowed(), "signup_disabled": CONFIG.is_signup_disabled(), "sso_enabled": CONFIG.sso_enabled(), diff --git a/src/static/templates/scss/vaultwarden.scss.hbs b/src/static/templates/scss/vaultwarden.scss.hbs index 1859c1ea..230ac2e7 100644 --- a/src/static/templates/scss/vaultwarden.scss.hbs +++ b/src/static/templates/scss/vaultwarden.scss.hbs @@ -158,6 +158,13 @@ app-root a[routerlink="/signup"] { {{/if}} {{/if}} +{{#if remember_2fa_disabled}} +/* Hide checkbox to remember 2FA token for 30 days */ +app-two-factor-auth > form > bit-form-control { + @extend %vw-hide; +} +{{/if}} + {{#unless mail_2fa_enabled}} /* Hide `Email` 2FA if mail is not enabled */ .providers-2fa-1 { From 74819b95bd09610b2dd4af633ceb1f9c72b353f4 Mon Sep 17 00:00:00 2001 From: proofofcopilot Date: Mon, 23 Feb 2026 20:28:12 +0100 Subject: [PATCH 23/89] fix(send_invite): add orgSsoIdentifier if sso_only is enabled (#6824) --- src/mail.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/mail.rs b/src/mail.rs index 270a839e..cdbd269a 100644 --- a/src/mail.rs +++ b/src/mail.rs @@ -302,10 +302,10 @@ pub async fn send_invite( .append_pair("organizationUserId", &member_id) .append_pair("token", &invite_token); - if CONFIG.sso_enabled() { - query_params.append_pair("orgUserHasExistingUser", "false"); + if CONFIG.sso_enabled() && CONFIG.sso_only() { query_params.append_pair("orgSsoIdentifier", &org_id); - } else if user.private_key.is_some() { + } + if user.private_key.is_some() { query_params.append_pair("orgUserHasExistingUser", "true"); } } From c555f7d1980c8ab3ac8d64b0297e47c621590368 Mon Sep 17 00:00:00 2001 From: Mathijs van Veluw Date: Mon, 23 Feb 2026 21:52:44 +0100 Subject: [PATCH 24/89] Misc organization fixes (#6867) --- src/api/core/accounts.rs | 6 - src/api/core/ciphers.rs | 6 +- src/api/core/organizations.rs | 427 +++---------------------------- src/api/core/two_factor/email.rs | 3 + src/api/core/two_factor/mod.rs | 51 +--- src/auth.rs | 6 +- src/db/models/collection.rs | 11 +- 7 files changed, 51 insertions(+), 459 deletions(-) diff --git a/src/api/core/accounts.rs b/src/api/core/accounts.rs index 51ebbf03..0ce1f684 100644 --- a/src/api/core/accounts.rs +++ b/src/api/core/accounts.rs @@ -33,7 +33,6 @@ use rocket::{ pub fn routes() -> Vec { routes![ - register, profile, put_profile, post_profile, @@ -168,11 +167,6 @@ async fn is_email_2fa_required(member_id: Option, conn: &DbConn) - false } -#[post("/accounts/register", data = "")] -async fn register(data: Json, conn: DbConn) -> JsonResult { - _register(data, false, conn).await -} - pub async fn _register(data: Json, email_verification: bool, conn: DbConn) -> JsonResult { let mut data: RegisterData = data.into_inner(); let email = data.email.to_lowercase(); diff --git a/src/api/core/ciphers.rs b/src/api/core/ciphers.rs index d5f244f4..f7bf5cd3 100644 --- a/src/api/core/ciphers.rs +++ b/src/api/core/ciphers.rs @@ -715,9 +715,13 @@ async fn put_cipher_partial( let data: PartialCipherData = data.into_inner(); let Some(cipher) = Cipher::find_by_uuid(&cipher_id, &conn).await else { - err!("Cipher doesn't exist") + err!("Cipher does not exist") }; + if !cipher.is_accessible_to_user(&headers.user.uuid, &conn).await { + err!("Cipher does not exist", "Cipher is not accessible for the current user") + } + if let Some(ref folder_id) = data.folder_id { if Folder::find_by_uuid_and_user(folder_id, &headers.user.uuid, &conn).await.is_none() { err!("Invalid folder", "Folder does not exist or belongs to another user"); diff --git a/src/api/core/organizations.rs b/src/api/core/organizations.rs index f173f90f..4a5066ab 100644 --- a/src/api/core/organizations.rs +++ b/src/api/core/organizations.rs @@ -36,12 +36,9 @@ pub fn routes() -> Vec { get_org_collections_details, get_org_collection_detail, get_collection_users, - put_collection_users, put_organization, post_organization, post_organization_collections, - delete_organization_collection_member, - post_organization_collection_delete_member, post_bulk_access_collections, post_organization_collection_update, put_organization_collection_update, @@ -64,28 +61,20 @@ pub fn routes() -> Vec { put_member, delete_member, bulk_delete_member, - post_delete_member, post_org_import, list_policies, list_policies_token, get_master_password_policy, get_policy, put_policy, - get_organization_tax, + put_policy_vnext, get_plans, - get_plans_all, - get_plans_tax_rates, - import, post_org_keys, get_organization_keys, get_organization_public_key, bulk_public_keys, - deactivate_member, - bulk_deactivate_members, revoke_member, bulk_revoke_members, - activate_member, - bulk_activate_members, restore_member, bulk_restore_members, get_groups, @@ -100,10 +89,6 @@ pub fn routes() -> Vec { bulk_delete_groups, get_group_members, put_group_members, - get_user_groups, - post_user_groups, - put_user_groups, - delete_group_member, post_delete_group_member, put_reset_password_enrollment, get_reset_password_details, @@ -380,6 +365,11 @@ async fn get_org_collections(org_id: OrganizationId, headers: ManagerHeadersLoos if org_id != headers.membership.org_uuid { err!("Organization not found", "Organization id's do not match"); } + + if !headers.membership.has_full_access() { + err_code!("Resource not found.", "User does not have full access", rocket::http::Status::NotFound.code); + } + Ok(Json(json!({ "data": _get_org_collections(&org_id, &conn).await, "object": "list", @@ -392,7 +382,6 @@ async fn get_org_collections_details(org_id: OrganizationId, headers: ManagerHea if org_id != headers.membership.org_uuid { err!("Organization not found", "Organization id's do not match"); } - let mut data = Vec::new(); let Some(member) = Membership::find_by_user_and_org(&headers.user.uuid, &org_id, &conn).await else { err!("User is not part of organization") @@ -424,6 +413,7 @@ async fn get_org_collections_details(org_id: OrganizationId, headers: ManagerHea }) .collect(); + let mut data = Vec::new(); for col in Collection::find_by_organization(&org_id, &conn).await { // check whether the current user has access to the given collection let assigned = has_full_access_to_org @@ -566,6 +556,10 @@ async fn post_bulk_access_collections( err!("Collection not found") }; + if !collection.is_manageable_by_user(&headers.membership.user_uuid, &conn).await { + err!("Collection not found", "The current user isn't a manager for this collection") + } + // update collection modification date collection.save(&conn).await?; @@ -682,43 +676,6 @@ async fn post_organization_collection_update( Ok(Json(collection.to_json_details(&headers.user.uuid, None, &conn).await)) } -#[delete("/organizations//collections//user/")] -async fn delete_organization_collection_member( - org_id: OrganizationId, - col_id: CollectionId, - member_id: MembershipId, - headers: AdminHeaders, - conn: DbConn, -) -> EmptyResult { - if org_id != headers.org_id { - err!("Organization not found", "Organization id's do not match"); - } - let Some(collection) = Collection::find_by_uuid_and_org(&col_id, &org_id, &conn).await else { - err!("Collection not found", "Collection does not exist or does not belong to this organization") - }; - - match Membership::find_by_uuid_and_org(&member_id, &org_id, &conn).await { - None => err!("User not found in organization"), - Some(member) => { - match CollectionUser::find_by_collection_and_user(&collection.uuid, &member.user_uuid, &conn).await { - None => err!("User not assigned to collection"), - Some(col_user) => col_user.delete(&conn).await, - } - } - } -} - -#[post("/organizations//collections//delete-user/")] -async fn post_organization_collection_delete_member( - org_id: OrganizationId, - col_id: CollectionId, - member_id: MembershipId, - headers: AdminHeaders, - conn: DbConn, -) -> EmptyResult { - delete_organization_collection_member(org_id, col_id, member_id, headers, conn).await -} - async fn _delete_organization_collection( org_id: &OrganizationId, col_id: &CollectionId, @@ -887,41 +844,6 @@ async fn get_collection_users( Ok(Json(json!(member_list))) } -#[put("/organizations//collections//users", data = "")] -async fn put_collection_users( - org_id: OrganizationId, - col_id: CollectionId, - data: Json>, - headers: ManagerHeaders, - conn: DbConn, -) -> EmptyResult { - if org_id != headers.org_id { - err!("Organization not found", "Organization id's do not match"); - } - // Get org and collection, check that collection is from org - if Collection::find_by_uuid_and_org(&col_id, &org_id, &conn).await.is_none() { - err!("Collection not found in Organization") - } - - // Delete all the user-collections - CollectionUser::delete_all_by_collection(&col_id, &conn).await?; - - // And then add all the received ones (except if the user has access_all) - for d in data.iter() { - let Some(user) = Membership::find_by_uuid_and_org(&d.id, &org_id, &conn).await else { - err!("User is not part of organization") - }; - - if user.access_all { - continue; - } - - CollectionUser::save(&user.user_uuid, &col_id, d.read_only, d.hide_passwords, d.manage, &conn).await?; - } - - Ok(()) -} - #[derive(FromForm)] struct OrgIdData { #[field(name = "organizationId")] @@ -1719,17 +1641,6 @@ async fn delete_member( _delete_member(&org_id, &member_id, &headers, &conn, &nt).await } -#[post("/organizations//users//delete")] -async fn post_delete_member( - org_id: OrganizationId, - member_id: MembershipId, - headers: AdminHeaders, - conn: DbConn, - nt: Notify<'_>, -) -> EmptyResult { - _delete_member(&org_id, &member_id, &headers, &conn, &nt).await -} - async fn _delete_member( org_id: &OrganizationId, member_id: &MembershipId, @@ -2182,14 +2093,26 @@ async fn put_policy( Ok(Json(policy.to_json())) } -#[allow(unused_variables)] -#[get("/organizations//tax")] -fn get_organization_tax(org_id: OrganizationId, _headers: Headers) -> Json { - // Prevent a 404 error, which also causes Javascript errors. - // Upstream sends "Only allowed when not self hosted." As an error message. - // If we do the same it will also output this to the log, which is overkill. - // An empty list/data also works fine. - Json(_empty_data_json()) +#[derive(Deserialize)] +struct PolicyDataVnext { + policy: PolicyData, + // Ignore metadata for now as we do not yet support this + // "metadata": { + // "defaultUserCollectionName": "2.xx|xx==|xx=" + // } +} + +#[put("/organizations//policies//vnext", data = "")] +async fn put_policy_vnext( + org_id: OrganizationId, + pol_type: i32, + data: Json, + headers: AdminHeaders, + conn: DbConn, +) -> JsonResult { + let data: PolicyDataVnext = data.into_inner(); + let policy: PolicyData = data.policy; + put_policy(org_id, pol_type, Json(policy), headers, conn).await } #[get("/plans")] @@ -2220,17 +2143,6 @@ fn get_plans() -> Json { })) } -#[get("/plans/all")] -fn get_plans_all() -> Json { - get_plans() -} - -#[get("/plans/sales-tax-rates")] -fn get_plans_tax_rates(_headers: Headers) -> Json { - // Prevent a 404 error, which also causes Javascript errors. - Json(_empty_data_json()) -} - #[get("/organizations/<_org_id>/billing/metadata")] fn get_billing_metadata(_org_id: OrganizationId, _headers: Headers) -> Json { // Prevent a 404 error, which also causes Javascript errors. @@ -2255,174 +2167,12 @@ fn _empty_data_json() -> Value { }) } -#[derive(Deserialize, Debug)] -#[serde(rename_all = "camelCase")] -struct OrgImportGroupData { - #[allow(dead_code)] - name: String, // "GroupName" - #[allow(dead_code)] - external_id: String, // "cn=GroupName,ou=Groups,dc=example,dc=com" - #[allow(dead_code)] - users: Vec, // ["uid=user,ou=People,dc=example,dc=com"] -} - -#[derive(Deserialize, Debug)] -#[serde(rename_all = "camelCase")] -struct OrgImportUserData { - email: String, // "user@maildomain.net" - #[allow(dead_code)] - external_id: String, // "uid=user,ou=People,dc=example,dc=com" - deleted: bool, -} - -#[derive(Deserialize, Debug)] -#[serde(rename_all = "camelCase")] -struct OrgImportData { - #[allow(dead_code)] - groups: Vec, - overwrite_existing: bool, - users: Vec, -} - -/// This function seems to be deprecated -/// It is only used with older directory connectors -/// TODO: Cleanup Tech debt -#[post("/organizations//import", data = "")] -async fn import(org_id: OrganizationId, data: Json, headers: Headers, conn: DbConn) -> EmptyResult { - let data = data.into_inner(); - - // TODO: Currently we aren't storing the externalId's anywhere, so we also don't have a way - // to differentiate between auto-imported users and manually added ones. - // This means that this endpoint can end up removing users that were added manually by an admin, - // as opposed to upstream which only removes auto-imported users. - - // User needs to be admin or owner to use the Directory Connector - match Membership::find_by_user_and_org(&headers.user.uuid, &org_id, &conn).await { - Some(member) if member.atype >= MembershipType::Admin => { /* Okay, nothing to do */ } - Some(_) => err!("User has insufficient permissions to use Directory Connector"), - None => err!("User not part of organization"), - }; - - for user_data in &data.users { - if user_data.deleted { - // If user is marked for deletion and it exists, delete it - if let Some(member) = Membership::find_by_email_and_org(&user_data.email, &org_id, &conn).await { - log_event( - EventType::OrganizationUserRemoved as i32, - &member.uuid, - &org_id, - &headers.user.uuid, - headers.device.atype, - &headers.ip.ip, - &conn, - ) - .await; - - member.delete(&conn).await?; - } - - // If user is not part of the organization, but it exists - } else if Membership::find_by_email_and_org(&user_data.email, &org_id, &conn).await.is_none() { - if let Some(user) = User::find_by_mail(&user_data.email, &conn).await { - let member_status = if CONFIG.mail_enabled() { - MembershipStatus::Invited as i32 - } else { - MembershipStatus::Accepted as i32 // Automatically mark user as accepted if no email invites - }; - - let mut new_member = - Membership::new(user.uuid.clone(), org_id.clone(), Some(headers.user.email.clone())); - new_member.access_all = false; - new_member.atype = MembershipType::User as i32; - new_member.status = member_status; - - if CONFIG.mail_enabled() { - let org_name = match Organization::find_by_uuid(&org_id, &conn).await { - Some(org) => org.name, - None => err!("Error looking up organization"), - }; - - mail::send_invite( - &user, - org_id.clone(), - new_member.uuid.clone(), - &org_name, - Some(headers.user.email.clone()), - ) - .await?; - } - - // Save the member after sending an email - // If sending fails the member will not be saved to the database, and will not result in the admin needing to reinvite the users manually - new_member.save(&conn).await?; - - log_event( - EventType::OrganizationUserInvited as i32, - &new_member.uuid, - &org_id, - &headers.user.uuid, - headers.device.atype, - &headers.ip.ip, - &conn, - ) - .await; - } - } - } - - // If this flag is enabled, any user that isn't provided in the Users list will be removed (by default they will be kept unless they have Deleted == true) - if data.overwrite_existing { - for member in Membership::find_by_org_and_type(&org_id, MembershipType::User, &conn).await { - if let Some(user_email) = User::find_by_uuid(&member.user_uuid, &conn).await.map(|u| u.email) { - if !data.users.iter().any(|u| u.email == user_email) { - log_event( - EventType::OrganizationUserRemoved as i32, - &member.uuid, - &org_id, - &headers.user.uuid, - headers.device.atype, - &headers.ip.ip, - &conn, - ) - .await; - - member.delete(&conn).await?; - } - } - } - } - - Ok(()) -} - -// Pre web-vault v2022.9.x endpoint -#[put("/organizations//users//deactivate")] -async fn deactivate_member( - org_id: OrganizationId, - member_id: MembershipId, - headers: AdminHeaders, - conn: DbConn, -) -> EmptyResult { - _revoke_member(&org_id, &member_id, &headers, &conn).await -} - #[derive(Deserialize, Debug)] #[serde(rename_all = "camelCase")] struct BulkRevokeMembershipIds { ids: Option>, } -// Pre web-vault v2022.9.x endpoint -#[put("/organizations//users/deactivate", data = "")] -async fn bulk_deactivate_members( - org_id: OrganizationId, - data: Json, - headers: AdminHeaders, - conn: DbConn, -) -> JsonResult { - bulk_revoke_members(org_id, data, headers, conn).await -} - #[put("/organizations//users//revoke")] async fn revoke_member( org_id: OrganizationId, @@ -2516,28 +2266,6 @@ async fn _revoke_member( Ok(()) } -// Pre web-vault v2022.9.x endpoint -#[put("/organizations//users//activate")] -async fn activate_member( - org_id: OrganizationId, - member_id: MembershipId, - headers: AdminHeaders, - conn: DbConn, -) -> EmptyResult { - _restore_member(&org_id, &member_id, &headers, &conn).await -} - -// Pre web-vault v2022.9.x endpoint -#[put("/organizations//users/activate", data = "")] -async fn bulk_activate_members( - org_id: OrganizationId, - data: Json, - headers: AdminHeaders, - conn: DbConn, -) -> JsonResult { - bulk_restore_members(org_id, data, headers, conn).await -} - #[put("/organizations//users//restore")] async fn restore_member( org_id: OrganizationId, @@ -3006,88 +2734,6 @@ async fn put_group_members( Ok(()) } -#[get("/organizations//users//groups")] -async fn get_user_groups( - org_id: OrganizationId, - member_id: MembershipId, - headers: AdminHeaders, - conn: DbConn, -) -> JsonResult { - if org_id != headers.org_id { - err!("Organization not found", "Organization id's do not match"); - } - if !CONFIG.org_groups_enabled() { - err!("Group support is disabled"); - } - - if Membership::find_by_uuid_and_org(&member_id, &org_id, &conn).await.is_none() { - err!("User could not be found!") - }; - - let user_groups: Vec = - GroupUser::find_by_member(&member_id, &conn).await.iter().map(|entry| entry.groups_uuid.clone()).collect(); - - Ok(Json(json!(user_groups))) -} - -#[derive(Deserialize)] -#[serde(rename_all = "camelCase")] -struct OrganizationUserUpdateGroupsRequest { - group_ids: Vec, -} - -#[post("/organizations//users//groups", data = "")] -async fn post_user_groups( - org_id: OrganizationId, - member_id: MembershipId, - data: Json, - headers: AdminHeaders, - conn: DbConn, -) -> EmptyResult { - put_user_groups(org_id, member_id, data, headers, conn).await -} - -#[put("/organizations//users//groups", data = "")] -async fn put_user_groups( - org_id: OrganizationId, - member_id: MembershipId, - data: Json, - headers: AdminHeaders, - conn: DbConn, -) -> EmptyResult { - if org_id != headers.org_id { - err!("Organization not found", "Organization id's do not match"); - } - if !CONFIG.org_groups_enabled() { - err!("Group support is disabled"); - } - - if Membership::find_by_uuid_and_org(&member_id, &org_id, &conn).await.is_none() { - err!("User could not be found or does not belong to the organization."); - } - - GroupUser::delete_all_by_member(&member_id, &conn).await?; - - let assigned_group_ids = data.into_inner(); - for assigned_group_id in assigned_group_ids.group_ids { - let mut group_user = GroupUser::new(assigned_group_id.clone(), member_id.clone()); - group_user.save(&conn).await?; - } - - log_event( - EventType::OrganizationUserUpdatedGroups as i32, - &member_id, - &org_id, - &headers.user.uuid, - headers.device.atype, - &headers.ip.ip, - &conn, - ) - .await; - - Ok(()) -} - #[post("/organizations//groups//delete-user/")] async fn post_delete_group_member( org_id: OrganizationId, @@ -3095,17 +2741,6 @@ async fn post_delete_group_member( member_id: MembershipId, headers: AdminHeaders, conn: DbConn, -) -> EmptyResult { - delete_group_member(org_id, group_id, member_id, headers, conn).await -} - -#[delete("/organizations//groups//users/")] -async fn delete_group_member( - org_id: OrganizationId, - group_id: GroupId, - member_id: MembershipId, - headers: AdminHeaders, - conn: DbConn, ) -> EmptyResult { if org_id != headers.org_id { err!("Organization not found", "Organization id's do not match"); diff --git a/src/api/core/two_factor/email.rs b/src/api/core/two_factor/email.rs index 25218069..e7d1aed2 100644 --- a/src/api/core/two_factor/email.rs +++ b/src/api/core/two_factor/email.rs @@ -44,6 +44,9 @@ async fn send_email_login(data: Json, client_headers: Client err!("Email 2FA is disabled") } + // Ratelimit the login + crate::ratelimit::check_limit_login(&client_headers.ip.ip)?; + // Get the user let email = match &data.email { Some(email) if !email.is_empty() => Some(email), diff --git a/src/api/core/two_factor/mod.rs b/src/api/core/two_factor/mod.rs index dfaae77a..34fbfaa9 100644 --- a/src/api/core/two_factor/mod.rs +++ b/src/api/core/two_factor/mod.rs @@ -9,7 +9,7 @@ use crate::{ core::{log_event, log_user_event}, EmptyResult, JsonResult, PasswordOrOtpData, }, - auth::{ClientHeaders, Headers}, + auth::Headers, crypto, db::{ models::{ @@ -35,7 +35,6 @@ pub fn routes() -> Vec { let mut routes = routes![ get_twofactor, get_recover, - recover, disable_twofactor, disable_twofactor_put, get_device_verification_settings, @@ -76,54 +75,6 @@ async fn get_recover(data: Json, headers: Headers, conn: DbCo }))) } -#[derive(Deserialize)] -#[serde(rename_all = "camelCase")] -struct RecoverTwoFactor { - master_password_hash: String, - email: String, - recovery_code: String, -} - -#[post("/two-factor/recover", data = "")] -async fn recover(data: Json, client_headers: ClientHeaders, conn: DbConn) -> JsonResult { - let data: RecoverTwoFactor = data.into_inner(); - - use crate::db::models::User; - - // Get the user - let Some(mut user) = User::find_by_mail(&data.email, &conn).await else { - err!("Username or password is incorrect. Try again.") - }; - - // Check password - if !user.check_valid_password(&data.master_password_hash) { - err!("Username or password is incorrect. Try again.") - } - - // Check if recovery code is correct - if !user.check_valid_recovery_code(&data.recovery_code) { - err!("Recovery code is incorrect. Try again.") - } - - // Remove all twofactors from the user - TwoFactor::delete_all_by_user(&user.uuid, &conn).await?; - enforce_2fa_policy(&user, &user.uuid, client_headers.device_type, &client_headers.ip.ip, &conn).await?; - - log_user_event( - EventType::UserRecovered2fa as i32, - &user.uuid, - client_headers.device_type, - &client_headers.ip.ip, - &conn, - ) - .await; - - // Remove the recovery code, not needed without twofactors - user.totp_recover = None; - user.save(&conn).await?; - Ok(Json(Value::Object(serde_json::Map::new()))) -} - async fn _generate_recover_code(user: &mut User, conn: &DbConn) { if user.totp_recover.is_none() { let totp_recover = crypto::encode_random_bytes::<20>(&BASE32); diff --git a/src/auth.rs b/src/auth.rs index ab41898f..b71a5bd9 100644 --- a/src/auth.rs +++ b/src/auth.rs @@ -826,7 +826,7 @@ impl<'r> FromRequest<'r> for ManagerHeaders { _ => err_handler!("Error getting DB"), }; - if !Collection::can_access_collection(&headers.membership, &col_id, &conn).await { + if !Collection::is_coll_manageable_by_user(&col_id, &headers.membership.user_uuid, &conn).await { err_handler!("The current user isn't a manager for this collection") } } @@ -908,8 +908,8 @@ impl ManagerHeaders { if uuid::Uuid::parse_str(col_id.as_ref()).is_err() { err!("Collection Id is malformed!"); } - if !Collection::can_access_collection(&h.membership, col_id, conn).await { - err!("You don't have access to all collections!"); + if !Collection::is_coll_manageable_by_user(col_id, &h.membership.user_uuid, conn).await { + err!("Collection not found", "The current user isn't a manager for this collection") } } diff --git a/src/db/models/collection.rs b/src/db/models/collection.rs index 52ded966..3e6ccf21 100644 --- a/src/db/models/collection.rs +++ b/src/db/models/collection.rs @@ -513,7 +513,8 @@ impl Collection { }} } - pub async fn is_manageable_by_user(&self, user_uuid: &UserId, conn: &DbConn) -> bool { + pub async fn is_coll_manageable_by_user(uuid: &CollectionId, user_uuid: &UserId, conn: &DbConn) -> bool { + let uuid = uuid.to_string(); let user_uuid = user_uuid.to_string(); db_run! { conn: { collections::table @@ -538,9 +539,9 @@ impl Collection { collections_groups::collections_uuid.eq(collections::uuid) ) )) - .filter(collections::uuid.eq(&self.uuid)) + .filter(collections::uuid.eq(&uuid)) .filter( - users_collections::collection_uuid.eq(&self.uuid).and(users_collections::manage.eq(true)).or(// Directly accessed collection + users_collections::collection_uuid.eq(&uuid).and(users_collections::manage.eq(true)).or(// Directly accessed collection users_organizations::access_all.eq(true).or( // access_all in Organization users_organizations::atype.le(MembershipType::Admin as i32) // Org admin or owner )).or( @@ -558,6 +559,10 @@ impl Collection { .unwrap_or(0) != 0 }} } + + pub async fn is_manageable_by_user(&self, user_uuid: &UserId, conn: &DbConn) -> bool { + Self::is_coll_manageable_by_user(&self.uuid, user_uuid, conn).await + } } /// Database methods From ba5519167634ebe1e1f0fc10d610d10d1f405101 Mon Sep 17 00:00:00 2001 From: Stefan Melmuk <509385+stefan0xC@users.noreply.github.com> Date: Wed, 4 Mar 2026 06:58:39 +0100 Subject: [PATCH 25/89] apply policies only to confirmed members (#6892) --- src/db/models/org_policy.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/db/models/org_policy.rs b/src/db/models/org_policy.rs index 0607f146..96811a2b 100644 --- a/src/db/models/org_policy.rs +++ b/src/db/models/org_policy.rs @@ -269,7 +269,7 @@ impl OrgPolicy { continue; } - if let Some(user) = Membership::find_by_user_and_org(user_uuid, &policy.org_uuid, conn).await { + if let Some(user) = Membership::find_confirmed_by_user_and_org(user_uuid, &policy.org_uuid, conn).await { if user.atype < MembershipType::Admin { return true; } From 937857a0bc518710f1ff33e27e0c420d5e030da8 Mon Sep 17 00:00:00 2001 From: Ken Watanabe <51844896+dorakemon@users.noreply.github.com> Date: Tue, 10 Mar 2026 01:50:21 +0900 Subject: [PATCH 26/89] Merge commit from fork * Fix WebAuthn backup flag update before signature verification * fix format rust file * Add test for migration update * Remove webauthn test --- src/api/core/two_factor/webauthn.rs | 23 +++++++---------------- 1 file changed, 7 insertions(+), 16 deletions(-) diff --git a/src/api/core/two_factor/webauthn.rs b/src/api/core/two_factor/webauthn.rs index b10a5ded..6ae12752 100644 --- a/src/api/core/two_factor/webauthn.rs +++ b/src/api/core/two_factor/webauthn.rs @@ -438,7 +438,7 @@ pub async fn validate_webauthn_login(user_id: &UserId, response: &str, conn: &Db // We need to check for and update the backup_eligible flag when needed. // Vaultwarden did not have knowledge of this flag prior to migrating to webauthn-rs v0.5.x // Because of this we check the flag at runtime and update the registrations and state when needed - check_and_update_backup_eligible(user_id, &rsp, &mut registrations, &mut state, conn).await?; + let backup_flags_updated = check_and_update_backup_eligible(&rsp, &mut registrations, &mut state)?; let authentication_result = WEBAUTHN.finish_passkey_authentication(&rsp, &state)?; @@ -446,7 +446,8 @@ pub async fn validate_webauthn_login(user_id: &UserId, response: &str, conn: &Db if ct_eq(reg.credential.cred_id(), authentication_result.cred_id()) { // If the cred id matches and the credential is updated, Some(true) is returned // In those cases, update the record, else leave it alone - if reg.credential.update_credential(&authentication_result) == Some(true) { + let credential_updated = reg.credential.update_credential(&authentication_result) == Some(true); + if credential_updated || backup_flags_updated { TwoFactor::new(user_id.clone(), TwoFactorType::Webauthn, serde_json::to_string(®istrations)?) .save(conn) .await?; @@ -463,13 +464,11 @@ pub async fn validate_webauthn_login(user_id: &UserId, response: &str, conn: &Db ) } -async fn check_and_update_backup_eligible( - user_id: &UserId, +fn check_and_update_backup_eligible( rsp: &PublicKeyCredential, registrations: &mut Vec, state: &mut PasskeyAuthentication, - conn: &DbConn, -) -> EmptyResult { +) -> Result { // The feature flags from the response // For details see: https://www.w3.org/TR/webauthn-3/#sctn-authenticator-data const FLAG_BACKUP_ELIGIBLE: u8 = 0b0000_1000; @@ -486,16 +485,7 @@ async fn check_and_update_backup_eligible( let rsp_id = rsp.raw_id.as_slice(); for reg in &mut *registrations { if ct_eq(reg.credential.cred_id().as_slice(), rsp_id) { - // Try to update the key, and if needed also update the database, before the actual state check is done if reg.set_backup_eligible(backup_eligible, backup_state) { - TwoFactor::new( - user_id.clone(), - TwoFactorType::Webauthn, - serde_json::to_string(®istrations)?, - ) - .save(conn) - .await?; - // We also need to adjust the current state which holds the challenge used to start the authentication verification // Because Vaultwarden supports multiple keys, we need to loop through the deserialized state and check which key to update let mut raw_state = serde_json::to_value(&state)?; @@ -517,11 +507,12 @@ async fn check_and_update_backup_eligible( } *state = serde_json::from_value(raw_state)?; + return Ok(true); } break; } } } } - Ok(()) + Ok(false) } From e60105411b1c31a2ba4a975fec671b153eaf6bdb Mon Sep 17 00:00:00 2001 From: DerPlayer2001 <84600865+DerPlayer2001@users.noreply.github.com> Date: Mon, 9 Mar 2026 17:57:10 +0100 Subject: [PATCH 27/89] Feat(config): add feature flag for Safari account switching (#6891) This enables the use of the Feature from this PR https://github.com/bitwarden/clients/pull/18339 --- .env.template | 1 + src/config.rs | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/.env.template b/.env.template index 67f531fc..01b2a76e 100644 --- a/.env.template +++ b/.env.template @@ -372,6 +372,7 @@ ## Note that clients cache the /api/config endpoint for about 1 hour and it could take some time before they are enabled or disabled! ## ## The following flags are available: +## - "pm-5594-safari-account-switching": Enable account switching in Safari. (Needs Safari >=2026.2.0) ## - "inline-menu-positioning-improvements": Enable the use of inline menu password generator and identity suggestions in the browser extension. ## - "inline-menu-totp": Enable the use of inline menu TOTP codes in the browser extension. ## - "ssh-agent": Enable SSH agent support on Desktop. (Needs desktop >=2024.12.0) diff --git a/src/config.rs b/src/config.rs index 4fb103fa..fdf72608 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1027,12 +1027,14 @@ fn validate_config(cfg: &ConfigItems) -> Result<(), Error> { } // Server (v2025.6.2): https://github.com/bitwarden/server/blob/d094be3267f2030bd0dc62106bc6871cf82682f5/src/Core/Constants.cs#L103 - // Client (web-v2025.6.1): https://github.com/bitwarden/clients/blob/747c2fd6a1c348a57a76e4a7de8128466ffd3c01/libs/common/src/enums/feature-flag.enum.ts#L12 + // Client (web-v2026.2.0): https://github.com/bitwarden/clients/blob/a2fefe804d8c9b4a56c42f9904512c5c5821e2f6/libs/common/src/enums/feature-flag.enum.ts#L12 // Android (v2025.6.0): https://github.com/bitwarden/android/blob/b5b022caaad33390c31b3021b2c1205925b0e1a2/app/src/main/kotlin/com/x8bit/bitwarden/data/platform/manager/model/FlagKey.kt#L22 // iOS (v2025.6.0): https://github.com/bitwarden/ios/blob/ff06d9c6cc8da89f78f37f376495800201d7261a/BitwardenShared/Core/Platform/Models/Enum/FeatureFlag.swift#L7 // // NOTE: Move deprecated flags to the utils::parse_experimental_client_feature_flags() DEPRECATED_FLAGS const! const KNOWN_FLAGS: &[&str] = &[ + // Auth Team + "pm-5594-safari-account-switching", // Autofill Team "inline-menu-positioning-improvements", "inline-menu-totp", From 747286dccdf94f762c3b4c93a82da935563418a7 Mon Sep 17 00:00:00 2001 From: Chris Kruger Date: Tue, 10 Mar 2026 01:04:17 +0800 Subject: [PATCH 28/89] fix: add ForcePasswordReset to api key login (#6904) --- src/api/identity.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/api/identity.rs b/src/api/identity.rs index 0ac0a730..bf093536 100644 --- a/src/api/identity.rs +++ b/src/api/identity.rs @@ -647,6 +647,7 @@ async fn _user_api_key_login( "KdfMemory": user.client_kdf_memory, "KdfParallelism": user.client_kdf_parallelism, "ResetMasterPassword": false, // TODO: according to official server seems something like: user.password_hash.is_empty(), but would need testing + "ForcePasswordReset": false, "scope": AuthMethod::UserApiKey.scope(), "UserDecryptionOptions": { "HasMasterPassword": has_master_password, From df25d316d68a90ecf42c31f044d09ca23a393282 Mon Sep 17 00:00:00 2001 From: pasarenicu <140453985+pasarenicu@users.noreply.github.com> Date: Mon, 9 Mar 2026 19:06:41 +0200 Subject: [PATCH 29/89] Add Webauthn related origins flag to known flags. (#6900) support pm-30529-webauthn-related-origins flag --- src/config.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/config.rs b/src/config.rs index fdf72608..c12ded48 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1048,6 +1048,8 @@ fn validate_config(cfg: &ConfigItems) -> Result<(), Error> { "anon-addy-self-host-alias", "simple-login-self-host-alias", "mutual-tls", + // Webauthn Related Origins + "pm-30529-webauthn-related-origins", ]; 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(); From ecdb18fcde34743833fad40a2b8510b4e25a8cd1 Mon Sep 17 00:00:00 2001 From: Timshel Date: Mon, 9 Mar 2026 17:10:06 +0000 Subject: [PATCH 30/89] Add 30s cache to SSO exchange_refresh_token (#6866) Co-authored-by: Timshel --- Cargo.lock | 125 +++------------------------------------------- Cargo.toml | 2 +- src/sso_client.rs | 48 +++++++++++------- 3 files changed, 37 insertions(+), 138 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index da9e0d79..e9605aa6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -815,12 +815,6 @@ version = "3.19.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510" -[[package]] -name = "bytecount" -version = "0.6.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "175812e0be2bccb6abe50bb8d566126198344f707e304f45c648fd8f2cc0365e" - [[package]] name = "bytemuck" version = "1.25.0" @@ -885,37 +879,6 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ade8366b8bd5ba243f0a58f036cc0ca8a2f069cff1a2351ef1cac6b083e16fc0" -[[package]] -name = "camino" -version = "1.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e629a66d692cb9ff1a1c664e41771b3dcaf961985a9774c0eb0bd1b51cf60a48" -dependencies = [ - "serde_core", -] - -[[package]] -name = "cargo-platform" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e35af189006b9c0f00a064685c727031e3ed2d8020f7ba284d78cc2671bd36ea" -dependencies = [ - "serde", -] - -[[package]] -name = "cargo_metadata" -version = "0.14.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4acbb09d9ee8e23699b9634375c72795d095bf268439da88562cf9b501f181fa" -dependencies = [ - "camino", - "cargo-platform", - "semver", - "serde", - "serde_json", -] - [[package]] name = "cbc" version = "0.1.2" @@ -1331,19 +1294,6 @@ dependencies = [ "syn", ] -[[package]] -name = "dashmap" -version = "5.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "978747c1d849a7d2ee5e8adc0159961c48fb7e5db2f06af6723b80123bb53856" -dependencies = [ - "cfg-if", - "hashbrown 0.14.5", - "lock_api", - "once_cell", - "parking_lot_core", -] - [[package]] name = "dashmap" version = "6.1.0" @@ -1762,15 +1712,6 @@ dependencies = [ "windows-sys 0.61.2", ] -[[package]] -name = "error-chain" -version = "0.12.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d2f06b9cac1506ece98fe3231e3cc9c4410ec3d5b1f24ae1c8946f0742cdefc" -dependencies = [ - "version_check", -] - [[package]] name = "event-listener" version = "2.5.3" @@ -2101,7 +2042,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9efcab3c1958580ff1f25a2a41be1668f7603d849bb63af523b208a3cc1223b8" dependencies = [ "cfg-if", - "dashmap 6.1.0", + "dashmap", "futures-sink", "futures-timer", "futures-util", @@ -3063,21 +3004,6 @@ version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" -[[package]] -name = "mini-moka" -version = "0.10.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c325dfab65f261f386debee8b0969da215b3fa0037e74c8a1234db7ba986d803" -dependencies = [ - "crossbeam-channel", - "crossbeam-utils", - "dashmap 5.5.3", - "skeptic", - "smallvec", - "tagptr", - "triomphe", -] - [[package]] name = "minimal-lexical" version = "0.2.1" @@ -3111,10 +3037,13 @@ version = "0.12.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4ac832c50ced444ef6be0767a008b02c106a909ba79d1d830501e94b96f6b7e" dependencies = [ + "async-lock", "crossbeam-channel", "crossbeam-epoch", "crossbeam-utils", "equivalent", + "event-listener 5.4.1", + "futures-util", "parking_lot", "portable-atomic", "smallvec", @@ -3921,17 +3850,6 @@ dependencies = [ "psl-types", ] -[[package]] -name = "pulldown-cmark" -version = "0.9.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57206b407293d2bcd3af849ce869d52068623f19e1b5ff8e8778e3309439682b" -dependencies = [ - "bitflags", - "memchr", - "unicase", -] - [[package]] name = "quanta" version = "0.12.6" @@ -4769,10 +4687,6 @@ name = "semver" version = "1.0.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" -dependencies = [ - "serde", - "serde_core", -] [[package]] name = "serde" @@ -5009,21 +4923,6 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b2aa850e253778c88a04c3d7323b043aeda9d3e30d5971937c1855769763678e" -[[package]] -name = "skeptic" -version = "0.13.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16d23b015676c90a0f01c197bfdc786c20342c73a0afdda9025adb0bc42940a8" -dependencies = [ - "bytecount", - "cargo_metadata", - "error-chain", - "glob", - "pulldown-cmark", - "tempfile", - "walkdir", -] - [[package]] name = "slab" version = "0.4.12" @@ -5644,12 +5543,6 @@ dependencies = [ "tracing-log", ] -[[package]] -name = "triomphe" -version = "0.1.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd69c5aa8f924c7519d6372789a74eac5b94fb0f8fcf0d4a97eb0bfc3e785f39" - [[package]] name = "try-lock" version = "0.2.5" @@ -5706,12 +5599,6 @@ dependencies = [ "version_check", ] -[[package]] -name = "unicase" -version = "2.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142" - [[package]] name = "unicode-ident" version = "1.0.24" @@ -5807,7 +5694,7 @@ dependencies = [ "chrono-tz", "cookie", "cookie_store", - "dashmap 6.1.0", + "dashmap", "data-encoding", "data-url", "derive_more", @@ -5831,7 +5718,7 @@ dependencies = [ "log", "macros", "mimalloc", - "mini-moka", + "moka", "num-derive", "num-traits", "opendal", diff --git a/Cargo.toml b/Cargo.toml index fd910852..e50e006a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -173,7 +173,7 @@ governor = "0.10.4" # OIDC for SSO openidconnect = { version = "4.0.1", features = ["reqwest", "rustls-tls"] } -mini-moka = "0.10.3" +moka = { version = "0.12.13", features = ["future"] } # Check client versions for specific features. semver = "1.0.27" diff --git a/src/sso_client.rs b/src/sso_client.rs index 0d73d906..6204ab48 100644 --- a/src/sso_client.rs +++ b/src/sso_client.rs @@ -1,6 +1,5 @@ use std::{borrow::Cow, sync::LazyLock, time::Duration}; -use mini_moka::sync::Cache; use openidconnect::{core::*, reqwest, *}; use regex::Regex; use url::Url; @@ -13,9 +12,14 @@ use crate::{ }; static CLIENT_CACHE_KEY: LazyLock = LazyLock::new(|| "sso-client".to_string()); -static CLIENT_CACHE: LazyLock> = LazyLock::new(|| { - Cache::builder().max_capacity(1).time_to_live(Duration::from_secs(CONFIG.sso_client_cache_expiration())).build() +static CLIENT_CACHE: LazyLock> = LazyLock::new(|| { + moka::sync::Cache::builder() + .max_capacity(1) + .time_to_live(Duration::from_secs(CONFIG.sso_client_cache_expiration())) + .build() }); +static REFRESH_CACHE: LazyLock>> = + LazyLock::new(|| moka::future::Cache::builder().max_capacity(1000).time_to_live(Duration::from_secs(30)).build()); /// OpenID Connect Core client. pub type CustomClient = openidconnect::Client< @@ -38,6 +42,8 @@ pub type CustomClient = openidconnect::Client< EndpointSet, >; +pub type RefreshTokenResponse = (Option, String, Option); + #[derive(Clone)] pub struct Client { pub http_client: reqwest::Client, @@ -231,23 +237,29 @@ impl Client { verifier } - pub async fn exchange_refresh_token( - refresh_token: String, - ) -> ApiResult<(Option, String, Option)> { + pub async fn exchange_refresh_token(refresh_token: String) -> ApiResult { + let client = Client::cached().await?; + + REFRESH_CACHE + .get_with(refresh_token.clone(), async move { client._exchange_refresh_token(refresh_token).await }) + .await + .map_err(Into::into) + } + + async fn _exchange_refresh_token(&self, refresh_token: String) -> Result { let rt = RefreshToken::new(refresh_token); - let client = Client::cached().await?; - let token_response = - match client.core_client.exchange_refresh_token(&rt).request_async(&client.http_client).await { - Err(err) => err!(format!("Request to exchange_refresh_token endpoint failed: {:?}", err)), - Ok(token_response) => token_response, - }; - - Ok(( - token_response.refresh_token().map(|token| token.secret().clone()), - token_response.access_token().secret().clone(), - token_response.expires_in(), - )) + match self.core_client.exchange_refresh_token(&rt).request_async(&self.http_client).await { + Err(err) => { + error!("Request to exchange_refresh_token endpoint failed: {err}"); + Err(format!("Request to exchange_refresh_token endpoint failed: {err}")) + } + Ok(token_response) => Ok(( + token_response.refresh_token().map(|token| token.secret().clone()), + token_response.access_token().secret().clone(), + token_response.expires_in(), + )), + } } } From c6e99489847c5edd31bf53e1ed2f974497e88a60 Mon Sep 17 00:00:00 2001 From: phoeagon Date: Mon, 9 Mar 2026 10:21:23 -0700 Subject: [PATCH 31/89] Add cxp-import-mobile and cxp-export-mobile: feature flags on mobile (#6853) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Daniel GarcĂ­a --- .env.template | 2 ++ src/config.rs | 2 ++ 2 files changed, 4 insertions(+) diff --git a/.env.template b/.env.template index 01b2a76e..c5563a1d 100644 --- a/.env.template +++ b/.env.template @@ -382,6 +382,8 @@ ## - "anon-addy-self-host-alias": Enable configuring self-hosted Anon Addy alias generator. (Needs Android >=2025.3.0, iOS >=2025.4.0) ## - "simple-login-self-host-alias": Enable configuring self-hosted Simple Login alias generator. (Needs Android >=2025.3.0, iOS >=2025.4.0) ## - "mutual-tls": Enable the use of mutual TLS on Android (Client >= 2025.2.0) +## - "cxp-import-mobile": Enable the import via CXP on iOS (Clients >=2025.9.2) +## - "cxp-export-mobile": Enable the export via CXP on iOS (Clients >=2025.9.2) # EXPERIMENTAL_CLIENT_FEATURE_FLAGS=fido2-vault-credentials ## Require new device emails. When a user logs in an email is required to be sent. diff --git a/src/config.rs b/src/config.rs index c12ded48..0221fd9a 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1048,6 +1048,8 @@ fn validate_config(cfg: &ConfigItems) -> Result<(), Error> { "anon-addy-self-host-alias", "simple-login-self-host-alias", "mutual-tls", + "cxp-import-mobile", + "cxp-export-mobile", // Webauthn Related Origins "pm-30529-webauthn-related-origins", ]; From 2b16a05e54363ea231333e3bb55f2a481afd1acb Mon Sep 17 00:00:00 2001 From: Mathijs van Veluw Date: Mon, 9 Mar 2026 18:38:22 +0100 Subject: [PATCH 32/89] Misc updates and fixes (#6910) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Fix collection details response Signed-off-by: BlackDex * Misc updates and fixes - Some clippy fixes - Crate updates - Updated Rust to v1.94.0 - Updated all GitHub Actions - Updated web-vault v2026.2.0 Signed-off-by: BlackDex * Remove commented out code --------- Signed-off-by: BlackDex Co-authored-by: Daniel GarcĂ­a --- .github/workflows/hadolint.yml | 2 +- .github/workflows/release.yml | 32 ++-- .github/workflows/trivy.yml | 4 +- .github/workflows/typos.yml | 2 +- .pre-commit-config.yaml | 2 +- Cargo.lock | 276 +++++++++++++++++---------------- Cargo.toml | 18 +-- docker/DockerSettings.yaml | 6 +- docker/Dockerfile.alpine | 20 +-- docker/Dockerfile.debian | 14 +- macros/Cargo.toml | 4 +- rust-toolchain.toml | 2 +- src/api/core/organizations.rs | 7 +- src/api/icons.rs | 10 +- 14 files changed, 204 insertions(+), 195 deletions(-) diff --git a/.github/workflows/hadolint.yml b/.github/workflows/hadolint.yml index 1e0fa48c..29fce9b4 100644 --- a/.github/workflows/hadolint.yml +++ b/.github/workflows/hadolint.yml @@ -20,7 +20,7 @@ jobs: steps: # Start Docker Buildx - name: Setup Docker Buildx - uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0 + uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0 # https://github.com/moby/buildkit/issues/3969 # Also set max parallelism to 2, the default of 4 breaks GitHub Actions and causes OOMKills with: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index fc117e30..8898e055 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -54,13 +54,13 @@ jobs: steps: - name: Initialize QEMU binfmt support - uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3.7.0 + uses: docker/setup-qemu-action@ce360397dd3f832beb865e1373c09c0e9f86d70a # v4.0.0 with: platforms: "arm64,arm" # Start Docker Buildx - name: Setup Docker Buildx - uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0 + uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0 # https://github.com/moby/buildkit/issues/3969 # Also set max parallelism to 2, the default of 4 breaks GitHub Actions and causes OOMKills with: @@ -102,7 +102,7 @@ jobs: # Login to Docker Hub - name: Login to Docker Hub - uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0 + uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} @@ -117,7 +117,7 @@ jobs: # Login to GitHub Container Registry - name: Login to GitHub Container Registry - uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0 + uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0 with: registry: ghcr.io username: ${{ github.repository_owner }} @@ -133,7 +133,7 @@ jobs: # Login to Quay.io - name: Login to Quay.io - uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0 + uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0 with: registry: quay.io username: ${{ secrets.QUAY_USERNAME }} @@ -181,7 +181,7 @@ jobs: - name: Bake ${{ matrix.base_image }} containers id: bake_vw - uses: docker/bake-action@5be5f02ff8819ecd3092ea6b2e6261c31774f2b4 # v6.10.0 + uses: docker/bake-action@82490499d2e5613fcead7e128237ef0b0ea210f7 # v7.0.0 env: BASE_TAGS: "${{ steps.determine-version.outputs.BASE_TAGS }}" SOURCE_COMMIT: "${{ env.SOURCE_COMMIT }}" @@ -218,7 +218,7 @@ jobs: touch "${RUNNER_TEMP}/digests/${digest#sha256:}" - name: Upload digest - uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 with: name: digests-${{ env.NORMALIZED_ARCH }}-${{ matrix.base_image }} path: ${{ runner.temp }}/digests/* @@ -233,12 +233,12 @@ jobs: # Upload artifacts to Github Actions and Attest the binaries - name: Attest binaries - uses: actions/attest-build-provenance@96278af6caaf10aea03fd8d33a09a777ca52d62f # v3.2.0 + uses: actions/attest-build-provenance@a2bbfa25375fe432b6a289bc6b6cd05ecd0c4c32 # v4.1.0 with: subject-path: vaultwarden-${{ env.NORMALIZED_ARCH }} - name: Upload binaries as artifacts - uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 with: name: vaultwarden-${{ env.SOURCE_VERSION }}-linux-${{ env.NORMALIZED_ARCH }}-${{ matrix.base_image }} path: vaultwarden-${{ env.NORMALIZED_ARCH }} @@ -257,7 +257,7 @@ jobs: steps: - name: Download digests - uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0 + uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.0 with: path: ${{ runner.temp }}/digests pattern: digests-*-${{ matrix.base_image }} @@ -265,7 +265,7 @@ jobs: # Login to Docker Hub - name: Login to Docker Hub - uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0 + uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} @@ -280,7 +280,7 @@ jobs: # Login to GitHub Container Registry - name: Login to GitHub Container Registry - uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0 + uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0 with: registry: ghcr.io username: ${{ github.repository_owner }} @@ -296,7 +296,7 @@ jobs: # Login to Quay.io - name: Login to Quay.io - uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0 + uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0 with: registry: quay.io username: ${{ secrets.QUAY_USERNAME }} @@ -358,7 +358,7 @@ jobs: # Attest container images - name: Attest - docker.io - ${{ matrix.base_image }} if: ${{ env.HAVE_DOCKERHUB_LOGIN == 'true' && env.DIGEST_SHA != ''}} - uses: actions/attest-build-provenance@96278af6caaf10aea03fd8d33a09a777ca52d62f # v3.2.0 + uses: actions/attest-build-provenance@a2bbfa25375fe432b6a289bc6b6cd05ecd0c4c32 # v4.1.0 with: subject-name: ${{ vars.DOCKERHUB_REPO }} subject-digest: ${{ env.DIGEST_SHA }} @@ -366,7 +366,7 @@ jobs: - name: Attest - ghcr.io - ${{ matrix.base_image }} if: ${{ env.HAVE_GHCR_LOGIN == 'true' && env.DIGEST_SHA != ''}} - uses: actions/attest-build-provenance@96278af6caaf10aea03fd8d33a09a777ca52d62f # v3.2.0 + uses: actions/attest-build-provenance@a2bbfa25375fe432b6a289bc6b6cd05ecd0c4c32 # v4.1.0 with: subject-name: ${{ vars.GHCR_REPO }} subject-digest: ${{ env.DIGEST_SHA }} @@ -374,7 +374,7 @@ jobs: - name: Attest - quay.io - ${{ matrix.base_image }} if: ${{ env.HAVE_QUAY_LOGIN == 'true' && env.DIGEST_SHA != ''}} - uses: actions/attest-build-provenance@96278af6caaf10aea03fd8d33a09a777ca52d62f # v3.2.0 + uses: actions/attest-build-provenance@a2bbfa25375fe432b6a289bc6b6cd05ecd0c4c32 # v4.1.0 with: subject-name: ${{ vars.QUAY_REPO }} subject-digest: ${{ env.DIGEST_SHA }} diff --git a/.github/workflows/trivy.yml b/.github/workflows/trivy.yml index 13cd2e24..5fc16e71 100644 --- a/.github/workflows/trivy.yml +++ b/.github/workflows/trivy.yml @@ -38,7 +38,7 @@ jobs: persist-credentials: false - name: Run Trivy vulnerability scanner - uses: aquasecurity/trivy-action@c1824fd6edce30d7ab345a9989de00bbd46ef284 # 0.34.0 + uses: aquasecurity/trivy-action@97e0b3872f55f89b95b2f65b3dbab56962816478 # 0.34.2 env: TRIVY_DB_REPOSITORY: docker.io/aquasec/trivy-db:2,public.ecr.aws/aquasecurity/trivy-db:2,ghcr.io/aquasecurity/trivy-db:2 TRIVY_JAVA_DB_REPOSITORY: docker.io/aquasec/trivy-java-db:1,public.ecr.aws/aquasecurity/trivy-java-db:1,ghcr.io/aquasecurity/trivy-java-db:1 @@ -50,6 +50,6 @@ jobs: severity: CRITICAL,HIGH - name: Upload Trivy scan results to GitHub Security tab - uses: github/codeql-action/upload-sarif@9e907b5e64f6b83e7804b09294d44122997950d6 # v4.32.3 + uses: github/codeql-action/upload-sarif@0d579ffd059c29b07949a3cce3983f0780820c98 # v4.32.6 with: sarif_file: 'trivy-results.sarif' diff --git a/.github/workflows/typos.yml b/.github/workflows/typos.yml index 5726a6fc..fc61e266 100644 --- a/.github/workflows/typos.yml +++ b/.github/workflows/typos.yml @@ -23,4 +23,4 @@ jobs: # When this version is updated, do not forget to update this in `.pre-commit-config.yaml` too - name: Spell Check Repo - uses: crate-ci/typos@57b11c6b7e54c402ccd9cda953f1072ec4f78e33 # v1.43.5 + uses: crate-ci/typos@631208b7aac2daa8b707f55e7331f9112b0e062d # v1.44.0 diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 54f09b8b..3a151637 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -53,6 +53,6 @@ repos: - "cd docker && make" # When this version is updated, do not forget to update this in `.github/workflows/typos.yaml` too - repo: https://github.com/crate-ci/typos - rev: 57b11c6b7e54c402ccd9cda953f1072ec4f78e33 # v1.43.5 + rev: 631208b7aac2daa8b707f55e7331f9112b0e062d # v1.44.0 hooks: - id: typos diff --git a/Cargo.lock b/Cargo.lock index e9605aa6..8a694069 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -72,9 +72,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.101" +version = "1.0.102" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f0e0fee31ef5ed1ba1316088939cea399010ed7731dba877ed44aeb407a75ea" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" [[package]] name = "ar_archive_writer" @@ -161,9 +161,9 @@ dependencies = [ [[package]] name = "async-compression" -version = "0.4.39" +version = "0.4.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68650b7df54f0293fd061972a0fb05aaf4fc0879d3b3d21a638a182c5c543b9f" +checksum = "d0f9ee0f6e02ffd7ad5816e9464499fba7b3effd01123b515c41d1697c43dad1" dependencies = [ "compression-codecs", "compression-core", @@ -360,9 +360,9 @@ checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" [[package]] name = "aws-config" -version = "1.8.14" +version = "1.8.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a8fc176d53d6fe85017f230405e3255cedb4a02221cb55ed6d76dccbbb099b2" +checksum = "11493b0bad143270fb8ad284a096dd529ba91924c5409adeac856cc1bf047dbc" dependencies = [ "aws-credential-types", "aws-runtime", @@ -380,7 +380,7 @@ dependencies = [ "fastrand", "hex", "http 1.4.0", - "ring", + "sha1", "time", "tokio", "tracing", @@ -390,9 +390,9 @@ dependencies = [ [[package]] name = "aws-credential-types" -version = "1.2.13" +version = "1.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d203b0bf2626dcba8665f5cd0871d7c2c0930223d6b6be9097592fea21242d0" +checksum = "8f20799b373a1be121fe3005fba0c2090af9411573878f224df44b42727fcaf7" dependencies = [ "aws-smithy-async", "aws-smithy-runtime-api", @@ -402,9 +402,9 @@ dependencies = [ [[package]] name = "aws-runtime" -version = "1.7.1" +version = "1.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ede2ddc593e6c8acc6ce3358c28d6677a6dc49b65ba4b37a2befe14a11297e75" +checksum = "5fc0651c57e384202e47153c1260b84a9936e19803d747615edf199dc3b98d17" dependencies = [ "aws-credential-types", "aws-sigv4", @@ -427,9 +427,9 @@ dependencies = [ [[package]] name = "aws-sdk-sso" -version = "1.95.0" +version = "1.96.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00c5ff27c6ba2cbd95e6e26e2e736676fdf6bcf96495b187733f521cfe4ce448" +checksum = "f64a6eded248c6b453966e915d32aeddb48ea63ad17932682774eb026fbef5b1" dependencies = [ "aws-credential-types", "aws-runtime", @@ -451,9 +451,9 @@ dependencies = [ [[package]] name = "aws-sdk-ssooidc" -version = "1.97.0" +version = "1.98.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d186f1e5a3694a188e5a0640b3115ccc6e084d104e16fd6ba968dca072ffef8" +checksum = "db96d720d3c622fcbe08bae1c4b04a72ce6257d8b0584cb5418da00ae20a344f" dependencies = [ "aws-credential-types", "aws-runtime", @@ -475,9 +475,9 @@ dependencies = [ [[package]] name = "aws-sdk-sts" -version = "1.99.0" +version = "1.100.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9acba7c62f3d4e2408fa998a3a8caacd8b9a5b5549cf36e2372fbdae329d5449" +checksum = "fafbdda43b93f57f699c5dfe8328db590b967b8a820a13ccdd6687355dfcc7ca" dependencies = [ "aws-credential-types", "aws-runtime", @@ -500,9 +500,9 @@ dependencies = [ [[package]] name = "aws-sigv4" -version = "1.4.1" +version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37411f8e0f4bea0c3ca0958ce7f18f6439db24d555dbd809787262cd00926aa9" +checksum = "b0b660013a6683ab23797778e21f1f854744fdf05f68204b4cca4c8c04b5d1f4" dependencies = [ "aws-credential-types", "aws-smithy-http", @@ -522,9 +522,9 @@ dependencies = [ [[package]] name = "aws-smithy-async" -version = "1.2.13" +version = "1.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5cc50d0f63e714784b84223abd7abbc8577de8c35d699e0edd19f0a88a08ae13" +checksum = "2ffcaf626bdda484571968400c326a244598634dc75fd451325a54ad1a59acfc" dependencies = [ "futures-util", "pin-project-lite", @@ -533,9 +533,9 @@ dependencies = [ [[package]] name = "aws-smithy-http" -version = "0.63.5" +version = "0.63.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d619373d490ad70966994801bc126846afaa0d1ee920697a031f0cf63f2568e7" +checksum = "ba1ab2dc1c2c3749ead27180d333c42f11be8b0e934058fb4b2258ee8dbe5231" dependencies = [ "aws-smithy-runtime-api", "aws-smithy-types", @@ -554,27 +554,27 @@ dependencies = [ [[package]] name = "aws-smithy-json" -version = "0.62.4" +version = "0.62.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "27b3a779093e18cad88bbae08dc4261e1d95018c4c5b9356a52bcae7c0b6e9bb" +checksum = "9648b0bb82a2eedd844052c6ad2a1a822d1f8e3adee5fbf668366717e428856a" dependencies = [ "aws-smithy-types", ] [[package]] name = "aws-smithy-observability" -version = "0.2.5" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d3f39d5bb871aaf461d59144557f16d5927a5248a983a40654d9cf3b9ba183b" +checksum = "a06c2315d173edbf1920da8ba3a7189695827002e4c0fc961973ab1c54abca9c" dependencies = [ "aws-smithy-runtime-api", ] [[package]] name = "aws-smithy-query" -version = "0.60.14" +version = "0.60.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05f76a580e3d8f8961e5d48763214025a2af65c2fa4cd1fb7f270a0e107a71b0" +checksum = "1a56d79744fb3edb5d722ef79d86081e121d3b9422cb209eb03aea6aa4f21ebd" dependencies = [ "aws-smithy-types", "urlencoding", @@ -582,9 +582,9 @@ dependencies = [ [[package]] name = "aws-smithy-runtime" -version = "1.10.2" +version = "1.10.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22ccf7f6eba8b2dcf8ce9b74806c6c185659c311665c4bf8d6e71ebd454db6bf" +checksum = "028999056d2d2fd58a697232f9eec4a643cf73a71cf327690a7edad1d2af2110" dependencies = [ "aws-smithy-async", "aws-smithy-http", @@ -606,9 +606,9 @@ dependencies = [ [[package]] name = "aws-smithy-runtime-api" -version = "1.11.5" +version = "1.11.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4af6e5def28be846479bbeac55aa4603d6f7986fc5da4601ba324dd5d377516" +checksum = "876ab3c9c29791ba4ba02b780a3049e21ec63dabda09268b175272c3733a79e6" dependencies = [ "aws-smithy-async", "aws-smithy-types", @@ -623,9 +623,9 @@ dependencies = [ [[package]] name = "aws-smithy-types" -version = "1.4.5" +version = "1.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ca2734c16913a45343b37313605d84e7d8b34a4611598ce1d25b35860a2bed3" +checksum = "d2b1117b3b2bbe166d11199b540ceed0d0f7676e36e7b962b5a437a9971eac75" dependencies = [ "base64-simd", "bytes", @@ -646,18 +646,18 @@ dependencies = [ [[package]] name = "aws-smithy-xml" -version = "0.60.14" +version = "0.60.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b53543b4b86ed43f051644f704a98c7291b3618b67adf057ee77a366fa52fcaa" +checksum = "0ce02add1aa3677d022f8adf81dcbe3046a95f17a1b1e8979c145cd21d3d22b3" dependencies = [ "xmlparser", ] [[package]] name = "aws-types" -version = "1.3.13" +version = "1.3.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0470cc047657c6e286346bdf10a8719d26efd6a91626992e0e64481e44323e96" +checksum = "47c8323699dd9b3c8d5b3c13051ae9cdef58fd179957c882f8374dd8725962d9" dependencies = [ "aws-credential-types", "aws-smithy-async", @@ -811,9 +811,9 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.19.1" +version = "3.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510" +checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" [[package]] name = "bytemuck" @@ -925,9 +925,9 @@ dependencies = [ [[package]] name = "chrono" -version = "0.4.43" +version = "0.4.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fac4744fb15ae8337dc853fee7fb3f4e48c0fbaa23d0afe49c447b4fab126118" +checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0" dependencies = [ "iana-time-zone", "js-sys", @@ -975,9 +975,9 @@ checksum = "b9e769b5c8c8283982a987c6e948e540254f1058d5a74b8794914d4ef5fc2a24" [[package]] name = "compression-codecs" -version = "0.4.36" +version = "0.4.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00828ba6fd27b45a448e57dbfe84f1029d4c9f26b368157e9a448a5f49a2ec2a" +checksum = "eb7b51a7d9c967fc26773061ba86150f19c50c0d65c887cb1fbe295fd16619b7" dependencies = [ "brotli", "compression-core", @@ -1347,9 +1347,9 @@ dependencies = [ [[package]] name = "deranged" -version = "0.5.6" +version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc3dc5ad92c2e2d1c193bbbbdf2ea477cb81331de4f3103f267ca18368b988c4" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" dependencies = [ "powerfmt", "serde_core", @@ -1998,20 +1998,20 @@ dependencies = [ "cfg-if", "js-sys", "libc", - "r-efi", + "r-efi 5.3.0", "wasip2", "wasm-bindgen", ] [[package]] name = "getrandom" -version = "0.4.1" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "139ef39800118c7683f2fd3c98c1b23c09ae076556b435f8e9064ae108aaeeec" +checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" dependencies = [ "cfg-if", "libc", - "r-efi", + "r-efi 6.0.0", "rand_core 0.10.0", "wasip2", "wasip3", @@ -2399,7 +2399,7 @@ dependencies = [ "http 1.4.0", "hyper 1.8.1", "hyper-util", - "rustls 0.23.36", + "rustls 0.23.37", "rustls-native-certs", "rustls-pki-types", "tokio", @@ -2425,7 +2425,7 @@ dependencies = [ "libc", "percent-encoding", "pin-project-lite", - "socket2 0.6.2", + "socket2 0.6.3", "system-configuration", "tokio", "tower-service", @@ -2624,9 +2624,9 @@ dependencies = [ [[package]] name = "ipnet" -version = "2.11.0" +version = "2.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" +checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" [[package]] name = "iri-string" @@ -2672,9 +2672,9 @@ checksum = "47f142fe24a9c9944451e8349de0a56af5f3e7226dc46f3ed4d4ecc0b85af75e" [[package]] name = "jiff" -version = "0.2.20" +version = "0.2.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c867c356cc096b33f4981825ab281ecba3db0acefe60329f044c1789d94c6543" +checksum = "1a3546dc96b6d42c5f24902af9e2538e82e39ad350b0c766eb3fbf2d8f3d8359" dependencies = [ "jiff-static", "jiff-tzdb-platform", @@ -2687,9 +2687,9 @@ dependencies = [ [[package]] name = "jiff-static" -version = "0.2.20" +version = "0.2.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7946b4325269738f270bb55b3c19ab5c5040525f83fd625259422a9d25d9be5" +checksum = "2a8c8b344124222efd714b73bb41f8b5120b27a7cc1c75593a6ff768d9d05aa4" dependencies = [ "proc-macro2", "quote", @@ -2698,9 +2698,9 @@ dependencies = [ [[package]] name = "jiff-tzdb" -version = "0.1.5" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68971ebff725b9e2ca27a601c5eb38a4c5d64422c4cbab0c535f248087eda5c2" +checksum = "c900ef84826f1338a557697dc8fc601df9ca9af4ac137c7fb61d4c6f2dfd3076" [[package]] name = "jiff-tzdb-platform" @@ -2734,9 +2734,9 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.85" +version = "0.3.91" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c942ebf8e95485ca0d52d97da7c5a2c387d0e7f0ba4c35e93bfcaee045955b3" +checksum = "b49715b7073f385ba4bc528e5747d02e66cb39c6146efb66b781f131f0fb399c" dependencies = [ "once_cell", "wasm-bindgen", @@ -2835,10 +2835,10 @@ dependencies = [ "nom 8.0.0", "percent-encoding", "quoted_printable", - "rustls 0.23.36", + "rustls 0.23.37", "rustls-native-certs", "serde", - "socket2 0.6.2", + "socket2 0.6.3", "tokio", "tokio-rustls 0.26.4", "tracing", @@ -2880,9 +2880,9 @@ dependencies = [ [[package]] name = "linux-raw-sys" -version = "0.11.0" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" [[package]] name = "litemap" @@ -3033,9 +3033,9 @@ dependencies = [ [[package]] name = "moka" -version = "0.12.13" +version = "0.12.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4ac832c50ced444ef6be0767a008b02c106a909ba79d1d830501e94b96f6b7e" +checksum = "85f8024e1c8e71c778968af91d43700ce1d11b219d127d79fb2934153b82b42b" dependencies = [ "async-lock", "crossbeam-channel", @@ -3654,9 +3654,9 @@ checksum = "5be167a7af36ee22fe3115051bc51f6e6c7054c9348e28deb4f49bd6f705a315" [[package]] name = "pin-project-lite" -version = "0.2.16" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" [[package]] name = "pin-utils" @@ -3666,9 +3666,9 @@ checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" [[package]] name = "piper" -version = "0.2.4" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96c8c490f422ef9a4efd2cb5b42b76c8613d7e7dfc1caf667b8a3350a5acc066" +checksum = "c835479a4443ded371d6c535cbfd8d31ad92c5d23ae9770a61bc155e4992a3c1" dependencies = [ "atomic-waker", "fastrand", @@ -3903,8 +3903,8 @@ dependencies = [ "quinn-proto", "quinn-udp", "rustc-hash", - "rustls 0.23.36", - "socket2 0.6.2", + "rustls 0.23.37", + "socket2 0.6.3", "thiserror 2.0.18", "tokio", "tracing", @@ -3923,7 +3923,7 @@ dependencies = [ "rand 0.9.2", "ring", "rustc-hash", - "rustls 0.23.36", + "rustls 0.23.37", "rustls-pki-types", "slab", "thiserror 2.0.18", @@ -3941,16 +3941,16 @@ dependencies = [ "cfg_aliases", "libc", "once_cell", - "socket2 0.6.2", + "socket2 0.6.3", "tracing", "windows-sys 0.60.2", ] [[package]] name = "quote" -version = "1.0.44" +version = "1.0.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" dependencies = [ "proc-macro2", ] @@ -3967,6 +3967,12 @@ version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + [[package]] name = "r2d2" version = "0.8.10" @@ -4006,7 +4012,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bc266eb313df6c5c09c1c7b1fbe2510961e5bcd3add930c1e31f7ed9da0feff8" dependencies = [ "chacha20", - "getrandom 0.4.1", + "getrandom 0.4.2", "rand_core 0.10.0", ] @@ -4123,9 +4129,9 @@ checksum = "cab834c73d247e67f4fae452806d17d3c7501756d98c8808d7c9c7aa7d18f973" [[package]] name = "regex-syntax" -version = "0.8.9" +version = "0.8.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a96887878f22d7bad8a3b6dc5b7440e0ada9a245242924394987b21cf2210a4c" +checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" [[package]] name = "reopen" @@ -4198,7 +4204,7 @@ dependencies = [ "percent-encoding", "pin-project-lite", "quinn", - "rustls 0.23.36", + "rustls 0.23.37", "rustls-native-certs", "rustls-pki-types", "serde", @@ -4450,9 +4456,9 @@ dependencies = [ [[package]] name = "rustix" -version = "1.1.3" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "146c9e247ccc180c1f61615433868c99f3de3ae256a30a43b49f67c2d9171f34" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" dependencies = [ "bitflags", "errno", @@ -4475,9 +4481,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.36" +version = "0.23.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c665f33d38cea657d9614f766881e4d510e0eda4239891eea56b4cadcf01801b" +checksum = "758025cb5fccfd3bc2fd74708fd4682be41d99e5dff73c377c0646c6012c73a4" dependencies = [ "log", "once_cell", @@ -4661,9 +4667,9 @@ dependencies = [ [[package]] name = "security-framework" -version = "3.6.0" +version = "3.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d17b898a6d6948c3a8ee4372c17cb384f90d2e6e912ef00895b14fd7ab54ec38" +checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" dependencies = [ "bitflags", "core-foundation 0.10.1", @@ -4674,9 +4680,9 @@ dependencies = [ [[package]] name = "security-framework-sys" -version = "2.16.0" +version = "2.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "321c8673b092a9a42605034a9879d73cb79101ed5fd117bc9a597b89b4e9e61a" +checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" dependencies = [ "core-foundation-sys", "libc", @@ -4803,9 +4809,9 @@ dependencies = [ [[package]] name = "serde_with" -version = "3.16.1" +version = "3.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fa237f2807440d238e0364a218270b98f767a00d3dada77b1c53ae88940e2e7" +checksum = "381b283ce7bc6b476d903296fb59d0d36633652b633b27f64db4fb46dcbfc3b9" dependencies = [ "base64 0.22.1", "chrono", @@ -4822,9 +4828,9 @@ dependencies = [ [[package]] name = "serde_with_macros" -version = "3.16.1" +version = "3.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52a8e3ca0ca629121f70ab50f95249e5a6f925cc0f6ffe8256c45b728875706c" +checksum = "a6d4e30573c8cb306ed6ab1dca8423eec9a463ea0e155f45399455e0368b27e0" dependencies = [ "darling 0.21.3", "proc-macro2", @@ -4947,12 +4953,12 @@ dependencies = [ [[package]] name = "socket2" -version = "0.6.2" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86f4aa3ad99f2088c990dfa82d367e19cb29268ed67c574d10d0a4bfe71f07e0" +checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" dependencies = [ "libc", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -5056,9 +5062,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.116" +version = "2.0.117" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3df424c70518695237746f84cede799c9c58fcb37450d7b23716568cc8bc69cb" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" dependencies = [ "proc-macro2", "quote", @@ -5126,12 +5132,12 @@ checksum = "7b2093cf4c8eb1e67749a6762251bc9cd836b6fc171623bd0a9d324d37af2417" [[package]] name = "tempfile" -version = "3.25.0" +version = "3.26.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0136791f7c95b1f6dd99f9cc786b91bb81c3800b639b3478e561ddb7be95e5f1" +checksum = "82a72c767771b47409d2345987fda8628641887d5466101319899796367354a0" dependencies = [ "fastrand", - "getrandom 0.4.1", + "getrandom 0.4.2", "once_cell", "rustix", "windows-sys 0.61.2", @@ -5264,9 +5270,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.49.0" +version = "1.50.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72a2903cd7736441aac9df9d7688bd0ce48edccaadf181c3b90be801e81d3d86" +checksum = "27ad5e34374e03cfffefc301becb44e9dc3c17584f414349ebe29ed26661822d" dependencies = [ "bytes", "libc", @@ -5274,16 +5280,16 @@ dependencies = [ "parking_lot", "pin-project-lite", "signal-hook-registry", - "socket2 0.6.2", + "socket2 0.6.3", "tokio-macros", "windows-sys 0.61.2", ] [[package]] name = "tokio-macros" -version = "2.6.0" +version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" +checksum = "5c55a2eff8b69ce66c84f85e1da1c233edc36ceb85a2058d11b0d6a3c7e7569c" dependencies = [ "proc-macro2", "quote", @@ -5306,7 +5312,7 @@ version = "0.26.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" dependencies = [ - "rustls 0.23.36", + "rustls 0.23.37", "tokio", ] @@ -5369,7 +5375,7 @@ dependencies = [ "serde_spanned 1.0.4", "toml_datetime 0.7.5+spec-1.1.0", "toml_parser", - "winnow 0.7.14", + "winnow 0.7.15", ] [[package]] @@ -5401,7 +5407,7 @@ dependencies = [ "serde_spanned 0.6.9", "toml_datetime 0.6.11", "toml_write", - "winnow 0.7.14", + "winnow 0.7.15", ] [[package]] @@ -5410,7 +5416,7 @@ version = "1.0.9+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "702d4415e08923e7e1ef96cd5727c0dfed80b4d2fa25db9647fe5eb6f7c5a4c4" dependencies = [ - "winnow 0.7.14", + "winnow 0.7.15", ] [[package]] @@ -5656,11 +5662,11 @@ checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" [[package]] name = "uuid" -version = "1.21.0" +version = "1.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b672338555252d43fd2240c714dc444b8c6fb0a5c5335e65a07bba7742735ddb" +checksum = "a68d3c8f01c0cfa54a75291d83601161799e4a89a39e0929f4b0354d88757a37" dependencies = [ - "getrandom 0.4.1", + "getrandom 0.4.2", "js-sys", "serde_core", "wasm-bindgen", @@ -5819,9 +5825,9 @@ dependencies = [ [[package]] name = "wasm-bindgen" -version = "0.2.108" +version = "0.2.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64024a30ec1e37399cf85a7ffefebdb72205ca1c972291c51512360d90bd8566" +checksum = "6532f9a5c1ece3798cb1c2cfdba640b9b3ba884f5db45973a6f442510a87d38e" dependencies = [ "cfg-if", "once_cell", @@ -5832,9 +5838,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.58" +version = "0.4.64" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70a6e77fd0ae8029c9ea0063f87c46fde723e7d887703d74ad2616d792e51e6f" +checksum = "e9c5522b3a28661442748e09d40924dfb9ca614b21c00d3fd135720e48b67db8" dependencies = [ "cfg-if", "futures-util", @@ -5846,9 +5852,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.108" +version = "0.2.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "008b239d9c740232e71bd39e8ef6429d27097518b6b30bdf9086833bd5b6d608" +checksum = "18a2d50fcf105fb33bb15f00e7a77b772945a2ee45dcf454961fd843e74c18e6" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -5856,9 +5862,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.108" +version = "0.2.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5256bae2d58f54820e6490f9839c49780dff84c65aeab9e772f15d5f0e913a55" +checksum = "03ce4caeaac547cdf713d280eda22a730824dd11e6b8c3ca9e42247b25c631e3" dependencies = [ "bumpalo", "proc-macro2", @@ -5869,9 +5875,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.108" +version = "0.2.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f01b580c9ac74c8d8f0c0e4afb04eeef2acf145458e52c03845ee9cd23e3d12" +checksum = "75a326b8c223ee17883a4251907455a2431acc2791c98c26279376490c378c16" dependencies = [ "unicode-ident", ] @@ -5925,9 +5931,9 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.85" +version = "0.3.91" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "312e32e551d92129218ea9a2452120f4aabc03529ef03e4d0d82fb2780608598" +checksum = "854ba17bb104abfb26ba36da9729addc7ce7f06f5c0f90f3c391f8461cca21f9" dependencies = [ "js-sys", "wasm-bindgen", @@ -6022,9 +6028,9 @@ dependencies = [ [[package]] name = "which" -version = "8.0.0" +version = "8.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3fabb953106c3c8eea8306e4393700d7657561cb43122571b172bbfb7c7ba1d" +checksum = "3a824aeba0fbb27264f815ada4cff43d65b1741b7a4ed7629ff9089148c4a4e0" dependencies = [ "env_home", "rustix", @@ -6389,9 +6395,9 @@ dependencies = [ [[package]] name = "winnow" -version = "0.7.14" +version = "0.7.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829" +checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" dependencies = [ "memchr", ] @@ -6585,18 +6591,18 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.8.39" +version = "0.8.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db6d35d663eadb6c932438e763b262fe1a70987f9ae936e60158176d710cae4a" +checksum = "a789c6e490b576db9f7e6b6d661bcc9799f7c0ac8352f56ea20193b2681532e5" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.39" +version = "0.8.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4122cd3169e94605190e77839c9a40d40ed048d305bfdc146e7df40ab0f3e517" +checksum = "f65c489a7071a749c849713807783f70672b28094011623e200cb86dcb835953" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index e50e006a..39de03e9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [workspace.package] edition = "2021" -rust-version = "1.91.0" +rust-version = "1.92.0" license = "AGPL-3.0-only" repository = "https://github.com/dani-garcia/vaultwarden" publish = false @@ -79,7 +79,7 @@ dashmap = "6.1.0" # Async futures futures = "0.3.32" -tokio = { version = "1.49.0", features = ["rt-multi-thread", "fs", "io-util", "parking_lot", "time", "signal", "net"] } +tokio = { version = "1.50.0", features = ["rt-multi-thread", "fs", "io-util", "parking_lot", "time", "signal", "net"] } tokio-util = { version = "0.7.18", features = ["compat"]} # A generic serialization/deserialization framework @@ -103,10 +103,10 @@ ring = "0.17.14" subtle = "2.6.1" # UUID generation -uuid = { version = "1.21.0", features = ["v4"] } +uuid = { version = "1.22.0", features = ["v4"] } # Date and time libraries -chrono = { version = "0.4.43", features = ["clock", "serde"], default-features = false } +chrono = { version = "0.4.44", features = ["clock", "serde"], default-features = false } chrono-tz = "0.10.4" time = "0.3.47" @@ -182,7 +182,7 @@ semver = "1.0.27" # Mainly used for the musl builds, since the default musl malloc is very slow mimalloc = { version = "0.1.48", features = ["secure"], default-features = false, optional = true } -which = "8.0.0" +which = "8.0.1" # Argon2 library with support for the PHC format argon2 = "0.5.3" @@ -197,10 +197,10 @@ grass_compiler = { version = "0.13.4", default-features = false } opendal = { version = "0.55.0", features = ["services-fs"], default-features = false } # For retrieving AWS credentials, including temporary SSO credentials -anyhow = { version = "1.0.101", optional = true } -aws-config = { version = "1.8.14", features = ["behavior-version-latest", "rt-tokio", "credentials-process", "sso"], default-features = false, optional = true } -aws-credential-types = { version = "1.2.13", optional = true } -aws-smithy-runtime-api = { version = "1.11.5", optional = true } +anyhow = { version = "1.0.102", optional = true } +aws-config = { version = "1.8.15", features = ["behavior-version-latest", "rt-tokio", "credentials-process", "sso"], default-features = false, optional = true } +aws-credential-types = { version = "1.2.14", optional = true } +aws-smithy-runtime-api = { version = "1.11.6", optional = true } http = { version = "1.4.0", optional = true } reqsign = { version = "0.16.5", optional = true } diff --git a/docker/DockerSettings.yaml b/docker/DockerSettings.yaml index 7b4a9af7..610f6b4a 100644 --- a/docker/DockerSettings.yaml +++ b/docker/DockerSettings.yaml @@ -1,11 +1,11 @@ --- -vault_version: "v2026.1.1" -vault_image_digest: "sha256:062fcf0d5dc37247dae61b0ee1ba5d20f9296e290d7ad1f6114ea5888f5738a7" +vault_version: "v2026.2.0" +vault_image_digest: "sha256:37c8661fa59dcdfbd3baa8366b6e950ef292b15adfeff1f57812b075c1fd3447" # Cross Compile Docker Helper Scripts v1.9.0 # We use the linux/amd64 platform shell scripts since there is no difference between the different platform scripts # https://github.com/tonistiigi/xx | https://hub.docker.com/r/tonistiigi/xx/tags xx_image_digest: "sha256:c64defb9ed5a91eacb37f96ccc3d4cd72521c4bd18d5442905b95e2226b0e707" -rust_version: 1.93.1 # Rust version to be used +rust_version: 1.94.0 # Rust version to be used debian_version: trixie # Debian release name to be used alpine_version: "3.23" # Alpine version to be used # For which platforms/architectures will we try to build images diff --git a/docker/Dockerfile.alpine b/docker/Dockerfile.alpine index b3df2f4e..44242e08 100644 --- a/docker/Dockerfile.alpine +++ b/docker/Dockerfile.alpine @@ -19,23 +19,23 @@ # - From https://hub.docker.com/r/vaultwarden/web-vault/tags, # click the tag name to view the digest of the image it currently points to. # - From the command line: -# $ docker pull docker.io/vaultwarden/web-vault:v2026.1.1 -# $ docker image inspect --format "{{.RepoDigests}}" docker.io/vaultwarden/web-vault:v2026.1.1 -# [docker.io/vaultwarden/web-vault@sha256:062fcf0d5dc37247dae61b0ee1ba5d20f9296e290d7ad1f6114ea5888f5738a7] +# $ docker pull docker.io/vaultwarden/web-vault:v2026.2.0 +# $ docker image inspect --format "{{.RepoDigests}}" docker.io/vaultwarden/web-vault:v2026.2.0 +# [docker.io/vaultwarden/web-vault@sha256:37c8661fa59dcdfbd3baa8366b6e950ef292b15adfeff1f57812b075c1fd3447] # # - Conversely, to get the tag name from the digest: -# $ docker image inspect --format "{{.RepoTags}}" docker.io/vaultwarden/web-vault@sha256:062fcf0d5dc37247dae61b0ee1ba5d20f9296e290d7ad1f6114ea5888f5738a7 -# [docker.io/vaultwarden/web-vault:v2026.1.1] +# $ docker image inspect --format "{{.RepoTags}}" docker.io/vaultwarden/web-vault@sha256:37c8661fa59dcdfbd3baa8366b6e950ef292b15adfeff1f57812b075c1fd3447 +# [docker.io/vaultwarden/web-vault:v2026.2.0] # -FROM --platform=linux/amd64 docker.io/vaultwarden/web-vault@sha256:062fcf0d5dc37247dae61b0ee1ba5d20f9296e290d7ad1f6114ea5888f5738a7 AS vault +FROM --platform=linux/amd64 docker.io/vaultwarden/web-vault@sha256:37c8661fa59dcdfbd3baa8366b6e950ef292b15adfeff1f57812b075c1fd3447 AS vault ########################## ALPINE BUILD IMAGES ########################## ## NOTE: The Alpine Base Images do not support other platforms then linux/amd64 and linux/arm64 ## And for Alpine we define all build images here, they will only be loaded when actually used -FROM --platform=$BUILDPLATFORM ghcr.io/blackdex/rust-musl:x86_64-musl-stable-1.93.1 AS build_amd64 -FROM --platform=$BUILDPLATFORM ghcr.io/blackdex/rust-musl:aarch64-musl-stable-1.93.1 AS build_arm64 -FROM --platform=$BUILDPLATFORM ghcr.io/blackdex/rust-musl:armv7-musleabihf-stable-1.93.1 AS build_armv7 -FROM --platform=$BUILDPLATFORM ghcr.io/blackdex/rust-musl:arm-musleabi-stable-1.93.1 AS build_armv6 +FROM --platform=$BUILDPLATFORM ghcr.io/blackdex/rust-musl:x86_64-musl-stable-1.94.0 AS build_amd64 +FROM --platform=$BUILDPLATFORM ghcr.io/blackdex/rust-musl:aarch64-musl-stable-1.94.0 AS build_arm64 +FROM --platform=$BUILDPLATFORM ghcr.io/blackdex/rust-musl:armv7-musleabihf-stable-1.94.0 AS build_armv7 +FROM --platform=$BUILDPLATFORM ghcr.io/blackdex/rust-musl:arm-musleabi-stable-1.94.0 AS build_armv6 ########################## BUILD IMAGE ########################## # hadolint ignore=DL3006 diff --git a/docker/Dockerfile.debian b/docker/Dockerfile.debian index 8796dedb..a60c485d 100644 --- a/docker/Dockerfile.debian +++ b/docker/Dockerfile.debian @@ -19,15 +19,15 @@ # - From https://hub.docker.com/r/vaultwarden/web-vault/tags, # click the tag name to view the digest of the image it currently points to. # - From the command line: -# $ docker pull docker.io/vaultwarden/web-vault:v2026.1.1 -# $ docker image inspect --format "{{.RepoDigests}}" docker.io/vaultwarden/web-vault:v2026.1.1 -# [docker.io/vaultwarden/web-vault@sha256:062fcf0d5dc37247dae61b0ee1ba5d20f9296e290d7ad1f6114ea5888f5738a7] +# $ docker pull docker.io/vaultwarden/web-vault:v2026.2.0 +# $ docker image inspect --format "{{.RepoDigests}}" docker.io/vaultwarden/web-vault:v2026.2.0 +# [docker.io/vaultwarden/web-vault@sha256:37c8661fa59dcdfbd3baa8366b6e950ef292b15adfeff1f57812b075c1fd3447] # # - Conversely, to get the tag name from the digest: -# $ docker image inspect --format "{{.RepoTags}}" docker.io/vaultwarden/web-vault@sha256:062fcf0d5dc37247dae61b0ee1ba5d20f9296e290d7ad1f6114ea5888f5738a7 -# [docker.io/vaultwarden/web-vault:v2026.1.1] +# $ docker image inspect --format "{{.RepoTags}}" docker.io/vaultwarden/web-vault@sha256:37c8661fa59dcdfbd3baa8366b6e950ef292b15adfeff1f57812b075c1fd3447 +# [docker.io/vaultwarden/web-vault:v2026.2.0] # -FROM --platform=linux/amd64 docker.io/vaultwarden/web-vault@sha256:062fcf0d5dc37247dae61b0ee1ba5d20f9296e290d7ad1f6114ea5888f5738a7 AS vault +FROM --platform=linux/amd64 docker.io/vaultwarden/web-vault@sha256:37c8661fa59dcdfbd3baa8366b6e950ef292b15adfeff1f57812b075c1fd3447 AS vault ########################## Cross Compile Docker Helper Scripts ########################## ## We use the linux/amd64 no matter which Build Platform, since these are all bash scripts @@ -36,7 +36,7 @@ FROM --platform=linux/amd64 docker.io/tonistiigi/xx@sha256:c64defb9ed5a91eacb37f ########################## BUILD IMAGE ########################## # hadolint ignore=DL3006 -FROM --platform=$BUILDPLATFORM docker.io/library/rust:1.93.1-slim-trixie AS build +FROM --platform=$BUILDPLATFORM docker.io/library/rust:1.94.0-slim-trixie AS build COPY --from=xx / / ARG TARGETARCH ARG TARGETVARIANT diff --git a/macros/Cargo.toml b/macros/Cargo.toml index 0a560a74..eb3bd670 100644 --- a/macros/Cargo.toml +++ b/macros/Cargo.toml @@ -13,8 +13,8 @@ path = "src/lib.rs" proc-macro = true [dependencies] -quote = "1.0.44" -syn = "2.0.114" +quote = "1.0.45" +syn = "2.0.117" [lints] workspace = true diff --git a/rust-toolchain.toml b/rust-toolchain.toml index 585747e0..0fc3f36d 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -1,4 +1,4 @@ [toolchain] -channel = "1.93.1" +channel = "1.94.0" components = [ "rustfmt", "clippy" ] profile = "minimal" diff --git a/src/api/core/organizations.rs b/src/api/core/organizations.rs index 4a5066ab..0213a006 100644 --- a/src/api/core/organizations.rs +++ b/src/api/core/organizations.rs @@ -395,7 +395,7 @@ async fn get_org_collections_details(org_id: OrganizationId, headers: ManagerHea Membership::find_confirmed_by_org(&org_id, &conn).await.into_iter().map(|m| (m.uuid, m.atype)).collect(); // check if current user has full access to the organization (either directly or via any group) - let has_full_access_to_org = member.access_all + let has_full_access_to_org = member.has_full_access() || (CONFIG.org_groups_enabled() && GroupUser::has_full_access_by_member(&org_id, &member.uuid, &conn).await); // Get all admins, owners and managers who can manage/access all @@ -421,6 +421,11 @@ async fn get_org_collections_details(org_id: OrganizationId, headers: ManagerHea || (CONFIG.org_groups_enabled() && GroupUser::has_access_to_collection_by_member(&col.uuid, &member.uuid, &conn).await); + // If the user is a manager, and is not assigned to this collection, skip this and continue with the next collection + if !assigned { + continue; + } + // get the users assigned directly to the given collection let mut users: Vec = col_users .iter() diff --git a/src/api/icons.rs b/src/api/icons.rs index 35a1de30..da83d0c4 100644 --- a/src/api/icons.rs +++ b/src/api/icons.rs @@ -513,13 +513,11 @@ fn parse_sizes(sizes: &str) -> (u16, u16) { if !sizes.is_empty() { match ICON_SIZE_REGEX.captures(sizes.trim()) { - None => {} - Some(dimensions) => { - if dimensions.len() >= 3 { - width = dimensions[1].parse::().unwrap_or_default(); - height = dimensions[2].parse::().unwrap_or_default(); - } + Some(dimensions) if dimensions.len() >= 3 => { + width = dimensions[1].parse::().unwrap_or_default(); + height = dimensions[2].parse::().unwrap_or_default(); } + _ => {} } } From 1a1d7f578a6a9cad24af667ad0eb6380d04affab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Garc=C3=ADa?= Date: Mon, 9 Mar 2026 19:14:28 +0100 Subject: [PATCH 33/89] Support new desktop origin on CORS (#6920) --- src/util.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/util.rs b/src/util.rs index aa4e7914..6da1c3df 100644 --- a/src/util.rs +++ b/src/util.rs @@ -153,9 +153,11 @@ impl Cors { fn get_allowed_origin(headers: &HeaderMap<'_>) -> Option { let origin = Cors::get_header(headers, "Origin"); let safari_extension_origin = "file://"; + let desktop_custom_file_origin = "bw-desktop-file://bundle"; if origin == CONFIG.domain_origin() || origin == safari_extension_origin + || origin == desktop_custom_file_origin || (CONFIG.sso_enabled() && origin == CONFIG.sso_authority()) { Some(origin) From 065c1f2cd515dfc16251ce993768d174b309275f Mon Sep 17 00:00:00 2001 From: Daniel Date: Mon, 9 Mar 2026 20:35:14 +0200 Subject: [PATCH 34/89] Fix `checkout` action version (#6921) - wasn't getting picked up when updating action due to being formatted as `#v6.0.0` instead of `# v6.0.0` --- .github/workflows/build.yml | 2 +- .github/workflows/check-templates.yml | 2 +- .github/workflows/hadolint.yml | 2 +- .github/workflows/release.yml | 2 +- .github/workflows/trivy.yml | 2 +- .github/workflows/typos.yml | 2 +- .github/workflows/zizmor.yml | 2 +- 7 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 2e3468f4..8d03ee00 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -62,7 +62,7 @@ jobs: # Checkout the repo - name: "Checkout" - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 #v6.0.0 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false fetch-depth: 0 diff --git a/.github/workflows/check-templates.yml b/.github/workflows/check-templates.yml index a8415dde..57b53bf4 100644 --- a/.github/workflows/check-templates.yml +++ b/.github/workflows/check-templates.yml @@ -20,7 +20,7 @@ jobs: steps: # Checkout the repo - name: "Checkout" - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 #v6.0.0 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false # End Checkout the repo diff --git a/.github/workflows/hadolint.yml b/.github/workflows/hadolint.yml index 29fce9b4..2b476904 100644 --- a/.github/workflows/hadolint.yml +++ b/.github/workflows/hadolint.yml @@ -40,7 +40,7 @@ jobs: # End Download hadolint # Checkout the repo - name: Checkout - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 #v6.0.0 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false # End Checkout the repo diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 8898e055..c3b0b9a7 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -73,7 +73,7 @@ jobs: # Checkout the repo - name: Checkout - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 #v6.0.0 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 # We need fetch-depth of 0 so we also get all the tag metadata with: persist-credentials: false diff --git a/.github/workflows/trivy.yml b/.github/workflows/trivy.yml index 5fc16e71..1f36ab01 100644 --- a/.github/workflows/trivy.yml +++ b/.github/workflows/trivy.yml @@ -33,7 +33,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 #v6.0.0 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false diff --git a/.github/workflows/typos.yml b/.github/workflows/typos.yml index fc61e266..d7b645e0 100644 --- a/.github/workflows/typos.yml +++ b/.github/workflows/typos.yml @@ -16,7 +16,7 @@ jobs: steps: # Checkout the repo - name: Checkout - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 #v6.0.0 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false # End Checkout the repo diff --git a/.github/workflows/zizmor.yml b/.github/workflows/zizmor.yml index 4051a8b2..22f3e7e9 100644 --- a/.github/workflows/zizmor.yml +++ b/.github/workflows/zizmor.yml @@ -19,7 +19,7 @@ jobs: security-events: write # To write the security report steps: - name: Checkout repository - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 #v6.0.0 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false From 9c7df6412c088832e1aeb673000716f5801dc8ed Mon Sep 17 00:00:00 2001 From: Mathijs van Veluw Date: Mon, 9 Mar 2026 21:13:27 +0100 Subject: [PATCH 35/89] Fix apikey login (#6922) The API Key login needs some extra JSON return key's, same as password login. Fixes #6912 Signed-off-by: BlackDex --- src/api/identity.rs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/api/identity.rs b/src/api/identity.rs index bf093536..f3fd3d1a 100644 --- a/src/api/identity.rs +++ b/src/api/identity.rs @@ -633,6 +633,19 @@ async fn _user_api_key_login( Value::Null }; + let account_keys = if user.private_key.is_some() { + json!({ + "publicKeyEncryptionKeyPair": { + "wrappedPrivateKey": user.private_key, + "publicKey": user.public_key, + "Object": "publicKeyEncryptionKeyPair" + }, + "Object": "privateKeys" + }) + } else { + Value::Null + }; + // Note: No refresh_token is returned. The CLI just repeats the // client_credentials login flow when the existing token expires. let result = json!({ @@ -649,6 +662,7 @@ async fn _user_api_key_login( "ResetMasterPassword": false, // TODO: according to official server seems something like: user.password_hash.is_empty(), but would need testing "ForcePasswordReset": false, "scope": AuthMethod::UserApiKey.scope(), + "AccountKeys": account_keys, "UserDecryptionOptions": { "HasMasterPassword": has_master_password, "MasterPasswordUnlock": master_password_unlock, From 2b3736802d5ce1de3ba73b7ae82b9ecdb8cbd1be Mon Sep 17 00:00:00 2001 From: Mathijs van Veluw Date: Tue, 17 Mar 2026 17:01:32 +0100 Subject: [PATCH 36/89] Fix email header base64 padding (#6961) Newer versions of the Bitwarden client use Base64 with padding. Since this is not a streaming string, but a defined length, we can just strip the `=` chars. Fixes #6960 Signed-off-by: BlackDex --- src/api/core/accounts.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/api/core/accounts.rs b/src/api/core/accounts.rs index 0ce1f684..d91eb4cd 100644 --- a/src/api/core/accounts.rs +++ b/src/api/core/accounts.rs @@ -1328,6 +1328,11 @@ impl<'r> FromRequest<'r> for KnownDevice { async fn from_request(req: &'r Request<'_>) -> Outcome { let email = if let Some(email_b64) = req.headers().get_one("X-Request-Email") { + // Bitwarden seems to send padded Base64 strings since 2026.2.1 + // Since these values are not streamed and Headers are always split by newlines + // we can safely ignore padding here and remove any '=' appended. + let email_b64 = email_b64.trim_end_matches('='); + let Ok(email_bytes) = data_encoding::BASE64URL_NOPAD.decode(email_b64.as_bytes()) else { return Outcome::Error((Status::BadRequest, "X-Request-Email value failed to decode as base64url")); }; From 650defac75571b9ec74e38d92bed0c49f64e3ba2 Mon Sep 17 00:00:00 2001 From: Mathijs van Veluw Date: Mon, 23 Mar 2026 21:21:21 +0100 Subject: [PATCH 37/89] Update Feature Flags (#6981) * Update Feature Flags Added new feature flags which could be supported without issues. Removed all deprecated feature flags and only match supported flags. Do not error on invalid flags during load, but do on config save via admin interface. During load it will print a `WARNING`, this is to prevent breaking setups when flags are removed, but are still configured. There are no feature flags anymore currently needed to be set by default, so those are removed now. Signed-off-by: BlackDex * Adjust code a bit and add Diagnostics check Signed-off-by: BlackDex * Update .env template Signed-off-by: BlackDex --------- Signed-off-by: BlackDex --- .env.template | 29 ++++---- src/api/admin.rs | 10 ++- src/api/core/mod.rs | 31 ++++----- src/config.rs | 80 ++++++++++++---------- src/static/scripts/admin_diagnostics.js | 7 ++ src/static/templates/admin/diagnostics.hbs | 8 +++ src/util.rs | 34 +++++---- 7 files changed, 122 insertions(+), 77 deletions(-) diff --git a/.env.template b/.env.template index c5563a1d..03990820 100644 --- a/.env.template +++ b/.env.template @@ -372,19 +372,22 @@ ## Note that clients cache the /api/config endpoint for about 1 hour and it could take some time before they are enabled or disabled! ## ## The following flags are available: -## - "pm-5594-safari-account-switching": Enable account switching in Safari. (Needs Safari >=2026.2.0) -## - "inline-menu-positioning-improvements": Enable the use of inline menu password generator and identity suggestions in the browser extension. -## - "inline-menu-totp": Enable the use of inline menu TOTP codes in the browser extension. -## - "ssh-agent": Enable SSH agent support on Desktop. (Needs desktop >=2024.12.0) -## - "ssh-key-vault-item": Enable the creation and use of SSH key vault items. (Needs clients >=2024.12.0) -## - "pm-25373-windows-biometrics-v2": Enable the new implementation of biometrics on Windows. (Needs desktop >= 2025.11.0) -## - "export-attachments": Enable support for exporting attachments (Clients >=2025.4.0) -## - "anon-addy-self-host-alias": Enable configuring self-hosted Anon Addy alias generator. (Needs Android >=2025.3.0, iOS >=2025.4.0) -## - "simple-login-self-host-alias": Enable configuring self-hosted Simple Login alias generator. (Needs Android >=2025.3.0, iOS >=2025.4.0) -## - "mutual-tls": Enable the use of mutual TLS on Android (Client >= 2025.2.0) -## - "cxp-import-mobile": Enable the import via CXP on iOS (Clients >=2025.9.2) -## - "cxp-export-mobile": Enable the export via CXP on iOS (Clients >=2025.9.2) -# EXPERIMENTAL_CLIENT_FEATURE_FLAGS=fido2-vault-credentials +## - "pm-5594-safari-account-switching": Enable account switching in Safari. (Safari >= 2026.2.0) +## - "ssh-agent": Enable SSH agent support on Desktop. (Desktop >= 2024.12.0) +## - "ssh-agent-v2": Enable newer SSH agent support. (Desktop >= 2026.2.1) +## - "ssh-key-vault-item": Enable the creation and use of SSH key vault items. (Clients >= 2024.12.0) +## - "pm-25373-windows-biometrics-v2": Enable the new implementation of biometrics on Windows. (Desktop >= 2025.11.0) +## - "anon-addy-self-host-alias": Enable configuring self-hosted Anon Addy alias generator. (Android >= 2025.3.0, iOS >= 2025.4.0) +## - "simple-login-self-host-alias": Enable configuring self-hosted Simple Login alias generator. (Android >= 2025.3.0, iOS >= 2025.4.0) +## - "mutual-tls": Enable the use of mutual TLS on Android (Clients >= 2025.2.0) +## - "cxp-import-mobile": Enable the import via CXP on iOS (Clients >= 2025.9.2) +## - "cxp-export-mobile": Enable the export via CXP on iOS (Clients >= 2025.9.2) +## - "pm-30529-webauthn-related-origins": +## - "desktop-ui-migration-milestone-1": Special feature flag for desktop UI (Desktop >= 2026.2.0) +## - "desktop-ui-migration-milestone-2": Special feature flag for desktop UI (Desktop >= 2026.2.0) +## - "desktop-ui-migration-milestone-3": Special feature flag for desktop UI (Desktop >= 2026.2.0) +## - "desktop-ui-migration-milestone-4": Special feature flag for desktop UI (Desktop >= 2026.2.0) +# EXPERIMENTAL_CLIENT_FEATURE_FLAGS= ## Require new device emails. When a user logs in an email is required to be sent. ## If sending the email fails the login attempt will fail!! diff --git a/src/api/admin.rs b/src/api/admin.rs index badfaa3a..475797a7 100644 --- a/src/api/admin.rs +++ b/src/api/admin.rs @@ -32,7 +32,7 @@ use crate::{ mail, util::{ container_base_image, format_naive_datetime_local, get_active_web_release, get_display_size, - is_running_in_container, NumberOrString, + is_running_in_container, parse_experimental_client_feature_flags, FeatureFlagFilter, NumberOrString, }, CONFIG, VERSION, }; @@ -734,6 +734,13 @@ async fn diagnostics(_token: AdminToken, ip_header: IpHeader, conn: DbConn) -> A let ip_header_name = &ip_header.0.unwrap_or_default(); + let invalid_feature_flags: Vec = parse_experimental_client_feature_flags( + &CONFIG.experimental_client_feature_flags(), + FeatureFlagFilter::InvalidOnly, + ) + .into_keys() + .collect(); + let diagnostics_json = json!({ "dns_resolved": dns_resolved, "current_release": VERSION, @@ -756,6 +763,7 @@ async fn diagnostics(_token: AdminToken, ip_header: IpHeader, conn: DbConn) -> A "db_version": get_sql_server_version(&conn).await, "admin_url": format!("{}/diagnostics", admin_url()), "overrides": &CONFIG.get_overrides().join(", "), + "invalid_feature_flags": invalid_feature_flags, "host_arch": env::consts::ARCH, "host_os": env::consts::OS, "tz_env": env::var("TZ").unwrap_or_default(), diff --git a/src/api/core/mod.rs b/src/api/core/mod.rs index dc7f4628..038b9a6d 100644 --- a/src/api/core/mod.rs +++ b/src/api/core/mod.rs @@ -59,7 +59,8 @@ use crate::{ error::Error, http_client::make_http_request, mail, - util::parse_experimental_client_feature_flags, + util::{parse_experimental_client_feature_flags, FeatureFlagFilter}, + CONFIG, }; #[derive(Debug, Serialize, Deserialize)] @@ -136,7 +137,7 @@ async fn put_eq_domains(data: Json, headers: Headers, conn: DbC #[get("/hibp/breach?")] async fn hibp_breach(username: &str, _headers: Headers) -> JsonResult { let username: String = url::form_urlencoded::byte_serialize(username.as_bytes()).collect(); - if let Some(api_key) = crate::CONFIG.hibp_api_key() { + if let Some(api_key) = CONFIG.hibp_api_key() { let url = format!( "https://haveibeenpwned.com/api/v3/breachedaccount/{username}?truncateResponse=false&includeUnverified=false" ); @@ -197,19 +198,17 @@ fn get_api_webauthn(_headers: Headers) -> Json { #[get("/config")] fn config() -> Json { - let domain = crate::CONFIG.domain(); + let domain = CONFIG.domain(); // Official available feature flags can be found here: - // Server (v2025.6.2): https://github.com/bitwarden/server/blob/d094be3267f2030bd0dc62106bc6871cf82682f5/src/Core/Constants.cs#L103 - // Client (web-v2025.6.1): https://github.com/bitwarden/clients/blob/747c2fd6a1c348a57a76e4a7de8128466ffd3c01/libs/common/src/enums/feature-flag.enum.ts#L12 - // Android (v2025.6.0): https://github.com/bitwarden/android/blob/b5b022caaad33390c31b3021b2c1205925b0e1a2/app/src/main/kotlin/com/x8bit/bitwarden/data/platform/manager/model/FlagKey.kt#L22 - // iOS (v2025.6.0): https://github.com/bitwarden/ios/blob/ff06d9c6cc8da89f78f37f376495800201d7261a/BitwardenShared/Core/Platform/Models/Enum/FeatureFlag.swift#L7 - let mut feature_states = - parse_experimental_client_feature_flags(&crate::CONFIG.experimental_client_feature_flags()); - feature_states.insert("duo-redirect".to_string(), true); - feature_states.insert("email-verification".to_string(), true); - feature_states.insert("unauth-ui-refresh".to_string(), true); - feature_states.insert("enable-pm-flight-recorder".to_string(), true); - feature_states.insert("mobile-error-reporting".to_string(), true); + // Server (v2026.2.1): https://github.com/bitwarden/server/blob/0e42725d0837bd1c0dabd864ff621a579959744b/src/Core/Constants.cs#L135 + // Client (v2026.2.1): https://github.com/bitwarden/clients/blob/f96380c3138291a028bdd2c7a5fee540d5c98ba5/libs/common/src/enums/feature-flag.enum.ts#L12 + // Android (v2026.2.1): https://github.com/bitwarden/android/blob/6902c19c0093fa476bbf74ccaa70c9f14afbb82f/core/src/main/kotlin/com/bitwarden/core/data/manager/model/FlagKey.kt#L31 + // iOS (v2026.2.1): https://github.com/bitwarden/ios/blob/cdd9ba1770ca2ffc098d02d12cc3208e3a830454/BitwardenShared/Core/Platform/Models/Enum/FeatureFlag.swift#L7 + let feature_states = parse_experimental_client_feature_flags( + &CONFIG.experimental_client_feature_flags(), + FeatureFlagFilter::ValidOnly, + ); + // Add default feature_states here if needed, currently no features are needed by default. Json(json!({ // Note: The clients use this version to handle backwards compatibility concerns @@ -225,7 +224,7 @@ fn config() -> Json { "url": "https://github.com/dani-garcia/vaultwarden" }, "settings": { - "disableUserRegistration": crate::CONFIG.is_signup_disabled() + "disableUserRegistration": CONFIG.is_signup_disabled() }, "environment": { "vault": domain, @@ -278,7 +277,7 @@ async fn accept_org_invite( member.save(conn).await?; - if crate::CONFIG.mail_enabled() { + if CONFIG.mail_enabled() { let org = match Organization::find_by_uuid(&member.org_uuid, conn).await { Some(org) => org, None => err!("Organization not found."), diff --git a/src/config.rs b/src/config.rs index 0221fd9a..6ff09467 100644 --- a/src/config.rs +++ b/src/config.rs @@ -14,7 +14,10 @@ use serde::de::{self, Deserialize, Deserializer, MapAccess, Visitor}; use crate::{ error::Error, - util::{get_active_web_release, get_env, get_env_bool, is_valid_email, parse_experimental_client_feature_flags}, + util::{ + get_active_web_release, get_env, get_env_bool, is_valid_email, parse_experimental_client_feature_flags, + FeatureFlagFilter, + }, }; static CONFIG_FILE: LazyLock = LazyLock::new(|| { @@ -920,7 +923,7 @@ make_config! { }, } -fn validate_config(cfg: &ConfigItems) -> Result<(), Error> { +fn validate_config(cfg: &ConfigItems, on_update: bool) -> Result<(), Error> { // Validate connection URL is valid and DB feature is enabled #[cfg(sqlite)] { @@ -1026,39 +1029,17 @@ fn validate_config(cfg: &ConfigItems) -> Result<(), Error> { } } - // Server (v2025.6.2): https://github.com/bitwarden/server/blob/d094be3267f2030bd0dc62106bc6871cf82682f5/src/Core/Constants.cs#L103 - // Client (web-v2026.2.0): https://github.com/bitwarden/clients/blob/a2fefe804d8c9b4a56c42f9904512c5c5821e2f6/libs/common/src/enums/feature-flag.enum.ts#L12 - // Android (v2025.6.0): https://github.com/bitwarden/android/blob/b5b022caaad33390c31b3021b2c1205925b0e1a2/app/src/main/kotlin/com/x8bit/bitwarden/data/platform/manager/model/FlagKey.kt#L22 - // iOS (v2025.6.0): https://github.com/bitwarden/ios/blob/ff06d9c6cc8da89f78f37f376495800201d7261a/BitwardenShared/Core/Platform/Models/Enum/FeatureFlag.swift#L7 - // - // NOTE: Move deprecated flags to the utils::parse_experimental_client_feature_flags() DEPRECATED_FLAGS const! - const KNOWN_FLAGS: &[&str] = &[ - // Auth Team - "pm-5594-safari-account-switching", - // Autofill Team - "inline-menu-positioning-improvements", - "inline-menu-totp", - "ssh-agent", - // Key Management Team - "ssh-key-vault-item", - "pm-25373-windows-biometrics-v2", - // Tools - "export-attachments", - // Mobile Team - "anon-addy-self-host-alias", - "simple-login-self-host-alias", - "mutual-tls", - "cxp-import-mobile", - "cxp-export-mobile", - // Webauthn Related Origins - "pm-30529-webauthn-related-origins", - ]; - let configured_flags = parse_experimental_client_feature_flags(&cfg.experimental_client_feature_flags); - let invalid_flags: Vec<_> = configured_flags.keys().filter(|flag| !KNOWN_FLAGS.contains(&flag.as_str())).collect(); + let invalid_flags = + parse_experimental_client_feature_flags(&cfg.experimental_client_feature_flags, FeatureFlagFilter::InvalidOnly); if !invalid_flags.is_empty() { - err!(format!("Unrecognized experimental client feature flags: {invalid_flags:?}.\n\n\ + let feature_flags_error = format!("Unrecognized experimental client feature flags: {:?}.\n\ Please ensure all feature flags are spelled correctly and that they are supported in this version.\n\ - Supported flags: {KNOWN_FLAGS:?}")); + Supported flags: {:?}\n", invalid_flags, SUPPORTED_FEATURE_FLAGS); + if on_update { + err!(feature_flags_error); + } else { + println!("[WARNING] {feature_flags_error}"); + } } const MAX_FILESIZE_KB: i64 = i64::MAX >> 10; @@ -1477,6 +1458,35 @@ pub enum PathType { RsaKey, } +// Official available feature flags can be found here: +// Server (v2026.2.1): https://github.com/bitwarden/server/blob/0e42725d0837bd1c0dabd864ff621a579959744b/src/Core/Constants.cs#L135 +// Client (v2026.2.1): https://github.com/bitwarden/clients/blob/f96380c3138291a028bdd2c7a5fee540d5c98ba5/libs/common/src/enums/feature-flag.enum.ts#L12 +// Android (v2026.2.1): https://github.com/bitwarden/android/blob/6902c19c0093fa476bbf74ccaa70c9f14afbb82f/core/src/main/kotlin/com/bitwarden/core/data/manager/model/FlagKey.kt#L31 +// iOS (v2026.2.1): https://github.com/bitwarden/ios/blob/cdd9ba1770ca2ffc098d02d12cc3208e3a830454/BitwardenShared/Core/Platform/Models/Enum/FeatureFlag.swift#L7 +pub const SUPPORTED_FEATURE_FLAGS: &[&str] = &[ + // Architecture + "desktop-ui-migration-milestone-1", + "desktop-ui-migration-milestone-2", + "desktop-ui-migration-milestone-3", + "desktop-ui-migration-milestone-4", + // Auth Team + "pm-5594-safari-account-switching", + // Autofill Team + "ssh-agent", + "ssh-agent-v2", + // Key Management Team + "ssh-key-vault-item", + "pm-25373-windows-biometrics-v2", + // Mobile Team + "anon-addy-self-host-alias", + "simple-login-self-host-alias", + "mutual-tls", + "cxp-import-mobile", + "cxp-export-mobile", + // Platform Team + "pm-30529-webauthn-related-origins", +]; + impl Config { pub async fn load() -> Result { // Loading from env and file @@ -1490,7 +1500,7 @@ impl Config { // Fill any missing with defaults let config = builder.build(); if !SKIP_CONFIG_VALIDATION.load(Ordering::Relaxed) { - validate_config(&config)?; + validate_config(&config, false)?; } Ok(Config { @@ -1526,7 +1536,7 @@ impl Config { let env = &self.inner.read().unwrap()._env; env.merge(&builder, false, &mut overrides).build() }; - validate_config(&config)?; + validate_config(&config, true)?; // Save both the user and the combined config { diff --git a/src/static/scripts/admin_diagnostics.js b/src/static/scripts/admin_diagnostics.js index 5594b439..2cff4410 100644 --- a/src/static/scripts/admin_diagnostics.js +++ b/src/static/scripts/admin_diagnostics.js @@ -109,6 +109,9 @@ async function generateSupportString(event, dj) { supportString += "* Websocket Check: disabled\n"; } supportString += `* HTTP Response Checks: ${httpResponseCheck}\n`; + if (dj.invalid_feature_flags != "") { + supportString += `* Invalid feature flags: true\n`; + } const jsonResponse = await fetch(`${BASE_URL}/admin/diagnostics/config`, { "headers": { "Accept": "application/json" } @@ -128,6 +131,10 @@ async function generateSupportString(event, dj) { supportString += `\n**Environment settings which are overridden:** ${dj.overrides}\n`; } + if (dj.invalid_feature_flags != "") { + supportString += `\n**Invalid feature flags:** ${dj.invalid_feature_flags}\n`; + } + // Add http response check messages if they exists if (httpResponseCheck === false) { supportString += "\n**Failed HTTP Checks:**\n"; diff --git a/src/static/templates/admin/diagnostics.hbs b/src/static/templates/admin/diagnostics.hbs index 77f2c95b..d27bce4c 100644 --- a/src/static/templates/admin/diagnostics.hbs +++ b/src/static/templates/admin/diagnostics.hbs @@ -194,6 +194,14 @@
+ {{#if page_data.invalid_feature_flags}} +
Invalid Feature Flags + Warning +
+
+ Flags: {{page_data.invalid_feature_flags}} +
+ {{/if}} diff --git a/src/util.rs b/src/util.rs index 6da1c3df..d336689d 100644 --- a/src/util.rs +++ b/src/util.rs @@ -16,7 +16,10 @@ use tokio::{ time::{sleep, Duration}, }; -use crate::{config::PathType, CONFIG}; +use crate::{ + config::{PathType, SUPPORTED_FEATURE_FLAGS}, + CONFIG, +}; pub struct AppHeaders(); @@ -765,21 +768,28 @@ pub fn convert_json_key_lcase_first(src_json: Value) -> Value { } } +pub enum FeatureFlagFilter { + #[allow(dead_code)] + Unfiltered, + ValidOnly, + InvalidOnly, +} + /// Parses the experimental client feature flags string into a HashMap. -pub fn parse_experimental_client_feature_flags(experimental_client_feature_flags: &str) -> HashMap { - // These flags could still be configured, but are deprecated and not used anymore - // To prevent old installations from starting filter these out and not error out - const DEPRECATED_FLAGS: &[&str] = - &["autofill-overlay", "autofill-v2", "browser-fileless-import", "extension-refresh", "fido2-vault-credentials"]; +pub fn parse_experimental_client_feature_flags( + experimental_client_feature_flags: &str, + filter_mode: FeatureFlagFilter, +) -> HashMap { experimental_client_feature_flags .split(',') - .filter_map(|f| { - let flag = f.trim(); - if !flag.is_empty() && !DEPRECATED_FLAGS.contains(&flag) { - return Some((flag.to_owned(), true)); - } - None + .map(str::trim) + .filter(|flag| !flag.is_empty()) + .filter(|flag| match filter_mode { + FeatureFlagFilter::Unfiltered => true, + FeatureFlagFilter::ValidOnly => SUPPORTED_FEATURE_FLAGS.contains(flag), + FeatureFlagFilter::InvalidOnly => !SUPPORTED_FEATURE_FLAGS.contains(flag), }) + .map(|flag| (flag.to_owned(), true)) .collect() } From 711bb53d3d0e5d1aa63bbd1b8ba23ae654d9896f Mon Sep 17 00:00:00 2001 From: Mathijs van Veluw Date: Mon, 23 Mar 2026 21:26:11 +0100 Subject: [PATCH 38/89] Update crates and GHA (#6980) Updated all crates which are possible. Updated all GitHub Actions to their latest version. There was a supply-chain attack on the trivy action to which we were not exposed since we were using pinned sha hashes. The latest version v0.35.0 is not vulnerable and that version will be used with this commit. Also removed `dtolnay/rust-toolchain` as suggested by zizmor and adjusted the way to install the correct toolchain. Since this GitHub Action did not used any version tagging, it was also cumbersome to update. Signed-off-by: BlackDex --- .github/workflows/build.yml | 35 +++---- .github/workflows/release.yml | 2 +- .github/workflows/trivy.yml | 4 +- .github/workflows/zizmor.yml | 2 +- Cargo.lock | 187 +++++++++++++++++++--------------- Cargo.toml | 12 +-- src/api/admin.rs | 1 - 7 files changed, 129 insertions(+), 114 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 8d03ee00..6269e595 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -85,32 +85,23 @@ jobs: # End Determine rust-toolchain version - # Only install the clippy and rustfmt components on the default rust-toolchain - - name: "Install rust-toolchain version" - uses: dtolnay/rust-toolchain@efa25f7f19611383d5b0ccf2d1c8914531636bf9 # master @ Feb 13, 2026, 3:46 AM GMT+1 - if: ${{ matrix.channel == 'rust-toolchain' }} - with: - toolchain: "${{steps.toolchain.outputs.RUST_TOOLCHAIN}}" - components: clippy, rustfmt - # End Uses the rust-toolchain file to determine version - - - # Install the any other channel to be used for which we do not execute clippy and rustfmt - - name: "Install MSRV version" - uses: dtolnay/rust-toolchain@efa25f7f19611383d5b0ccf2d1c8914531636bf9 # master @ Feb 13, 2026, 3:46 AM GMT+1 - if: ${{ matrix.channel != 'rust-toolchain' }} - with: - toolchain: "${{steps.toolchain.outputs.RUST_TOOLCHAIN}}" - # End Install the MSRV channel to be used - - # Set the current matrix toolchain version as default - - name: "Set toolchain ${{steps.toolchain.outputs.RUST_TOOLCHAIN}} as default" + - name: "Install toolchain ${{steps.toolchain.outputs.RUST_TOOLCHAIN}} as default" env: + CHANNEL: ${{ matrix.channel }} RUST_TOOLCHAIN: ${{steps.toolchain.outputs.RUST_TOOLCHAIN}} run: | # Remove the rust-toolchain.toml rm rust-toolchain.toml - # Set the default + + # Install the correct toolchain version + rustup toolchain install "${RUST_TOOLCHAIN}" --profile minimal --no-self-update + + # If this matrix is the `rust-toolchain` flow, also install rustfmt and clippy + if [[ "${CHANNEL}" == 'rust-toolchain' ]]; then + rustup component add --toolchain "${RUST_TOOLCHAIN}" rustfmt clippy + fi + + # Set as the default toolchain rustup default "${RUST_TOOLCHAIN}" # Show environment @@ -122,7 +113,7 @@ jobs: # Enable Rust Caching - name: Rust Caching - uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2 + uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1 with: # Use a custom prefix-key to force a fresh start. This is sometimes needed with bigger changes. # Like changing the build host from Ubuntu 20.04 to 22.04 for example. diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index c3b0b9a7..f2080cb2 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -257,7 +257,7 @@ jobs: steps: - name: Download digests - uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.0 + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: path: ${{ runner.temp }}/digests pattern: digests-*-${{ matrix.base_image }} diff --git a/.github/workflows/trivy.yml b/.github/workflows/trivy.yml index 1f36ab01..94ff6f63 100644 --- a/.github/workflows/trivy.yml +++ b/.github/workflows/trivy.yml @@ -38,7 +38,7 @@ jobs: persist-credentials: false - name: Run Trivy vulnerability scanner - uses: aquasecurity/trivy-action@97e0b3872f55f89b95b2f65b3dbab56962816478 # 0.34.2 + uses: aquasecurity/trivy-action@57a97c7e7821a5776cebc9bb87c984fa69cba8f1 # v0.35.0 env: TRIVY_DB_REPOSITORY: docker.io/aquasec/trivy-db:2,public.ecr.aws/aquasecurity/trivy-db:2,ghcr.io/aquasecurity/trivy-db:2 TRIVY_JAVA_DB_REPOSITORY: docker.io/aquasec/trivy-java-db:1,public.ecr.aws/aquasecurity/trivy-java-db:1,ghcr.io/aquasecurity/trivy-java-db:1 @@ -50,6 +50,6 @@ jobs: severity: CRITICAL,HIGH - name: Upload Trivy scan results to GitHub Security tab - uses: github/codeql-action/upload-sarif@0d579ffd059c29b07949a3cce3983f0780820c98 # v4.32.6 + uses: github/codeql-action/upload-sarif@38697555549f1db7851b81482ff19f1fa5c4fedc # v4.34.1 with: sarif_file: 'trivy-results.sarif' diff --git a/.github/workflows/zizmor.yml b/.github/workflows/zizmor.yml index 22f3e7e9..4bd40db3 100644 --- a/.github/workflows/zizmor.yml +++ b/.github/workflows/zizmor.yml @@ -24,7 +24,7 @@ jobs: persist-credentials: false - name: Run zizmor - uses: zizmorcore/zizmor-action@0dce2577a4760a2749d8cfb7a84b7d5585ebcb7d # v0.5.0 + uses: zizmorcore/zizmor-action@71321a20a9ded102f6e9ce5718a2fcec2c4f70d8 # v0.5.2 with: # intentionally not scanning the entire repository, # since it contains integration tests. diff --git a/Cargo.lock b/Cargo.lock index 8a694069..b16d3fdb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -427,9 +427,9 @@ dependencies = [ [[package]] name = "aws-sdk-sso" -version = "1.96.0" +version = "1.97.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f64a6eded248c6b453966e915d32aeddb48ea63ad17932682774eb026fbef5b1" +checksum = "9aadc669e184501caaa6beafb28c6267fc1baef0810fb58f9b205485ca3f2567" dependencies = [ "aws-credential-types", "aws-runtime", @@ -451,9 +451,9 @@ dependencies = [ [[package]] name = "aws-sdk-ssooidc" -version = "1.98.0" +version = "1.99.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db96d720d3c622fcbe08bae1c4b04a72ce6257d8b0584cb5418da00ae20a344f" +checksum = "1342a7db8f358d3de0aed2007a0b54e875458e39848d54cc1d46700b2bfcb0a8" dependencies = [ "aws-credential-types", "aws-runtime", @@ -475,9 +475,9 @@ dependencies = [ [[package]] name = "aws-sdk-sts" -version = "1.100.0" +version = "1.101.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fafbdda43b93f57f699c5dfe8328db590b967b8a820a13ccdd6687355dfcc7ca" +checksum = "ab41ad64e4051ecabeea802d6a17845a91e83287e1dd249e6963ea1ba78c428a" dependencies = [ "aws-credential-types", "aws-runtime", @@ -623,9 +623,9 @@ dependencies = [ [[package]] name = "aws-smithy-types" -version = "1.4.6" +version = "1.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2b1117b3b2bbe166d11199b540ceed0d0f7676e36e7b962b5a437a9971eac75" +checksum = "9d73dbfbaa8e4bc57b9045137680b958d274823509a360abfd8e1d514d40c95c" dependencies = [ "base64-simd", "bytes", @@ -845,17 +845,18 @@ dependencies = [ [[package]] name = "cached" -version = "0.56.0" +version = "0.59.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "801927ee168e17809ab8901d9f01f700cd7d8d6a6527997fee44e4b0327a253c" +checksum = "53b6f5d101f0f6322c8646a45b7c581a673e476329040d97565815c2461dd0c4" dependencies = [ "ahash", "async-trait", "cached_proc_macro", "cached_proc_macro_types", "futures", - "hashbrown 0.15.5", + "hashbrown 0.16.1", "once_cell", + "parking_lot", "thiserror 2.0.18", "tokio", "web-time", @@ -863,9 +864,9 @@ dependencies = [ [[package]] name = "cached_proc_macro" -version = "0.25.0" +version = "0.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9225bdcf4e4a9a4c08bf16607908eb2fbf746828d5e0b5e019726dbf6571f201" +checksum = "8ebcf9c75f17a17d55d11afc98e46167d4790a263f428891b8705ab2f793eca3" dependencies = [ "darling 0.20.11", "proc-macro2", @@ -890,9 +891,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.56" +version = "1.2.57" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aebf35691d1bfb0ac386a69bac2fde4dd276fb618cf8bf4f5318fe285e821bb2" +checksum = "7a0dd1ca384932ff3641c8718a02769f1698e7563dc6974ffd03346116310423" dependencies = [ "find-msvc-tools", "jobserver", @@ -1244,6 +1245,16 @@ dependencies = [ "darling_macro 0.21.3", ] +[[package]] +name = "darling" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d" +dependencies = [ + "darling_core 0.23.0", + "darling_macro 0.23.0", +] + [[package]] name = "darling_core" version = "0.20.11" @@ -1272,6 +1283,19 @@ dependencies = [ "syn", ] +[[package]] +name = "darling_core" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9865a50f7c335f53564bb694ef660825eb8610e0a53d3e11bf1b0d3df31e03b0" +dependencies = [ + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn", +] + [[package]] name = "darling_macro" version = "0.20.11" @@ -1294,6 +1318,17 @@ dependencies = [ "syn", ] +[[package]] +name = "darling_macro" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" +dependencies = [ + "darling_core 0.23.0", + "quote", + "syn", +] + [[package]] name = "dashmap" version = "6.1.0" @@ -1444,9 +1479,9 @@ dependencies = [ [[package]] name = "diesel" -version = "2.3.6" +version = "2.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9b6c2fc184a6fb6ebcf5f9a5e3bbfa84d8fd268cdfcce4ed508979a6259494d" +checksum = "f4ae09a41a4b89f94ec1e053623da8340d996bc32c6517d325a9daad9b239358" dependencies = [ "bigdecimal", "bitflags", @@ -1690,12 +1725,6 @@ dependencies = [ "syn", ] -[[package]] -name = "env_home" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7f84e12ccf0a7ddc17a6c41c93326024c42920d7ee630d04950e6926645c0fe" - [[package]] name = "equivalent" version = "1.0.2" @@ -2151,8 +2180,6 @@ version = "0.15.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" dependencies = [ - "allocator-api2", - "equivalent", "foldhash 0.1.5", ] @@ -2630,9 +2657,9 @@ checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" [[package]] name = "iri-string" -version = "0.7.10" +version = "0.7.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c91338f0783edbd6195decb37bae672fd3b165faffb89bf7b9e6942f8b1a731a" +checksum = "d8e7418f59cc01c88316161279a7f665217ae316b388e58a0d10e29f54f1e5eb" dependencies = [ "memchr", "serde", @@ -2660,9 +2687,9 @@ dependencies = [ [[package]] name = "itoa" -version = "1.0.17" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" [[package]] name = "jetscii" @@ -2847,9 +2874,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.182" +version = "0.2.183" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6800badb6cb2082ffd7b6a67e6125bb39f18782f793520caee8cb8846be06112" +checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d" [[package]] name = "libm" @@ -2869,9 +2896,9 @@ dependencies = [ [[package]] name = "libsqlite3-sys" -version = "0.35.0" +version = "0.36.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "133c182a6a2c87864fe97778797e46c7e999672690dc9fa3ee8e241aa4a9c13f" +checksum = "95b4103cffefa72eb8428cb6b47d6627161e51c2739fc5e3b734584157bc642a" dependencies = [ "cc", "pkg-config", @@ -3033,9 +3060,9 @@ dependencies = [ [[package]] name = "moka" -version = "0.12.14" +version = "0.12.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85f8024e1c8e71c778968af91d43700ce1d11b219d127d79fb2934153b82b42b" +checksum = "957228ad12042ee839f93c8f257b62b4c0ab5eaae1d4fa60de53b27c9d7c5046" dependencies = [ "async-lock", "crossbeam-channel", @@ -3262,9 +3289,9 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.21.3" +version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" dependencies = [ "critical-section", "portable-atomic", @@ -3332,9 +3359,9 @@ dependencies = [ [[package]] name = "openssl" -version = "0.10.75" +version = "0.10.76" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08838db121398ad17ab8531ce9de97b244589089e290a384c900cb9ff7434328" +checksum = "951c002c75e16ea2c65b8c7e4d3d51d5530d8dfa7d060b4776828c88cfb18ecf" dependencies = [ "bitflags", "cfg-if", @@ -3373,9 +3400,9 @@ dependencies = [ [[package]] name = "openssl-sys" -version = "0.9.111" +version = "0.9.112" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82cab2d520aa75e3c58898289429321eb788c3106963d0dc886ec7a5f4adc321" +checksum = "57d55af3b3e226502be1526dfdba67ab0e9c96fc293004e79576b2b9edb0dbdb" dependencies = [ "cc", "libc", @@ -3741,9 +3768,9 @@ checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" [[package]] name = "portable-atomic-util" -version = "0.2.5" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a9db96d7fa8782dd8c15ce32ffe8680bbd1e978a43bf51a34d39483540495f5" +checksum = "091397be61a01d4be58e7841595bd4bfedb15f1cd54977d79b8271e94ed799a3" dependencies = [ "portable-atomic", ] @@ -3913,9 +3940,9 @@ dependencies = [ [[package]] name = "quinn-proto" -version = "0.11.13" +version = "0.11.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1906b49b0c3bc04b5fe5d86a77925ae6524a19b816ae38ce1e426255f1d8a31" +checksum = "434b42fec591c96ef50e21e886936e66d3cc3f737104fdb9b737c40ffb94c098" dependencies = [ "bytes", "getrandom 0.3.4", @@ -3957,9 +3984,9 @@ dependencies = [ [[package]] name = "quoted_printable" -version = "0.5.1" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "640c9bd8497b02465aeef5375144c26062e0dcd5939dfcbb0f5db76cb8c17c73" +checksum = "478e0585659a122aa407eb7e3c0e1fa51b1d8a870038bd29f0cf4a8551eea972" [[package]] name = "r-efi" @@ -4489,7 +4516,7 @@ dependencies = [ "once_cell", "ring", "rustls-pki-types", - "rustls-webpki 0.103.9", + "rustls-webpki 0.103.10", "subtle", "zeroize", ] @@ -4537,9 +4564,9 @@ dependencies = [ [[package]] name = "rustls-webpki" -version = "0.103.9" +version = "0.103.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7df23109aa6c1567d1c575b9952556388da57401e4ace1d15f79eedad0d8f53" +checksum = "df33b2b81ac578cabaf06b89b0631153a3f416b0a886e8a7a1707fb51abbd1ef" dependencies = [ "ring", "rustls-pki-types", @@ -4578,9 +4605,9 @@ dependencies = [ [[package]] name = "schannel" -version = "0.1.28" +version = "0.1.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "891d81b926048e76efe18581bf793546b4c0eaf8448d72be8de2bbee5fd166e1" +checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939" dependencies = [ "windows-sys 0.61.2", ] @@ -4809,9 +4836,9 @@ dependencies = [ [[package]] name = "serde_with" -version = "3.17.0" +version = "3.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "381b283ce7bc6b476d903296fb59d0d36633652b633b27f64db4fb46dcbfc3b9" +checksum = "dd5414fad8e6907dbdd5bc441a50ae8d6e26151a03b1de04d89a5576de61d01f" dependencies = [ "base64 0.22.1", "chrono", @@ -4828,11 +4855,11 @@ dependencies = [ [[package]] name = "serde_with_macros" -version = "3.17.0" +version = "3.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6d4e30573c8cb306ed6ab1dca8423eec9a463ea0e155f45399455e0368b27e0" +checksum = "d3db8978e608f1fe7357e211969fd9abdcae80bac1ba7a3369bb7eb6b404eb65" dependencies = [ - "darling 0.21.3", + "darling 0.23.0", "proc-macro2", "quote", "syn", @@ -5132,9 +5159,9 @@ checksum = "7b2093cf4c8eb1e67749a6762251bc9cd836b6fc171623bd0a9d324d37af2417" [[package]] name = "tempfile" -version = "3.26.0" +version = "3.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82a72c767771b47409d2345987fda8628641887d5466101319899796367354a0" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" dependencies = [ "fastrand", "getrandom 0.4.2", @@ -5255,9 +5282,9 @@ dependencies = [ [[package]] name = "tinyvec" -version = "1.10.0" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa" +checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3" dependencies = [ "tinyvec_macros", ] @@ -5412,11 +5439,11 @@ dependencies = [ [[package]] name = "toml_parser" -version = "1.0.9+spec-1.1.0" +version = "1.0.10+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "702d4415e08923e7e1ef96cd5727c0dfed80b4d2fa25db9647fe5eb6f7c5a4c4" +checksum = "7df25b4befd31c4816df190124375d5a20c6b6921e2cad937316de3fccd63420" dependencies = [ - "winnow 0.7.15", + "winnow 1.0.0", ] [[package]] @@ -5533,9 +5560,9 @@ dependencies = [ [[package]] name = "tracing-subscriber" -version = "0.3.22" +version = "0.3.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f30143827ddab0d256fd843b7a66d164e9f271cfa0dde49142c5ca0ca291f1e" +checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319" dependencies = [ "matchers", "nu-ansi-term", @@ -6028,13 +6055,11 @@ dependencies = [ [[package]] name = "which" -version = "8.0.1" +version = "8.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a824aeba0fbb27264f815ada4cff43d65b1741b7a4ed7629ff9089148c4a4e0" +checksum = "81995fafaaaf6ae47a7d0cc83c67caf92aeb7e5331650ae6ff856f7c0c60c459" dependencies = [ - "env_home", - "rustix", - "winsafe", + "libc", ] [[package]] @@ -6402,6 +6427,12 @@ dependencies = [ "memchr", ] +[[package]] +name = "winnow" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a90e88e4667264a994d34e6d1ab2d26d398dcdca8b7f52bec8668957517fc7d8" + [[package]] name = "winreg" version = "0.50.0" @@ -6412,12 +6443,6 @@ dependencies = [ "windows-sys 0.48.0", ] -[[package]] -name = "winsafe" -version = "0.0.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d135d17ab770252ad95e9a872d365cf3090e3be864a34ab46f48555993efc904" - [[package]] name = "wit-bindgen" version = "0.51.0" @@ -6591,18 +6616,18 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.8.40" +version = "0.8.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a789c6e490b576db9f7e6b6d661bcc9799f7c0ac8352f56ea20193b2681532e5" +checksum = "efbb2a062be311f2ba113ce66f697a4dc589f85e78a4aea276200804cea0ed87" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.40" +version = "0.8.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f65c489a7071a749c849713807783f70672b28094011623e200cb86dcb835953" +checksum = "0e8bc7269b54418e7aeeef514aa68f8690b8c0489a06b0136e5f57c4c5ccab89" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index 39de03e9..48d977ab 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -88,14 +88,14 @@ serde_json = "1.0.149" # A safe, extensible ORM and Query builder # Currently pinned diesel to v2.3.3 as newer version break MySQL/MariaDB compatibility -diesel = { version = "2.3.6", features = ["chrono", "r2d2", "numeric"] } +diesel = { version = "2.3.7", features = ["chrono", "r2d2", "numeric"] } diesel_migrations = "2.3.1" derive_more = { version = "2.1.1", features = ["from", "into", "as_ref", "deref", "display"] } diesel-derive-newtype = "2.1.2" # Bundled/Static SQLite -libsqlite3-sys = { version = "0.35.0", features = ["bundled"], optional = true } +libsqlite3-sys = { version = "0.36.0", features = ["bundled"], optional = true } # Crypto-related libraries rand = "0.10.0" @@ -155,14 +155,14 @@ bytes = "1.11.1" svg-hush = "0.9.6" # Cache function results (Used for version check and favicon fetching) -cached = { version = "0.56.0", features = ["async"] } +cached = { version = "0.59.0", features = ["async"] } # Used for custom short lived cookie jar during favicon extraction cookie = "0.18.1" cookie_store = "0.22.1" # Used by U2F, JWT and PostgreSQL -openssl = "0.10.75" +openssl = "0.10.76" # CLI argument parsing pico-args = "0.5.0" @@ -173,7 +173,7 @@ governor = "0.10.4" # OIDC for SSO openidconnect = { version = "4.0.1", features = ["reqwest", "rustls-tls"] } -moka = { version = "0.12.13", features = ["future"] } +moka = { version = "0.12.15", features = ["future"] } # Check client versions for specific features. semver = "1.0.27" @@ -182,7 +182,7 @@ semver = "1.0.27" # Mainly used for the musl builds, since the default musl malloc is very slow mimalloc = { version = "0.1.48", features = ["secure"], default-features = false, optional = true } -which = "8.0.1" +which = "8.0.2" # Argon2 library with support for the PHC format argon2 = "0.5.3" diff --git a/src/api/admin.rs b/src/api/admin.rs index 475797a7..6cedc040 100644 --- a/src/api/admin.rs +++ b/src/api/admin.rs @@ -637,7 +637,6 @@ use cached::proc_macro::cached; /// Cache this function to prevent API call rate limit. Github only allows 60 requests per hour, and we use 3 here already /// It will cache this function for 600 seconds (10 minutes) which should prevent the exhaustion of the rate limit /// Any cache will be lost if Vaultwarden is restarted -use std::time::Duration; // Needed for cached #[cached(time = 600, sync_writes = "default")] async fn get_release_info(has_http_access: bool) -> (String, String, String) { // If the HTTP Check failed, do not even attempt to check for new versions since we were not able to connect with github.com anyway. From c0a78dd55a4b37a78e4fd34d943920f4d4b76569 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Garc=C3=ADa?= Date: Mon, 23 Mar 2026 22:25:03 +0100 Subject: [PATCH 39/89] Use protected CI environment (#7004) --- .github/workflows/release.yml | 59 ++++++++++++++++++----------------- 1 file changed, 31 insertions(+), 28 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f2080cb2..8a3a7937 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -20,23 +20,25 @@ defaults: run: shell: bash -env: - # The *_REPO variables need to be configured as repository variables - # Append `/settings/variables/actions` to your repo url - # DOCKERHUB_REPO needs to be 'index.docker.io//' - # Check for Docker hub credentials in secrets - HAVE_DOCKERHUB_LOGIN: ${{ vars.DOCKERHUB_REPO != '' && secrets.DOCKERHUB_USERNAME != '' && secrets.DOCKERHUB_TOKEN != '' }} - # GHCR_REPO needs to be 'ghcr.io//' - # Check for Github credentials in secrets - HAVE_GHCR_LOGIN: ${{ vars.GHCR_REPO != '' && github.repository_owner != '' && secrets.GITHUB_TOKEN != '' }} - # QUAY_REPO needs to be 'quay.io//' - # Check for Quay.io credentials in secrets - HAVE_QUAY_LOGIN: ${{ vars.QUAY_REPO != '' && secrets.QUAY_USERNAME != '' && secrets.QUAY_TOKEN != '' }} +# A "release" environment must be created in the repository settings +# (Settings > Environments > New environment) with the following +# variables and secrets configured as needed. +# +# Variables (only set the ones for registries you want to push to): +# DOCKERHUB_REPO: 'index.docker.io//' +# QUAY_REPO: 'quay.io//' +# GHCR_REPO: 'ghcr.io//' +# +# Secrets (only required when the corresponding *_REPO variable is set): +# DOCKERHUB_REPO => DOCKERHUB_USERNAME, DOCKERHUB_TOKEN +# QUAY_REPO => QUAY_USERNAME, QUAY_TOKEN +# GITHUB_TOKEN is provided automatically jobs: docker-build: name: Build Vaultwarden containers if: ${{ github.repository == 'dani-garcia/vaultwarden' }} + environment: release permissions: packages: write # Needed to upload packages and artifacts contents: read @@ -106,10 +108,10 @@ jobs: with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - if: ${{ env.HAVE_DOCKERHUB_LOGIN == 'true' }} + if: ${{ vars.DOCKERHUB_REPO != '' }} - name: Add registry for DockerHub - if: ${{ env.HAVE_DOCKERHUB_LOGIN == 'true' }} + if: ${{ vars.DOCKERHUB_REPO != '' }} env: DOCKERHUB_REPO: ${{ vars.DOCKERHUB_REPO }} run: | @@ -122,10 +124,10 @@ jobs: registry: ghcr.io username: ${{ github.repository_owner }} password: ${{ secrets.GITHUB_TOKEN }} - if: ${{ env.HAVE_GHCR_LOGIN == 'true' }} + if: ${{ vars.GHCR_REPO != '' }} - name: Add registry for ghcr.io - if: ${{ env.HAVE_GHCR_LOGIN == 'true' }} + if: ${{ vars.GHCR_REPO != '' }} env: GHCR_REPO: ${{ vars.GHCR_REPO }} run: | @@ -138,10 +140,10 @@ jobs: registry: quay.io username: ${{ secrets.QUAY_USERNAME }} password: ${{ secrets.QUAY_TOKEN }} - if: ${{ env.HAVE_QUAY_LOGIN == 'true' }} + if: ${{ vars.QUAY_REPO != '' }} - name: Add registry for Quay.io - if: ${{ env.HAVE_QUAY_LOGIN == 'true' }} + if: ${{ vars.QUAY_REPO != '' }} env: QUAY_REPO: ${{ vars.QUAY_REPO }} run: | @@ -155,7 +157,7 @@ jobs: run: | # # Check if there is a GitHub Container Registry Login and use it for caching - if [[ -n "${HAVE_GHCR_LOGIN}" ]]; then + if [[ -n "${GHCR_REPO}" ]]; then echo "BAKE_CACHE_FROM=type=registry,ref=${GHCR_REPO}-buildcache:${BASE_IMAGE}-${NORMALIZED_ARCH}" | tee -a "${GITHUB_ENV}" echo "BAKE_CACHE_TO=type=registry,ref=${GHCR_REPO}-buildcache:${BASE_IMAGE}-${NORMALIZED_ARCH},compression=zstd,mode=max" | tee -a "${GITHUB_ENV}" else @@ -247,6 +249,7 @@ jobs: name: Merge manifests runs-on: ubuntu-latest needs: docker-build + environment: release permissions: packages: write # Needed to upload packages and artifacts attestations: write # Needed to generate an artifact attestation for a build @@ -269,10 +272,10 @@ jobs: with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - if: ${{ env.HAVE_DOCKERHUB_LOGIN == 'true' }} + if: ${{ vars.DOCKERHUB_REPO != '' }} - name: Add registry for DockerHub - if: ${{ env.HAVE_DOCKERHUB_LOGIN == 'true' }} + if: ${{ vars.DOCKERHUB_REPO != '' }} env: DOCKERHUB_REPO: ${{ vars.DOCKERHUB_REPO }} run: | @@ -285,10 +288,10 @@ jobs: registry: ghcr.io username: ${{ github.repository_owner }} password: ${{ secrets.GITHUB_TOKEN }} - if: ${{ env.HAVE_GHCR_LOGIN == 'true' }} + if: ${{ vars.GHCR_REPO != '' }} - name: Add registry for ghcr.io - if: ${{ env.HAVE_GHCR_LOGIN == 'true' }} + if: ${{ vars.GHCR_REPO != '' }} env: GHCR_REPO: ${{ vars.GHCR_REPO }} run: | @@ -301,10 +304,10 @@ jobs: registry: quay.io username: ${{ secrets.QUAY_USERNAME }} password: ${{ secrets.QUAY_TOKEN }} - if: ${{ env.HAVE_QUAY_LOGIN == 'true' }} + if: ${{ vars.QUAY_REPO != '' }} - name: Add registry for Quay.io - if: ${{ env.HAVE_QUAY_LOGIN == 'true' }} + if: ${{ vars.QUAY_REPO != '' }} env: QUAY_REPO: ${{ vars.QUAY_REPO }} run: | @@ -357,7 +360,7 @@ jobs: # Attest container images - name: Attest - docker.io - ${{ matrix.base_image }} - if: ${{ env.HAVE_DOCKERHUB_LOGIN == 'true' && env.DIGEST_SHA != ''}} + if: ${{ vars.DOCKERHUB_REPO != '' && env.DIGEST_SHA != ''}} uses: actions/attest-build-provenance@a2bbfa25375fe432b6a289bc6b6cd05ecd0c4c32 # v4.1.0 with: subject-name: ${{ vars.DOCKERHUB_REPO }} @@ -365,7 +368,7 @@ jobs: push-to-registry: true - name: Attest - ghcr.io - ${{ matrix.base_image }} - if: ${{ env.HAVE_GHCR_LOGIN == 'true' && env.DIGEST_SHA != ''}} + if: ${{ vars.GHCR_REPO != '' && env.DIGEST_SHA != ''}} uses: actions/attest-build-provenance@a2bbfa25375fe432b6a289bc6b6cd05ecd0c4c32 # v4.1.0 with: subject-name: ${{ vars.GHCR_REPO }} @@ -373,7 +376,7 @@ jobs: push-to-registry: true - name: Attest - quay.io - ${{ matrix.base_image }} - if: ${{ env.HAVE_QUAY_LOGIN == 'true' && env.DIGEST_SHA != ''}} + if: ${{ vars.QUAY_REPO != '' && env.DIGEST_SHA != ''}} uses: actions/attest-build-provenance@a2bbfa25375fe432b6a289bc6b6cd05ecd0c4c32 # v4.1.0 with: subject-name: ${{ vars.QUAY_REPO }} From 235cf88231edff38a0afac9ce75835a8592f101a Mon Sep 17 00:00:00 2001 From: Mathijs van Veluw Date: Mon, 23 Mar 2026 23:12:07 +0100 Subject: [PATCH 40/89] Fix 2FA Remember to actually be 30 days (#6929) Currently we always regenerate the 2FA Remember token, and always send that back to the client. This is not the correct way, and in turn causes the remember token to never expire. While this might be convenient, it is not really safe. This commit changes the 2FA Remember Tokens from random string to a JWT. This JWT has a lifetime of 30 days and is validated per device & user combination. This does mean that once this commit is merged, and users are using this version, all their remember tokens will be invalidated. From my point of view this isn't a bad thing, since those tokens should have expired already. Only users who recently checked the remember checkbox within 30 days have to login again, but that is a minor inconvenience I think. Signed-off-by: BlackDex --- src/api/identity.rs | 21 +++++++++++++++------ src/auth.rs | 30 ++++++++++++++++++++++++++++++ src/db/models/device.rs | 11 +++++++---- 3 files changed, 52 insertions(+), 10 deletions(-) diff --git a/src/api/identity.rs b/src/api/identity.rs index f3fd3d1a..fcd8c388 100644 --- a/src/api/identity.rs +++ b/src/api/identity.rs @@ -757,7 +757,6 @@ async fn twofactor_auth( use crate::crypto::ct_eq; let selected_data = _selected_data(selected_twofactor); - let mut remember = data.two_factor_remember.unwrap_or(0); match TwoFactorType::from_i32(selected_id) { Some(TwoFactorType::Authenticator) => { @@ -789,13 +788,23 @@ async fn twofactor_auth( } Some(TwoFactorType::Remember) => { match device.twofactor_remember { - Some(ref code) if !CONFIG.disable_2fa_remember() && ct_eq(code, twofactor_code) => { - remember = 1; // Make sure we also return the token here, otherwise it will only remember the first time - } + // When a 2FA Remember token is used, check and validate this JWT token, if it is valid, just continue + // If it is invalid we need to trigger the 2FA Login prompt + Some(ref token) + if !CONFIG.disable_2fa_remember() + && (ct_eq(token, twofactor_code) + && auth::decode_2fa_remember(twofactor_code) + .is_ok_and(|t| t.sub == device.uuid && t.user_uuid == user.uuid)) => {} _ => { + // Always delete the current twofactor remember token here if it exists + if device.twofactor_remember.is_some() { + device.delete_twofactor_remember(); + // We need to save here, since we send a err_json!() which prevents saving `device` at a later stage + device.save(true, conn).await?; + } err_json!( _json_err_twofactor(&twofactor_ids, &user.uuid, data, client_version, conn).await?, - "2FA Remember token not provided" + "2FA Remember token not provided or expired" ) } } @@ -826,10 +835,10 @@ async fn twofactor_auth( TwoFactorIncomplete::mark_complete(&user.uuid, &device.uuid, conn).await?; + let remember = data.two_factor_remember.unwrap_or(0); let two_factor = if !CONFIG.disable_2fa_remember() && remember == 1 { Some(device.refresh_twofactor_remember()) } else { - device.delete_twofactor_remember(); None }; Ok(two_factor) diff --git a/src/auth.rs b/src/auth.rs index b71a5bd9..99741277 100644 --- a/src/auth.rs +++ b/src/auth.rs @@ -46,6 +46,7 @@ static JWT_FILE_DOWNLOAD_ISSUER: LazyLock = LazyLock::new(|| format!("{}|file_download", CONFIG.domain_origin())); static JWT_REGISTER_VERIFY_ISSUER: LazyLock = LazyLock::new(|| format!("{}|register_verify", CONFIG.domain_origin())); +static JWT_2FA_REMEMBER_ISSUER: LazyLock = LazyLock::new(|| format!("{}|2faremember", CONFIG.domain_origin())); static PRIVATE_RSA_KEY: OnceLock = OnceLock::new(); static PUBLIC_RSA_KEY: OnceLock = OnceLock::new(); @@ -160,6 +161,10 @@ pub fn decode_register_verify(token: &str) -> Result Result { + decode_jwt(token, JWT_2FA_REMEMBER_ISSUER.to_string()) +} + #[derive(Debug, Serialize, Deserialize)] pub struct LoginJwtClaims { // Not before @@ -440,6 +445,31 @@ pub fn generate_register_verify_claims(email: String, name: Option, veri } } +#[derive(Serialize, Deserialize)] +pub struct TwoFactorRememberClaims { + // Not before + pub nbf: i64, + // Expiration time + pub exp: i64, + // Issuer + pub iss: String, + // Subject + pub sub: DeviceId, + // UserId + pub user_uuid: UserId, +} + +pub fn generate_2fa_remember_claims(device_uuid: DeviceId, user_uuid: UserId) -> TwoFactorRememberClaims { + let time_now = Utc::now(); + TwoFactorRememberClaims { + nbf: time_now.timestamp(), + exp: (time_now + TimeDelta::try_days(30).unwrap()).timestamp(), + iss: JWT_2FA_REMEMBER_ISSUER.to_string(), + sub: device_uuid, + user_uuid, + } +} + #[derive(Debug, Serialize, Deserialize)] pub struct BasicJwtClaims { // Not before diff --git a/src/db/models/device.rs b/src/db/models/device.rs index 4e3d0197..d3c32213 100644 --- a/src/db/models/device.rs +++ b/src/db/models/device.rs @@ -1,6 +1,6 @@ use chrono::{NaiveDateTime, Utc}; -use data_encoding::{BASE64, BASE64URL}; +use data_encoding::BASE64URL; use derive_more::{Display, From}; use serde_json::Value; @@ -67,10 +67,13 @@ impl Device { } pub fn refresh_twofactor_remember(&mut self) -> String { - let twofactor_remember = crypto::encode_random_bytes::<180>(&BASE64); - self.twofactor_remember = Some(twofactor_remember.clone()); + use crate::auth::{encode_jwt, generate_2fa_remember_claims}; - twofactor_remember + let two_factor_remember_claim = generate_2fa_remember_claims(self.uuid.clone(), self.user_uuid.clone()); + let two_factor_remember_string = encode_jwt(&two_factor_remember_claim); + self.twofactor_remember = Some(two_factor_remember_string.clone()); + + two_factor_remember_string } pub fn delete_twofactor_remember(&mut self) { From dde63e209e00e4e1a02d8e43175976386a31c53b Mon Sep 17 00:00:00 2001 From: Mathijs van Veluw Date: Sun, 29 Mar 2026 22:21:39 +0200 Subject: [PATCH 41/89] Misc Updates (#7027) - Update Rust to v1.94.1 - Updated all crates - Update GHA - Update global domains and ensure a newline is always present Signed-off-by: BlackDex --- .github/workflows/trivy.yml | 2 +- Cargo.lock | 166 ++++++++++----------------------- Cargo.toml | 4 +- docker/DockerSettings.yaml | 2 +- docker/Dockerfile.alpine | 8 +- docker/Dockerfile.debian | 2 +- rust-toolchain.toml | 2 +- src/static/global_domains.json | 13 ++- tools/global_domains.py | 1 + 9 files changed, 69 insertions(+), 131 deletions(-) diff --git a/.github/workflows/trivy.yml b/.github/workflows/trivy.yml index 94ff6f63..c9e02cf9 100644 --- a/.github/workflows/trivy.yml +++ b/.github/workflows/trivy.yml @@ -50,6 +50,6 @@ jobs: severity: CRITICAL,HIGH - name: Upload Trivy scan results to GitHub Security tab - uses: github/codeql-action/upload-sarif@38697555549f1db7851b81482ff19f1fa5c4fedc # v4.34.1 + uses: github/codeql-action/upload-sarif@c10b8064de6f491fea524254123dbe5e09572f13 # v4.35.1 with: sarif_file: 'trivy-results.sarif' diff --git a/Cargo.lock b/Cargo.lock index b16d3fdb..290627c0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -76,15 +76,6 @@ version = "1.0.102" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" -[[package]] -name = "ar_archive_writer" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7eb93bbb63b9c227414f6eb3a0adfddca591a8ce1e9b60661bb08969b87e340b" -dependencies = [ - "object", -] - [[package]] name = "argon2" version = "0.5.3" @@ -891,9 +882,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.57" +version = "1.2.58" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a0dd1ca384932ff3641c8718a02769f1698e7563dc6974ffd03346116310423" +checksum = "e1e928d4b69e3077709075a938a05ffbedfa53a84c8f766efbf8220bb1ff60e1" dependencies = [ "find-msvc-tools", "jobserver", @@ -948,16 +939,6 @@ dependencies = [ "phf 0.12.1", ] -[[package]] -name = "chumsky" -version = "0.9.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8eebd66744a15ded14960ab4ccdbfb51ad3b81f51f3f04a80adac98c985396c9" -dependencies = [ - "hashbrown 0.14.5", - "stacker", -] - [[package]] name = "cipher" version = "0.4.4" @@ -2639,14 +2620,15 @@ dependencies = [ [[package]] name = "ipconfig" -version = "0.3.2" +version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b58db92f96b720de98181bbbe63c831e87005ab460c1bf306eb2622b4707997f" +checksum = "4d40460c0ce33d6ce4b0630ad68ff63d6661961c48b6dba35e5a4d81cfb48222" dependencies = [ - "socket2 0.5.10", + "socket2 0.6.3", "widestring", - "windows-sys 0.48.0", - "winreg", + "windows-registry", + "windows-result", + "windows-sys 0.61.2", ] [[package]] @@ -2761,10 +2743,12 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.91" +version = "0.3.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b49715b7073f385ba4bc528e5747d02e66cb39c6146efb66b781f131f0fb399c" +checksum = "cc4c90f45aa2e6eacbe8645f77fdea542ac97a494bcd117a67df9ff4d611f995" dependencies = [ + "cfg-if", + "futures-util", "once_cell", "wasm-bindgen", ] @@ -2842,14 +2826,13 @@ checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" [[package]] name = "lettre" -version = "0.11.19" +version = "0.11.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e13e10e8818f8b2a60f52cb127041d388b89f3a96a62be9ceaffa22262fef7f" +checksum = "471816f3e24b85e820dee02cde962379ea1a669e5242f19c61bcbcffedf4c4fb" dependencies = [ "async-std", "async-trait", "base64 0.22.1", - "chumsky", "email-encoding", "email_address", "fastrand", @@ -3049,9 +3032,9 @@ dependencies = [ [[package]] name = "mio" -version = "1.1.1" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" +checksum = "50b7e5b27aa02a74bac8c3f23f448f8d87ff11f92d3aac1a6ed369ee08cc56c1" dependencies = [ "libc", "wasi", @@ -3170,9 +3153,9 @@ dependencies = [ [[package]] name = "num-conv" -version = "0.2.0" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf97ec579c3c42f953ef76dbf8d55ac91fb219dde70e49aa4a6b7d74e9919050" +checksum = "c6673768db2d862beb9b39a78fdcb1a69439615d5794a1be50caa9bc92c81967" [[package]] name = "num-derive" @@ -3269,15 +3252,6 @@ dependencies = [ "url", ] -[[package]] -name = "object" -version = "0.37.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe" -dependencies = [ - "memchr", -] - [[package]] name = "oid-registry" version = "0.7.1" @@ -3857,16 +3831,6 @@ version = "2.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "33cb294fe86a74cbcf50d4445b37da762029549ebeea341421c7c70370f86cac" -[[package]] -name = "psm" -version = "0.1.30" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3852766467df634d74f0b2d7819bf8dc483a0eb2e3b0f50f756f9cfe8b0d18d8" -dependencies = [ - "ar_archive_writer", - "cc", -] - [[package]] name = "publicsuffix" version = "2.3.0" @@ -4459,9 +4423,9 @@ dependencies = [ [[package]] name = "rustc-hash" -version = "2.1.1" +version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" +checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe" [[package]] name = "rustc_version" @@ -4815,9 +4779,9 @@ dependencies = [ [[package]] name = "serde_spanned" -version = "1.0.4" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8bbf91e5a4d6315eee45e704372590b30e260ee83af6639d64557f51b067776" +checksum = "876ac351060d4f882bb1032b6369eb0aef79ad9df1ea8bc404874d8cc3d0cd98" dependencies = [ "serde_core", ] @@ -4934,9 +4898,9 @@ dependencies = [ [[package]] name = "simd-adler32" -version = "0.3.8" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2" +checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" [[package]] name = "simple_asn1" @@ -5040,19 +5004,6 @@ version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" -[[package]] -name = "stacker" -version = "0.1.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08d74a23609d509411d10e2176dc2a4346e3b4aea2e7b1869f19fdedbc71c013" -dependencies = [ - "cc", - "cfg-if", - "libc", - "psm", - "windows-sys 0.59.0", -] - [[package]] name = "state" version = "0.6.0" @@ -5399,7 +5350,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf92845e79fc2e2def6a5d828f0801e29a2f8acc037becc5ab08595c7d5e9863" dependencies = [ "serde_core", - "serde_spanned 1.0.4", + "serde_spanned 1.1.0", "toml_datetime 0.7.5+spec-1.1.0", "toml_parser", "winnow 0.7.15", @@ -5439,9 +5390,9 @@ dependencies = [ [[package]] name = "toml_parser" -version = "1.0.10+spec-1.1.0" +version = "1.1.0+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7df25b4befd31c4816df190124375d5a20c6b6921e2cad937316de3fccd63420" +checksum = "2334f11ee363607eb04df9b8fc8a13ca1715a72ba8662a26ac285c98aabb4011" dependencies = [ "winnow 1.0.0", ] @@ -5640,9 +5591,9 @@ checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" [[package]] name = "unicode-segmentation" -version = "1.12.0" +version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" +checksum = "9629274872b2bfaf8d66f5f15725007f635594914870f65218920345aa11aa8c" [[package]] name = "unicode-xid" @@ -5689,9 +5640,9 @@ checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" [[package]] name = "uuid" -version = "1.22.0" +version = "1.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a68d3c8f01c0cfa54a75291d83601161799e4a89a39e0929f4b0354d88757a37" +checksum = "5ac8b6f42ead25368cf5b098aeb3dc8a1a2c05a3eee8a9a1a68c640edbfc79d9" dependencies = [ "getrandom 0.4.2", "js-sys", @@ -5852,9 +5803,9 @@ dependencies = [ [[package]] name = "wasm-bindgen" -version = "0.2.114" +version = "0.2.115" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6532f9a5c1ece3798cb1c2cfdba640b9b3ba884f5db45973a6f442510a87d38e" +checksum = "6523d69017b7633e396a89c5efab138161ed5aafcbc8d3e5c5a42ae38f50495a" dependencies = [ "cfg-if", "once_cell", @@ -5865,23 +5816,19 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.64" +version = "0.4.65" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e9c5522b3a28661442748e09d40924dfb9ca614b21c00d3fd135720e48b67db8" +checksum = "2d1faf851e778dfa54db7cd438b70758eba9755cb47403f3496edd7c8fc212f0" dependencies = [ - "cfg-if", - "futures-util", "js-sys", - "once_cell", "wasm-bindgen", - "web-sys", ] [[package]] name = "wasm-bindgen-macro" -version = "0.2.114" +version = "0.2.115" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18a2d50fcf105fb33bb15f00e7a77b772945a2ee45dcf454961fd843e74c18e6" +checksum = "4e3a6c758eb2f701ed3d052ff5737f5bfe6614326ea7f3bbac7156192dc32e67" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -5889,9 +5836,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.114" +version = "0.2.115" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03ce4caeaac547cdf713d280eda22a730824dd11e6b8c3ca9e42247b25c631e3" +checksum = "921de2737904886b52bcbb237301552d05969a6f9c40d261eb0533c8b055fedf" dependencies = [ "bumpalo", "proc-macro2", @@ -5902,9 +5849,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.114" +version = "0.2.115" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75a326b8c223ee17883a4251907455a2431acc2791c98c26279376490c378c16" +checksum = "a93e946af942b58934c604527337bad9ae33ba1d5c6900bbb41c2c07c2364a93" dependencies = [ "unicode-ident", ] @@ -5958,9 +5905,9 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.91" +version = "0.3.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "854ba17bb104abfb26ba36da9729addc7ce7f06f5c0f90f3c391f8461cca21f9" +checksum = "84cde8507f4d7cfcb1185b8cb5890c494ffea65edbe1ba82cfd63661c805ed94" dependencies = [ "js-sys", "wasm-bindgen", @@ -6178,15 +6125,6 @@ dependencies = [ "windows-link", ] -[[package]] -name = "windows-sys" -version = "0.48.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" -dependencies = [ - "windows-targets 0.48.5", -] - [[package]] name = "windows-sys" version = "0.52.0" @@ -6433,16 +6371,6 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a90e88e4667264a994d34e6d1ab2d26d398dcdca8b7f52bec8668957517fc7d8" -[[package]] -name = "winreg" -version = "0.50.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1" -dependencies = [ - "cfg-if", - "windows-sys 0.48.0", -] - [[package]] name = "wit-bindgen" version = "0.51.0" @@ -6616,18 +6544,18 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.8.47" +version = "0.8.48" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "efbb2a062be311f2ba113ce66f697a4dc589f85e78a4aea276200804cea0ed87" +checksum = "eed437bf9d6692032087e337407a86f04cd8d6a16a37199ed57949d415bd68e9" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.47" +version = "0.8.48" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e8bc7269b54418e7aeeef514aa68f8690b8c0489a06b0136e5f57c4c5ccab89" +checksum = "70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index 48d977ab..60286287 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -103,7 +103,7 @@ ring = "0.17.14" subtle = "2.6.1" # UUID generation -uuid = { version = "1.22.0", features = ["v4"] } +uuid = { version = "1.23.0", features = ["v4"] } # Date and time libraries chrono = { version = "0.4.44", features = ["clock", "serde"], default-features = false } @@ -136,7 +136,7 @@ webauthn-rs-core = "0.5.4" url = "2.5.8" # Email libraries -lettre = { version = "0.11.19", features = ["smtp-transport", "sendmail-transport", "builder", "serde", "hostname", "tracing", "tokio1-rustls", "ring", "rustls-native-certs"], default-features = false } +lettre = { version = "0.11.20", features = ["smtp-transport", "sendmail-transport", "builder", "serde", "hostname", "tracing", "tokio1-rustls", "ring", "rustls-native-certs"], default-features = false } percent-encoding = "2.3.2" # URL encoding library used for URL's in the emails email_address = "0.2.9" diff --git a/docker/DockerSettings.yaml b/docker/DockerSettings.yaml index 610f6b4a..c679b0da 100644 --- a/docker/DockerSettings.yaml +++ b/docker/DockerSettings.yaml @@ -5,7 +5,7 @@ vault_image_digest: "sha256:37c8661fa59dcdfbd3baa8366b6e950ef292b15adfeff1f57812 # We use the linux/amd64 platform shell scripts since there is no difference between the different platform scripts # https://github.com/tonistiigi/xx | https://hub.docker.com/r/tonistiigi/xx/tags xx_image_digest: "sha256:c64defb9ed5a91eacb37f96ccc3d4cd72521c4bd18d5442905b95e2226b0e707" -rust_version: 1.94.0 # Rust version to be used +rust_version: 1.94.1 # Rust version to be used debian_version: trixie # Debian release name to be used alpine_version: "3.23" # Alpine version to be used # For which platforms/architectures will we try to build images diff --git a/docker/Dockerfile.alpine b/docker/Dockerfile.alpine index 44242e08..ddcc9efe 100644 --- a/docker/Dockerfile.alpine +++ b/docker/Dockerfile.alpine @@ -32,10 +32,10 @@ FROM --platform=linux/amd64 docker.io/vaultwarden/web-vault@sha256:37c8661fa59dc ########################## ALPINE BUILD IMAGES ########################## ## NOTE: The Alpine Base Images do not support other platforms then linux/amd64 and linux/arm64 ## And for Alpine we define all build images here, they will only be loaded when actually used -FROM --platform=$BUILDPLATFORM ghcr.io/blackdex/rust-musl:x86_64-musl-stable-1.94.0 AS build_amd64 -FROM --platform=$BUILDPLATFORM ghcr.io/blackdex/rust-musl:aarch64-musl-stable-1.94.0 AS build_arm64 -FROM --platform=$BUILDPLATFORM ghcr.io/blackdex/rust-musl:armv7-musleabihf-stable-1.94.0 AS build_armv7 -FROM --platform=$BUILDPLATFORM ghcr.io/blackdex/rust-musl:arm-musleabi-stable-1.94.0 AS build_armv6 +FROM --platform=$BUILDPLATFORM ghcr.io/blackdex/rust-musl:x86_64-musl-stable-1.94.1 AS build_amd64 +FROM --platform=$BUILDPLATFORM ghcr.io/blackdex/rust-musl:aarch64-musl-stable-1.94.1 AS build_arm64 +FROM --platform=$BUILDPLATFORM ghcr.io/blackdex/rust-musl:armv7-musleabihf-stable-1.94.1 AS build_armv7 +FROM --platform=$BUILDPLATFORM ghcr.io/blackdex/rust-musl:arm-musleabi-stable-1.94.1 AS build_armv6 ########################## BUILD IMAGE ########################## # hadolint ignore=DL3006 diff --git a/docker/Dockerfile.debian b/docker/Dockerfile.debian index a60c485d..18dd3d6c 100644 --- a/docker/Dockerfile.debian +++ b/docker/Dockerfile.debian @@ -36,7 +36,7 @@ FROM --platform=linux/amd64 docker.io/tonistiigi/xx@sha256:c64defb9ed5a91eacb37f ########################## BUILD IMAGE ########################## # hadolint ignore=DL3006 -FROM --platform=$BUILDPLATFORM docker.io/library/rust:1.94.0-slim-trixie AS build +FROM --platform=$BUILDPLATFORM docker.io/library/rust:1.94.1-slim-trixie AS build COPY --from=xx / / ARG TARGETARCH ARG TARGETVARIANT diff --git a/rust-toolchain.toml b/rust-toolchain.toml index 0fc3f36d..151be09f 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -1,4 +1,4 @@ [toolchain] -channel = "1.94.0" +channel = "1.94.1" components = [ "rustfmt", "clippy" ] profile = "minimal" diff --git a/src/static/global_domains.json b/src/static/global_domains.json index e3f08813..3b13a3e9 100644 --- a/src/static/global_domains.json +++ b/src/static/global_domains.json @@ -111,7 +111,8 @@ "microsoftstore.com", "xbox.com", "azure.com", - "windowsazure.com" + "windowsazure.com", + "cloud.microsoft" ], "excluded": false }, @@ -971,5 +972,13 @@ "pinterest.se" ], "excluded": false + }, + { + "type": 91, + "domains": [ + "twitter.com", + "x.com" + ], + "excluded": false } -] \ No newline at end of file +] diff --git a/tools/global_domains.py b/tools/global_domains.py index 66edca31..78a31701 100755 --- a/tools/global_domains.py +++ b/tools/global_domains.py @@ -79,3 +79,4 @@ for name, domain_list in domain_lists.items(): # Write out the global domains JSON file. with open(file=OUTPUT_FILE, mode='w', encoding='utf-8') as f: json.dump(global_domains, f, indent=2) + f.write("\n") From 3a1378f469c66dc4cd92a32add3c47594c40604f Mon Sep 17 00:00:00 2001 From: Daniel Date: Sun, 29 Mar 2026 23:22:27 +0300 Subject: [PATCH 42/89] Switch to `attest` action (#7017) From the `attest-build-provenance` changelog: > As of version 4, actions/attest-build-provenance is simply a wrapper on top of actions/attest. > Existing applications may continue to use the attest-build-provenance action, but new implementations should use actions/attest instead. Please see the actions/attest repository for usage information. --- .github/workflows/release.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 8a3a7937..35e995c5 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -235,7 +235,7 @@ jobs: # Upload artifacts to Github Actions and Attest the binaries - name: Attest binaries - uses: actions/attest-build-provenance@a2bbfa25375fe432b6a289bc6b6cd05ecd0c4c32 # v4.1.0 + uses: actions/attest@59d89421af93a897026c735860bf21b6eb4f7b26 # v4.1.0 with: subject-path: vaultwarden-${{ env.NORMALIZED_ARCH }} @@ -361,7 +361,7 @@ jobs: # Attest container images - name: Attest - docker.io - ${{ matrix.base_image }} if: ${{ vars.DOCKERHUB_REPO != '' && env.DIGEST_SHA != ''}} - uses: actions/attest-build-provenance@a2bbfa25375fe432b6a289bc6b6cd05ecd0c4c32 # v4.1.0 + uses: actions/attest@59d89421af93a897026c735860bf21b6eb4f7b26 # v4.1.0 with: subject-name: ${{ vars.DOCKERHUB_REPO }} subject-digest: ${{ env.DIGEST_SHA }} @@ -369,7 +369,7 @@ jobs: - name: Attest - ghcr.io - ${{ matrix.base_image }} if: ${{ vars.GHCR_REPO != '' && env.DIGEST_SHA != ''}} - uses: actions/attest-build-provenance@a2bbfa25375fe432b6a289bc6b6cd05ecd0c4c32 # v4.1.0 + uses: actions/attest@59d89421af93a897026c735860bf21b6eb4f7b26 # v4.1.0 with: subject-name: ${{ vars.GHCR_REPO }} subject-digest: ${{ env.DIGEST_SHA }} @@ -377,7 +377,7 @@ jobs: - name: Attest - quay.io - ${{ matrix.base_image }} if: ${{ vars.QUAY_REPO != '' && env.DIGEST_SHA != ''}} - uses: actions/attest-build-provenance@a2bbfa25375fe432b6a289bc6b6cd05ecd0c4c32 # v4.1.0 + uses: actions/attest@59d89421af93a897026c735860bf21b6eb4f7b26 # v4.1.0 with: subject-name: ${{ vars.QUAY_REPO }} subject-digest: ${{ env.DIGEST_SHA }} From f62a7a66c8768204e82e0efa8b04108f3a544ce9 Mon Sep 17 00:00:00 2001 From: Mathijs van Veluw Date: Sun, 29 Mar 2026 22:43:36 +0200 Subject: [PATCH 43/89] Rotate refresh-tokens on sstamp reset (#7031) When a security-stamp gets reset/rotated we should also rotate all device refresh-tokens to invalidate them. Else clients are still able to use old refresh tokens. Signed-off-by: BlackDex --- src/api/admin.rs | 4 ++-- src/api/core/accounts.rs | 24 ++++++++++++++++-------- src/api/core/emergency_access.rs | 2 +- src/api/core/organizations.rs | 3 ++- src/db/models/device.rs | 18 +++++++++++++++++- src/db/models/user.rs | 12 ++++++++---- 6 files changed, 46 insertions(+), 17 deletions(-) diff --git a/src/api/admin.rs b/src/api/admin.rs index 6cedc040..a4f53e0e 100644 --- a/src/api/admin.rs +++ b/src/api/admin.rs @@ -472,7 +472,7 @@ async fn deauth_user(user_id: UserId, _token: AdminToken, conn: DbConn, nt: Noti } Device::delete_all_by_user(&user.uuid, &conn).await?; - user.reset_security_stamp(); + user.reset_security_stamp(&conn).await?; user.save(&conn).await } @@ -481,7 +481,7 @@ async fn deauth_user(user_id: UserId, _token: AdminToken, conn: DbConn, nt: Noti async fn disable_user(user_id: UserId, _token: AdminToken, conn: DbConn, nt: Notify<'_>) -> EmptyResult { let mut user = get_user_or_404(&user_id, &conn).await?; Device::delete_all_by_user(&user.uuid, &conn).await?; - user.reset_security_stamp(); + user.reset_security_stamp(&conn).await?; user.enabled = false; let save_result = user.save(&conn).await; diff --git a/src/api/core/accounts.rs b/src/api/core/accounts.rs index d91eb4cd..e0869c63 100644 --- a/src/api/core/accounts.rs +++ b/src/api/core/accounts.rs @@ -296,7 +296,7 @@ pub async fn _register(data: Json, email_verification: bool, conn: set_kdf_data(&mut user, &data.kdf)?; - user.set_password(&data.master_password_hash, Some(data.key), true, None); + user.set_password(&data.master_password_hash, Some(data.key), true, None, &conn).await?; user.password_hint = password_hint; // Add extra fields if present @@ -364,7 +364,9 @@ async fn post_set_password(data: Json, headers: Headers, conn: Some(data.key), false, Some(vec![String::from("revision_date")]), // We need to allow revision-date to use the old security_timestamp - ); + &conn, + ) + .await?; user.password_hint = password_hint; if let Some(keys) = data.keys { @@ -532,7 +534,9 @@ async fn post_password(data: Json, headers: Headers, conn: DbCon String::from("get_public_keys"), String::from("get_api_webauthn"), ]), - ); + &conn, + ) + .await?; let save_result = user.save(&conn).await; @@ -633,7 +637,9 @@ async fn post_kdf(data: Json, headers: Headers, conn: DbConn, nt: Some(data.unlock_data.master_key_wrapped_user_key), true, None, - ); + &conn, + ) + .await?; let save_result = user.save(&conn).await; nt.send_logout(&user, Some(headers.device.uuid.clone()), &conn).await; @@ -900,7 +906,9 @@ async fn post_rotatekey(data: Json, headers: Headers, conn: DbConn, nt: Some(data.account_unlock_data.master_password_unlock_data.master_key_encrypted_user_key), true, None, - ); + &conn, + ) + .await?; let save_result = user.save(&conn).await; @@ -920,7 +928,7 @@ async fn post_sstamp(data: Json, headers: Headers, conn: DbCo data.validate(&user, true, &conn).await?; Device::delete_all_by_user(&user.uuid, &conn).await?; - user.reset_security_stamp(); + user.reset_security_stamp(&conn).await?; let save_result = user.save(&conn).await; nt.send_logout(&user, None, &conn).await; @@ -1042,7 +1050,7 @@ async fn post_email(data: Json, headers: Headers, conn: DbConn, user.email_new = None; user.email_new_token = None; - user.set_password(&data.new_master_password_hash, Some(data.key), true, None); + user.set_password(&data.new_master_password_hash, Some(data.key), true, None, &conn).await?; let save_result = user.save(&conn).await; @@ -1254,7 +1262,7 @@ struct SecretVerificationRequest { pub async fn kdf_upgrade(user: &mut User, pwd_hash: &str, conn: &DbConn) -> ApiResult<()> { if user.password_iterations < CONFIG.password_iterations() { user.password_iterations = CONFIG.password_iterations(); - user.set_password(pwd_hash, None, false, None); + user.set_password(pwd_hash, None, false, None, conn).await?; if let Err(e) = user.save(conn).await { error!("Error updating user: {e:#?}"); diff --git a/src/api/core/emergency_access.rs b/src/api/core/emergency_access.rs index 1897f995..29a15c8d 100644 --- a/src/api/core/emergency_access.rs +++ b/src/api/core/emergency_access.rs @@ -653,7 +653,7 @@ async fn password_emergency_access( }; // change grantor_user password - grantor_user.set_password(new_master_password_hash, Some(data.key), true, None); + grantor_user.set_password(new_master_password_hash, Some(data.key), true, None, &conn).await?; grantor_user.save(&conn).await?; // Disable TwoFactor providers since they will otherwise block logins diff --git a/src/api/core/organizations.rs b/src/api/core/organizations.rs index 0213a006..0beb7036 100644 --- a/src/api/core/organizations.rs +++ b/src/api/core/organizations.rs @@ -2858,7 +2858,8 @@ async fn put_reset_password( let reset_request = data.into_inner(); let mut user = user; - user.set_password(reset_request.new_master_password_hash.as_str(), Some(reset_request.key), true, None); + user.set_password(reset_request.new_master_password_hash.as_str(), Some(reset_request.key), true, None, &conn) + .await?; user.save(&conn).await?; nt.send_logout(&user, None, &conn).await; diff --git a/src/db/models/device.rs b/src/db/models/device.rs index d3c32213..1026574c 100644 --- a/src/db/models/device.rs +++ b/src/db/models/device.rs @@ -49,11 +49,16 @@ impl Device { push_uuid: Some(PushId(get_uuid())), push_token: None, - refresh_token: crypto::encode_random_bytes::<64>(&BASE64URL), + refresh_token: Device::generate_refresh_token(), twofactor_remember: None, } } + #[inline(always)] + pub fn generate_refresh_token() -> String { + crypto::encode_random_bytes::<64>(&BASE64URL) + } + pub fn to_json(&self) -> Value { json!({ "id": self.uuid, @@ -260,6 +265,17 @@ impl Device { .unwrap_or(0) != 0 }} } + + pub async fn rotate_refresh_tokens_by_user(user_uuid: &UserId, conn: &DbConn) -> EmptyResult { + // Generate a new token per device. + // We cannot do a single UPDATE with one value because each device needs a unique token. + let devices = Self::find_by_user(user_uuid, conn).await; + for mut device in devices { + device.refresh_token = Device::generate_refresh_token(); + device.save(false, conn).await?; + } + Ok(()) + } } #[derive(Display)] diff --git a/src/db/models/user.rs b/src/db/models/user.rs index e88c7296..ebc72101 100644 --- a/src/db/models/user.rs +++ b/src/db/models/user.rs @@ -185,13 +185,14 @@ impl User { /// These routes are able to use the previous stamp id for the next 2 minutes. /// After these 2 minutes this stamp will expire. /// - pub fn set_password( + pub async fn set_password( &mut self, password: &str, new_key: Option, reset_security_stamp: bool, allow_next_route: Option>, - ) { + conn: &DbConn, + ) -> EmptyResult { self.password_hash = crypto::hash_password(password.as_bytes(), &self.salt, self.password_iterations as u32); if let Some(route) = allow_next_route { @@ -203,12 +204,15 @@ impl User { } if reset_security_stamp { - self.reset_security_stamp() + self.reset_security_stamp(conn).await?; } + Ok(()) } - pub fn reset_security_stamp(&mut self) { + pub async fn reset_security_stamp(&mut self, conn: &DbConn) -> EmptyResult { self.security_stamp = get_uuid(); + Device::rotate_refresh_tokens_by_user(&self.uuid, conn).await?; + Ok(()) } /// Set the stamp_exception to only allow a subsequent request matching a specific route using the current security-stamp. From 787822854cb73aeb8375a4e69ea188aa5f02db02 Mon Sep 17 00:00:00 2001 From: Mathijs van Veluw Date: Sun, 29 Mar 2026 23:15:48 +0200 Subject: [PATCH 44/89] Misc org fixes (#7032) * Split vault org/personal purge endpoints Signed-off-by: BlackDex * Adjust several other call-sites Signed-off-by: BlackDex * Several other misc fixes Signed-off-by: BlackDex * Add some more validation for groups, collections and memberships Signed-off-by: BlackDex --------- Signed-off-by: BlackDex --- src/api/core/accounts.rs | 16 ++- src/api/core/ciphers.rs | 116 +++++++++++---------- src/api/core/events.rs | 2 +- src/api/core/organizations.rs | 188 +++++++++++++++++++++++----------- src/api/core/public.rs | 2 +- src/auth.rs | 51 ++++++--- src/db/models/cipher.rs | 51 +++++---- src/db/models/collection.rs | 22 ++-- src/db/models/group.rs | 82 +++++++++++---- src/db/models/org_policy.rs | 2 +- src/db/models/organization.rs | 4 +- src/db/models/two_factor.rs | 1 - 12 files changed, 343 insertions(+), 194 deletions(-) diff --git a/src/api/core/accounts.rs b/src/api/core/accounts.rs index e0869c63..ba36fc9b 100644 --- a/src/api/core/accounts.rs +++ b/src/api/core/accounts.rs @@ -106,7 +106,6 @@ pub struct RegisterData { name: Option, - #[allow(dead_code)] organization_user_id: Option, // Used only from the register/finish endpoint @@ -376,14 +375,12 @@ async fn post_set_password(data: Json, headers: Headers, conn: if let Some(identifier) = data.org_identifier { if identifier != crate::sso::FAKE_IDENTIFIER && identifier != crate::api::admin::FAKE_ADMIN_UUID { - let org = match Organization::find_by_uuid(&identifier.into(), &conn).await { - None => err!("Failed to retrieve the associated organization"), - Some(org) => org, + let Some(org) = Organization::find_by_uuid(&identifier.into(), &conn).await else { + err!("Failed to retrieve the associated organization") }; - let membership = match Membership::find_by_user_and_org(&user.uuid, &org.uuid, &conn).await { - None => err!("Failed to retrieve the invitation"), - Some(org) => org, + let Some(membership) = Membership::find_by_user_and_org(&user.uuid, &org.uuid, &conn).await else { + err!("Failed to retrieve the invitation") }; accept_org_invite(&user, membership, None, &conn).await?; @@ -583,7 +580,6 @@ fn set_kdf_data(user: &mut User, data: &KDFData) -> EmptyResult { Ok(()) } -#[allow(dead_code)] #[derive(Deserialize)] #[serde(rename_all = "camelCase")] struct AuthenticationData { @@ -592,7 +588,6 @@ struct AuthenticationData { master_password_authentication_hash: String, } -#[allow(dead_code)] #[derive(Deserialize)] #[serde(rename_all = "camelCase")] struct UnlockData { @@ -601,11 +596,12 @@ struct UnlockData { master_key_wrapped_user_key: String, } -#[allow(dead_code)] #[derive(Deserialize)] #[serde(rename_all = "camelCase")] struct ChangeKdfData { + #[allow(dead_code)] new_master_password_hash: String, + #[allow(dead_code)] key: String, authentication_data: AuthenticationData, unlock_data: UnlockData, diff --git a/src/api/core/ciphers.rs b/src/api/core/ciphers.rs index f7bf5cd3..8a8d9838 100644 --- a/src/api/core/ciphers.rs +++ b/src/api/core/ciphers.rs @@ -14,7 +14,7 @@ use crate::auth::ClientVersion; use crate::util::{save_temp_file, NumberOrString}; use crate::{ api::{self, core::log_event, EmptyResult, JsonResult, Notify, PasswordOrOtpData, UpdateType}, - auth::Headers, + auth::{Headers, OrgIdGuard, OwnerHeaders}, config::PathType, crypto, db::{ @@ -86,7 +86,8 @@ pub fn routes() -> Vec { restore_cipher_put_admin, restore_cipher_selected, restore_cipher_selected_admin, - delete_all, + purge_org_vault, + purge_personal_vault, move_cipher_selected, move_cipher_selected_put, put_collections2_update, @@ -425,7 +426,7 @@ pub async fn update_cipher_from_data( let transfer_cipher = cipher.organization_uuid.is_none() && data.organization_id.is_some(); if let Some(org_id) = data.organization_id { - match Membership::find_by_user_and_org(&headers.user.uuid, &org_id, conn).await { + match Membership::find_confirmed_by_user_and_org(&headers.user.uuid, &org_id, conn).await { None => err!("You don't have permission to add item to organization"), Some(member) => { if shared_to_collections.is_some() @@ -1642,9 +1643,51 @@ struct OrganizationIdData { org_id: OrganizationId, } +// Use the OrgIdGuard here, to ensure there an organization id present. +// If there is no organization id present, it should be forwarded to purge_personal_vault. +// This guard needs to be the first argument, else OwnerHeaders will be triggered which will logout the user. #[post("/ciphers/purge?", data = "")] -async fn delete_all( - organization: Option, +async fn purge_org_vault( + _org_id_guard: OrgIdGuard, + organization: OrganizationIdData, + data: Json, + headers: OwnerHeaders, + conn: DbConn, + nt: Notify<'_>, +) -> EmptyResult { + if organization.org_id != headers.org_id { + err!("Organization not found", "Organization id's do not match"); + } + + let data: PasswordOrOtpData = data.into_inner(); + let user = headers.user; + + data.validate(&user, true, &conn).await?; + + match Membership::find_confirmed_by_user_and_org(&user.uuid, &organization.org_id, &conn).await { + Some(member) if member.atype == MembershipType::Owner => { + Cipher::delete_all_by_organization(&organization.org_id, &conn).await?; + nt.send_user_update(UpdateType::SyncVault, &user, &headers.device.push_uuid, &conn).await; + + log_event( + EventType::OrganizationPurgedVault as i32, + &organization.org_id, + &organization.org_id, + &user.uuid, + headers.device.atype, + &headers.ip.ip, + &conn, + ) + .await; + + Ok(()) + } + _ => err!("You don't have permission to purge the organization vault"), + } +} + +#[post("/ciphers/purge", data = "")] +async fn purge_personal_vault( data: Json, headers: Headers, conn: DbConn, @@ -1655,52 +1698,18 @@ async fn delete_all( data.validate(&user, true, &conn).await?; - match organization { - Some(org_data) => { - // Organization ID in query params, purging organization vault - match Membership::find_by_user_and_org(&user.uuid, &org_data.org_id, &conn).await { - None => err!("You don't have permission to purge the organization vault"), - Some(member) => { - if member.atype == MembershipType::Owner { - Cipher::delete_all_by_organization(&org_data.org_id, &conn).await?; - nt.send_user_update(UpdateType::SyncVault, &user, &headers.device.push_uuid, &conn).await; - - log_event( - EventType::OrganizationPurgedVault as i32, - &org_data.org_id, - &org_data.org_id, - &user.uuid, - headers.device.atype, - &headers.ip.ip, - &conn, - ) - .await; - - Ok(()) - } else { - err!("You don't have permission to purge the organization vault"); - } - } - } - } - None => { - // No organization ID in query params, purging user vault - // Delete ciphers and their attachments - for cipher in Cipher::find_owned_by_user(&user.uuid, &conn).await { - cipher.delete(&conn).await?; - } - - // Delete folders - for f in Folder::find_by_user(&user.uuid, &conn).await { - f.delete(&conn).await?; - } - - user.update_revision(&conn).await?; - nt.send_user_update(UpdateType::SyncVault, &user, &headers.device.push_uuid, &conn).await; - - Ok(()) - } + for cipher in Cipher::find_owned_by_user(&user.uuid, &conn).await { + cipher.delete(&conn).await?; } + + for f in Folder::find_by_user(&user.uuid, &conn).await { + f.delete(&conn).await?; + } + + user.update_revision(&conn).await?; + nt.send_user_update(UpdateType::SyncVault, &user, &headers.device.push_uuid, &conn).await; + + Ok(()) } #[derive(PartialEq)] @@ -1980,8 +1989,11 @@ impl CipherSyncData { } // Generate a HashMap with the Organization UUID as key and the Membership record - let members: HashMap = - Membership::find_by_user(user_id, conn).await.into_iter().map(|m| (m.org_uuid.clone(), m)).collect(); + let members: HashMap = Membership::find_confirmed_by_user(user_id, conn) + .await + .into_iter() + .map(|m| (m.org_uuid.clone(), m)) + .collect(); // Generate a HashMap with the User_Collections UUID as key and the CollectionUser record let user_collections: HashMap = CollectionUser::find_by_user(user_id, conn) diff --git a/src/api/core/events.rs b/src/api/core/events.rs index 2f33a407..d1612255 100644 --- a/src/api/core/events.rs +++ b/src/api/core/events.rs @@ -240,7 +240,7 @@ async fn _log_user_event( ip: &IpAddr, conn: &DbConn, ) { - let memberships = Membership::find_by_user(user_id, conn).await; + let memberships = Membership::find_confirmed_by_user(user_id, conn).await; let mut events: Vec = Vec::with_capacity(memberships.len() + 1); // We need an event per org and one without an org // Upstream saves the event also without any org_id. diff --git a/src/api/core/organizations.rs b/src/api/core/organizations.rs index 0beb7036..36e3e4a0 100644 --- a/src/api/core/organizations.rs +++ b/src/api/core/organizations.rs @@ -131,6 +131,24 @@ struct FullCollectionData { external_id: Option, } +impl FullCollectionData { + pub async fn validate(&self, org_id: &OrganizationId, conn: &DbConn) -> EmptyResult { + let org_groups = Group::find_by_organization(org_id, conn).await; + let org_group_ids: HashSet<&GroupId> = org_groups.iter().map(|c| &c.uuid).collect(); + if let Some(e) = self.groups.iter().find(|g| !org_group_ids.contains(&g.id)) { + err!("Invalid group", format!("Group {} does not belong to organization {}!", e.id, org_id)) + } + + let org_memberships = Membership::find_by_org(org_id, conn).await; + let org_membership_ids: HashSet<&MembershipId> = org_memberships.iter().map(|m| &m.uuid).collect(); + if let Some(e) = self.users.iter().find(|m| !org_membership_ids.contains(&m.id)) { + err!("Invalid member", format!("Member {} does not belong to organization {}!", e.id, org_id)) + } + + Ok(()) + } +} + #[derive(Deserialize)] #[serde(rename_all = "camelCase")] struct CollectionGroupData { @@ -233,30 +251,30 @@ async fn post_delete_organization( } #[post("/organizations//leave")] -async fn leave_organization(org_id: OrganizationId, headers: Headers, conn: DbConn) -> EmptyResult { - match Membership::find_by_user_and_org(&headers.user.uuid, &org_id, &conn).await { - None => err!("User not part of organization"), - Some(member) => { - if member.atype == MembershipType::Owner - && Membership::count_confirmed_by_org_and_type(&org_id, MembershipType::Owner, &conn).await <= 1 - { - err!("The last owner can't leave") - } - - log_event( - EventType::OrganizationUserLeft as i32, - &member.uuid, - &org_id, - &headers.user.uuid, - headers.device.atype, - &headers.ip.ip, - &conn, - ) - .await; - - member.delete(&conn).await - } +async fn leave_organization(org_id: OrganizationId, headers: OrgMemberHeaders, conn: DbConn) -> EmptyResult { + if headers.membership.status != MembershipStatus::Confirmed as i32 { + err!("You need to be a Member of the Organization to call this endpoint") } + let membership = headers.membership; + + if membership.atype == MembershipType::Owner + && Membership::count_confirmed_by_org_and_type(&org_id, MembershipType::Owner, &conn).await <= 1 + { + err!("The last owner can't leave") + } + + log_event( + EventType::OrganizationUserLeft as i32, + &membership.uuid, + &org_id, + &headers.user.uuid, + headers.device.atype, + &headers.ip.ip, + &conn, + ) + .await; + + membership.delete(&conn).await } #[get("/organizations/")] @@ -480,12 +498,9 @@ async fn post_organization_collections( err!("Organization not found", "Organization id's do not match"); } let data: FullCollectionData = data.into_inner(); + data.validate(&org_id, &conn).await?; - let Some(org) = Organization::find_by_uuid(&org_id, &conn).await else { - err!("Can't find organization details") - }; - - let collection = Collection::new(org.uuid, data.name, data.external_id); + let collection = Collection::new(org_id.clone(), data.name, data.external_id); collection.save(&conn).await?; log_event( @@ -501,7 +516,7 @@ async fn post_organization_collections( for group in data.groups { CollectionGroup::new(collection.uuid.clone(), group.id, group.read_only, group.hide_passwords, group.manage) - .save(&conn) + .save(&org_id, &conn) .await?; } @@ -579,10 +594,10 @@ async fn post_bulk_access_collections( ) .await; - CollectionGroup::delete_all_by_collection(&col_id, &conn).await?; + CollectionGroup::delete_all_by_collection(&col_id, &org_id, &conn).await?; for group in &data.groups { CollectionGroup::new(col_id.clone(), group.id.clone(), group.read_only, group.hide_passwords, group.manage) - .save(&conn) + .save(&org_id, &conn) .await?; } @@ -627,6 +642,7 @@ async fn post_organization_collection_update( err!("Organization not found", "Organization id's do not match"); } let data: FullCollectionData = data.into_inner(); + data.validate(&org_id, &conn).await?; if Organization::find_by_uuid(&org_id, &conn).await.is_none() { err!("Can't find organization details") @@ -655,11 +671,11 @@ async fn post_organization_collection_update( ) .await; - CollectionGroup::delete_all_by_collection(&col_id, &conn).await?; + CollectionGroup::delete_all_by_collection(&col_id, &org_id, &conn).await?; for group in data.groups { CollectionGroup::new(col_id.clone(), group.id, group.read_only, group.hide_passwords, group.manage) - .save(&conn) + .save(&org_id, &conn) .await?; } @@ -1003,6 +1019,24 @@ struct InviteData { permissions: HashMap, } +impl InviteData { + async fn validate(&self, org_id: &OrganizationId, conn: &DbConn) -> EmptyResult { + let org_collections = Collection::find_by_organization(org_id, conn).await; + let org_collection_ids: HashSet<&CollectionId> = org_collections.iter().map(|c| &c.uuid).collect(); + if let Some(e) = self.collections.iter().flatten().find(|c| !org_collection_ids.contains(&c.id)) { + err!("Invalid collection", format!("Collection {} does not belong to organization {}!", e.id, org_id)) + } + + let org_groups = Group::find_by_organization(org_id, conn).await; + let org_group_ids: HashSet<&GroupId> = org_groups.iter().map(|c| &c.uuid).collect(); + if let Some(e) = self.groups.iter().find(|g| !org_group_ids.contains(g)) { + err!("Invalid group", format!("Group {} does not belong to organization {}!", e, org_id)) + } + + Ok(()) + } +} + #[post("/organizations//users/invite", data = "")] async fn send_invite( org_id: OrganizationId, @@ -1014,6 +1048,7 @@ async fn send_invite( err!("Organization not found", "Organization id's do not match"); } let data: InviteData = data.into_inner(); + data.validate(&org_id, &conn).await?; // HACK: We need the raw user-type to be sure custom role is selected to determine the access_all permission // The from_str() will convert the custom role type into a manager role type @@ -1273,20 +1308,20 @@ async fn accept_invite( // skip invitation logic when we were invited via the /admin panel if **member_id != FAKE_ADMIN_UUID { - let Some(mut member) = Membership::find_by_uuid_and_org(member_id, &claims.org_id, &conn).await else { + let Some(mut membership) = Membership::find_by_uuid_and_org(member_id, &claims.org_id, &conn).await else { err!("Error accepting the invitation") }; - let reset_password_key = match OrgPolicy::org_is_reset_password_auto_enroll(&member.org_uuid, &conn).await { + let reset_password_key = match OrgPolicy::org_is_reset_password_auto_enroll(&membership.org_uuid, &conn).await { true if data.reset_password_key.is_none() => err!("Reset password key is required, but not provided."), true => data.reset_password_key, false => None, }; // In case the user was invited before the mail was saved in db. - member.invited_by_email = member.invited_by_email.or(claims.invited_by_email); + membership.invited_by_email = membership.invited_by_email.or(claims.invited_by_email); - accept_org_invite(&headers.user, member, reset_password_key, &conn).await?; + accept_org_invite(&headers.user, membership, reset_password_key, &conn).await?; } else if CONFIG.mail_enabled() { // User was invited from /admin, so they are automatically confirmed let org_name = CONFIG.invitation_org_name(); @@ -1520,9 +1555,8 @@ async fn edit_member( && data.permissions.get("deleteAnyCollection") == Some(&json!(true)) && data.permissions.get("createNewCollections") == Some(&json!(true))); - let mut member_to_edit = match Membership::find_by_uuid_and_org(&member_id, &org_id, &conn).await { - Some(member) => member, - None => err!("The specified user isn't member of the organization"), + let Some(mut member_to_edit) = Membership::find_by_uuid_and_org(&member_id, &org_id, &conn).await else { + err!("The specified user isn't member of the organization") }; if new_type != member_to_edit.atype @@ -1839,7 +1873,6 @@ async fn post_org_import( #[derive(Deserialize)] #[serde(rename_all = "camelCase")] -#[allow(dead_code)] struct BulkCollectionsData { organization_id: OrganizationId, cipher_ids: Vec, @@ -1853,6 +1886,10 @@ struct BulkCollectionsData { async fn post_bulk_collections(data: Json, headers: Headers, conn: DbConn) -> EmptyResult { let data: BulkCollectionsData = data.into_inner(); + if Membership::find_confirmed_by_user_and_org(&headers.user.uuid, &data.organization_id, &conn).await.is_none() { + err!("You need to be a Member of the Organization to call this endpoint") + } + // Get all the collection available to the user in one query // Also filter based upon the provided collections let user_collections: HashMap = @@ -1941,7 +1978,7 @@ async fn list_policies_token(org_id: OrganizationId, token: &str, conn: DbConn) // Called during the SSO enrollment. // Return the org policy if it exists, otherwise use the default one. #[get("/organizations//policies/master-password", rank = 1)] -async fn get_master_password_policy(org_id: OrganizationId, _headers: Headers, conn: DbConn) -> JsonResult { +async fn get_master_password_policy(org_id: OrganizationId, _headers: OrgMemberHeaders, conn: DbConn) -> JsonResult { let policy = OrgPolicy::find_by_org_and_type(&org_id, OrgPolicyType::MasterPassword, &conn).await.unwrap_or_else(|| { let (enabled, data) = match CONFIG.sso_master_password_policy_value() { @@ -2149,13 +2186,13 @@ fn get_plans() -> Json { } #[get("/organizations/<_org_id>/billing/metadata")] -fn get_billing_metadata(_org_id: OrganizationId, _headers: Headers) -> Json { +fn get_billing_metadata(_org_id: OrganizationId, _headers: OrgMemberHeaders) -> Json { // Prevent a 404 error, which also causes Javascript errors. Json(_empty_data_json()) } #[get("/organizations/<_org_id>/billing/vnext/warnings")] -fn get_billing_warnings(_org_id: OrganizationId, _headers: Headers) -> Json { +fn get_billing_warnings(_org_id: OrganizationId, _headers: OrgMemberHeaders) -> Json { Json(json!({ "freeTrial":null, "inactiveSubscription":null, @@ -2427,6 +2464,23 @@ impl GroupRequest { group } + + /// Validate if all the collections and members belong to the provided organization + pub async fn validate(&self, org_id: &OrganizationId, conn: &DbConn) -> EmptyResult { + let org_collections = Collection::find_by_organization(org_id, conn).await; + let org_collection_ids: HashSet<&CollectionId> = org_collections.iter().map(|c| &c.uuid).collect(); + if let Some(e) = self.collections.iter().find(|c| !org_collection_ids.contains(&c.id)) { + err!("Invalid collection", format!("Collection {} does not belong to organization {}!", e.id, org_id)) + } + + let org_memberships = Membership::find_by_org(org_id, conn).await; + let org_membership_ids: HashSet<&MembershipId> = org_memberships.iter().map(|m| &m.uuid).collect(); + if let Some(e) = self.users.iter().find(|m| !org_membership_ids.contains(m)) { + err!("Invalid member", format!("Member {} does not belong to organization {}!", e, org_id)) + } + + Ok(()) + } } #[derive(Deserialize, Serialize)] @@ -2470,6 +2524,8 @@ async fn post_groups( } let group_request = data.into_inner(); + group_request.validate(&org_id, &conn).await?; + let group = group_request.to_group(&org_id); log_event( @@ -2506,10 +2562,12 @@ async fn put_group( }; let group_request = data.into_inner(); + group_request.validate(&org_id, &conn).await?; + let updated_group = group_request.update_group(group); - CollectionGroup::delete_all_by_group(&group_id, &conn).await?; - GroupUser::delete_all_by_group(&group_id, &conn).await?; + CollectionGroup::delete_all_by_group(&group_id, &org_id, &conn).await?; + GroupUser::delete_all_by_group(&group_id, &org_id, &conn).await?; log_event( EventType::GroupUpdated as i32, @@ -2537,7 +2595,7 @@ async fn add_update_group( for col_selection in collections { let mut collection_group = col_selection.to_collection_group(group.uuid.clone()); - collection_group.save(conn).await?; + collection_group.save(&org_id, conn).await?; } for assigned_member in members { @@ -2630,7 +2688,7 @@ async fn _delete_group( ) .await; - group.delete(conn).await + group.delete(org_id, conn).await } #[delete("/organizations//groups", data = "")] @@ -2689,7 +2747,7 @@ async fn get_group_members( err!("Group could not be found!", "Group uuid is invalid or does not belong to the organization") }; - let group_members: Vec = GroupUser::find_by_group(&group_id, &conn) + let group_members: Vec = GroupUser::find_by_group(&group_id, &org_id, &conn) .await .iter() .map(|entry| entry.users_organizations_uuid.clone()) @@ -2717,9 +2775,15 @@ async fn put_group_members( err!("Group could not be found!", "Group uuid is invalid or does not belong to the organization") }; - GroupUser::delete_all_by_group(&group_id, &conn).await?; - let assigned_members = data.into_inner(); + + let org_memberships = Membership::find_by_org(&org_id, &conn).await; + let org_membership_ids: HashSet<&MembershipId> = org_memberships.iter().map(|m| &m.uuid).collect(); + if let Some(e) = assigned_members.iter().find(|m| !org_membership_ids.contains(m)) { + err!("Invalid member", format!("Member {} does not belong to organization {}!", e, org_id)) + } + + GroupUser::delete_all_by_group(&group_id, &org_id, &conn).await?; for assigned_member in assigned_members { let mut user_entry = GroupUser::new(group_id.clone(), assigned_member.clone()); user_entry.save(&conn).await?; @@ -2951,15 +3015,20 @@ async fn check_reset_password_applicable(org_id: &OrganizationId, conn: &DbConn) Ok(()) } -#[put("/organizations//users//reset-password-enrollment", data = "")] +#[put("/organizations//users//reset-password-enrollment", data = "")] async fn put_reset_password_enrollment( org_id: OrganizationId, - member_id: MembershipId, - headers: Headers, + user_id: UserId, + headers: OrgMemberHeaders, data: Json, conn: DbConn, ) -> EmptyResult { - let Some(mut member) = Membership::find_by_user_and_org(&headers.user.uuid, &org_id, &conn).await else { + if user_id != headers.user.uuid { + err!("User to enroll isn't member of required organization", "The user_id and acting user do not match"); + } + + let Some(mut membership) = Membership::find_confirmed_by_user_and_org(&headers.user.uuid, &org_id, &conn).await + else { err!("User to enroll isn't member of required organization") }; @@ -2986,16 +3055,17 @@ async fn put_reset_password_enrollment( .await?; } - member.reset_password_key = reset_password_key; - member.save(&conn).await?; + membership.reset_password_key = reset_password_key; + membership.save(&conn).await?; - let log_id = if member.reset_password_key.is_some() { + let event_type = if membership.reset_password_key.is_some() { EventType::OrganizationUserResetPasswordEnroll as i32 } else { EventType::OrganizationUserResetPasswordWithdraw as i32 }; - log_event(log_id, &member_id, &org_id, &headers.user.uuid, headers.device.atype, &headers.ip.ip, &conn).await; + log_event(event_type, &membership.uuid, &org_id, &headers.user.uuid, headers.device.atype, &headers.ip.ip, &conn) + .await; Ok(()) } diff --git a/src/api/core/public.rs b/src/api/core/public.rs index 6a317b96..d757d953 100644 --- a/src/api/core/public.rs +++ b/src/api/core/public.rs @@ -156,7 +156,7 @@ async fn ldap_import(data: Json, token: PublicToken, conn: DbConn } }; - GroupUser::delete_all_by_group(&group_uuid, &conn).await?; + GroupUser::delete_all_by_group(&group_uuid, &org_id, &conn).await?; for ext_id in &group_data.member_external_ids { if let Some(member) = Membership::find_by_external_id_and_org(ext_id, &org_id, &conn).await { diff --git a/src/auth.rs b/src/auth.rs index 99741277..43184369 100644 --- a/src/auth.rs +++ b/src/auth.rs @@ -704,10 +704,9 @@ pub struct OrgHeaders { impl OrgHeaders { fn is_member(&self) -> bool { - // NOTE: we don't care about MembershipStatus at the moment because this is only used - // where an invited, accepted or confirmed user is expected if this ever changes or - // if from_i32 is changed to return Some(Revoked) this check needs to be changed accordingly - self.membership_type >= MembershipType::User + // Only allow not revoked members, we can not use the Confirmed status here + // as some endpoints can be triggered by invited users during joining + self.membership_status != MembershipStatus::Revoked && self.membership_type >= MembershipType::User } fn is_confirmed_and_admin(&self) -> bool { self.membership_status == MembershipStatus::Confirmed && self.membership_type >= MembershipType::Admin @@ -720,6 +719,36 @@ impl OrgHeaders { } } +// org_id is usually the second path param ("/organizations/"), +// but there are cases where it is a query value. +// First check the path, if this is not a valid uuid, try the query values. +fn get_org_id(request: &Request<'_>) -> Option { + if let Some(Ok(org_id)) = request.param::(1) { + Some(org_id) + } else if let Some(Ok(org_id)) = request.query_value::("organizationId") { + Some(org_id) + } else { + None + } +} + +// Special Guard to ensure that there is an organization id present +// If there is no org id trigger the Outcome::Forward. +// This is useful for endpoints which work for both organization and personal vaults, like purge. +pub struct OrgIdGuard; + +#[rocket::async_trait] +impl<'r> FromRequest<'r> for OrgIdGuard { + type Error = &'static str; + + async fn from_request(request: &'r Request<'_>) -> Outcome { + match get_org_id(request) { + Some(_) => Outcome::Success(OrgIdGuard), + None => Outcome::Forward(rocket::http::Status::NotFound), + } + } +} + #[rocket::async_trait] impl<'r> FromRequest<'r> for OrgHeaders { type Error = &'static str; @@ -727,18 +756,8 @@ impl<'r> FromRequest<'r> for OrgHeaders { async fn from_request(request: &'r Request<'_>) -> Outcome { let headers = try_outcome!(Headers::from_request(request).await); - // org_id is usually the second path param ("/organizations/"), - // but there are cases where it is a query value. - // First check the path, if this is not a valid uuid, try the query values. - let url_org_id: Option = { - if let Some(Ok(org_id)) = request.param::(1) { - Some(org_id) - } else if let Some(Ok(org_id)) = request.query_value::("organizationId") { - Some(org_id) - } else { - None - } - }; + // Extract the org_id from the request + let url_org_id = get_org_id(request); match url_org_id { Some(org_id) if uuid::Uuid::parse_str(&org_id).is_ok() => { diff --git a/src/db/models/cipher.rs b/src/db/models/cipher.rs index b28a25cd..edc5f8c9 100644 --- a/src/db/models/cipher.rs +++ b/src/db/models/cipher.rs @@ -559,7 +559,7 @@ impl Cipher { if let Some(cached_member) = cipher_sync_data.members.get(org_uuid) { return cached_member.has_full_access(); } - } else if let Some(member) = Membership::find_by_user_and_org(user_uuid, org_uuid, conn).await { + } else if let Some(member) = Membership::find_confirmed_by_user_and_org(user_uuid, org_uuid, conn).await { return member.has_full_access(); } } @@ -668,10 +668,12 @@ impl Cipher { ciphers::table .filter(ciphers::uuid.eq(&self.uuid)) .inner_join(ciphers_collections::table.on( - ciphers::uuid.eq(ciphers_collections::cipher_uuid))) + ciphers::uuid.eq(ciphers_collections::cipher_uuid) + )) .inner_join(users_collections::table.on( ciphers_collections::collection_uuid.eq(users_collections::collection_uuid) - .and(users_collections::user_uuid.eq(user_uuid)))) + .and(users_collections::user_uuid.eq(user_uuid)) + )) .select((users_collections::read_only, users_collections::hide_passwords, users_collections::manage)) .load::<(bool, bool, bool)>(conn) .expect("Error getting user access restrictions") @@ -697,6 +699,9 @@ impl Cipher { .inner_join(users_organizations::table.on( users_organizations::uuid.eq(groups_users::users_organizations_uuid) )) + .inner_join(groups::table.on(groups::uuid.eq(collections_groups::groups_uuid) + .and(groups::organizations_uuid.eq(users_organizations::org_uuid)) + )) .filter(users_organizations::user_uuid.eq(user_uuid)) .select((collections_groups::read_only, collections_groups::hide_passwords, collections_groups::manage)) .load::<(bool, bool, bool)>(conn) @@ -795,28 +800,28 @@ impl Cipher { let mut query = ciphers::table .left_join(ciphers_collections::table.on( ciphers::uuid.eq(ciphers_collections::cipher_uuid) - )) + )) .left_join(users_organizations::table.on( - ciphers::organization_uuid.eq(users_organizations::org_uuid.nullable()) + ciphers::organization_uuid.eq(users_organizations::org_uuid.nullable()) .and(users_organizations::user_uuid.eq(user_uuid)) .and(users_organizations::status.eq(MembershipStatus::Confirmed as i32)) - )) + )) .left_join(users_collections::table.on( - ciphers_collections::collection_uuid.eq(users_collections::collection_uuid) + ciphers_collections::collection_uuid.eq(users_collections::collection_uuid) // Ensure that users_collections::user_uuid is NULL for unconfirmed users. .and(users_organizations::user_uuid.eq(users_collections::user_uuid)) - )) + )) .left_join(groups_users::table.on( - groups_users::users_organizations_uuid.eq(users_organizations::uuid) - )) - .left_join(groups::table.on( - groups::uuid.eq(groups_users::groups_uuid) - )) + groups_users::users_organizations_uuid.eq(users_organizations::uuid) + )) + .left_join(groups::table.on(groups::uuid.eq(groups_users::groups_uuid) + // Ensure that group and membership belong to the same org + .and(groups::organizations_uuid.eq(users_organizations::org_uuid)) + )) .left_join(collections_groups::table.on( - collections_groups::collections_uuid.eq(ciphers_collections::collection_uuid).and( - collections_groups::groups_uuid.eq(groups::uuid) - ) - )) + collections_groups::collections_uuid.eq(ciphers_collections::collection_uuid) + .and(collections_groups::groups_uuid.eq(groups::uuid)) + )) .filter(ciphers::user_uuid.eq(user_uuid)) // Cipher owner .or_filter(users_organizations::access_all.eq(true)) // access_all in org .or_filter(users_collections::user_uuid.eq(user_uuid)) // Access to collection @@ -986,7 +991,9 @@ impl Cipher { .left_join(groups_users::table.on( groups_users::users_organizations_uuid.eq(users_organizations::uuid) )) - .left_join(groups::table.on(groups::uuid.eq(groups_users::groups_uuid))) + .left_join(groups::table.on(groups::uuid.eq(groups_users::groups_uuid) + .and(groups::organizations_uuid.eq(users_organizations::org_uuid)) + )) .left_join(collections_groups::table.on( collections_groups::collections_uuid.eq(ciphers_collections::collection_uuid) .and(collections_groups::groups_uuid.eq(groups::uuid)) @@ -1047,7 +1054,9 @@ impl Cipher { .left_join(groups_users::table.on( groups_users::users_organizations_uuid.eq(users_organizations::uuid) )) - .left_join(groups::table.on(groups::uuid.eq(groups_users::groups_uuid))) + .left_join(groups::table.on(groups::uuid.eq(groups_users::groups_uuid) + .and(groups::organizations_uuid.eq(users_organizations::org_uuid)) + )) .left_join(collections_groups::table.on( collections_groups::collections_uuid.eq(ciphers_collections::collection_uuid) .and(collections_groups::groups_uuid.eq(groups::uuid)) @@ -1115,8 +1124,8 @@ impl Cipher { .left_join(groups_users::table.on( groups_users::users_organizations_uuid.eq(users_organizations::uuid) )) - .left_join(groups::table.on( - groups::uuid.eq(groups_users::groups_uuid) + .left_join(groups::table.on(groups::uuid.eq(groups_users::groups_uuid) + .and(groups::organizations_uuid.eq(users_organizations::org_uuid)) )) .left_join(collections_groups::table.on( collections_groups::collections_uuid.eq(ciphers_collections::collection_uuid).and( diff --git a/src/db/models/collection.rs b/src/db/models/collection.rs index 3e6ccf21..b1f82335 100644 --- a/src/db/models/collection.rs +++ b/src/db/models/collection.rs @@ -191,7 +191,7 @@ impl Collection { self.update_users_revision(conn).await; CollectionCipher::delete_all_by_collection(&self.uuid, conn).await?; CollectionUser::delete_all_by_collection(&self.uuid, conn).await?; - CollectionGroup::delete_all_by_collection(&self.uuid, conn).await?; + CollectionGroup::delete_all_by_collection(&self.uuid, &self.org_uuid, conn).await?; db_run! { conn: { diesel::delete(collections::table.filter(collections::uuid.eq(self.uuid))) @@ -239,8 +239,8 @@ impl Collection { .left_join(groups_users::table.on( groups_users::users_organizations_uuid.eq(users_organizations::uuid) )) - .left_join(groups::table.on( - groups::uuid.eq(groups_users::groups_uuid) + .left_join(groups::table.on(groups::uuid.eq(groups_users::groups_uuid) + .and(groups::organizations_uuid.eq(users_organizations::org_uuid)) )) .left_join(collections_groups::table.on( collections_groups::groups_uuid.eq(groups_users::groups_uuid).and( @@ -355,8 +355,8 @@ impl Collection { .left_join(groups_users::table.on( groups_users::users_organizations_uuid.eq(users_organizations::uuid) )) - .left_join(groups::table.on( - groups::uuid.eq(groups_users::groups_uuid) + .left_join(groups::table.on(groups::uuid.eq(groups_users::groups_uuid) + .and(groups::organizations_uuid.eq(users_organizations::org_uuid)) )) .left_join(collections_groups::table.on( collections_groups::groups_uuid.eq(groups_users::groups_uuid).and( @@ -422,8 +422,8 @@ impl Collection { .left_join(groups_users::table.on( groups_users::users_organizations_uuid.eq(users_organizations::uuid) )) - .left_join(groups::table.on( - groups::uuid.eq(groups_users::groups_uuid) + .left_join(groups::table.on(groups::uuid.eq(groups_users::groups_uuid) + .and(groups::organizations_uuid.eq(users_organizations::org_uuid)) )) .left_join(collections_groups::table.on( collections_groups::groups_uuid.eq(groups_users::groups_uuid) @@ -484,8 +484,8 @@ impl Collection { .left_join(groups_users::table.on( groups_users::users_organizations_uuid.eq(users_organizations::uuid) )) - .left_join(groups::table.on( - groups::uuid.eq(groups_users::groups_uuid) + .left_join(groups::table.on(groups::uuid.eq(groups_users::groups_uuid) + .and(groups::organizations_uuid.eq(users_organizations::org_uuid)) )) .left_join(collections_groups::table.on( collections_groups::groups_uuid.eq(groups_users::groups_uuid).and( @@ -531,8 +531,8 @@ impl Collection { .left_join(groups_users::table.on( groups_users::users_organizations_uuid.eq(users_organizations::uuid) )) - .left_join(groups::table.on( - groups::uuid.eq(groups_users::groups_uuid) + .left_join(groups::table.on(groups::uuid.eq(groups_users::groups_uuid) + .and(groups::organizations_uuid.eq(users_organizations::org_uuid)) )) .left_join(collections_groups::table.on( collections_groups::groups_uuid.eq(groups_users::groups_uuid).and( diff --git a/src/db/models/group.rs b/src/db/models/group.rs index a24b5325..f41ad9ca 100644 --- a/src/db/models/group.rs +++ b/src/db/models/group.rs @@ -1,6 +1,6 @@ use super::{CollectionId, Membership, MembershipId, OrganizationId, User, UserId}; use crate::api::EmptyResult; -use crate::db::schema::{collections_groups, groups, groups_users, users_organizations}; +use crate::db::schema::{collections, collections_groups, groups, groups_users, users_organizations}; use crate::db::DbConn; use crate::error::MapResult; use chrono::{NaiveDateTime, Utc}; @@ -81,7 +81,7 @@ impl Group { // If both read_only and hide_passwords are false, then manage should be true // You can't have an entry with read_only and manage, or hide_passwords and manage // Or an entry with everything to false - let collections_groups: Vec = CollectionGroup::find_by_group(&self.uuid, conn) + let collections_groups: Vec = CollectionGroup::find_by_group(&self.uuid, &self.organizations_uuid, conn) .await .iter() .map(|entry| { @@ -191,7 +191,7 @@ impl Group { pub async fn delete_all_by_organization(org_uuid: &OrganizationId, conn: &DbConn) -> EmptyResult { for group in Self::find_by_organization(org_uuid, conn).await { - group.delete(conn).await?; + group.delete(org_uuid, conn).await?; } Ok(()) } @@ -246,8 +246,8 @@ impl Group { .inner_join(users_organizations::table.on( users_organizations::uuid.eq(groups_users::users_organizations_uuid) )) - .inner_join(groups::table.on( - groups::uuid.eq(groups_users::groups_uuid) + .inner_join(groups::table.on(groups::uuid.eq(groups_users::groups_uuid) + .and(groups::organizations_uuid.eq(users_organizations::org_uuid)) )) .filter(users_organizations::user_uuid.eq(user_uuid)) .filter(groups::access_all.eq(true)) @@ -276,9 +276,9 @@ impl Group { }} } - pub async fn delete(&self, conn: &DbConn) -> EmptyResult { - CollectionGroup::delete_all_by_group(&self.uuid, conn).await?; - GroupUser::delete_all_by_group(&self.uuid, conn).await?; + pub async fn delete(&self, org_uuid: &OrganizationId, conn: &DbConn) -> EmptyResult { + CollectionGroup::delete_all_by_group(&self.uuid, org_uuid, conn).await?; + GroupUser::delete_all_by_group(&self.uuid, org_uuid, conn).await?; db_run! { conn: { diesel::delete(groups::table.filter(groups::uuid.eq(&self.uuid))) @@ -306,8 +306,8 @@ impl Group { } impl CollectionGroup { - pub async fn save(&mut self, conn: &DbConn) -> EmptyResult { - let group_users = GroupUser::find_by_group(&self.groups_uuid, conn).await; + pub async fn save(&mut self, org_uuid: &OrganizationId, conn: &DbConn) -> EmptyResult { + let group_users = GroupUser::find_by_group(&self.groups_uuid, org_uuid, conn).await; for group_user in group_users { group_user.update_user_revision(conn).await; } @@ -365,10 +365,19 @@ impl CollectionGroup { } } - pub async fn find_by_group(group_uuid: &GroupId, conn: &DbConn) -> Vec { + pub async fn find_by_group(group_uuid: &GroupId, org_uuid: &OrganizationId, conn: &DbConn) -> Vec { db_run! { conn: { collections_groups::table + .inner_join(groups::table.on( + groups::uuid.eq(collections_groups::groups_uuid) + )) + .inner_join(collections::table.on( + collections::uuid.eq(collections_groups::collections_uuid) + .and(collections::org_uuid.eq(groups::organizations_uuid)) + )) .filter(collections_groups::groups_uuid.eq(group_uuid)) + .filter(collections::org_uuid.eq(org_uuid)) + .select(collections_groups::all_columns) .load::(conn) .expect("Error loading collection groups") }} @@ -383,6 +392,13 @@ impl CollectionGroup { .inner_join(users_organizations::table.on( users_organizations::uuid.eq(groups_users::users_organizations_uuid) )) + .inner_join(groups::table.on(groups::uuid.eq(collections_groups::groups_uuid) + .and(groups::organizations_uuid.eq(users_organizations::org_uuid)) + )) + .inner_join(collections::table.on( + collections::uuid.eq(collections_groups::collections_uuid) + .and(collections::org_uuid.eq(groups::organizations_uuid)) + )) .filter(users_organizations::user_uuid.eq(user_uuid)) .select(collections_groups::all_columns) .load::(conn) @@ -394,14 +410,20 @@ impl CollectionGroup { db_run! { conn: { collections_groups::table .filter(collections_groups::collections_uuid.eq(collection_uuid)) + .inner_join(collections::table.on( + collections::uuid.eq(collections_groups::collections_uuid) + )) + .inner_join(groups::table.on(groups::uuid.eq(collections_groups::groups_uuid) + .and(groups::organizations_uuid.eq(collections::org_uuid)) + )) .select(collections_groups::all_columns) .load::(conn) .expect("Error loading collection groups") }} } - pub async fn delete(&self, conn: &DbConn) -> EmptyResult { - let group_users = GroupUser::find_by_group(&self.groups_uuid, conn).await; + pub async fn delete(&self, org_uuid: &OrganizationId, conn: &DbConn) -> EmptyResult { + let group_users = GroupUser::find_by_group(&self.groups_uuid, org_uuid, conn).await; for group_user in group_users { group_user.update_user_revision(conn).await; } @@ -415,8 +437,8 @@ impl CollectionGroup { }} } - pub async fn delete_all_by_group(group_uuid: &GroupId, conn: &DbConn) -> EmptyResult { - let group_users = GroupUser::find_by_group(group_uuid, conn).await; + pub async fn delete_all_by_group(group_uuid: &GroupId, org_uuid: &OrganizationId, conn: &DbConn) -> EmptyResult { + let group_users = GroupUser::find_by_group(group_uuid, org_uuid, conn).await; for group_user in group_users { group_user.update_user_revision(conn).await; } @@ -429,10 +451,14 @@ impl CollectionGroup { }} } - pub async fn delete_all_by_collection(collection_uuid: &CollectionId, conn: &DbConn) -> EmptyResult { + pub async fn delete_all_by_collection( + collection_uuid: &CollectionId, + org_uuid: &OrganizationId, + conn: &DbConn, + ) -> EmptyResult { let collection_assigned_to_groups = CollectionGroup::find_by_collection(collection_uuid, conn).await; for collection_assigned_to_group in collection_assigned_to_groups { - let group_users = GroupUser::find_by_group(&collection_assigned_to_group.groups_uuid, conn).await; + let group_users = GroupUser::find_by_group(&collection_assigned_to_group.groups_uuid, org_uuid, conn).await; for group_user in group_users { group_user.update_user_revision(conn).await; } @@ -494,10 +520,19 @@ impl GroupUser { } } - pub async fn find_by_group(group_uuid: &GroupId, conn: &DbConn) -> Vec { + pub async fn find_by_group(group_uuid: &GroupId, org_uuid: &OrganizationId, conn: &DbConn) -> Vec { db_run! { conn: { groups_users::table + .inner_join(groups::table.on( + groups::uuid.eq(groups_users::groups_uuid) + )) + .inner_join(users_organizations::table.on( + users_organizations::uuid.eq(groups_users::users_organizations_uuid) + .and(users_organizations::org_uuid.eq(groups::organizations_uuid)) + )) .filter(groups_users::groups_uuid.eq(group_uuid)) + .filter(groups::organizations_uuid.eq(org_uuid)) + .select(groups_users::all_columns) .load::(conn) .expect("Error loading group users") }} @@ -522,6 +557,13 @@ impl GroupUser { .inner_join(collections_groups::table.on( collections_groups::groups_uuid.eq(groups_users::groups_uuid) )) + .inner_join(groups::table.on( + groups::uuid.eq(groups_users::groups_uuid) + )) + .inner_join(collections::table.on( + collections::uuid.eq(collections_groups::collections_uuid) + .and(collections::org_uuid.eq(groups::organizations_uuid)) + )) .filter(collections_groups::collections_uuid.eq(collection_uuid)) .filter(groups_users::users_organizations_uuid.eq(member_uuid)) .count() @@ -575,8 +617,8 @@ impl GroupUser { }} } - pub async fn delete_all_by_group(group_uuid: &GroupId, conn: &DbConn) -> EmptyResult { - let group_users = GroupUser::find_by_group(group_uuid, conn).await; + pub async fn delete_all_by_group(group_uuid: &GroupId, org_uuid: &OrganizationId, conn: &DbConn) -> EmptyResult { + let group_users = GroupUser::find_by_group(group_uuid, org_uuid, conn).await; for group_user in group_users { group_user.update_user_revision(conn).await; } diff --git a/src/db/models/org_policy.rs b/src/db/models/org_policy.rs index 96811a2b..7e922f35 100644 --- a/src/db/models/org_policy.rs +++ b/src/db/models/org_policy.rs @@ -332,7 +332,7 @@ impl OrgPolicy { for policy in OrgPolicy::find_confirmed_by_user_and_active_policy(user_uuid, OrgPolicyType::SendOptions, conn).await { - if let Some(user) = Membership::find_by_user_and_org(user_uuid, &policy.org_uuid, conn).await { + if let Some(user) = Membership::find_confirmed_by_user_and_org(user_uuid, &policy.org_uuid, conn).await { if user.atype < MembershipType::Admin { match serde_json::from_str::(&policy.data) { Ok(opts) => { diff --git a/src/db/models/organization.rs b/src/db/models/organization.rs index 0b722ef6..9021c739 100644 --- a/src/db/models/organization.rs +++ b/src/db/models/organization.rs @@ -1073,7 +1073,9 @@ impl Membership { .left_join(collections_groups::table.on( collections_groups::groups_uuid.eq(groups_users::groups_uuid) )) - .left_join(groups::table.on(groups::uuid.eq(groups_users::groups_uuid))) + .left_join(groups::table.on(groups::uuid.eq(groups_users::groups_uuid) + .and(groups::organizations_uuid.eq(users_organizations::org_uuid)) + )) .left_join(ciphers_collections::table.on( ciphers_collections::collection_uuid.eq(collections_groups::collections_uuid).and(ciphers_collections::cipher_uuid.eq(&cipher_uuid)) diff --git a/src/db/models/two_factor.rs b/src/db/models/two_factor.rs index f0a1e663..0dc08e3e 100644 --- a/src/db/models/two_factor.rs +++ b/src/db/models/two_factor.rs @@ -20,7 +20,6 @@ pub struct TwoFactor { pub last_used: i64, } -#[allow(dead_code)] #[derive(num_derive::FromPrimitive)] pub enum TwoFactorType { Authenticator = 0, From f07a91141a8b8f62fd1c5ec3240d51c6ec72af73 Mon Sep 17 00:00:00 2001 From: Mathijs van Veluw Date: Wed, 1 Apr 2026 23:04:10 +0200 Subject: [PATCH 45/89] Fix empty string FolderId (#7048) In newer versions of Bitwarden Clients instead of using `null` the folder_id will be an empty string. This commit adds a special deserialize_with function to keep the same way of working code-wise. Fixes #6962 Signed-off-by: BlackDex --- src/api/core/accounts.rs | 3 ++- src/api/core/ciphers.rs | 5 ++++- src/api/core/folders.rs | 2 ++ src/util.rs | 15 +++++++++++++++ 4 files changed, 23 insertions(+), 2 deletions(-) diff --git a/src/api/core/accounts.rs b/src/api/core/accounts.rs index ba36fc9b..4855e8c2 100644 --- a/src/api/core/accounts.rs +++ b/src/api/core/accounts.rs @@ -22,7 +22,7 @@ use crate::{ DbConn, }, mail, - util::{format_date, NumberOrString}, + util::{deser_opt_nonempty_str, format_date, NumberOrString}, CONFIG, }; @@ -649,6 +649,7 @@ struct UpdateFolderData { // There is a bug in 2024.3.x which adds a `null` item. // To bypass this we allow a Option here, but skip it during the updates // See: https://github.com/bitwarden/clients/issues/8453 + #[serde(default, deserialize_with = "deser_opt_nonempty_str")] id: Option, name: String, } diff --git a/src/api/core/ciphers.rs b/src/api/core/ciphers.rs index 8a8d9838..6d4e1f41 100644 --- a/src/api/core/ciphers.rs +++ b/src/api/core/ciphers.rs @@ -11,7 +11,7 @@ use rocket::{ use serde_json::Value; use crate::auth::ClientVersion; -use crate::util::{save_temp_file, NumberOrString}; +use crate::util::{deser_opt_nonempty_str, save_temp_file, NumberOrString}; use crate::{ api::{self, core::log_event, EmptyResult, JsonResult, Notify, PasswordOrOtpData, UpdateType}, auth::{Headers, OrgIdGuard, OwnerHeaders}, @@ -248,6 +248,7 @@ pub struct CipherData { // Id is optional as it is included only in bulk share pub id: Option, // Folder id is not included in import + #[serde(default, deserialize_with = "deser_opt_nonempty_str")] pub folder_id: Option, // TODO: Some of these might appear all the time, no need for Option #[serde(alias = "organizationID")] @@ -297,6 +298,7 @@ pub struct CipherData { #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] pub struct PartialCipherData { + #[serde(default, deserialize_with = "deser_opt_nonempty_str")] folder_id: Option, favorite: bool, } @@ -1569,6 +1571,7 @@ async fn restore_cipher_selected( #[derive(Deserialize)] #[serde(rename_all = "camelCase")] struct MoveCipherData { + #[serde(default, deserialize_with = "deser_opt_nonempty_str")] folder_id: Option, ids: Vec, } diff --git a/src/api/core/folders.rs b/src/api/core/folders.rs index dc971a13..1b3fd714 100644 --- a/src/api/core/folders.rs +++ b/src/api/core/folders.rs @@ -8,6 +8,7 @@ use crate::{ models::{Folder, FolderId}, DbConn, }, + util::deser_opt_nonempty_str, }; pub fn routes() -> Vec { @@ -38,6 +39,7 @@ async fn get_folder(folder_id: FolderId, headers: Headers, conn: DbConn) -> Json #[serde(rename_all = "camelCase")] pub struct FolderData { pub name: String, + #[serde(default, deserialize_with = "deser_opt_nonempty_str")] pub id: Option, } diff --git a/src/util.rs b/src/util.rs index d336689d..182b7b3b 100644 --- a/src/util.rs +++ b/src/util.rs @@ -634,6 +634,21 @@ fn _process_key(key: &str) -> String { } } +pub fn deser_opt_nonempty_str<'de, D, T>(deserializer: D) -> Result, D::Error> +where + D: Deserializer<'de>, + T: From, +{ + use serde::Deserialize; + Ok(Option::::deserialize(deserializer)?.and_then(|s| { + if s.is_empty() { + None + } else { + Some(T::from(s)) + } + })) +} + #[derive(Clone, Debug, Deserialize)] #[serde(untagged)] pub enum NumberOrString { From 8f0e99b875bfde3bdf0f3eba8aff806da85d25e5 Mon Sep 17 00:00:00 2001 From: Daniel Date: Thu, 2 Apr 2026 00:04:34 +0300 Subject: [PATCH 46/89] Disable deployments for release env (#7033) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit As according to the docs: https://docs.github.com/en/actions/how-tos/deploy/configure-and-manage-deployments/control-deployments#using-environments-without-deployments This is useful when you want to use environments for: Organizing secrets—group related secrets under an environment name without creating deployment records. Access control—restrict which branches can use certain secrets via environment branch policies, without deployment tracking. CI and testing jobs—reference an environment for its configuration without adding noise to the deployment history. --- .github/workflows/release.yml | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 35e995c5..5b72da3a 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -38,7 +38,9 @@ jobs: docker-build: name: Build Vaultwarden containers if: ${{ github.repository == 'dani-garcia/vaultwarden' }} - environment: release + environment: + name: release + deployment: false permissions: packages: write # Needed to upload packages and artifacts contents: read @@ -249,7 +251,9 @@ jobs: name: Merge manifests runs-on: ubuntu-latest needs: docker-build - environment: release + environment: + name: release + deployment: false permissions: packages: write # Needed to upload packages and artifacts attestations: write # Needed to generate an artifact attestation for a build From 2811df29539d686400e83c1af0296aa5fc6e44d9 Mon Sep 17 00:00:00 2001 From: Mathijs van Veluw Date: Sun, 5 Apr 2026 22:35:21 +0200 Subject: [PATCH 47/89] Fix Send icons (#7051) Send uses icons to display if it is protected by password or not. Bitwarden has added a feature to use email with an OTP for newer versions. Vaultwarden does not yet support this, but this commit adds an Enum with all 3 the options. The email option currently needs a feature-flag and newer web-vault/clients. For now, this will at least fix the display of icons. Fixes #6976 Signed-off-by: BlackDex --- src/db/models/send.rs | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/db/models/send.rs b/src/db/models/send.rs index 8180f843..84802c54 100644 --- a/src/db/models/send.rs +++ b/src/db/models/send.rs @@ -46,6 +46,16 @@ pub enum SendType { File = 1, } +enum SendAuthType { + #[allow(dead_code)] + // Send requires email OTP verification + Email = 0, // Not yet supported by Vaultwarden + // Send requires a password + Password = 1, + // Send requires no auth + None = 2, +} + impl Send { pub fn new(atype: i32, name: String, data: String, akey: String, deletion_date: NaiveDateTime) -> Self { let now = Utc::now().naive_utc(); @@ -145,6 +155,7 @@ impl Send { "maxAccessCount": self.max_access_count, "accessCount": self.access_count, "password": self.password_hash.as_deref().map(|h| BASE64URL_NOPAD.encode(h)), + "authType": if self.password_hash.is_some() { SendAuthType::Password as i32 } else { SendAuthType::None as i32 }, "disabled": self.disabled, "hideEmail": self.hide_email, From d29cd29f55c4fddf71aac842d337c0eec58c140c Mon Sep 17 00:00:00 2001 From: Stefan Melmuk <509385+stefan0xC@users.noreply.github.com> Date: Sun, 5 Apr 2026 22:39:33 +0200 Subject: [PATCH 48/89] prevent managers from creating collections (#6890) managers without the access_all flag should not be able to create collections. the manage all collections permission actually consists of three separate custom permissions that have not been implemented yet for more fine-grain access control. --- src/api/core/organizations.rs | 8 ++++---- src/db/models/organization.rs | 3 ++- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/api/core/organizations.rs b/src/api/core/organizations.rs index 36e3e4a0..9a5079cb 100644 --- a/src/api/core/organizations.rs +++ b/src/api/core/organizations.rs @@ -500,6 +500,10 @@ async fn post_organization_collections( let data: FullCollectionData = data.into_inner(); data.validate(&org_id, &conn).await?; + if headers.membership.atype == MembershipType::Manager && !headers.membership.access_all { + err!("You don't have permission to create collections") + } + let collection = Collection::new(org_id.clone(), data.name, data.external_id); collection.save(&conn).await?; @@ -540,10 +544,6 @@ async fn post_organization_collections( .await?; } - if headers.membership.atype == MembershipType::Manager && !headers.membership.access_all { - CollectionUser::save(&headers.membership.user_uuid, &collection.uuid, false, false, false, &conn).await?; - } - Ok(Json(collection.to_json_details(&headers.membership.user_uuid, None, &conn).await)) } diff --git a/src/db/models/organization.rs b/src/db/models/organization.rs index 9021c739..ae19b30c 100644 --- a/src/db/models/organization.rs +++ b/src/db/models/organization.rs @@ -514,7 +514,8 @@ impl Membership { "familySponsorshipValidUntil": null, "familySponsorshipToDelete": null, "accessSecretsManager": false, - "limitCollectionCreation": self.atype < MembershipType::Manager, // If less then a manager return true, to limit collection creations + // limit collection creation to managers with access_all permission to prevent issues + "limitCollectionCreation": self.atype < MembershipType::Manager || !self.access_all, "limitCollectionDeletion": true, "limitItemDeletion": false, "allowAdminAccessToAllCollectionItems": true, From 43df0fb7f4bf18c253b2e114e3b6408d34909c2a Mon Sep 17 00:00:00 2001 From: Aaron Brager <789577+getaaron@users.noreply.github.com> Date: Sun, 5 Apr 2026 15:40:00 -0500 Subject: [PATCH 49/89] Change SQLite backup to use VACUUM INTO query (#6989) * Refactor SQLite backup to use VACUUM INTO query Replaced manual file creation for SQLite backup with a VACUUM INTO query. * Fix VACUUM INTO query error handling --- src/db/mod.rs | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/src/db/mod.rs b/src/db/mod.rs index ae2b1221..d2ed9479 100644 --- a/src/db/mod.rs +++ b/src/db/mod.rs @@ -387,7 +387,6 @@ pub mod models; #[cfg(sqlite)] pub fn backup_sqlite() -> Result { use diesel::Connection; - use std::{fs::File, io::Write}; let db_url = CONFIG.database_url(); if DbConnType::from_url(&CONFIG.database_url()).map(|t| t == DbConnType::Sqlite).unwrap_or(false) { @@ -401,16 +400,13 @@ pub fn backup_sqlite() -> Result { .to_string_lossy() .into_owned(); - match File::create(backup_file.clone()) { - Ok(mut f) => { - let serialized_db = conn.serialize_database_to_buffer(); - f.write_all(serialized_db.as_slice()).expect("Error writing SQLite backup"); - Ok(backup_file) - } - Err(e) => { - err_silent!(format!("Unable to save SQLite backup: {e:?}")) - } - } + diesel::sql_query("VACUUM INTO ?") + .bind::(&backup_file) + .execute(&mut conn) + .map(|_| ()) + .map_res("VACUUM INTO failed")?; + + Ok(backup_file) } else { err_silent!("The database type is not SQLite. Backups only works for SQLite databases") } From fc43737868463daf81f44c9330b5bdd06dbe6cf4 Mon Sep 17 00:00:00 2001 From: Hex <0x484558@pm.me> Date: Sun, 5 Apr 2026 22:41:14 +0200 Subject: [PATCH 50/89] Handle `SIGTERM` and `SIGQUIT` shutdown signals. (#7008) * handle more shutdown signals * disable Rocket's built-in signal handlers --- src/main.rs | 40 +++++++++++++++++++++++++++++++++++----- 1 file changed, 35 insertions(+), 5 deletions(-) diff --git a/src/main.rs b/src/main.rs index 8eef2e8c..b4885831 100644 --- a/src/main.rs +++ b/src/main.rs @@ -558,6 +558,11 @@ async fn launch_rocket(pool: db::DbPool, extra_debug: bool) -> Result<(), Error> let basepath = &CONFIG.domain_path(); let mut config = rocket::Config::from(rocket::Config::figment()); + + // We install our own signal handlers below; disable Rocket's built-in handlers + config.shutdown.ctrlc = false; + config.shutdown.signals.clear(); + config.temp_dir = canonicalize(CONFIG.tmp_folder()).unwrap().into(); config.cli_colors = false; // Make sure Rocket does not color any values for logging. config.limits = Limits::new() @@ -589,11 +594,7 @@ async fn launch_rocket(pool: db::DbPool, extra_debug: bool) -> Result<(), Error> CONFIG.set_rocket_shutdown_handle(instance.shutdown()); - tokio::spawn(async move { - tokio::signal::ctrl_c().await.expect("Error setting Ctrl-C handler"); - info!("Exiting Vaultwarden!"); - CONFIG.shutdown(); - }); + spawn_shutdown_signal_handler(); #[cfg(all(unix, sqlite))] { @@ -621,6 +622,35 @@ async fn launch_rocket(pool: db::DbPool, extra_debug: bool) -> Result<(), Error> Ok(()) } +#[cfg(unix)] +fn spawn_shutdown_signal_handler() { + tokio::spawn(async move { + use tokio::signal::unix::signal; + + let mut sigint = signal(SignalKind::interrupt()).expect("Error setting SIGINT handler"); + let mut sigterm = signal(SignalKind::terminate()).expect("Error setting SIGTERM handler"); + let mut sigquit = signal(SignalKind::quit()).expect("Error setting SIGQUIT handler"); + + let signal_name = tokio::select! { + _ = sigint.recv() => "SIGINT", + _ = sigterm.recv() => "SIGTERM", + _ = sigquit.recv() => "SIGQUIT", + }; + + info!("Received {signal_name}, initiating graceful shutdown"); + CONFIG.shutdown(); + }); +} + +#[cfg(not(unix))] +fn spawn_shutdown_signal_handler() { + tokio::spawn(async move { + tokio::signal::ctrl_c().await.expect("Error setting Ctrl-C handler"); + info!("Received Ctrl-C, initiating graceful shutdown"); + CONFIG.shutdown(); + }); +} + fn schedule_jobs(pool: db::DbPool) { if CONFIG.job_poll_interval_ms() == 0 { info!("Job scheduler disabled."); From d4f67429d6e6d4b83d49491ac32b8abf3ce43bd9 Mon Sep 17 00:00:00 2001 From: Hex <0x484558@pm.me> Date: Sun, 5 Apr 2026 22:43:06 +0200 Subject: [PATCH 51/89] Do not display unavailable 2FA options (#7013) * do not display unavailable 2FA options * use existing function to check webauthn support * clarity in 2fa skip code --- src/api/core/two_factor/mod.rs | 49 +++++++++++++++++++++++++++-- src/api/core/two_factor/webauthn.rs | 4 +-- src/api/identity.rs | 25 +++++++++++++-- 3 files changed, 71 insertions(+), 7 deletions(-) diff --git a/src/api/core/two_factor/mod.rs b/src/api/core/two_factor/mod.rs index 34fbfaa9..3a503a23 100644 --- a/src/api/core/two_factor/mod.rs +++ b/src/api/core/two_factor/mod.rs @@ -1,7 +1,9 @@ use chrono::{TimeDelta, Utc}; use data_encoding::BASE32; +use num_traits::FromPrimitive; use rocket::serde::json::Json; use rocket::Route; +use serde::Deserialize; use serde_json::Value; use crate::{ @@ -14,7 +16,7 @@ use crate::{ db::{ models::{ DeviceType, EventType, Membership, MembershipType, OrgPolicyType, Organization, OrganizationId, TwoFactor, - TwoFactorIncomplete, User, UserId, + TwoFactorIncomplete, TwoFactorType, User, UserId, }, DbConn, DbPool, }, @@ -31,6 +33,43 @@ pub mod protected_actions; pub mod webauthn; pub mod yubikey; +fn has_global_duo_credentials() -> bool { + CONFIG._enable_duo() && CONFIG.duo_host().is_some() && CONFIG.duo_ikey().is_some() && CONFIG.duo_skey().is_some() +} + +pub fn is_twofactor_provider_usable(provider_type: TwoFactorType, provider_data: Option<&str>) -> bool { + #[derive(Deserialize)] + struct DuoProviderData { + host: String, + ik: String, + sk: String, + } + + match provider_type { + TwoFactorType::Authenticator => true, + TwoFactorType::Email => CONFIG._enable_email_2fa(), + TwoFactorType::Duo | TwoFactorType::OrganizationDuo => { + provider_data + .and_then(|raw| serde_json::from_str::(raw).ok()) + .is_some_and(|duo| !duo.host.is_empty() && !duo.ik.is_empty() && !duo.sk.is_empty()) + || has_global_duo_credentials() + } + TwoFactorType::YubiKey => { + CONFIG._enable_yubico() && CONFIG.yubico_client_id().is_some() && CONFIG.yubico_secret_key().is_some() + } + TwoFactorType::Webauthn => CONFIG.is_webauthn_2fa_supported(), + TwoFactorType::Remember => !CONFIG.disable_2fa_remember(), + TwoFactorType::RecoveryCode => true, + TwoFactorType::U2f + | TwoFactorType::U2fRegisterChallenge + | TwoFactorType::U2fLoginChallenge + | TwoFactorType::EmailVerificationChallenge + | TwoFactorType::WebauthnRegisterChallenge + | TwoFactorType::WebauthnLoginChallenge + | TwoFactorType::ProtectedActions => false, + } +} + pub fn routes() -> Vec { let mut routes = routes![ get_twofactor, @@ -53,7 +92,13 @@ pub fn routes() -> Vec { #[get("/two-factor")] async fn get_twofactor(headers: Headers, conn: DbConn) -> Json { let twofactors = TwoFactor::find_by_user(&headers.user.uuid, &conn).await; - let twofactors_json: Vec = twofactors.iter().map(TwoFactor::to_json_provider).collect(); + let twofactors_json: Vec = twofactors + .iter() + .filter_map(|tf| { + let provider_type = TwoFactorType::from_i32(tf.atype)?; + is_twofactor_provider_usable(provider_type, Some(&tf.data)).then(|| TwoFactor::to_json_provider(tf)) + }) + .collect(); Json(json!({ "data": twofactors_json, diff --git a/src/api/core/two_factor/webauthn.rs b/src/api/core/two_factor/webauthn.rs index 6ae12752..0ec0e30e 100644 --- a/src/api/core/two_factor/webauthn.rs +++ b/src/api/core/two_factor/webauthn.rs @@ -108,8 +108,8 @@ impl WebauthnRegistration { #[post("/two-factor/get-webauthn", data = "")] async fn get_webauthn(data: Json, headers: Headers, conn: DbConn) -> JsonResult { - if !CONFIG.domain_set() { - err!("`DOMAIN` environment variable is not set. Webauthn disabled") + if !CONFIG.is_webauthn_2fa_supported() { + err!("Configured `DOMAIN` is not compatible with Webauthn") } let data: PasswordOrOtpData = data.into_inner(); diff --git a/src/api/identity.rs b/src/api/identity.rs index fcd8c388..b9a753b9 100644 --- a/src/api/identity.rs +++ b/src/api/identity.rs @@ -14,7 +14,10 @@ use crate::{ core::{ accounts::{PreloginData, RegisterData, _prelogin, _register, kdf_upgrade}, log_user_event, - two_factor::{authenticator, duo, duo_oidc, email, enforce_2fa_policy, webauthn, yubikey}, + two_factor::{ + authenticator, duo, duo_oidc, email, enforce_2fa_policy, is_twofactor_provider_usable, webauthn, + yubikey, + }, }, master_password_policy, push::register_push_device, @@ -739,8 +742,24 @@ async fn twofactor_auth( TwoFactorIncomplete::mark_incomplete(&user.uuid, &device.uuid, &device.name, device.atype, ip, conn).await?; - let twofactor_ids: Vec<_> = twofactors.iter().map(|tf| tf.atype).collect(); + let twofactor_ids: Vec<_> = twofactors + .iter() + .filter_map(|tf| { + let provider_type = TwoFactorType::from_i32(tf.atype)?; + (tf.enabled && is_twofactor_provider_usable(provider_type, Some(&tf.data))).then_some(tf.atype) + }) + .collect(); + if twofactor_ids.is_empty() { + err!("No enabled and usable two factor providers are available for this account") + } + let selected_id = data.two_factor_provider.unwrap_or(twofactor_ids[0]); // If we aren't given a two factor provider, assume the first one + if !twofactor_ids.contains(&selected_id) { + err_json!( + _json_err_twofactor(&twofactor_ids, &user.uuid, data, client_version, conn).await?, + "Invalid two factor provider" + ) + } let twofactor_code = match data.two_factor_token { Some(ref code) => code, @@ -871,7 +890,7 @@ async fn _json_err_twofactor( match TwoFactorType::from_i32(*provider) { Some(TwoFactorType::Authenticator) => { /* Nothing to do for TOTP */ } - Some(TwoFactorType::Webauthn) if CONFIG.domain_set() => { + Some(TwoFactorType::Webauthn) if CONFIG.is_webauthn_2fa_supported() => { let request = webauthn::generate_webauthn_login(user_id, conn).await?; result["TwoFactorProviders2"][provider.to_string()] = request.0; } From 3f28b583dbad90ba5851760c1722044ac4b66a3c Mon Sep 17 00:00:00 2001 From: qaz741wsd856 Date: Mon, 6 Apr 2026 04:43:58 +0800 Subject: [PATCH 52/89] Fix logout push identifiers and send logout before clearing devices (#7047) * Fix logout push identifiers and send logout before clearing devices * Refactor logout function parameters * Fix parameters in logout notification functions --- src/api/admin.rs | 3 ++- src/api/core/accounts.rs | 9 +++++---- src/api/notifications.rs | 7 ++++--- src/api/push.rs | 10 ++++------ 4 files changed, 15 insertions(+), 14 deletions(-) diff --git a/src/api/admin.rs b/src/api/admin.rs index a4f53e0e..1546676f 100644 --- a/src/api/admin.rs +++ b/src/api/admin.rs @@ -480,7 +480,6 @@ async fn deauth_user(user_id: UserId, _token: AdminToken, conn: DbConn, nt: Noti #[post("/users//disable", format = "application/json")] async fn disable_user(user_id: UserId, _token: AdminToken, conn: DbConn, nt: Notify<'_>) -> EmptyResult { let mut user = get_user_or_404(&user_id, &conn).await?; - Device::delete_all_by_user(&user.uuid, &conn).await?; user.reset_security_stamp(&conn).await?; user.enabled = false; @@ -488,6 +487,8 @@ async fn disable_user(user_id: UserId, _token: AdminToken, conn: DbConn, nt: Not nt.send_logout(&user, None, &conn).await; + Device::delete_all_by_user(&user.uuid, &conn).await?; + save_result } diff --git a/src/api/core/accounts.rs b/src/api/core/accounts.rs index 4855e8c2..8841c184 100644 --- a/src/api/core/accounts.rs +++ b/src/api/core/accounts.rs @@ -540,7 +540,7 @@ async fn post_password(data: Json, headers: Headers, conn: DbCon // Prevent logging out the client where the user requested this endpoint from. // If you do logout the user it will causes issues at the client side. // Adding the device uuid will prevent this. - nt.send_logout(&user, Some(headers.device.uuid.clone()), &conn).await; + nt.send_logout(&user, Some(&headers.device), &conn).await; save_result } @@ -638,7 +638,7 @@ async fn post_kdf(data: Json, headers: Headers, conn: DbConn, nt: .await?; let save_result = user.save(&conn).await; - nt.send_logout(&user, Some(headers.device.uuid.clone()), &conn).await; + nt.send_logout(&user, Some(&headers.device), &conn).await; save_result } @@ -912,7 +912,7 @@ async fn post_rotatekey(data: Json, headers: Headers, conn: DbConn, nt: // Prevent logging out the client where the user requested this endpoint from. // If you do logout the user it will causes issues at the client side. // Adding the device uuid will prevent this. - nt.send_logout(&user, Some(headers.device.uuid.clone()), &conn).await; + nt.send_logout(&user, Some(&headers.device), &conn).await; save_result } @@ -924,12 +924,13 @@ async fn post_sstamp(data: Json, headers: Headers, conn: DbCo data.validate(&user, true, &conn).await?; - Device::delete_all_by_user(&user.uuid, &conn).await?; user.reset_security_stamp(&conn).await?; let save_result = user.save(&conn).await; nt.send_logout(&user, None, &conn).await; + Device::delete_all_by_user(&user.uuid, &conn).await?; + save_result } diff --git a/src/api/notifications.rs b/src/api/notifications.rs index 42157ac3..492fdb19 100644 --- a/src/api/notifications.rs +++ b/src/api/notifications.rs @@ -358,15 +358,16 @@ impl WebSocketUsers { } } - pub async fn send_logout(&self, user: &User, acting_device_id: Option, conn: &DbConn) { + pub async fn send_logout(&self, user: &User, acting_device: Option<&Device>, conn: &DbConn) { // Skip any processing if both WebSockets and Push are not active if *NOTIFICATIONS_DISABLED { return; } + let acting_device_id = acting_device.map(|d| d.uuid.clone()); let data = create_update( vec![("UserId".into(), user.uuid.to_string().into()), ("Date".into(), serialize_date(user.updated_at))], UpdateType::LogOut, - acting_device_id.clone(), + acting_device_id, ); if CONFIG.enable_websocket() { @@ -374,7 +375,7 @@ impl WebSocketUsers { } if CONFIG.push_enabled() { - push_logout(user, acting_device_id.clone(), conn).await; + push_logout(user, acting_device, conn).await; } } diff --git a/src/api/push.rs b/src/api/push.rs index a7e88455..5000869d 100644 --- a/src/api/push.rs +++ b/src/api/push.rs @@ -13,7 +13,7 @@ use tokio::sync::RwLock; use crate::{ api::{ApiResult, EmptyResult, UpdateType}, db::{ - models::{AuthRequestId, Cipher, Device, DeviceId, Folder, PushId, Send, User, UserId}, + models::{AuthRequestId, Cipher, Device, Folder, PushId, Send, User, UserId}, DbConn, }, http_client::make_http_request, @@ -188,15 +188,13 @@ pub async fn push_cipher_update(ut: UpdateType, cipher: &Cipher, device: &Device } } -pub async fn push_logout(user: &User, acting_device_id: Option, conn: &DbConn) { - let acting_device_id: Value = acting_device_id.map(|v| v.to_string().into()).unwrap_or_else(|| Value::Null); - +pub async fn push_logout(user: &User, acting_device: Option<&Device>, conn: &DbConn) { if Device::check_user_has_push_device(&user.uuid, conn).await { tokio::task::spawn(send_to_push_relay(json!({ "userId": user.uuid, "organizationId": (), - "deviceId": acting_device_id, - "identifier": acting_device_id, + "deviceId": acting_device.and_then(|d| d.push_uuid.as_ref()), + "identifier": acting_device.map(|d| &d.uuid), "type": UpdateType::LogOut as i32, "payload": { "userId": user.uuid, From a6b43651ca2896fad9ecf8583f0a20d8101f2443 Mon Sep 17 00:00:00 2001 From: idontneedonetho Date: Wed, 8 Apr 2026 09:35:18 -0400 Subject: [PATCH 53/89] Fix windows build issues (#7065) Need to set signals to UNIX only so we can build on windows. --- src/main.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main.rs b/src/main.rs index b4885831..60c5a593 100644 --- a/src/main.rs +++ b/src/main.rs @@ -561,6 +561,7 @@ async fn launch_rocket(pool: db::DbPool, extra_debug: bool) -> Result<(), Error> // We install our own signal handlers below; disable Rocket's built-in handlers config.shutdown.ctrlc = false; + #[cfg(unix)] config.shutdown.signals.clear(); config.temp_dir = canonicalize(CONFIG.tmp_folder()).unwrap().into(); From 39954af96aff8eb70eb4d26969abc1458bc4804f Mon Sep 17 00:00:00 2001 From: Mathijs van Veluw Date: Sat, 11 Apr 2026 20:27:07 +0200 Subject: [PATCH 54/89] Crate and GHA updates (#7081) Signed-off-by: BlackDex --- .github/workflows/release.yml | 18 +-- .github/workflows/typos.yml | 2 +- .pre-commit-config.yaml | 2 +- Cargo.lock | 236 +++++++++++++++++----------------- Cargo.toml | 8 +- src/api/core/organizations.rs | 2 +- 6 files changed, 137 insertions(+), 131 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 5b72da3a..8db56c38 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -106,7 +106,7 @@ jobs: # Login to Docker Hub - name: Login to Docker Hub - uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0 + uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} @@ -121,7 +121,7 @@ jobs: # Login to GitHub Container Registry - name: Login to GitHub Container Registry - uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0 + uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0 with: registry: ghcr.io username: ${{ github.repository_owner }} @@ -137,7 +137,7 @@ jobs: # Login to Quay.io - name: Login to Quay.io - uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0 + uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0 with: registry: quay.io username: ${{ secrets.QUAY_USERNAME }} @@ -185,7 +185,7 @@ jobs: - name: Bake ${{ matrix.base_image }} containers id: bake_vw - uses: docker/bake-action@82490499d2e5613fcead7e128237ef0b0ea210f7 # v7.0.0 + uses: docker/bake-action@a66e1c87e2eca0503c343edf1d208c716d54b8a8 # v7.1.0 env: BASE_TAGS: "${{ steps.determine-version.outputs.BASE_TAGS }}" SOURCE_COMMIT: "${{ env.SOURCE_COMMIT }}" @@ -222,7 +222,7 @@ jobs: touch "${RUNNER_TEMP}/digests/${digest#sha256:}" - name: Upload digest - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: digests-${{ env.NORMALIZED_ARCH }}-${{ matrix.base_image }} path: ${{ runner.temp }}/digests/* @@ -242,7 +242,7 @@ jobs: subject-path: vaultwarden-${{ env.NORMALIZED_ARCH }} - name: Upload binaries as artifacts - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: vaultwarden-${{ env.SOURCE_VERSION }}-linux-${{ env.NORMALIZED_ARCH }}-${{ matrix.base_image }} path: vaultwarden-${{ env.NORMALIZED_ARCH }} @@ -272,7 +272,7 @@ jobs: # Login to Docker Hub - name: Login to Docker Hub - uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0 + uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} @@ -287,7 +287,7 @@ jobs: # Login to GitHub Container Registry - name: Login to GitHub Container Registry - uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0 + uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0 with: registry: ghcr.io username: ${{ github.repository_owner }} @@ -303,7 +303,7 @@ jobs: # Login to Quay.io - name: Login to Quay.io - uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0 + uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0 with: registry: quay.io username: ${{ secrets.QUAY_USERNAME }} diff --git a/.github/workflows/typos.yml b/.github/workflows/typos.yml index d7b645e0..f68ef29d 100644 --- a/.github/workflows/typos.yml +++ b/.github/workflows/typos.yml @@ -23,4 +23,4 @@ jobs: # When this version is updated, do not forget to update this in `.pre-commit-config.yaml` too - name: Spell Check Repo - uses: crate-ci/typos@631208b7aac2daa8b707f55e7331f9112b0e062d # v1.44.0 + uses: crate-ci/typos@02ea592e44b3a53c302f697cddca7641cd051c3d # v1.45.0 diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 3a151637..0b6ad451 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -53,6 +53,6 @@ repos: - "cd docker && make" # When this version is updated, do not forget to update this in `.github/workflows/typos.yaml` too - repo: https://github.com/crate-ci/typos - rev: 631208b7aac2daa8b707f55e7331f9112b0e062d # v1.44.0 + rev: 02ea592e44b3a53c302f697cddca7641cd051c3d # v1.45.0 hooks: - id: typos diff --git a/Cargo.lock b/Cargo.lock index 290627c0..3d4d5921 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -240,9 +240,9 @@ dependencies = [ [[package]] name = "async-signal" -version = "0.2.13" +version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43c070bbf59cd3570b6b2dd54cd772527c7c3620fce8be898406dd3ed6adc64c" +checksum = "52b5aaafa020cf5053a01f2a60e8ff5dccf550f0f77ec54a4e47285ac2bab485" dependencies = [ "async-io", "async-lock", @@ -882,9 +882,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.58" +version = "1.2.60" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1e928d4b69e3077709075a938a05ffbedfa53a84c8f766efbf8220bb1ff60e1" +checksum = "43c5703da9466b66a946814e1adf53ea2c90f10063b86290cc9eb67ce3478a20" dependencies = [ "find-msvc-tools", "jobserver", @@ -1751,9 +1751,9 @@ dependencies = [ [[package]] name = "fastrand" -version = "2.3.0" +version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" +checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" [[package]] name = "fern" @@ -2062,7 +2062,7 @@ dependencies = [ "parking_lot", "portable-atomic", "quanta", - "rand 0.9.2", + "rand 0.9.3", "smallvec", "spinning_top", "web-time", @@ -2075,7 +2075,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2d9e3df7f0222ce5184154973d247c591d9aadc28ce7a73c6cd31100c9facff6" dependencies = [ "codemap", - "indexmap 2.13.0", + "indexmap 2.14.0", "lasso", "once_cell", "phf 0.11.3", @@ -2104,7 +2104,7 @@ dependencies = [ "futures-core", "futures-sink", "http 1.4.0", - "indexmap 2.13.0", + "indexmap 2.14.0", "slab", "tokio", "tokio-util", @@ -2175,6 +2175,12 @@ dependencies = [ "foldhash 0.2.0", ] +[[package]] +name = "hashbrown" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51" + [[package]] name = "heck" version = "0.5.0" @@ -2209,7 +2215,7 @@ dependencies = [ "idna", "ipnet", "once_cell", - "rand 0.9.2", + "rand 0.9.3", "ring", "thiserror 2.0.18", "tinyvec", @@ -2231,7 +2237,7 @@ dependencies = [ "moka", "once_cell", "parking_lot", - "rand 0.9.2", + "rand 0.9.3", "resolv-conf", "smallvec", "thiserror 2.0.18", @@ -2378,9 +2384,9 @@ dependencies = [ [[package]] name = "hyper" -version = "1.8.1" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11" +checksum = "6299f016b246a94207e63da54dbe807655bf9e00044f73ded42c3ac5305fbcca" dependencies = [ "atomic-waker", "bytes", @@ -2392,7 +2398,6 @@ dependencies = [ "httparse", "itoa", "pin-project-lite", - "pin-utils", "smallvec", "tokio", "want", @@ -2405,7 +2410,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" dependencies = [ "http 1.4.0", - "hyper 1.8.1", + "hyper 1.9.0", "hyper-util", "rustls 0.23.37", "rustls-native-certs", @@ -2428,7 +2433,7 @@ dependencies = [ "futures-util", "http 1.4.0", "http-body 1.0.1", - "hyper 1.8.1", + "hyper 1.9.0", "ipnet", "libc", "percent-encoding", @@ -2467,12 +2472,13 @@ dependencies = [ [[package]] name = "icu_collections" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" +checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c" dependencies = [ "displaydoc", "potential_utf", + "utf8_iter", "yoke", "zerofrom", "zerovec", @@ -2480,9 +2486,9 @@ dependencies = [ [[package]] name = "icu_locale_core" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" +checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29" dependencies = [ "displaydoc", "litemap", @@ -2493,9 +2499,9 @@ dependencies = [ [[package]] name = "icu_normalizer" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" +checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4" dependencies = [ "icu_collections", "icu_normalizer_data", @@ -2507,15 +2513,15 @@ dependencies = [ [[package]] name = "icu_normalizer_data" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" +checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38" [[package]] name = "icu_properties" -version = "2.1.2" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec" +checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de" dependencies = [ "icu_collections", "icu_locale_core", @@ -2527,15 +2533,15 @@ dependencies = [ [[package]] name = "icu_properties_data" -version = "2.1.2" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af" +checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14" [[package]] name = "icu_provider" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" +checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421" dependencies = [ "displaydoc", "icu_locale_core", @@ -2592,12 +2598,12 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.13.0" +version = "2.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" dependencies = [ "equivalent", - "hashbrown 0.16.1", + "hashbrown 0.17.0", "serde", "serde_core", ] @@ -2639,9 +2645,9 @@ checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" [[package]] name = "iri-string" -version = "0.7.11" +version = "0.7.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8e7418f59cc01c88316161279a7f665217ae316b388e58a0d10e29f54f1e5eb" +checksum = "25e659a4bb38e810ebc252e53b5814ff908a8c58c2a9ce2fae1bbec24cbf4e20" dependencies = [ "memchr", "serde", @@ -2743,9 +2749,9 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.92" +version = "0.3.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc4c90f45aa2e6eacbe8645f77fdea542ac97a494bcd117a67df9ff4d611f995" +checksum = "2964e92d1d9dc3364cae4d718d93f227e3abb088e747d92e0395bfdedf1c12ca" dependencies = [ "cfg-if", "futures-util", @@ -2826,9 +2832,9 @@ checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" [[package]] name = "lettre" -version = "0.11.20" +version = "0.11.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "471816f3e24b85e820dee02cde962379ea1a669e5242f19c61bcbcffedf4c4fb" +checksum = "dabda5859ee7c06b995b9d1165aa52c39110e079ef609db97178d86aeb051fa7" dependencies = [ "async-std", "async-trait", @@ -2857,9 +2863,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.183" +version = "0.2.184" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d" +checksum = "48f5d2a454e16a5ea0f4ced81bd44e4cfc7bd3a507b61887c99fd3538b28e4af" [[package]] name = "libm" @@ -2896,9 +2902,9 @@ checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" [[package]] name = "litemap" -version = "0.8.1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" +checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" [[package]] name = "litrs" @@ -3082,9 +3088,9 @@ dependencies = [ [[package]] name = "mysqlclient-sys" -version = "0.5.0" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92ed7312f0cfc4032aea6f8ea2abb4d288e4413e33bf0c80ad30eef8aa8fb9d8" +checksum = "822bc60a9459abe384dd85d81ac59167ed2da99fba6eb810000e6ab64d9404b2" dependencies = [ "pkg-config", "semver", @@ -3365,9 +3371,9 @@ checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" [[package]] name = "openssl-src" -version = "300.5.5+3.5.5" +version = "300.6.0+3.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f1787d533e03597a7934fd0a765f0d28e94ecc5fb7789f8053b1e699a56f709" +checksum = "a8e8cbfd3a4a8c8f089147fd7aaa33cf8c7450c4d09f8f80698a0cf093abeff4" dependencies = [ "cc", ] @@ -3751,9 +3757,9 @@ dependencies = [ [[package]] name = "potential_utf" -version = "0.1.4" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" +checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" dependencies = [ "zerovec", ] @@ -3911,7 +3917,7 @@ dependencies = [ "bytes", "getrandom 0.3.4", "lru-slab", - "rand 0.9.2", + "rand 0.9.3", "ring", "rustc-hash", "rustls 0.23.37", @@ -3988,9 +3994,9 @@ dependencies = [ [[package]] name = "rand" -version = "0.9.2" +version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" +checksum = "7ec095654a25171c2124e9e3393a930bddbffdc939556c914957a4c3e0a87166" dependencies = [ "rand_chacha 0.9.0", "rand_core 0.9.5", @@ -3998,9 +4004,9 @@ dependencies = [ [[package]] name = "rand" -version = "0.10.0" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc266eb313df6c5c09c1c7b1fbe2510961e5bcd3add930c1e31f7ed9da0feff8" +checksum = "d2e8e8bcc7961af1fdac401278c6a831614941f6164ee3bf4ce61b7edb162207" dependencies = [ "chacha20", "getrandom 0.4.2", @@ -4186,7 +4192,7 @@ dependencies = [ "http 1.4.0", "http-body 1.0.1", "http-body-util", - "hyper 1.8.1", + "hyper 1.9.0", "hyper-rustls", "hyper-util", "js-sys", @@ -4278,7 +4284,7 @@ dependencies = [ "either", "figment", "futures", - "indexmap 2.13.0", + "indexmap 2.14.0", "log", "memchr", "multer", @@ -4310,7 +4316,7 @@ checksum = "575d32d7ec1a9770108c879fc7c47815a80073f96ca07ff9525a94fcede1dd46" dependencies = [ "devise", "glob", - "indexmap 2.13.0", + "indexmap 2.14.0", "proc-macro2", "quote", "rocket_http", @@ -4330,7 +4336,7 @@ dependencies = [ "futures", "http 0.2.12", "hyper 0.14.32", - "indexmap 2.13.0", + "indexmap 2.14.0", "log", "memchr", "pear", @@ -4480,7 +4486,7 @@ dependencies = [ "once_cell", "ring", "rustls-pki-types", - "rustls-webpki 0.103.10", + "rustls-webpki 0.103.11", "subtle", "zeroize", ] @@ -4528,9 +4534,9 @@ dependencies = [ [[package]] name = "rustls-webpki" -version = "0.103.10" +version = "0.103.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df33b2b81ac578cabaf06b89b0631153a3f416b0a886e8a7a1707fb51abbd1ef" +checksum = "20a6af516fea4b20eccceaf166e8aa666ac996208e8a644ce3ef5aa783bc7cd4" dependencies = [ "ring", "rustls-pki-types", @@ -4681,9 +4687,9 @@ dependencies = [ [[package]] name = "semver" -version = "1.0.27" +version = "1.0.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" [[package]] name = "serde" @@ -4779,9 +4785,9 @@ dependencies = [ [[package]] name = "serde_spanned" -version = "1.1.0" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "876ac351060d4f882bb1032b6369eb0aef79ad9df1ea8bc404874d8cc3d0cd98" +checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26" dependencies = [ "serde_core", ] @@ -4808,7 +4814,7 @@ dependencies = [ "chrono", "hex", "indexmap 1.9.3", - "indexmap 2.13.0", + "indexmap 2.14.0", "schemars 0.9.0", "schemars 1.2.1", "serde_core", @@ -5223,9 +5229,9 @@ dependencies = [ [[package]] name = "tinystr" -version = "0.8.2" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" +checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" dependencies = [ "displaydoc", "zerovec", @@ -5248,9 +5254,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.50.0" +version = "1.51.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "27ad5e34374e03cfffefc301becb44e9dc3c17584f414349ebe29ed26661822d" +checksum = "f66bf9585cda4b724d3e78ab34b73fb2bbaba9011b9bfdf69dc836382ea13b8c" dependencies = [ "bytes", "libc", @@ -5265,9 +5271,9 @@ dependencies = [ [[package]] name = "tokio-macros" -version = "2.6.1" +version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c55a2eff8b69ce66c84f85e1da1c233edc36ceb85a2058d11b0d6a3c7e7569c" +checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496" dependencies = [ "proc-macro2", "quote", @@ -5350,7 +5356,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf92845e79fc2e2def6a5d828f0801e29a2f8acc037becc5ab08595c7d5e9863" dependencies = [ "serde_core", - "serde_spanned 1.1.0", + "serde_spanned 1.1.1", "toml_datetime 0.7.5+spec-1.1.0", "toml_parser", "winnow 0.7.15", @@ -5380,7 +5386,7 @@ version = "0.22.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" dependencies = [ - "indexmap 2.13.0", + "indexmap 2.14.0", "serde", "serde_spanned 0.6.9", "toml_datetime 0.6.11", @@ -5390,11 +5396,11 @@ dependencies = [ [[package]] name = "toml_parser" -version = "1.1.0+spec-1.1.0" +version = "1.1.2+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2334f11ee363607eb04df9b8fc8a13ca1715a72ba8662a26ac285c98aabb4011" +checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" dependencies = [ - "winnow 1.0.0", + "winnow 1.0.1", ] [[package]] @@ -5711,7 +5717,7 @@ dependencies = [ "pastey 0.2.1", "percent-encoding", "pico-args", - "rand 0.10.0", + "rand 0.10.1", "regex", "reqsign", "reqwest", @@ -5803,9 +5809,9 @@ dependencies = [ [[package]] name = "wasm-bindgen" -version = "0.2.115" +version = "0.2.118" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6523d69017b7633e396a89c5efab138161ed5aafcbc8d3e5c5a42ae38f50495a" +checksum = "0bf938a0bacb0469e83c1e148908bd7d5a6010354cf4fb73279b7447422e3a89" dependencies = [ "cfg-if", "once_cell", @@ -5816,9 +5822,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.65" +version = "0.4.68" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d1faf851e778dfa54db7cd438b70758eba9755cb47403f3496edd7c8fc212f0" +checksum = "f371d383f2fb139252e0bfac3b81b265689bf45b6874af544ffa4c975ac1ebf8" dependencies = [ "js-sys", "wasm-bindgen", @@ -5826,9 +5832,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.115" +version = "0.2.118" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e3a6c758eb2f701ed3d052ff5737f5bfe6614326ea7f3bbac7156192dc32e67" +checksum = "eeff24f84126c0ec2db7a449f0c2ec963c6a49efe0698c4242929da037ca28ed" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -5836,9 +5842,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.115" +version = "0.2.118" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "921de2737904886b52bcbb237301552d05969a6f9c40d261eb0533c8b055fedf" +checksum = "9d08065faf983b2b80a79fd87d8254c409281cf7de75fc4b773019824196c904" dependencies = [ "bumpalo", "proc-macro2", @@ -5849,9 +5855,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.115" +version = "0.2.118" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a93e946af942b58934c604527337bad9ae33ba1d5c6900bbb41c2c07c2364a93" +checksum = "5fd04d9e306f1907bd13c6361b5c6bfc7b3b3c095ed3f8a9246390f8dbdee129" dependencies = [ "unicode-ident", ] @@ -5873,7 +5879,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" dependencies = [ "anyhow", - "indexmap 2.13.0", + "indexmap 2.14.0", "wasm-encoder", "wasmparser", ] @@ -5899,15 +5905,15 @@ checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" dependencies = [ "bitflags", "hashbrown 0.15.5", - "indexmap 2.13.0", + "indexmap 2.14.0", "semver", ] [[package]] name = "web-sys" -version = "0.3.92" +version = "0.3.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84cde8507f4d7cfcb1185b8cb5890c494ffea65edbe1ba82cfd63661c805ed94" +checksum = "4f2dfbb17949fa2088e5d39408c48368947b86f7834484e87b73de55bc14d97d" dependencies = [ "js-sys", "wasm-bindgen", @@ -5964,7 +5970,7 @@ dependencies = [ "nom 7.1.3", "openssl", "openssl-sys", - "rand 0.9.2", + "rand 0.9.3", "rand_chacha 0.9.0", "serde", "serde_cbor_2", @@ -6367,9 +6373,9 @@ dependencies = [ [[package]] name = "winnow" -version = "1.0.0" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a90e88e4667264a994d34e6d1ab2d26d398dcdca8b7f52bec8668957517fc7d8" +checksum = "09dac053f1cd375980747450bfc7250c264eaae0583872e845c0c7cd578872b5" [[package]] name = "wit-bindgen" @@ -6399,7 +6405,7 @@ checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" dependencies = [ "anyhow", "heck", - "indexmap 2.13.0", + "indexmap 2.14.0", "prettyplease", "syn", "wasm-metadata", @@ -6430,7 +6436,7 @@ checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" dependencies = [ "anyhow", "bitflags", - "indexmap 2.13.0", + "indexmap 2.14.0", "log", "serde", "serde_derive", @@ -6449,7 +6455,7 @@ checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" dependencies = [ "anyhow", "id-arena", - "indexmap 2.13.0", + "indexmap 2.14.0", "log", "semver", "serde", @@ -6461,9 +6467,9 @@ dependencies = [ [[package]] name = "writeable" -version = "0.6.2" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" +checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" [[package]] name = "x509-parser" @@ -6505,9 +6511,9 @@ dependencies = [ [[package]] name = "yoke" -version = "0.8.1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" +checksum = "abe8c5fda708d9ca3df187cae8bfb9ceda00dd96231bed36e445a1a48e66f9ca" dependencies = [ "stable_deref_trait", "yoke-derive", @@ -6516,9 +6522,9 @@ dependencies = [ [[package]] name = "yoke-derive" -version = "0.8.1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" +checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" dependencies = [ "proc-macro2", "quote", @@ -6536,7 +6542,7 @@ dependencies = [ "form_urlencoded", "futures", "hmac", - "rand 0.9.2", + "rand 0.9.3", "reqwest", "sha1", "threadpool", @@ -6564,18 +6570,18 @@ dependencies = [ [[package]] name = "zerofrom" -version = "0.1.6" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" +checksum = "69faa1f2a1ea75661980b013019ed6687ed0e83d069bc1114e2cc74c6c04c4df" dependencies = [ "zerofrom-derive", ] [[package]] name = "zerofrom-derive" -version = "0.1.6" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" +checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" dependencies = [ "proc-macro2", "quote", @@ -6591,9 +6597,9 @@ checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" [[package]] name = "zerotrie" -version = "0.2.3" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" +checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf" dependencies = [ "displaydoc", "yoke", @@ -6602,9 +6608,9 @@ dependencies = [ [[package]] name = "zerovec" -version = "0.11.5" +version = "0.11.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" +checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" dependencies = [ "yoke", "zerofrom", @@ -6613,9 +6619,9 @@ dependencies = [ [[package]] name = "zerovec-derive" -version = "0.11.2" +version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" +checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index 60286287..1ba9ddfd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -79,7 +79,7 @@ dashmap = "6.1.0" # Async futures futures = "0.3.32" -tokio = { version = "1.50.0", features = ["rt-multi-thread", "fs", "io-util", "parking_lot", "time", "signal", "net"] } +tokio = { version = "1.51.1", features = ["rt-multi-thread", "fs", "io-util", "parking_lot", "time", "signal", "net"] } tokio-util = { version = "0.7.18", features = ["compat"]} # A generic serialization/deserialization framework @@ -98,7 +98,7 @@ diesel-derive-newtype = "2.1.2" libsqlite3-sys = { version = "0.36.0", features = ["bundled"], optional = true } # Crypto-related libraries -rand = "0.10.0" +rand = "0.10.1" ring = "0.17.14" subtle = "2.6.1" @@ -136,7 +136,7 @@ webauthn-rs-core = "0.5.4" url = "2.5.8" # Email libraries -lettre = { version = "0.11.20", features = ["smtp-transport", "sendmail-transport", "builder", "serde", "hostname", "tracing", "tokio1-rustls", "ring", "rustls-native-certs"], default-features = false } +lettre = { version = "0.11.21", features = ["smtp-transport", "sendmail-transport", "builder", "serde", "hostname", "tracing", "tokio1-rustls", "ring", "rustls-native-certs"], default-features = false } percent-encoding = "2.3.2" # URL encoding library used for URL's in the emails email_address = "0.2.9" @@ -176,7 +176,7 @@ openidconnect = { version = "4.0.1", features = ["reqwest", "rustls-tls"] } moka = { version = "0.12.15", features = ["future"] } # Check client versions for specific features. -semver = "1.0.27" +semver = "1.0.28" # Allow overriding the default memory allocator # Mainly used for the musl builds, since the default musl malloc is very slow diff --git a/src/api/core/organizations.rs b/src/api/core/organizations.rs index 9a5079cb..254f60b4 100644 --- a/src/api/core/organizations.rs +++ b/src/api/core/organizations.rs @@ -1905,7 +1905,7 @@ async fn post_bulk_collections(data: Json, headers: Headers }) .collect(); - // Verify if all the collections requested exists and are writeable for the user, else abort + // Verify if all the collections requested exists and are writable for the user, else abort for collection_uuid in &data.collection_ids { match user_collections.get(collection_uuid) { Some(collection) if collection.is_writable_by_user(&headers.user.uuid, &conn).await => (), From bb549986e636ea3ee54ae7bc59bf267d0ec61dc8 Mon Sep 17 00:00:00 2001 From: Mathijs van Veluw Date: Sun, 12 Apr 2026 21:04:32 +0200 Subject: [PATCH 55/89] Fix MFA Remember (#7085) Signed-off-by: BlackDex --- Cargo.lock | 15 +++++++-------- src/api/identity.rs | 14 +++++++++++++- 2 files changed, 20 insertions(+), 9 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 3d4d5921..1794a386 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2405,16 +2405,15 @@ dependencies = [ [[package]] name = "hyper-rustls" -version = "0.27.7" +version = "0.27.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" +checksum = "c2b52f86d1d4bc0d6b4e6826d960b1b333217e07d36b882dca570a5e1c48895b" dependencies = [ "http 1.4.0", "hyper 1.9.0", "hyper-util", "rustls 0.23.37", "rustls-native-certs", - "rustls-pki-types", "tokio", "tokio-rustls 0.26.4", "tower-service", @@ -3722,9 +3721,9 @@ dependencies = [ [[package]] name = "pkg-config" -version = "0.3.32" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" +checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" [[package]] name = "polling" @@ -4409,12 +4408,12 @@ dependencies = [ [[package]] name = "rtoolbox" -version = "0.0.3" +version = "0.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7cc970b249fbe527d6e02e0a227762c9108b2f49d81094fe357ffc6d14d7f6f" +checksum = "327b72899159dfae8060c51a1f6aebe955245bcd9cc4997eed0f623caea022e4" dependencies = [ "libc", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] diff --git a/src/api/identity.rs b/src/api/identity.rs index b9a753b9..cab17331 100644 --- a/src/api/identity.rs +++ b/src/api/identity.rs @@ -742,7 +742,7 @@ async fn twofactor_auth( TwoFactorIncomplete::mark_incomplete(&user.uuid, &device.uuid, &device.name, device.atype, ip, conn).await?; - let twofactor_ids: Vec<_> = twofactors + let mut twofactor_ids: Vec<_> = twofactors .iter() .filter_map(|tf| { let provider_type = TwoFactorType::from_i32(tf.atype)?; @@ -753,6 +753,18 @@ async fn twofactor_auth( err!("No enabled and usable two factor providers are available for this account") } + // Add TwoFactorTypes which are not stored as a record but might be enabled + // Since these types could also be not valid, we do some custom checks here + twofactor_ids.extend( + [ + (!CONFIG.disable_2fa_remember() && device.twofactor_remember.is_some()) + .then_some(TwoFactorType::Remember as i32), + user.totp_recover.is_some().then_some(TwoFactorType::RecoveryCode as i32), + ] + .into_iter() + .flatten(), + ); + let selected_id = data.two_factor_provider.unwrap_or(twofactor_ids[0]); // If we aren't given a two factor provider, assume the first one if !twofactor_ids.contains(&selected_id) { err_json!( From e7e4b9a86d4effaadd67e3465d36100d47d0f073 Mon Sep 17 00:00:00 2001 From: Mathijs van Veluw Date: Mon, 13 Apr 2026 21:47:20 +0200 Subject: [PATCH 56/89] Fix 2FA for Android (#7093) The `RecoveryCode` Type should not be sent as a valid type which can be used. Fixes #7092 Signed-off-by: BlackDex --- src/api/identity.rs | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/src/api/identity.rs b/src/api/identity.rs index cab17331..d7248647 100644 --- a/src/api/identity.rs +++ b/src/api/identity.rs @@ -756,13 +756,8 @@ async fn twofactor_auth( // Add TwoFactorTypes which are not stored as a record but might be enabled // Since these types could also be not valid, we do some custom checks here twofactor_ids.extend( - [ - (!CONFIG.disable_2fa_remember() && device.twofactor_remember.is_some()) - .then_some(TwoFactorType::Remember as i32), - user.totp_recover.is_some().then_some(TwoFactorType::RecoveryCode as i32), - ] - .into_iter() - .flatten(), + (!CONFIG.disable_2fa_remember() && device.twofactor_remember.is_some()) + .then_some(TwoFactorType::Remember as i32), ); let selected_id = data.two_factor_provider.unwrap_or(twofactor_ids[0]); // If we aren't given a two factor provider, assume the first one From bfe420a0180107e646d0f4a80a93004ed7a1aa61 Mon Sep 17 00:00:00 2001 From: Timshel Date: Wed, 15 Apr 2026 18:44:55 +0000 Subject: [PATCH 57/89] Dummy org Master password policy auth fix (#7097) Co-authored-by: Timshel --- src/api/admin.rs | 13 +++++++++++-- src/api/core/accounts.rs | 2 +- src/api/core/organizations.rs | 28 ++++++++++++++++++++-------- src/sso.rs | 2 +- 4 files changed, 33 insertions(+), 12 deletions(-) diff --git a/src/api/admin.rs b/src/api/admin.rs index 1546676f..9a782046 100644 --- a/src/api/admin.rs +++ b/src/api/admin.rs @@ -30,6 +30,7 @@ use crate::{ error::{Error, MapResult}, http_client::make_http_request, mail, + sso::FAKE_SSO_IDENTIFIER, util::{ container_base_image, format_naive_datetime_local, get_active_web_release, get_display_size, is_running_in_container, parse_experimental_client_feature_flags, FeatureFlagFilter, NumberOrString, @@ -315,7 +316,11 @@ async fn invite_user(data: Json, _token: AdminToken, conn: DbConn) - async fn _generate_invite(user: &User, conn: &DbConn) -> EmptyResult { if CONFIG.mail_enabled() { - let org_id: OrganizationId = FAKE_ADMIN_UUID.to_string().into(); + let org_id: OrganizationId = if CONFIG.sso_enabled() { + FAKE_SSO_IDENTIFIER.into() + } else { + FAKE_ADMIN_UUID.into() + }; let member_id: MembershipId = FAKE_ADMIN_UUID.to_string().into(); mail::send_invite(user, org_id, member_id, &CONFIG.invitation_org_name(), None).await } else { @@ -518,7 +523,11 @@ async fn resend_user_invite(user_id: UserId, _token: AdminToken, conn: DbConn) - } if CONFIG.mail_enabled() { - let org_id: OrganizationId = FAKE_ADMIN_UUID.to_string().into(); + let org_id: OrganizationId = if CONFIG.sso_enabled() { + FAKE_SSO_IDENTIFIER.into() + } else { + FAKE_ADMIN_UUID.into() + }; let member_id: MembershipId = FAKE_ADMIN_UUID.to_string().into(); mail::send_invite(&user, org_id, member_id, &CONFIG.invitation_org_name(), None).await } else { diff --git a/src/api/core/accounts.rs b/src/api/core/accounts.rs index 8841c184..fa6a3fd2 100644 --- a/src/api/core/accounts.rs +++ b/src/api/core/accounts.rs @@ -374,7 +374,7 @@ async fn post_set_password(data: Json, headers: Headers, conn: } if let Some(identifier) = data.org_identifier { - if identifier != crate::sso::FAKE_IDENTIFIER && identifier != crate::api::admin::FAKE_ADMIN_UUID { + if identifier != crate::sso::FAKE_SSO_IDENTIFIER && identifier != crate::api::admin::FAKE_ADMIN_UUID { let Some(org) = Organization::find_by_uuid(&identifier.into(), &conn).await else { err!("Failed to retrieve the associated organization") }; diff --git a/src/api/core/organizations.rs b/src/api/core/organizations.rs index 254f60b4..a2448236 100644 --- a/src/api/core/organizations.rs +++ b/src/api/core/organizations.rs @@ -20,7 +20,8 @@ use crate::{ DbConn, }, mail, - util::{convert_json_key_lcase_first, get_uuid, NumberOrString}, + sso::FAKE_SSO_IDENTIFIER, + util::{convert_json_key_lcase_first, NumberOrString}, CONFIG, }; @@ -64,6 +65,7 @@ pub fn routes() -> Vec { post_org_import, list_policies, list_policies_token, + get_dummy_master_password_policy, get_master_password_policy, get_policy, put_policy, @@ -353,7 +355,7 @@ async fn get_user_collections(headers: Headers, conn: DbConn) -> Json { // The returned `Id` will then be passed to `get_master_password_policy` which will mainly ignore it #[get("/organizations//auto-enroll-status")] async fn get_auto_enroll_status(identifier: &str, headers: Headers, conn: DbConn) -> JsonResult { - let org = if identifier == crate::sso::FAKE_IDENTIFIER { + let org = if identifier == FAKE_SSO_IDENTIFIER { match Membership::find_main_user_org(&headers.user.uuid, &conn).await { Some(member) => Organization::find_by_uuid(&member.org_uuid, &conn).await, None => None, @@ -363,7 +365,7 @@ async fn get_auto_enroll_status(identifier: &str, headers: Headers, conn: DbConn }; let (id, identifier, rp_auto_enroll) = match org { - None => (get_uuid(), identifier.to_string(), false), + None => (identifier.to_string(), identifier.to_string(), false), Some(org) => ( org.uuid.to_string(), org.uuid.to_string(), @@ -924,7 +926,7 @@ async fn get_org_domain_sso_verified(data: Json, conn: DbConn) .collect::>() { v if !v.is_empty() => v, - _ => vec![(crate::sso::FAKE_IDENTIFIER.to_string(), crate::sso::FAKE_IDENTIFIER.to_string())], + _ => vec![(FAKE_SSO_IDENTIFIER.to_string(), FAKE_SSO_IDENTIFIER.to_string())], }; Ok(Json(json!({ @@ -1975,9 +1977,19 @@ async fn list_policies_token(org_id: OrganizationId, token: &str, conn: DbConn) }))) } -// Called during the SSO enrollment. -// Return the org policy if it exists, otherwise use the default one. -#[get("/organizations//policies/master-password", rank = 1)] +// Called during the SSO enrollment return the default policy +#[get("/organizations/vaultwarden-dummy-oidc-identifier/policies/master-password", rank = 1)] +fn get_dummy_master_password_policy() -> JsonResult { + let (enabled, data) = match CONFIG.sso_master_password_policy_value() { + Some(policy) if CONFIG.sso_enabled() => (true, policy.to_string()), + _ => (false, "null".to_string()), + }; + let policy = OrgPolicy::new(FAKE_SSO_IDENTIFIER.into(), OrgPolicyType::MasterPassword, enabled, data); + Ok(Json(policy.to_json())) +} + +// Called during the SSO enrollment return the org policy if it exists +#[get("/organizations//policies/master-password", rank = 2)] async fn get_master_password_policy(org_id: OrganizationId, _headers: OrgMemberHeaders, conn: DbConn) -> JsonResult { let policy = OrgPolicy::find_by_org_and_type(&org_id, OrgPolicyType::MasterPassword, &conn).await.unwrap_or_else(|| { @@ -1992,7 +2004,7 @@ async fn get_master_password_policy(org_id: OrganizationId, _headers: OrgMemberH Ok(Json(policy.to_json())) } -#[get("/organizations//policies/", rank = 2)] +#[get("/organizations//policies/", rank = 3)] async fn get_policy(org_id: OrganizationId, pol_type: i32, headers: AdminHeaders, conn: DbConn) -> JsonResult { if org_id != headers.org_id { err!("Organization not found", "Organization id's do not match"); diff --git a/src/sso.rs b/src/sso.rs index ee6d707a..2f56f3a6 100644 --- a/src/sso.rs +++ b/src/sso.rs @@ -17,7 +17,7 @@ use crate::{ CONFIG, }; -pub static FAKE_IDENTIFIER: &str = "VW_DUMMY_IDENTIFIER_FOR_OIDC"; +pub static FAKE_SSO_IDENTIFIER: &str = "vaultwarden-dummy-oidc-identifier"; static SSO_JWT_ISSUER: LazyLock = LazyLock::new(|| format!("{}|sso", CONFIG.domain_origin())); From dfebee57ec26837f82d8a883c9fadd026fe8aeac Mon Sep 17 00:00:00 2001 From: Mathijs van Veluw Date: Wed, 15 Apr 2026 20:49:58 +0200 Subject: [PATCH 58/89] Fix recovery-code not working (#7102) This commit fixes an issue where the recovery code isn't working anymore. Fixes #7096 Signed-off-by: BlackDex --- src/api/identity.rs | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/src/api/identity.rs b/src/api/identity.rs index d7248647..57db1adc 100644 --- a/src/api/identity.rs +++ b/src/api/identity.rs @@ -742,7 +742,7 @@ async fn twofactor_auth( TwoFactorIncomplete::mark_incomplete(&user.uuid, &device.uuid, &device.name, device.atype, ip, conn).await?; - let mut twofactor_ids: Vec<_> = twofactors + let twofactor_ids: Vec<_> = twofactors .iter() .filter_map(|tf| { let provider_type = TwoFactorType::from_i32(tf.atype)?; @@ -753,15 +753,11 @@ async fn twofactor_auth( err!("No enabled and usable two factor providers are available for this account") } - // Add TwoFactorTypes which are not stored as a record but might be enabled - // Since these types could also be not valid, we do some custom checks here - twofactor_ids.extend( - (!CONFIG.disable_2fa_remember() && device.twofactor_remember.is_some()) - .then_some(TwoFactorType::Remember as i32), - ); - let selected_id = data.two_factor_provider.unwrap_or(twofactor_ids[0]); // If we aren't given a two factor provider, assume the first one - if !twofactor_ids.contains(&selected_id) { + // Ignore Remember and RecoveryCode Types during this check, these are special + if ![TwoFactorType::Remember as i32, TwoFactorType::RecoveryCode as i32].contains(&selected_id) + && !twofactor_ids.contains(&selected_id) + { err_json!( _json_err_twofactor(&twofactor_ids, &user.uuid, data, client_version, conn).await?, "Invalid two factor provider" From 0ed8ab68f75dde97145a1aef8662f676bc54f0f7 Mon Sep 17 00:00:00 2001 From: Mathijs van Veluw Date: Thu, 16 Apr 2026 18:42:13 +0200 Subject: [PATCH 59/89] Fix invalid refresh token response (#7105) If the refresh token is invalid or expired we need to return a specific JSON and HTTP Status, else the clients will not logout. Fixes #7060 Closes #7080 Signed-off-by: BlackDex --- src/api/identity.rs | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/src/api/identity.rs b/src/api/identity.rs index 57db1adc..c24c4641 100644 --- a/src/api/identity.rs +++ b/src/api/identity.rs @@ -2,7 +2,6 @@ use chrono::Utc; use num_traits::FromPrimitive; use rocket::{ form::{Form, FromForm}, - http::Status, response::Redirect, serde::json::Json, Route, @@ -131,12 +130,14 @@ async fn login( login_result } -// Return Status::Unauthorized to trigger logout async fn _refresh_login(data: ConnectData, conn: &DbConn, ip: &ClientIp) -> JsonResult { - // Extract token - let refresh_token = match data.refresh_token { - Some(token) => token, - None => err_code!("Missing refresh_token", Status::Unauthorized.code), + // When a refresh token is invalid or missing we need to respond with an HTTP BadRequest (400) + // It also needs to return a json which holds at least a key `error` with the value `invalid_grant` + // See the link below for details + // https://github.com/bitwarden/clients/blob/2ee158e720a5e7dbe3641caf80b569e97a1dd91b/libs/common/src/services/api.service.ts#L1786-L1797 + + let Some(refresh_token) = data.refresh_token else { + err_json!(json!({"error": "invalid_grant"}), "Missing refresh_token") }; // --- @@ -147,7 +148,10 @@ async fn _refresh_login(data: ConnectData, conn: &DbConn, ip: &ClientIp) -> Json // let members = Membership::find_confirmed_by_user(&user.uuid, conn).await; match auth::refresh_tokens(ip, &refresh_token, data.client_id, conn).await { Err(err) => { - err_code!(format!("Unable to refresh login credentials: {}", err.message()), Status::Unauthorized.code) + err_json!( + json!({"error": "invalid_grant"}), + format!("Unable to refresh login credentials: {}", err.message()) + ) } Ok((mut device, auth_tokens)) => { // Save to update `device.updated_at` to track usage and toggle new status From b04ed75f9f278c57ede12f078fca645584f8bec5 Mon Sep 17 00:00:00 2001 From: Mathijs van Veluw Date: Sat, 18 Apr 2026 15:03:41 +0200 Subject: [PATCH 60/89] Update Rust, Crates, GHA and fix a DNS issue (#7108) * Update Rust, Crates and GHA - Updated Rust to v1.95.0 - Updated all the crates - Update GitHub Actions With the crate updates, hickory-resolver was updated which needed some changes. During testing I found a bug with the fallback resolving from Tokio. The resolver doesn't work if it receives only a `&str`, it needs a `port` too. This fixed the resolving if Hickory failed to load. Also, Hickory switched the resolving to prefer IPv6. While this is nice, it could break or slowdown resolving for IPv4 only environments. Since we already have a flag to prefer IPv6, we check if this is set, else resolve IPv4 first and IPv6 afterwards. Also, we returned just 1 IpAddr record, and ignored the rest. This could mean, a failed attempt to connect if the first IP endpoint has issues. Same if the first records is IPv6 but the server doesn't support this, it never tried a possible returned IPv4 address. We now return a full list of the resolved records unless one of the records matched a filtered address, than the whole resolving is ignored as was previously the case. Signed-off-by: BlackDex * Adjust resolver builder path Changed the way the resolver is constructed. This way the default is always selected no matter which part of the hickory build fails. Signed-off-by: BlackDex --------- Signed-off-by: BlackDex --- .gitattributes | 1 - .github/workflows/trivy.yml | 2 +- .github/workflows/typos.yml | 2 +- .github/workflows/zizmor.yml | 2 +- .pre-commit-config.yaml | 108 ++++++------- Cargo.lock | 295 +++++++++++++++++++++++++---------- Cargo.toml | 12 +- diesel.toml | 2 +- docker/DockerSettings.yaml | 2 +- docker/Dockerfile.alpine | 8 +- docker/Dockerfile.debian | 2 +- rust-toolchain.toml | 2 +- src/api/identity.rs | 2 +- src/http_client.rs | 45 +++--- 14 files changed, 307 insertions(+), 178 deletions(-) diff --git a/.gitattributes b/.gitattributes index b33a6211..4d7cadd3 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,3 +1,2 @@ # Ignore vendored scripts in GitHub stats src/static/scripts/* linguist-vendored - diff --git a/.github/workflows/trivy.yml b/.github/workflows/trivy.yml index c9e02cf9..3f27d339 100644 --- a/.github/workflows/trivy.yml +++ b/.github/workflows/trivy.yml @@ -50,6 +50,6 @@ jobs: severity: CRITICAL,HIGH - name: Upload Trivy scan results to GitHub Security tab - uses: github/codeql-action/upload-sarif@c10b8064de6f491fea524254123dbe5e09572f13 # v4.35.1 + uses: github/codeql-action/upload-sarif@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4.35.2 with: sarif_file: 'trivy-results.sarif' diff --git a/.github/workflows/typos.yml b/.github/workflows/typos.yml index f68ef29d..b3ee311b 100644 --- a/.github/workflows/typos.yml +++ b/.github/workflows/typos.yml @@ -23,4 +23,4 @@ jobs: # When this version is updated, do not forget to update this in `.pre-commit-config.yaml` too - name: Spell Check Repo - uses: crate-ci/typos@02ea592e44b3a53c302f697cddca7641cd051c3d # v1.45.0 + uses: crate-ci/typos@cf5f1c29a8ac336af8568821ec41919923b05a83 # v1.45.1 diff --git a/.github/workflows/zizmor.yml b/.github/workflows/zizmor.yml index 4bd40db3..2350ec61 100644 --- a/.github/workflows/zizmor.yml +++ b/.github/workflows/zizmor.yml @@ -24,7 +24,7 @@ jobs: persist-credentials: false - name: Run zizmor - uses: zizmorcore/zizmor-action@71321a20a9ded102f6e9ce5718a2fcec2c4f70d8 # v0.5.2 + uses: zizmorcore/zizmor-action@b1d7e1fb5de872772f31590499237e7cce841e8e # v0.5.3 with: # intentionally not scanning the entire repository, # since it contains integration tests. diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 0b6ad451..b16ae4c6 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,58 +1,60 @@ --- repos: -- repo: https://github.com/pre-commit/pre-commit-hooks + - repo: https://github.com/pre-commit/pre-commit-hooks rev: 3e8a8703264a2f4a69428a0aa4dcb512790b2c8c # v6.0.0 hooks: - - id: check-yaml - - id: check-json - - id: check-toml - - id: mixed-line-ending - args: ["--fix=no"] - - id: end-of-file-fixer - exclude: "(.*js$|.*css$)" - - id: check-case-conflict - - id: check-merge-conflict - - id: detect-private-key - - id: check-symlinks - - id: forbid-submodules -- repo: local + - id: check-yaml + - id: check-json + - id: check-toml + - id: mixed-line-ending + args: [ "--fix=no" ] + - id: end-of-file-fixer + exclude: "(.*js$|.*css$)" + - id: check-case-conflict + - id: check-merge-conflict + - id: detect-private-key + - id: check-symlinks + - id: forbid-submodules + + # When this version is updated, do not forget to update this in `.github/workflows/typos.yaml` too + - repo: https://github.com/crate-ci/typos + rev: cf5f1c29a8ac336af8568821ec41919923b05a83 # v1.45.1 hooks: - - id: fmt - name: fmt - description: Format files with cargo fmt. - entry: cargo fmt - language: system - always_run: true - pass_filenames: false - args: ["--", "--check"] - - id: cargo-test - name: cargo test - description: Test the package for errors. - entry: cargo test - language: system - args: ["--features", "sqlite,mysql,postgresql", "--"] - types_or: [rust, file] - files: (Cargo.toml|Cargo.lock|rust-toolchain.toml|rustfmt.toml|.*\.rs$) - pass_filenames: false - - id: cargo-clippy - name: cargo clippy - description: Lint Rust sources - entry: cargo clippy - language: system - args: ["--features", "sqlite,mysql,postgresql", "--", "-D", "warnings"] - types_or: [rust, file] - files: (Cargo.toml|Cargo.lock|rust-toolchain.toml|rustfmt.toml|.*\.rs$) - pass_filenames: false - - id: check-docker-templates - name: check docker templates - description: Check if the Docker templates are updated - language: system - entry: sh - args: - - "-c" - - "cd docker && make" -# When this version is updated, do not forget to update this in `.github/workflows/typos.yaml` too -- repo: https://github.com/crate-ci/typos - rev: 02ea592e44b3a53c302f697cddca7641cd051c3d # v1.45.0 - hooks: - - id: typos + - id: typos + + - repo: local + hooks: + - id: fmt + name: fmt + description: Format files with cargo fmt. + entry: cargo fmt + language: system + always_run: true + pass_filenames: false + args: [ "--", "--check" ] + - id: cargo-test + name: cargo test + description: Test the package for errors. + entry: cargo test + language: system + args: [ "--features", "sqlite,mysql,postgresql", "--" ] + types_or: [ rust, file ] + files: (Cargo.toml|Cargo.lock|rust-toolchain.toml|rustfmt.toml|.*\.rs$) + pass_filenames: false + - id: cargo-clippy + name: cargo clippy + description: Lint Rust sources + entry: cargo clippy + language: system + args: [ "--features", "sqlite,mysql,postgresql", "--", "-D", "warnings" ] + types_or: [ rust, file ] + files: (Cargo.toml|Cargo.lock|rust-toolchain.toml|rustfmt.toml|.*\.rs$) + pass_filenames: false + - id: check-docker-templates + name: check docker templates + description: Check if the Docker templates are updated + language: system + entry: sh + args: + - "-c" + - "cd docker && make" diff --git a/Cargo.lock b/Cargo.lock index 1794a386..da11650d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -466,9 +466,9 @@ dependencies = [ [[package]] name = "aws-sdk-sts" -version = "1.101.0" +version = "1.102.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab41ad64e4051ecabeea802d6a17845a91e83287e1dd249e6963ea1ba78c428a" +checksum = "0fc35b7a14cabdad13795fbbbd26d5ddec0882c01492ceedf2af575aad5f37dd" dependencies = [ "aws-credential-types", "aws-runtime", @@ -573,9 +573,9 @@ dependencies = [ [[package]] name = "aws-smithy-runtime" -version = "1.10.3" +version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "028999056d2d2fd58a697232f9eec4a643cf73a71cf327690a7edad1d2af2110" +checksum = "0504b1ab12debb5959e5165ee5fe97dd387e7aa7ea6a477bfd7635dfe769a4f5" dependencies = [ "aws-smithy-async", "aws-smithy-http", @@ -597,11 +597,12 @@ dependencies = [ [[package]] name = "aws-smithy-runtime-api" -version = "1.11.6" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "876ab3c9c29791ba4ba02b780a3049e21ec63dabda09268b175272c3733a79e6" +checksum = "b71a13df6ada0aafbf21a73bdfcdf9324cfa9df77d96b8446045be3cde61b42e" dependencies = [ "aws-smithy-async", + "aws-smithy-runtime-api-macros", "aws-smithy-types", "bytes", "http 0.2.12", @@ -612,6 +613,17 @@ dependencies = [ "zeroize", ] +[[package]] +name = "aws-smithy-runtime-api-macros" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d7396fd9500589e62e460e987ecb671bad374934e55ec3b5f498cc7a8a8a7b7" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "aws-smithy-types" version = "1.4.7" @@ -735,9 +747,9 @@ checksum = "383d29d513d8764dcdc42ea295d979eb99c3c9f00607b3692cf68a431f7dca72" [[package]] name = "bitflags" -version = "2.11.0" +version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" +checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" [[package]] name = "blake2" @@ -912,7 +924,7 @@ checksum = "6f8d983286843e49675a4b7a2d174efe136dc93a18d69130dd18198a6c167601" dependencies = [ "cfg-if", "cpufeatures 0.3.0", - "rand_core 0.10.0", + "rand_core 0.10.1", ] [[package]] @@ -955,6 +967,16 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9e769b5c8c8283982a987c6e948e540254f1058d5a74b8794914d4ef5fc2a24" +[[package]] +name = "combine" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" +dependencies = [ + "bytes", + "memchr", +] + [[package]] name = "compression-codecs" version = "0.4.37" @@ -1694,18 +1716,6 @@ dependencies = [ "cfg-if", ] -[[package]] -name = "enum-as-inner" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1e6a265c649f3f5979b601d26f1d05ada116434c87741c9493cb56218f76cbc" -dependencies = [ - "heck", - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "equivalent" version = "1.0.2" @@ -2022,7 +2032,7 @@ dependencies = [ "cfg-if", "libc", "r-efi 6.0.0", - "rand_core 0.10.0", + "rand_core 0.10.1", "wasip2", "wasip3", ] @@ -2062,7 +2072,7 @@ dependencies = [ "parking_lot", "portable-atomic", "quanta", - "rand 0.9.3", + "rand 0.9.4", "smallvec", "spinning_top", "web-time", @@ -2200,23 +2210,22 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" [[package]] -name = "hickory-proto" -version = "0.25.2" +name = "hickory-net" +version = "0.26.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8a6fe56c0038198998a6f217ca4e7ef3a5e51f46163bd6dd60b5c71ca6c6502" +checksum = "0c61c8db47fae51ba9f8f2a2748bd87542acfbe22f2ec9cf9c8ec72d1ee6e9a6" dependencies = [ "async-trait", "cfg-if", "data-encoding", - "enum-as-inner", "futures-channel", "futures-io", "futures-util", + "hickory-proto", "idna", "ipnet", - "once_cell", - "rand 0.9.3", - "ring", + "jni", + "rand 0.10.1", "thiserror 2.0.18", "tinyvec", "tokio", @@ -2225,21 +2234,46 @@ dependencies = [ ] [[package]] -name = "hickory-resolver" -version = "0.25.2" +name = "hickory-proto" +version = "0.26.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc62a9a99b0bfb44d2ab95a7208ac952d31060efc16241c87eaf36406fecf87a" +checksum = "a916d0494600d99ecb15aadfab677ad97c4de559e8f1af0c129353a733ac1fcc" +dependencies = [ + "data-encoding", + "idna", + "ipnet", + "jni", + "once_cell", + "prefix-trie", + "rand 0.10.1", + "ring", + "thiserror 2.0.18", + "tinyvec", + "tracing", + "url", +] + +[[package]] +name = "hickory-resolver" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a10bd64d950b4d38ca21e25c8ae230712e4955fb8290cfcb29a5e5dc6017e544" dependencies = [ "cfg-if", "futures-util", + "hickory-net", "hickory-proto", "ipconfig", + "ipnet", + "jni", "moka", + "ndk-context", "once_cell", "parking_lot", - "rand 0.9.3", + "rand 0.10.1", "resolv-conf", "smallvec", + "system-configuration", "thiserror 2.0.18", "tokio", "tracing", @@ -2405,14 +2439,14 @@ dependencies = [ [[package]] name = "hyper-rustls" -version = "0.27.8" +version = "0.27.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2b52f86d1d4bc0d6b4e6826d960b1b333217e07d36b882dca570a5e1c48895b" +checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f" dependencies = [ "http 1.4.0", "hyper 1.9.0", "hyper-util", - "rustls 0.23.37", + "rustls 0.23.38", "rustls-native-certs", "tokio", "tokio-rustls 0.26.4", @@ -2641,6 +2675,9 @@ name = "ipnet" version = "2.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" +dependencies = [ + "serde", +] [[package]] name = "iri-string" @@ -2725,6 +2762,55 @@ dependencies = [ "jiff-tzdb", ] +[[package]] +name = "jni" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5efd9a482cf3a427f00d6b35f14332adc7902ce91efb778580e180ff90fa3498" +dependencies = [ + "cfg-if", + "combine", + "jni-macros", + "jni-sys", + "log", + "simd_cesu8", + "thiserror 2.0.18", + "walkdir", + "windows-link", +] + +[[package]] +name = "jni-macros" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a00109accc170f0bdb141fed3e393c565b6f5e072365c3bd58f5b062591560a3" +dependencies = [ + "proc-macro2", + "quote", + "rustc_version", + "simd_cesu8", + "syn", +] + +[[package]] +name = "jni-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2" +dependencies = [ + "jni-sys-macros", +] + +[[package]] +name = "jni-sys-macros" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264" +dependencies = [ + "quote", + "syn", +] + [[package]] name = "job_scheduler_ng" version = "2.4.0" @@ -2787,7 +2873,7 @@ dependencies = [ "p256", "p384", "pem", - "rand 0.8.5", + "rand 0.8.6", "rsa", "serde", "serde_json", @@ -2850,7 +2936,7 @@ dependencies = [ "nom 8.0.0", "percent-encoding", "quoted_printable", - "rustls 0.23.37", + "rustls 0.23.38", "rustls-native-certs", "serde", "socket2 0.6.3", @@ -2862,9 +2948,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.184" +version = "0.2.185" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48f5d2a454e16a5ea0f4ced81bd44e4cfc7bd3a507b61887c99fd3538b28e4af" +checksum = "52ff2c0fe9bc6cb6b14a0592c2ff4fa9ceb83eea9db979b0487cd054946a2b8f" [[package]] name = "libm" @@ -3096,6 +3182,12 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "ndk-context" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b" + [[package]] name = "nom" version = "7.1.3" @@ -3151,7 +3243,7 @@ dependencies = [ "num-integer", "num-iter", "num-traits", - "rand 0.8.5", + "rand 0.8.6", "smallvec", "zeroize", ] @@ -3247,7 +3339,7 @@ dependencies = [ "chrono", "getrandom 0.2.17", "http 1.4.0", - "rand 0.8.5", + "rand 0.8.6", "reqwest", "serde", "serde_json", @@ -3322,7 +3414,7 @@ dependencies = [ "oauth2", "p256", "p384", - "rand 0.8.5", + "rand 0.8.6", "rsa", "serde", "serde-value", @@ -3338,9 +3430,9 @@ dependencies = [ [[package]] name = "openssl" -version = "0.10.76" +version = "0.10.77" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "951c002c75e16ea2c65b8c7e4d3d51d5530d8dfa7d060b4776828c88cfb18ecf" +checksum = "bfe4646e360ec77dff7dde40ed3d6c5fee52d156ef4a62f53973d38294dad87f" dependencies = [ "bitflags", "cfg-if", @@ -3379,9 +3471,9 @@ dependencies = [ [[package]] name = "openssl-sys" -version = "0.9.112" +version = "0.9.113" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57d55af3b3e226502be1526dfdba67ab0e9c96fc293004e79576b2b9edb0dbdb" +checksum = "ad2f2c0eba47118757e4c6d2bff2838f3e0523380021356e7875e858372ce644" dependencies = [ "cc", "libc", @@ -3618,7 +3710,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" dependencies = [ "phf_shared 0.11.3", - "rand 0.8.5", + "rand 0.8.6", ] [[package]] @@ -3747,9 +3839,9 @@ checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" [[package]] name = "portable-atomic-util" -version = "0.2.6" +version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "091397be61a01d4be58e7841595bd4bfedb15f1cd54977d79b8271e94ed799a3" +checksum = "c2a106d1259c23fac8e543272398ae0e3c0b8d33c88ed73d0cc71b0f1d902618" dependencies = [ "portable-atomic", ] @@ -3789,6 +3881,17 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "prefix-trie" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23370be78b7e5bcbb0cab4a02047eb040279a693c78daad04c2c5f1c24a83503" +dependencies = [ + "either", + "ipnet", + "num-traits", +] + [[package]] name = "prettyplease" version = "0.2.37" @@ -3899,7 +4002,7 @@ dependencies = [ "quinn-proto", "quinn-udp", "rustc-hash", - "rustls 0.23.37", + "rustls 0.23.38", "socket2 0.6.3", "thiserror 2.0.18", "tokio", @@ -3916,10 +4019,10 @@ dependencies = [ "bytes", "getrandom 0.3.4", "lru-slab", - "rand 0.9.3", + "rand 0.9.4", "ring", "rustc-hash", - "rustls 0.23.37", + "rustls 0.23.38", "rustls-pki-types", "slab", "thiserror 2.0.18", @@ -3982,9 +4085,9 @@ dependencies = [ [[package]] name = "rand" -version = "0.8.5" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a" dependencies = [ "libc", "rand_chacha 0.3.1", @@ -3993,9 +4096,9 @@ dependencies = [ [[package]] name = "rand" -version = "0.9.3" +version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ec095654a25171c2124e9e3393a930bddbffdc939556c914957a4c3e0a87166" +checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea" dependencies = [ "rand_chacha 0.9.0", "rand_core 0.9.5", @@ -4009,7 +4112,7 @@ checksum = "d2e8e8bcc7961af1fdac401278c6a831614941f6164ee3bf4ce61b7edb162207" dependencies = [ "chacha20", "getrandom 0.4.2", - "rand_core 0.10.0", + "rand_core 0.10.1", ] [[package]] @@ -4052,9 +4155,9 @@ dependencies = [ [[package]] name = "rand_core" -version = "0.10.0" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c8d0fd677905edcbeedbf2edb6494d676f0e98d54d5cf9bda0b061cb8fb8aba" +checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" [[package]] name = "raw-cpuid" @@ -4161,7 +4264,7 @@ dependencies = [ "once_cell", "percent-encoding", "quick-xml 0.37.5", - "rand 0.8.5", + "rand 0.8.6", "reqwest", "rsa", "rust-ini", @@ -4200,7 +4303,7 @@ dependencies = [ "percent-encoding", "pin-project-lite", "quinn", - "rustls 0.23.37", + "rustls 0.23.38", "rustls-native-certs", "rustls-pki-types", "serde", @@ -4290,7 +4393,7 @@ dependencies = [ "num_cpus", "parking_lot", "pin-project-lite", - "rand 0.8.5", + "rand 0.8.6", "ref-cast", "rocket_codegen", "rocket_http", @@ -4408,9 +4511,9 @@ dependencies = [ [[package]] name = "rtoolbox" -version = "0.0.4" +version = "0.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "327b72899159dfae8060c51a1f6aebe955245bcd9cc4997eed0f623caea022e4" +checksum = "50a0e551c1e27e1731aba276dbeaeac73f53c7cd34d1bda485d02bd1e0f36844" dependencies = [ "libc", "windows-sys 0.59.0", @@ -4477,15 +4580,15 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.37" +version = "0.23.38" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "758025cb5fccfd3bc2fd74708fd4682be41d99e5dff73c377c0646c6012c73a4" +checksum = "69f9466fb2c14ea04357e91413efb882e2a6d4a406e625449bc0a5d360d53a21" dependencies = [ "log", "once_cell", "ring", "rustls-pki-types", - "rustls-webpki 0.103.11", + "rustls-webpki 0.103.12", "subtle", "zeroize", ] @@ -4533,9 +4636,9 @@ dependencies = [ [[package]] name = "rustls-webpki" -version = "0.103.11" +version = "0.103.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20a6af516fea4b20eccceaf166e8aa666ac996208e8a644ce3ef5aa783bc7cd4" +checksum = "8279bb85272c9f10811ae6a6c547ff594d6a7f3c6c6b02ee9726d1d0dcfcdd06" dependencies = [ "ring", "rustls-pki-types", @@ -4907,6 +5010,22 @@ version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" +[[package]] +name = "simd_cesu8" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94f90157bb87cddf702797c5dadfa0be7d266cdf49e22da2fcaa32eff75b2c33" +dependencies = [ + "rustc_version", + "simdutf8", +] + +[[package]] +name = "simdutf8" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" + [[package]] name = "simple_asn1" version = "0.6.4" @@ -5253,9 +5372,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.51.1" +version = "1.52.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f66bf9585cda4b724d3e78ab34b73fb2bbaba9011b9bfdf69dc836382ea13b8c" +checksum = "b67dee974fe86fd92cc45b7a95fdd2f99a36a6d7b0d431a231178d3d670bbcc6" dependencies = [ "bytes", "libc", @@ -5295,7 +5414,7 @@ version = "0.26.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" dependencies = [ - "rustls 0.23.37", + "rustls 0.23.38", "tokio", ] @@ -5550,7 +5669,7 @@ dependencies = [ "http 1.4.0", "httparse", "log", - "rand 0.8.5", + "rand 0.8.6", "sha1", "thiserror 1.0.69", "url", @@ -5645,9 +5764,9 @@ checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" [[package]] name = "uuid" -version = "1.23.0" +version = "1.23.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ac8b6f42ead25368cf5b098aeb3dc8a1a2c05a3eee8a9a1a68c640edbfc79d9" +checksum = "ddd74a9687298c6858e9b88ec8935ec45d22e8fd5e6394fa1bd4e99a87789c76" dependencies = [ "getrandom 0.4.2", "js-sys", @@ -5790,11 +5909,11 @@ checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] name = "wasip2" -version = "1.0.2+wasi-0.2.9" +version = "1.0.3+wasi-0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" +checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6" dependencies = [ - "wit-bindgen", + "wit-bindgen 0.57.1", ] [[package]] @@ -5803,7 +5922,7 @@ version = "0.4.0+wasi-0.3.0-rc-2026-01-06" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" dependencies = [ - "wit-bindgen", + "wit-bindgen 0.51.0", ] [[package]] @@ -5969,7 +6088,7 @@ dependencies = [ "nom 7.1.3", "openssl", "openssl-sys", - "rand 0.9.3", + "rand 0.9.4", "rand_chacha 0.9.0", "serde", "serde_cbor_2", @@ -5998,9 +6117,9 @@ dependencies = [ [[package]] name = "webpki-roots" -version = "1.0.6" +version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22cfaf3c063993ff62e73cb4311efde4db1efb31ab78a3e5c457939ad5cc0bed" +checksum = "52f5ee44c96cf55f1b349600768e3ece3a8f26010c05265ab73f945bb1a2eb9d" dependencies = [ "rustls-pki-types", ] @@ -6385,6 +6504,12 @@ dependencies = [ "wit-bindgen-rust-macro", ] +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" + [[package]] name = "wit-bindgen-core" version = "0.51.0" @@ -6541,7 +6666,7 @@ dependencies = [ "form_urlencoded", "futures", "hmac", - "rand 0.9.3", + "rand 0.9.4", "reqwest", "sha1", "threadpool", diff --git a/Cargo.toml b/Cargo.toml index 1ba9ddfd..5b68b8d4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [workspace.package] edition = "2021" -rust-version = "1.92.0" +rust-version = "1.93.0" license = "AGPL-3.0-only" repository = "https://github.com/dani-garcia/vaultwarden" publish = false @@ -79,7 +79,7 @@ dashmap = "6.1.0" # Async futures futures = "0.3.32" -tokio = { version = "1.51.1", features = ["rt-multi-thread", "fs", "io-util", "parking_lot", "time", "signal", "net"] } +tokio = { version = "1.52.1", features = ["rt-multi-thread", "fs", "io-util", "parking_lot", "time", "signal", "net"] } tokio-util = { version = "0.7.18", features = ["compat"]} # A generic serialization/deserialization framework @@ -103,7 +103,7 @@ ring = "0.17.14" subtle = "2.6.1" # UUID generation -uuid = { version = "1.23.0", features = ["v4"] } +uuid = { version = "1.23.1", features = ["v4"] } # Date and time libraries chrono = { version = "0.4.44", features = ["clock", "serde"], default-features = false } @@ -145,7 +145,7 @@ handlebars = { version = "6.4.0", features = ["dir_source"] } # HTTP client (Used for favicons, version check, DUO and HIBP API) reqwest = { version = "0.12.28", features = ["rustls-tls", "rustls-tls-native-roots", "stream", "json", "deflate", "gzip", "brotli", "zstd", "socks", "cookies", "charset", "http2", "system-proxy"], default-features = false} -hickory-resolver = "0.25.2" +hickory-resolver = "0.26.0" # Favicon extraction libraries html5gum = "0.8.3" @@ -162,7 +162,7 @@ cookie = "0.18.1" cookie_store = "0.22.1" # Used by U2F, JWT and PostgreSQL -openssl = "0.10.76" +openssl = "0.10.77" # CLI argument parsing pico-args = "0.5.0" @@ -200,7 +200,7 @@ opendal = { version = "0.55.0", features = ["services-fs"], default-features = f anyhow = { version = "1.0.102", optional = true } aws-config = { version = "1.8.15", features = ["behavior-version-latest", "rt-tokio", "credentials-process", "sso"], default-features = false, optional = true } aws-credential-types = { version = "1.2.14", optional = true } -aws-smithy-runtime-api = { version = "1.11.6", optional = true } +aws-smithy-runtime-api = { version = "1.12.0", optional = true } http = { version = "1.4.0", optional = true } reqsign = { version = "0.16.5", optional = true } diff --git a/diesel.toml b/diesel.toml index 5a78b550..71215dbf 100644 --- a/diesel.toml +++ b/diesel.toml @@ -2,4 +2,4 @@ # see diesel.rs/guides/configuring-diesel-cli [print_schema] -file = "src/db/schema.rs" \ No newline at end of file +file = "src/db/schema.rs" diff --git a/docker/DockerSettings.yaml b/docker/DockerSettings.yaml index c679b0da..d105cc40 100644 --- a/docker/DockerSettings.yaml +++ b/docker/DockerSettings.yaml @@ -5,7 +5,7 @@ vault_image_digest: "sha256:37c8661fa59dcdfbd3baa8366b6e950ef292b15adfeff1f57812 # We use the linux/amd64 platform shell scripts since there is no difference between the different platform scripts # https://github.com/tonistiigi/xx | https://hub.docker.com/r/tonistiigi/xx/tags xx_image_digest: "sha256:c64defb9ed5a91eacb37f96ccc3d4cd72521c4bd18d5442905b95e2226b0e707" -rust_version: 1.94.1 # Rust version to be used +rust_version: 1.95.0 # Rust version to be used debian_version: trixie # Debian release name to be used alpine_version: "3.23" # Alpine version to be used # For which platforms/architectures will we try to build images diff --git a/docker/Dockerfile.alpine b/docker/Dockerfile.alpine index ddcc9efe..761d00f5 100644 --- a/docker/Dockerfile.alpine +++ b/docker/Dockerfile.alpine @@ -32,10 +32,10 @@ FROM --platform=linux/amd64 docker.io/vaultwarden/web-vault@sha256:37c8661fa59dc ########################## ALPINE BUILD IMAGES ########################## ## NOTE: The Alpine Base Images do not support other platforms then linux/amd64 and linux/arm64 ## And for Alpine we define all build images here, they will only be loaded when actually used -FROM --platform=$BUILDPLATFORM ghcr.io/blackdex/rust-musl:x86_64-musl-stable-1.94.1 AS build_amd64 -FROM --platform=$BUILDPLATFORM ghcr.io/blackdex/rust-musl:aarch64-musl-stable-1.94.1 AS build_arm64 -FROM --platform=$BUILDPLATFORM ghcr.io/blackdex/rust-musl:armv7-musleabihf-stable-1.94.1 AS build_armv7 -FROM --platform=$BUILDPLATFORM ghcr.io/blackdex/rust-musl:arm-musleabi-stable-1.94.1 AS build_armv6 +FROM --platform=$BUILDPLATFORM ghcr.io/blackdex/rust-musl:x86_64-musl-stable-1.95.0 AS build_amd64 +FROM --platform=$BUILDPLATFORM ghcr.io/blackdex/rust-musl:aarch64-musl-stable-1.95.0 AS build_arm64 +FROM --platform=$BUILDPLATFORM ghcr.io/blackdex/rust-musl:armv7-musleabihf-stable-1.95.0 AS build_armv7 +FROM --platform=$BUILDPLATFORM ghcr.io/blackdex/rust-musl:arm-musleabi-stable-1.95.0 AS build_armv6 ########################## BUILD IMAGE ########################## # hadolint ignore=DL3006 diff --git a/docker/Dockerfile.debian b/docker/Dockerfile.debian index 18dd3d6c..5ecee4fd 100644 --- a/docker/Dockerfile.debian +++ b/docker/Dockerfile.debian @@ -36,7 +36,7 @@ FROM --platform=linux/amd64 docker.io/tonistiigi/xx@sha256:c64defb9ed5a91eacb37f ########################## BUILD IMAGE ########################## # hadolint ignore=DL3006 -FROM --platform=$BUILDPLATFORM docker.io/library/rust:1.94.1-slim-trixie AS build +FROM --platform=$BUILDPLATFORM docker.io/library/rust:1.95.0-slim-trixie AS build COPY --from=xx / / ARG TARGETARCH ARG TARGETVARIANT diff --git a/rust-toolchain.toml b/rust-toolchain.toml index 151be09f..775ded5a 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -1,4 +1,4 @@ [toolchain] -channel = "1.94.1" +channel = "1.95.0" components = [ "rustfmt", "clippy" ] profile = "minimal" diff --git a/src/api/identity.rs b/src/api/identity.rs index c24c4641..b6d659c6 100644 --- a/src/api/identity.rs +++ b/src/api/identity.rs @@ -11,7 +11,7 @@ use serde_json::Value; use crate::{ api::{ core::{ - accounts::{PreloginData, RegisterData, _prelogin, _register, kdf_upgrade}, + accounts::{_prelogin, _register, kdf_upgrade, PreloginData, RegisterData}, log_user_event, two_factor::{ authenticator, duo, duo_oidc, email, enforce_2fa_policy, is_twofactor_provider_usable, webauthn, diff --git a/src/http_client.rs b/src/http_client.rs index 5462ef8e..df52e2bc 100644 --- a/src/http_client.rs +++ b/src/http_client.rs @@ -6,7 +6,7 @@ use std::{ time::Duration, }; -use hickory_resolver::{name_server::TokioConnectionProvider, TokioResolver}; +use hickory_resolver::{net::runtime::TokioRuntimeProvider, TokioResolver}; use regex::Regex; use reqwest::{ dns::{Name, Resolve, Resolving}, @@ -184,35 +184,35 @@ impl CustomDnsResolver { } fn new() -> Arc { - match TokioResolver::builder(TokioConnectionProvider::default()) { - Ok(mut builder) => { - if CONFIG.dns_prefer_ipv6() { - builder.options_mut().ip_strategy = hickory_resolver::config::LookupIpStrategy::Ipv6thenIpv4; + TokioResolver::builder(TokioRuntimeProvider::default()) + .and_then(|mut builder| { + // Hickory's default since v0.26 is `Ipv6AndIpv4`, which sorts IPv6 first + // This might cause issues on IPv4 only systems or containers + // Unless someone enabled DNS_PREFER_IPV6, use Ipv4AndIpv6, which returns IPv4 first which was our previous default + if !CONFIG.dns_prefer_ipv6() { + builder.options_mut().ip_strategy = hickory_resolver::config::LookupIpStrategy::Ipv4AndIpv6; } - let resolver = builder.build(); - Arc::new(Self::Hickory(Arc::new(resolver))) - } - Err(e) => { - warn!("Error creating Hickory resolver, falling back to default: {e:?}"); - Arc::new(Self::Default()) - } - } + builder.build() + }) + .inspect_err(|e| warn!("Error creating Hickory resolver, falling back to default: {e:?}")) + .map(|resolver| Arc::new(Self::Hickory(Arc::new(resolver)))) + .unwrap_or_else(|_| Arc::new(Self::Default())) } // Note that we get an iterator of addresses, but we only grab the first one for convenience - async fn resolve_domain(&self, name: &str) -> Result, BoxError> { + async fn resolve_domain(&self, name: &str) -> Result, BoxError> { pre_resolve(name)?; - let result = match self { - Self::Default() => tokio::net::lookup_host(name).await?.next(), - Self::Hickory(r) => r.lookup_ip(name).await?.iter().next().map(|a| SocketAddr::new(a, 0)), + let results: Vec = match self { + Self::Default() => tokio::net::lookup_host((name, 0)).await?.collect(), + Self::Hickory(r) => r.lookup_ip(name).await?.iter().map(|i| SocketAddr::new(i, 0)).collect(), }; - if let Some(addr) = &result { + for addr in &results { post_resolve(name, addr.ip())?; } - Ok(result) + Ok(results) } } @@ -242,8 +242,11 @@ impl Resolve for CustomDnsResolver { let this = self.clone(); Box::pin(async move { let name = name.as_str(); - let result = this.resolve_domain(name).await?; - Ok::(Box::new(result.into_iter())) + let results = this.resolve_domain(name).await?; + if results.is_empty() { + warn!("Unable to resolve {name} to any valid IP address"); + } + Ok::(Box::new(results.into_iter())) }) } } From 7cf0c5d67eb81c8b4f2e86b5c8d030bb330faa28 Mon Sep 17 00:00:00 2001 From: Mathijs van Veluw Date: Wed, 22 Apr 2026 14:29:35 +0200 Subject: [PATCH 61/89] Update web-vault and crates (#7121) - Updated web-vault to v2026.3.1 Added a new endpoint needed for the admin console to work - Updated all crates including webpki CVE fixes - Closes #7115 - Updated GHA Signed-off-by: BlackDex --- .github/workflows/trivy.yml | 2 +- Cargo.lock | 225 ++++++++++++------ Cargo.toml | 6 +- docker/DockerSettings.yaml | 4 +- docker/Dockerfile.alpine | 12 +- docker/Dockerfile.debian | 12 +- src/api/core/organizations.rs | 10 + .../templates/scss/vaultwarden.scss.hbs | 8 + 8 files changed, 188 insertions(+), 91 deletions(-) diff --git a/.github/workflows/trivy.yml b/.github/workflows/trivy.yml index 3f27d339..26f64aed 100644 --- a/.github/workflows/trivy.yml +++ b/.github/workflows/trivy.yml @@ -38,7 +38,7 @@ jobs: persist-credentials: false - name: Run Trivy vulnerability scanner - uses: aquasecurity/trivy-action@57a97c7e7821a5776cebc9bb87c984fa69cba8f1 # v0.35.0 + uses: aquasecurity/trivy-action@ed142fd0673e97e23eac54620cfb913e5ce36c25 # v0.36.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 diff --git a/Cargo.lock b/Cargo.lock index da11650d..298a8d80 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -351,9 +351,9 @@ checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" [[package]] name = "aws-config" -version = "1.8.15" +version = "1.8.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11493b0bad143270fb8ad284a096dd529ba91924c5409adeac856cc1bf047dbc" +checksum = "50f156acdd2cf55f5aa53ee416c4ac851cf1222694506c0b1f78c85695e9ca9d" dependencies = [ "aws-credential-types", "aws-runtime", @@ -393,9 +393,9 @@ dependencies = [ [[package]] name = "aws-runtime" -version = "1.7.2" +version = "1.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5fc0651c57e384202e47153c1260b84a9936e19803d747615edf199dc3b98d17" +checksum = "5dcd93c82209ac7413532388067dce79be5a8780c1786e5fae3df22e4dee2864" dependencies = [ "aws-credential-types", "aws-sigv4", @@ -418,9 +418,9 @@ dependencies = [ [[package]] name = "aws-sdk-sso" -version = "1.97.0" +version = "1.98.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9aadc669e184501caaa6beafb28c6267fc1baef0810fb58f9b205485ca3f2567" +checksum = "d69c77aafa20460c68b6b3213c84f6423b6e76dbf89accd3e1789a686ffd9489" dependencies = [ "aws-credential-types", "aws-runtime", @@ -442,9 +442,9 @@ dependencies = [ [[package]] name = "aws-sdk-ssooidc" -version = "1.99.0" +version = "1.100.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1342a7db8f358d3de0aed2007a0b54e875458e39848d54cc1d46700b2bfcb0a8" +checksum = "1c7e7b09346d5ca22a2a08267555843a6a0127fb20d8964cb6ecfb8fdb190225" dependencies = [ "aws-credential-types", "aws-runtime", @@ -466,9 +466,9 @@ dependencies = [ [[package]] name = "aws-sdk-sts" -version = "1.102.0" +version = "1.103.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fc35b7a14cabdad13795fbbbd26d5ddec0882c01492ceedf2af575aad5f37dd" +checksum = "c2249b81a2e73a8027c41c378463a81ec39b8510f184f2caab87de912af0f49b" dependencies = [ "aws-credential-types", "aws-runtime", @@ -491,9 +491,9 @@ dependencies = [ [[package]] name = "aws-sigv4" -version = "1.4.2" +version = "1.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0b660013a6683ab23797778e21f1f854744fdf05f68204b4cca4c8c04b5d1f4" +checksum = "68dc0b907359b120170613b5c09ccc61304eac3998ff6274b97d93ee6490115a" dependencies = [ "aws-credential-types", "aws-smithy-http", @@ -502,11 +502,11 @@ dependencies = [ "bytes", "form_urlencoded", "hex", - "hmac", + "hmac 0.13.0", "http 0.2.12", "http 1.4.0", "percent-encoding", - "sha2", + "sha2 0.11.0", "time", "tracing", ] @@ -658,9 +658,9 @@ dependencies = [ [[package]] name = "aws-types" -version = "1.3.14" +version = "1.3.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47c8323699dd9b3c8d5b3c13051ae9cdef58fd179957c882f8374dd8725962d9" +checksum = "2f4bbcaa9304ea40902d3d5f42a0428d1bd895a2b0f6999436fb279ffddc58ac" dependencies = [ "aws-credential-types", "aws-smithy-async", @@ -757,7 +757,7 @@ version = "0.10.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe" dependencies = [ - "digest", + "digest 0.10.7", ] [[package]] @@ -769,6 +769,15 @@ dependencies = [ "generic-array", ] +[[package]] +name = "block-buffer" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdd35008169921d80bc60d3d0ab416eecb028c4cd653352907921d95084790be" +dependencies = [ + "hybrid-array", +] + [[package]] name = "block-padding" version = "0.3.3" @@ -957,10 +966,16 @@ version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" dependencies = [ - "crypto-common", + "crypto-common 0.1.6", "inout", ] +[[package]] +name = "cmov" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f88a43d011fc4a6876cb7344703e297c71dda42494fee094d5f7c76bf13f746" + [[package]] name = "codemap" version = "0.1.3" @@ -1012,6 +1027,12 @@ version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" +[[package]] +name = "const-oid" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c" + [[package]] name = "const-random" version = "0.1.18" @@ -1201,6 +1222,24 @@ dependencies = [ "typenum", ] +[[package]] +name = "crypto-common" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77727bb15fa921304124b128af125e7e3b968275d1b108b379190264f4423710" +dependencies = [ + "hybrid-array", +] + +[[package]] +name = "ctutils" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d5515a3834141de9eafb9717ad39eea8247b5674e6066c404e8c4b365d2a29e" +dependencies = [ + "cmov", +] + [[package]] name = "curve25519-dalek" version = "4.1.3" @@ -1210,7 +1249,7 @@ dependencies = [ "cfg-if", "cpufeatures 0.2.17", "curve25519-dalek-derive", - "digest", + "digest 0.10.7", "fiat-crypto", "rustc_version", "subtle", @@ -1364,7 +1403,7 @@ version = "0.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" dependencies = [ - "const-oid", + "const-oid 0.9.6", "pem-rfc7468", "zeroize", ] @@ -1556,12 +1595,24 @@ version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ - "block-buffer", - "const-oid", - "crypto-common", + "block-buffer 0.10.4", + "const-oid 0.9.6", + "crypto-common 0.1.6", "subtle", ] +[[package]] +name = "digest" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4850db49bf08e663084f7fb5c87d202ef91a3907271aff24a94eb97ff039153c" +dependencies = [ + "block-buffer 0.12.0", + "const-oid 0.10.2", + "crypto-common 0.2.1", + "ctutils", +] + [[package]] name = "displaydoc" version = "0.2.5" @@ -1630,7 +1681,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca" dependencies = [ "der", - "digest", + "digest 0.10.7", "elliptic-curve", "rfc6979", "signature", @@ -1656,7 +1707,7 @@ dependencies = [ "curve25519-dalek", "ed25519", "serde", - "sha2", + "sha2 0.10.9", "subtle", "zeroize", ] @@ -1675,7 +1726,7 @@ checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47" dependencies = [ "base16ct", "crypto-bigint", - "digest", + "digest 0.10.7", "ff", "generic-array", "group", @@ -2285,7 +2336,7 @@ version = "0.12.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" dependencies = [ - "hmac", + "hmac 0.12.1", ] [[package]] @@ -2294,7 +2345,16 @@ version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" dependencies = [ - "digest", + "digest 0.10.7", +] + +[[package]] +name = "hmac" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6303bc9732ae41b04cb554b844a762b4115a61bfaa81e3e83050991eeb56863f" +dependencies = [ + "digest 0.11.2", ] [[package]] @@ -2393,6 +2453,15 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" +[[package]] +name = "hybrid-array" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3944cf8cf766b40e2a1a333ee5e9b563f854d5fa49d6a8ca2764e97c6eddb214" +dependencies = [ + "typenum", +] + [[package]] name = "hyper" version = "0.14.32" @@ -2868,7 +2937,7 @@ dependencies = [ "base64 0.22.1", "ed25519-dalek", "getrandom 0.2.17", - "hmac", + "hmac 0.12.1", "js-sys", "p256", "p384", @@ -2877,7 +2946,7 @@ dependencies = [ "rsa", "serde", "serde_json", - "sha2", + "sha2 0.10.9", "signature", "simple_asn1", ] @@ -2960,12 +3029,11 @@ checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" [[package]] name = "libmimalloc-sys" -version = "0.1.44" +version = "0.1.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "667f4fec20f29dfc6bc7357c582d91796c169ad7e2fce709468aefeb2c099870" +checksum = "2d1eacfa31c33ec25e873c136ba5669f00f9866d0688bea7be4d3f7e43067df6" dependencies = [ "cc", - "libc", ] [[package]] @@ -3060,7 +3128,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" dependencies = [ "cfg-if", - "digest", + "digest 0.10.7", ] [[package]] @@ -3092,9 +3160,9 @@ dependencies = [ [[package]] name = "mimalloc" -version = "0.1.48" +version = "0.1.50" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1ee66a4b64c74f4ef288bcbb9192ad9c3feaad75193129ac8509af543894fd8" +checksum = "b3627c4272df786b9260cabaa46aec1d59c93ede723d4c3ef646c503816b0640" dependencies = [ "libmimalloc-sys", ] @@ -3344,7 +3412,7 @@ dependencies = [ "serde", "serde_json", "serde_path_to_error", - "sha2", + "sha2 0.10.9", "thiserror 1.0.69", "url", ] @@ -3407,7 +3475,7 @@ dependencies = [ "chrono", "dyn-clone", "ed25519-dalek", - "hmac", + "hmac 0.12.1", "http 1.4.0", "itertools", "log", @@ -3422,7 +3490,7 @@ dependencies = [ "serde_path_to_error", "serde_plain", "serde_with", - "sha2", + "sha2 0.10.9", "subtle", "thiserror 1.0.69", "url", @@ -3430,9 +3498,9 @@ dependencies = [ [[package]] name = "openssl" -version = "0.10.77" +version = "0.10.78" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfe4646e360ec77dff7dde40ed3d6c5fee52d156ef4a62f53973d38294dad87f" +checksum = "f38c4372413cdaaf3cc79dd92d29d7d9f5ab09b51b10dded508fb90bb70b9222" dependencies = [ "bitflags", "cfg-if", @@ -3471,9 +3539,9 @@ dependencies = [ [[package]] name = "openssl-sys" -version = "0.9.113" +version = "0.9.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad2f2c0eba47118757e4c6d2bff2838f3e0523380021356e7875e858372ce644" +checksum = "13ce1245cd07fcc4cfdb438f7507b0c7e4f3849a69fd84d52374c66d83741bb6" dependencies = [ "cc", "libc", @@ -3516,7 +3584,7 @@ dependencies = [ "ecdsa", "elliptic-curve", "primeorder", - "sha2", + "sha2 0.10.9", ] [[package]] @@ -3528,7 +3596,7 @@ dependencies = [ "ecdsa", "elliptic-curve", "primeorder", - "sha2", + "sha2 0.10.9", ] [[package]] @@ -3589,8 +3657,8 @@ version = "0.12.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2" dependencies = [ - "digest", - "hmac", + "digest 0.10.7", + "hmac 0.12.1", ] [[package]] @@ -3681,7 +3749,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "89815c69d36021a140146f26659a81d6c2afa33d216d736dd4be5381a7362220" dependencies = [ "pest", - "sha2", + "sha2 0.10.9", ] [[package]] @@ -3795,7 +3863,7 @@ dependencies = [ "der", "pbkdf2", "scrypt", - "sha2", + "sha2 0.10.9", "spki", ] @@ -4256,7 +4324,7 @@ dependencies = [ "form_urlencoded", "getrandom 0.2.17", "hex", - "hmac", + "hmac 0.12.1", "home", "http 1.4.0", "jsonwebtoken 9.3.1", @@ -4271,7 +4339,7 @@ dependencies = [ "serde", "serde_json", "sha1", - "sha2", + "sha2 0.10.9", "tokio", "toml 0.8.23", ] @@ -4336,7 +4404,7 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2" dependencies = [ - "hmac", + "hmac 0.12.1", "subtle", ] @@ -4484,15 +4552,15 @@ version = "0.9.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b8573f03f5883dcaebdfcf4725caa1ecb9c15b2ef50c43a07b816e06799bb12d" dependencies = [ - "const-oid", - "digest", + "const-oid 0.9.6", + "digest 0.10.7", "num-bigint-dig", "num-integer", "num-traits", "pkcs1", "pkcs8", "rand_core 0.6.4", - "sha2", + "sha2 0.10.9", "signature", "spki", "subtle", @@ -4588,7 +4656,7 @@ dependencies = [ "once_cell", "ring", "rustls-pki-types", - "rustls-webpki 0.103.12", + "rustls-webpki 0.103.13", "subtle", "zeroize", ] @@ -4636,9 +4704,9 @@ dependencies = [ [[package]] name = "rustls-webpki" -version = "0.103.12" +version = "0.103.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8279bb85272c9f10811ae6a6c547ff594d6a7f3c6c6b02ee9726d1d0dcfcdd06" +checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e" dependencies = [ "ring", "rustls-pki-types", @@ -4737,7 +4805,7 @@ checksum = "0516a385866c09368f0b5bcd1caff3366aace790fcd46e2bb032697bb172fd1f" dependencies = [ "pbkdf2", "salsa20", - "sha2", + "sha2 0.10.9", ] [[package]] @@ -4945,7 +5013,7 @@ checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" dependencies = [ "cfg-if", "cpufeatures 0.2.17", - "digest", + "digest 0.10.7", ] [[package]] @@ -4956,7 +5024,18 @@ checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" dependencies = [ "cfg-if", "cpufeatures 0.2.17", - "digest", + "digest 0.10.7", +] + +[[package]] +name = "sha2" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "446ba717509524cb3f22f17ecc096f10f4822d76ab5c0b9822c5f9c284e825f4" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.0", + "digest 0.11.2", ] [[package]] @@ -5000,7 +5079,7 @@ version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" dependencies = [ - "digest", + "digest 0.10.7", "rand_core 0.6.4", ] @@ -5103,9 +5182,9 @@ dependencies = [ [[package]] name = "sqlite-wasm-rs" -version = "0.5.2" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f4206ed3a67690b9c29b77d728f6acc3ce78f16bf846d83c94f76400320181b" +checksum = "1b2c760607300407ddeaee518acf28c795661b7108c75421303dbefb237d3a36" dependencies = [ "cc", "js-sys", @@ -5518,7 +5597,7 @@ version = "1.1.2+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" dependencies = [ - "winnow 1.0.1", + "winnow 1.0.2", ] [[package]] @@ -5533,10 +5612,10 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8e43134db17199f7f721803383ac5854edd0d3d523cc34dba321d6acfbe76c3" dependencies = [ - "digest", - "hmac", + "digest 0.10.7", + "hmac 0.12.1", "sha1", - "sha2", + "sha2 0.10.9", ] [[package]] @@ -5678,9 +5757,9 @@ dependencies = [ [[package]] name = "typenum" -version = "1.19.0" +version = "1.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" +checksum = "40ce102ab67701b8526c123c1bab5cbe42d7040ccfd0f64af1a385808d2f43de" [[package]] name = "ubyte" @@ -6491,9 +6570,9 @@ dependencies = [ [[package]] name = "winnow" -version = "1.0.1" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09dac053f1cd375980747450bfc7250c264eaae0583872e845c0c7cd578872b5" +checksum = "2ee1708bef14716a11bae175f579062d4554d95be2c6829f518df847b7b3fdd0" [[package]] name = "wit-bindgen" @@ -6665,7 +6744,7 @@ dependencies = [ "base64 0.22.1", "form_urlencoded", "futures", - "hmac", + "hmac 0.12.1", "rand 0.9.4", "reqwest", "sha1", diff --git a/Cargo.toml b/Cargo.toml index 5b68b8d4..79eebec0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -162,7 +162,7 @@ cookie = "0.18.1" cookie_store = "0.22.1" # Used by U2F, JWT and PostgreSQL -openssl = "0.10.77" +openssl = "0.10.78" # CLI argument parsing pico-args = "0.5.0" @@ -180,7 +180,7 @@ semver = "1.0.28" # Allow overriding the default memory allocator # Mainly used for the musl builds, since the default musl malloc is very slow -mimalloc = { version = "0.1.48", features = ["secure"], default-features = false, optional = true } +mimalloc = { version = "0.1.50", features = ["secure"], default-features = false, optional = true } which = "8.0.2" @@ -198,7 +198,7 @@ opendal = { version = "0.55.0", features = ["services-fs"], default-features = f # For retrieving AWS credentials, including temporary SSO credentials anyhow = { version = "1.0.102", optional = true } -aws-config = { version = "1.8.15", features = ["behavior-version-latest", "rt-tokio", "credentials-process", "sso"], default-features = false, optional = true } +aws-config = { version = "1.8.16", features = ["behavior-version-latest", "rt-tokio", "credentials-process", "sso"], default-features = false, optional = true } aws-credential-types = { version = "1.2.14", optional = true } aws-smithy-runtime-api = { version = "1.12.0", optional = true } http = { version = "1.4.0", optional = true } diff --git a/docker/DockerSettings.yaml b/docker/DockerSettings.yaml index d105cc40..9a5d7f02 100644 --- a/docker/DockerSettings.yaml +++ b/docker/DockerSettings.yaml @@ -1,6 +1,6 @@ --- -vault_version: "v2026.2.0" -vault_image_digest: "sha256:37c8661fa59dcdfbd3baa8366b6e950ef292b15adfeff1f57812b075c1fd3447" +vault_version: "v2026.3.1" +vault_image_digest: "sha256:c1b1f212333f95bff4ef8d00e8e3589c4ae8eda018691f28f8bddc7e971dd767" # Cross Compile Docker Helper Scripts v1.9.0 # We use the linux/amd64 platform shell scripts since there is no difference between the different platform scripts # https://github.com/tonistiigi/xx | https://hub.docker.com/r/tonistiigi/xx/tags diff --git a/docker/Dockerfile.alpine b/docker/Dockerfile.alpine index 761d00f5..44517aa3 100644 --- a/docker/Dockerfile.alpine +++ b/docker/Dockerfile.alpine @@ -19,15 +19,15 @@ # - From https://hub.docker.com/r/vaultwarden/web-vault/tags, # click the tag name to view the digest of the image it currently points to. # - From the command line: -# $ docker pull docker.io/vaultwarden/web-vault:v2026.2.0 -# $ docker image inspect --format "{{.RepoDigests}}" docker.io/vaultwarden/web-vault:v2026.2.0 -# [docker.io/vaultwarden/web-vault@sha256:37c8661fa59dcdfbd3baa8366b6e950ef292b15adfeff1f57812b075c1fd3447] +# $ docker pull docker.io/vaultwarden/web-vault:v2026.3.1 +# $ docker image inspect --format "{{.RepoDigests}}" docker.io/vaultwarden/web-vault:v2026.3.1 +# [docker.io/vaultwarden/web-vault@sha256:c1b1f212333f95bff4ef8d00e8e3589c4ae8eda018691f28f8bddc7e971dd767] # # - Conversely, to get the tag name from the digest: -# $ docker image inspect --format "{{.RepoTags}}" docker.io/vaultwarden/web-vault@sha256:37c8661fa59dcdfbd3baa8366b6e950ef292b15adfeff1f57812b075c1fd3447 -# [docker.io/vaultwarden/web-vault:v2026.2.0] +# $ docker image inspect --format "{{.RepoTags}}" docker.io/vaultwarden/web-vault@sha256:c1b1f212333f95bff4ef8d00e8e3589c4ae8eda018691f28f8bddc7e971dd767 +# [docker.io/vaultwarden/web-vault:v2026.3.1] # -FROM --platform=linux/amd64 docker.io/vaultwarden/web-vault@sha256:37c8661fa59dcdfbd3baa8366b6e950ef292b15adfeff1f57812b075c1fd3447 AS vault +FROM --platform=linux/amd64 docker.io/vaultwarden/web-vault@sha256:c1b1f212333f95bff4ef8d00e8e3589c4ae8eda018691f28f8bddc7e971dd767 AS vault ########################## ALPINE BUILD IMAGES ########################## ## NOTE: The Alpine Base Images do not support other platforms then linux/amd64 and linux/arm64 diff --git a/docker/Dockerfile.debian b/docker/Dockerfile.debian index 5ecee4fd..d472cbb6 100644 --- a/docker/Dockerfile.debian +++ b/docker/Dockerfile.debian @@ -19,15 +19,15 @@ # - From https://hub.docker.com/r/vaultwarden/web-vault/tags, # click the tag name to view the digest of the image it currently points to. # - From the command line: -# $ docker pull docker.io/vaultwarden/web-vault:v2026.2.0 -# $ docker image inspect --format "{{.RepoDigests}}" docker.io/vaultwarden/web-vault:v2026.2.0 -# [docker.io/vaultwarden/web-vault@sha256:37c8661fa59dcdfbd3baa8366b6e950ef292b15adfeff1f57812b075c1fd3447] +# $ docker pull docker.io/vaultwarden/web-vault:v2026.3.1 +# $ docker image inspect --format "{{.RepoDigests}}" docker.io/vaultwarden/web-vault:v2026.3.1 +# [docker.io/vaultwarden/web-vault@sha256:c1b1f212333f95bff4ef8d00e8e3589c4ae8eda018691f28f8bddc7e971dd767] # # - Conversely, to get the tag name from the digest: -# $ docker image inspect --format "{{.RepoTags}}" docker.io/vaultwarden/web-vault@sha256:37c8661fa59dcdfbd3baa8366b6e950ef292b15adfeff1f57812b075c1fd3447 -# [docker.io/vaultwarden/web-vault:v2026.2.0] +# $ docker image inspect --format "{{.RepoTags}}" docker.io/vaultwarden/web-vault@sha256:c1b1f212333f95bff4ef8d00e8e3589c4ae8eda018691f28f8bddc7e971dd767 +# [docker.io/vaultwarden/web-vault:v2026.3.1] # -FROM --platform=linux/amd64 docker.io/vaultwarden/web-vault@sha256:37c8661fa59dcdfbd3baa8366b6e950ef292b15adfeff1f57812b075c1fd3447 AS vault +FROM --platform=linux/amd64 docker.io/vaultwarden/web-vault@sha256:c1b1f212333f95bff4ef8d00e8e3589c4ae8eda018691f28f8bddc7e971dd767 AS vault ########################## Cross Compile Docker Helper Scripts ########################## ## We use the linux/amd64 no matter which Build Platform, since these are all bash scripts diff --git a/src/api/core/organizations.rs b/src/api/core/organizations.rs index a2448236..318001dc 100644 --- a/src/api/core/organizations.rs +++ b/src/api/core/organizations.rs @@ -101,6 +101,7 @@ pub fn routes() -> Vec { get_billing_metadata, get_billing_warnings, get_auto_enroll_status, + get_self_host_billing_metadata, ] } @@ -2213,6 +2214,15 @@ fn get_billing_warnings(_org_id: OrganizationId, _headers: OrgMemberHeaders) -> })) } +#[get("/organizations/<_org_id>/billing/vnext/self-host/metadata")] +fn get_self_host_billing_metadata(_org_id: OrganizationId, _headers: OrgMemberHeaders) -> Json { + // Prevent a 404 error, which also causes Javascript errors. + Json(json!({ + "isOnSecretsManagerStandalone": false, // Secrets Manager is not supported by Vaultwarden + "organizationOccupiedSeats": 0 // Vaultwarden does not count seats + })) +} + fn _empty_data_json() -> Value { json!({ "object": "list", diff --git a/src/static/templates/scss/vaultwarden.scss.hbs b/src/static/templates/scss/vaultwarden.scss.hbs index 230ac2e7..477cdd34 100644 --- a/src/static/templates/scss/vaultwarden.scss.hbs +++ b/src/static/templates/scss/vaultwarden.scss.hbs @@ -137,6 +137,14 @@ bit-nav-logo bit-nav-item .bwi-shield { app-user-layout app-danger-zone button:nth-child(1) { @extend %vw-hide; } + +/* Hide unsupported Forwarding email alias options */ +ng-dropdown-panel div.ng-dropdown-panel-items div:has(> [title="Firefox Relay"]) { + @extend %vw-hide; +} +ng-dropdown-panel div.ng-dropdown-panel-items div:has(> [title="DuckDuckGo"]) { + @extend %vw-hide; +} /**** END Static Vaultwarden Changes ****/ /**** START Dynamic Vaultwarden Changes ****/ {{#if signup_disabled}} From e5681258f0ff9de2435c161f1543418b01cf5dc7 Mon Sep 17 00:00:00 2001 From: Timshel Date: Tue, 28 Apr 2026 16:33:45 +0000 Subject: [PATCH 62/89] SSO fallback to UserInfo preferred_username (#7128) Co-authored-by: Timshel --- src/sso.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/sso.rs b/src/sso.rs index 2f56f3a6..4844cf1a 100644 --- a/src/sso.rs +++ b/src/sso.rs @@ -283,7 +283,7 @@ pub async fn exchange_code( let email_verified = id_claims.email_verified().or(user_info.email_verified()); - let user_name = id_claims.preferred_username().map(|un| un.to_string()); + let user_name = id_claims.preferred_username().or(user_info.preferred_username()).map(|un| un.to_string()); let refresh_token = token_response.refresh_token().map(|t| t.secret()); if refresh_token.is_none() && CONFIG.sso_scopes_vec().contains(&"offline_access".to_string()) { From cc57e60886e147e38f4099742b110a37d1255f7d Mon Sep 17 00:00:00 2001 From: Timshel Date: Tue, 28 Apr 2026 16:33:49 +0000 Subject: [PATCH 63/89] Dummy identifier need to pass for a guid (#7154) Co-authored-by: Timshel --- src/sso.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/sso.rs b/src/sso.rs index 4844cf1a..26ea7375 100644 --- a/src/sso.rs +++ b/src/sso.rs @@ -17,7 +17,7 @@ use crate::{ CONFIG, }; -pub static FAKE_SSO_IDENTIFIER: &str = "vaultwarden-dummy-oidc-identifier"; +pub static FAKE_SSO_IDENTIFIER: &str = "00000000-01DC-01DC-01DC-000000000000"; static SSO_JWT_ISSUER: LazyLock = LazyLock::new(|| format!("{}|sso", CONFIG.domain_origin())); From fd2b6528a9f47f730bad223076a1ee90da5806f9 Mon Sep 17 00:00:00 2001 From: Stefan Melmuk <509385+stefan0xC@users.noreply.github.com> Date: Tue, 28 Apr 2026 18:33:52 +0200 Subject: [PATCH 64/89] add new /identity/accounts/prelogin/password (#7156) --- src/api/identity.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/api/identity.rs b/src/api/identity.rs index b6d659c6..c38fcd34 100644 --- a/src/api/identity.rs +++ b/src/api/identity.rs @@ -41,6 +41,7 @@ pub fn routes() -> Vec { routes![ login, prelogin, + prelogin_password, identity_register, register_verification_email, register_finish, @@ -982,6 +983,11 @@ async fn prelogin(data: Json, conn: DbConn) -> Json { _prelogin(data, conn).await } +#[post("/accounts/prelogin/password", data = "")] +async fn prelogin_password(data: Json, conn: DbConn) -> Json { + _prelogin(data, conn).await +} + #[post("/accounts/register", data = "")] async fn identity_register(data: Json, conn: DbConn) -> JsonResult { _register(data, false, conn).await From 7883da554e875fbd3b710ac4ec85601a666b7bef Mon Sep 17 00:00:00 2001 From: Daniel Date: Tue, 28 Apr 2026 19:34:03 +0300 Subject: [PATCH 65/89] Add DuckDuckGo browser device type (#7147) - sync with upstream --- src/db/models/device.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/db/models/device.rs b/src/db/models/device.rs index 1026574c..7364a2ec 100644 --- a/src/db/models/device.rs +++ b/src/db/models/device.rs @@ -25,7 +25,7 @@ pub struct Device { pub user_uuid: UserId, pub name: String, - pub atype: i32, // https://github.com/bitwarden/server/blob/9ebe16587175b1c0e9208f84397bb75d0d595510/src/Core/Enums/DeviceType.cs + pub atype: i32, // https://github.com/bitwarden/server/blob/8d547dcc280babab70dd4a3c94ced6a34b12dfbf/src/Core/Enums/DeviceType.cs pub push_uuid: Option, pub push_token: Option, @@ -332,6 +332,8 @@ pub enum DeviceType { MacOsCLI = 24, #[display("Linux CLI")] LinuxCLI = 25, + #[display("DuckDuckGo")] + DuckDuckGoBrowser = 26, } impl DeviceType { @@ -363,6 +365,7 @@ impl DeviceType { 23 => DeviceType::WindowsCLI, 24 => DeviceType::MacOsCLI, 25 => DeviceType::LinuxCLI, + 26 => DeviceType::DuckDuckGoBrowser, _ => DeviceType::UnknownBrowser, } } From 454b8e2a35f9b23050cc22736574fdf298da1bee Mon Sep 17 00:00:00 2001 From: Daniel Date: Tue, 28 Apr 2026 19:34:15 +0300 Subject: [PATCH 66/89] Apply `duration_suboptimal_units` lint findings (#7144) Quote from lint description: "Using a smaller unit for a duration that is evenly divisible by a larger unit reduces readability. Readers have to mentally convert values, which can be error-prone and makes the code less clear." --- Cargo.toml | 1 + src/api/core/sends.rs | 2 +- src/api/core/two_factor/webauthn.rs | 2 +- src/db/models/attachment.rs | 2 +- src/util.rs | 2 +- 5 files changed, 5 insertions(+), 4 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 79eebec0..3d719a66 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -301,6 +301,7 @@ branches_sharing_code = "deny" case_sensitive_file_extension_comparisons = "deny" cast_lossless = "deny" clone_on_ref_ptr = "deny" +duration_suboptimal_units = "deny" equatable_if_let = "deny" excessive_precision = "deny" filter_map_next = "deny" diff --git a/src/api/core/sends.rs b/src/api/core/sends.rs index 10bf85be..22abb396 100644 --- a/src/api/core/sends.rs +++ b/src/api/core/sends.rs @@ -574,7 +574,7 @@ async fn download_url(host: &Host, send_id: &SendId, file_id: &SendFileId) -> Re Ok(format!("{}/api/sends/{send_id}/{file_id}?t={token}", &host.host)) } else { - Ok(operator.presign_read(&format!("{send_id}/{file_id}"), Duration::from_secs(5 * 60)).await?.uri().to_string()) + Ok(operator.presign_read(&format!("{send_id}/{file_id}"), Duration::from_mins(5)).await?.uri().to_string()) } } diff --git a/src/api/core/two_factor/webauthn.rs b/src/api/core/two_factor/webauthn.rs index 0ec0e30e..ad17ce36 100644 --- a/src/api/core/two_factor/webauthn.rs +++ b/src/api/core/two_factor/webauthn.rs @@ -38,7 +38,7 @@ static WEBAUTHN: LazyLock = LazyLock::new(|| { let webauthn = WebauthnBuilder::new(&rp_id, &rp_origin) .expect("Creating WebauthnBuilder failed") .rp_name(&domain) - .timeout(Duration::from_millis(60000)); + .timeout(Duration::from_mins(1)); webauthn.build().expect("Building Webauthn failed") }); diff --git a/src/db/models/attachment.rs b/src/db/models/attachment.rs index 4273c22a..7611b927 100644 --- a/src/db/models/attachment.rs +++ b/src/db/models/attachment.rs @@ -50,7 +50,7 @@ impl Attachment { let token = encode_jwt(&generate_file_download_claims(self.cipher_uuid.clone(), self.id.clone())); Ok(format!("{host}/attachments/{}/{}?token={token}", self.cipher_uuid, self.id)) } else { - Ok(operator.presign_read(&self.get_file_path(), Duration::from_secs(5 * 60)).await?.uri().to_string()) + Ok(operator.presign_read(&self.get_file_path(), Duration::from_mins(5)).await?.uri().to_string()) } } diff --git a/src/util.rs b/src/util.rs index 182b7b3b..06f00b98 100644 --- a/src/util.rs +++ b/src/util.rs @@ -734,7 +734,7 @@ where warn!("Can't connect to database, retrying: {e:?}"); - sleep(Duration::from_millis(1_000)).await; + sleep(Duration::from_secs(1)).await; } } } From fcbdebd6d7a6c202af5e711c632d7a2d38449bb8 Mon Sep 17 00:00:00 2001 From: Daniel Date: Tue, 28 Apr 2026 19:34:40 +0300 Subject: [PATCH 67/89] Apply `ref_option` lint findings (#7143) Quote from the lint description: "More flexibility, better memory optimization, and more idiomatic Rust code. &Option in a function signature breaks encapsulation because the caller must own T and move it into an Option to call with it. When returned, the owner must internally store it as Option in order to return it. At a lower level, &Option points to memory with the presence bit flag plus the T value, whereas Option<&T> is usually optimized to a single pointer, so it may be more optimal." --- Cargo.toml | 1 + src/api/admin.rs | 2 +- src/api/core/accounts.rs | 18 ++++++------ src/api/core/ciphers.rs | 14 ++++----- src/api/core/mod.rs | 2 +- src/api/core/organizations.rs | 4 +-- src/api/identity.rs | 54 +++++++++++++++++------------------ src/api/notifications.rs | 2 +- src/api/push.rs | 4 +-- src/config.rs | 6 ++-- 10 files changed, 54 insertions(+), 53 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 3d719a66..1d8a6ca0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -323,6 +323,7 @@ needless_continue = "deny" needless_lifetimes = "deny" option_option = "deny" redundant_clone = "deny" +ref_option = "deny" string_add_assign = "deny" unnecessary_join = "deny" unnecessary_self_imports = "deny" diff --git a/src/api/admin.rs b/src/api/admin.rs index 9a782046..02c976cc 100644 --- a/src/api/admin.rs +++ b/src/api/admin.rs @@ -469,7 +469,7 @@ async fn deauth_user(user_id: UserId, _token: AdminToken, conn: DbConn, nt: Noti if CONFIG.push_enabled() { for device in Device::find_push_devices_by_user(&user.uuid, &conn).await { - match unregister_push_device(&device.push_uuid).await { + match unregister_push_device(device.push_uuid.as_ref()).await { Ok(r) => r, Err(e) => error!("Unable to unregister devices from Bitwarden server: {e}"), }; diff --git a/src/api/core/accounts.rs b/src/api/core/accounts.rs index fa6a3fd2..a8f9768e 100644 --- a/src/api/core/accounts.rs +++ b/src/api/core/accounts.rs @@ -137,7 +137,7 @@ struct KeysData { } /// Trims whitespace from password hints, and converts blank password hints to `None`. -fn clean_password_hint(password_hint: &Option) -> Option { +fn clean_password_hint(password_hint: Option<&String>) -> Option { match password_hint { None => None, Some(h) => match h.trim() { @@ -147,7 +147,7 @@ fn clean_password_hint(password_hint: &Option) -> Option { } } -fn enforce_password_hint_setting(password_hint: &Option) -> EmptyResult { +fn enforce_password_hint_setting(password_hint: Option<&String>) -> EmptyResult { if password_hint.is_some() && !CONFIG.password_hints_allowed() { err!("Password hints have been disabled by the administrator. Remove the hint and try again."); } @@ -245,8 +245,8 @@ pub async fn _register(data: Json, email_verification: bool, conn: // Check against the password hint setting here so if it fails, the user // can retry without losing their invitation below. - let password_hint = clean_password_hint(&data.master_password_hint); - enforce_password_hint_setting(&password_hint)?; + let password_hint = clean_password_hint(data.master_password_hint.as_ref()); + enforce_password_hint_setting(password_hint.as_ref())?; let mut user = match User::find_by_mail(&email, &conn).await { Some(user) => { @@ -353,8 +353,8 @@ async fn post_set_password(data: Json, headers: Headers, conn: // Check against the password hint setting here so if it fails, // the user can retry without losing their invitation below. - let password_hint = clean_password_hint(&data.master_password_hint); - enforce_password_hint_setting(&password_hint)?; + let password_hint = clean_password_hint(data.master_password_hint.as_ref()); + enforce_password_hint_setting(password_hint.as_ref())?; set_kdf_data(&mut user, &data.kdf)?; @@ -515,8 +515,8 @@ async fn post_password(data: Json, headers: Headers, conn: DbCon err!("Invalid password") } - user.password_hint = clean_password_hint(&data.master_password_hint); - enforce_password_hint_setting(&user.password_hint)?; + user.password_hint = clean_password_hint(data.master_password_hint.as_ref()); + enforce_password_hint_setting(user.password_hint.as_ref())?; log_user_event(EventType::UserChangedPassword as i32, &user.uuid, headers.device.atype, &headers.ip.ip, &conn) .await; @@ -1438,7 +1438,7 @@ async fn put_clear_device_token(device_id: DeviceId, conn: DbConn) -> EmptyResul if let Some(device) = Device::find_by_uuid(&device_id, &conn).await { Device::clear_push_token_by_uuid(&device_id, &conn).await?; - unregister_push_device(&device.push_uuid).await?; + unregister_push_device(device.push_uuid.as_ref()).await?; } Ok(()) diff --git a/src/api/core/ciphers.rs b/src/api/core/ciphers.rs index 6d4e1f41..29aa859e 100644 --- a/src/api/core/ciphers.rs +++ b/src/api/core/ciphers.rs @@ -630,7 +630,7 @@ async fn post_ciphers_import(data: Json, headers: Headers, conn: DbC let mut user = headers.user; user.update_revision(&conn).await?; - nt.send_user_update(UpdateType::SyncVault, &user, &headers.device.push_uuid, &conn).await; + nt.send_user_update(UpdateType::SyncVault, &user, headers.device.push_uuid.as_ref(), &conn).await; Ok(()) } @@ -1005,7 +1005,7 @@ async fn put_cipher_share_selected( } // Multi share actions do not send out a push for each cipher, we need to send a general sync here - nt.send_user_update(UpdateType::SyncCiphers, &headers.user, &headers.device.push_uuid, &conn).await; + nt.send_user_update(UpdateType::SyncCiphers, &headers.user, headers.device.push_uuid.as_ref(), &conn).await; Ok(()) } @@ -1618,7 +1618,7 @@ async fn move_cipher_selected( .await; } else { // Multi move actions do not send out a push for each cipher, we need to send a general sync here - nt.send_user_update(UpdateType::SyncCiphers, &headers.user, &headers.device.push_uuid, &conn).await; + nt.send_user_update(UpdateType::SyncCiphers, &headers.user, headers.device.push_uuid.as_ref(), &conn).await; } if cipher_count != accessible_ciphers_count { @@ -1670,7 +1670,7 @@ async fn purge_org_vault( match Membership::find_confirmed_by_user_and_org(&user.uuid, &organization.org_id, &conn).await { Some(member) if member.atype == MembershipType::Owner => { Cipher::delete_all_by_organization(&organization.org_id, &conn).await?; - nt.send_user_update(UpdateType::SyncVault, &user, &headers.device.push_uuid, &conn).await; + nt.send_user_update(UpdateType::SyncVault, &user, headers.device.push_uuid.as_ref(), &conn).await; log_event( EventType::OrganizationPurgedVault as i32, @@ -1710,7 +1710,7 @@ async fn purge_personal_vault( } user.update_revision(&conn).await?; - nt.send_user_update(UpdateType::SyncVault, &user, &headers.device.push_uuid, &conn).await; + nt.send_user_update(UpdateType::SyncVault, &user, headers.device.push_uuid.as_ref(), &conn).await; Ok(()) } @@ -1805,7 +1805,7 @@ async fn _delete_multiple_ciphers( } // Multi delete actions do not send out a push for each cipher, we need to send a general sync here - nt.send_user_update(UpdateType::SyncCiphers, &headers.user, &headers.device.push_uuid, &conn).await; + nt.send_user_update(UpdateType::SyncCiphers, &headers.user, headers.device.push_uuid.as_ref(), &conn).await; Ok(()) } @@ -1873,7 +1873,7 @@ async fn _restore_multiple_ciphers( } // Multi move actions do not send out a push for each cipher, we need to send a general sync here - nt.send_user_update(UpdateType::SyncCiphers, &headers.user, &headers.device.push_uuid, conn).await; + nt.send_user_update(UpdateType::SyncCiphers, &headers.user, headers.device.push_uuid.as_ref(), conn).await; Ok(Json(json!({ "data": ciphers, diff --git a/src/api/core/mod.rs b/src/api/core/mod.rs index 038b9a6d..e0a56e4d 100644 --- a/src/api/core/mod.rs +++ b/src/api/core/mod.rs @@ -124,7 +124,7 @@ async fn post_eq_domains(data: Json, headers: Headers, conn: Db user.save(&conn).await?; - nt.send_user_update(UpdateType::SyncSettings, &user, &headers.device.push_uuid, &conn).await; + nt.send_user_update(UpdateType::SyncSettings, &user, headers.device.push_uuid.as_ref(), &conn).await; Ok(Json(json!({}))) } diff --git a/src/api/core/organizations.rs b/src/api/core/organizations.rs index 318001dc..434f0f9d 100644 --- a/src/api/core/organizations.rs +++ b/src/api/core/organizations.rs @@ -1463,7 +1463,7 @@ async fn _confirm_invite( let save_result = member_to_confirm.save(conn).await; if let Some(user) = User::find_by_uuid(&member_to_confirm.user_uuid, conn).await { - nt.send_user_update(UpdateType::SyncOrgKeys, &user, &headers.device.push_uuid, conn).await; + nt.send_user_update(UpdateType::SyncOrgKeys, &user, headers.device.push_uuid.as_ref(), conn).await; } save_result @@ -1721,7 +1721,7 @@ async fn _delete_member( .await; if let Some(user) = User::find_by_uuid(&member_to_delete.user_uuid, conn).await { - nt.send_user_update(UpdateType::SyncOrgKeys, &user, &headers.device.push_uuid, conn).await; + nt.send_user_update(UpdateType::SyncOrgKeys, &user, headers.device.push_uuid.as_ref(), conn).await; } member_to_delete.delete(conn).await diff --git a/src/api/identity.rs b/src/api/identity.rs index c38fcd34..72323f73 100644 --- a/src/api/identity.rs +++ b/src/api/identity.rs @@ -65,43 +65,43 @@ async fn login( let login_result = match data.grant_type.as_ref() { "refresh_token" => { - _check_is_some(&data.refresh_token, "refresh_token cannot be blank")?; + _check_is_some(data.refresh_token.as_ref(), "refresh_token cannot be blank")?; _refresh_login(data, &conn, &client_header.ip).await } "password" if CONFIG.sso_enabled() && CONFIG.sso_only() => err!("SSO sign-in is required"), "password" => { - _check_is_some(&data.client_id, "client_id cannot be blank")?; - _check_is_some(&data.password, "password cannot be blank")?; - _check_is_some(&data.scope, "scope cannot be blank")?; - _check_is_some(&data.username, "username cannot be blank")?; + _check_is_some(data.client_id.as_ref(), "client_id cannot be blank")?; + _check_is_some(data.password.as_ref(), "password cannot be blank")?; + _check_is_some(data.scope.as_ref(), "scope cannot be blank")?; + _check_is_some(data.username.as_ref(), "username cannot be blank")?; - _check_is_some(&data.device_identifier, "device_identifier cannot be blank")?; - _check_is_some(&data.device_name, "device_name cannot be blank")?; - _check_is_some(&data.device_type, "device_type cannot be blank")?; + _check_is_some(data.device_identifier.as_ref(), "device_identifier cannot be blank")?; + _check_is_some(data.device_name.as_ref(), "device_name cannot be blank")?; + _check_is_some(data.device_type.as_ref(), "device_type cannot be blank")?; - _password_login(data, &mut user_id, &conn, &client_header.ip, &client_version).await + _password_login(data, &mut user_id, &conn, &client_header.ip, client_version.as_ref()).await } "client_credentials" => { - _check_is_some(&data.client_id, "client_id cannot be blank")?; - _check_is_some(&data.client_secret, "client_secret cannot be blank")?; - _check_is_some(&data.scope, "scope cannot be blank")?; + _check_is_some(data.client_id.as_ref(), "client_id cannot be blank")?; + _check_is_some(data.client_secret.as_ref(), "client_secret cannot be blank")?; + _check_is_some(data.scope.as_ref(), "scope cannot be blank")?; - _check_is_some(&data.device_identifier, "device_identifier cannot be blank")?; - _check_is_some(&data.device_name, "device_name cannot be blank")?; - _check_is_some(&data.device_type, "device_type cannot be blank")?; + _check_is_some(data.device_identifier.as_ref(), "device_identifier cannot be blank")?; + _check_is_some(data.device_name.as_ref(), "device_name cannot be blank")?; + _check_is_some(data.device_type.as_ref(), "device_type cannot be blank")?; _api_key_login(data, &mut user_id, &conn, &client_header.ip).await } "authorization_code" if CONFIG.sso_enabled() => { - _check_is_some(&data.client_id, "client_id cannot be blank")?; - _check_is_some(&data.code, "code cannot be blank")?; - _check_is_some(&data.code_verifier, "code verifier cannot be blank")?; + _check_is_some(data.client_id.as_ref(), "client_id cannot be blank")?; + _check_is_some(data.code.as_ref(), "code cannot be blank")?; + _check_is_some(data.code_verifier.as_ref(), "code verifier cannot be blank")?; - _check_is_some(&data.device_identifier, "device_identifier cannot be blank")?; - _check_is_some(&data.device_name, "device_name cannot be blank")?; - _check_is_some(&data.device_type, "device_type cannot be blank")?; + _check_is_some(data.device_identifier.as_ref(), "device_identifier cannot be blank")?; + _check_is_some(data.device_name.as_ref(), "device_name cannot be blank")?; + _check_is_some(data.device_type.as_ref(), "device_type cannot be blank")?; - _sso_login(data, &mut user_id, &conn, &client_header.ip, &client_version).await + _sso_login(data, &mut user_id, &conn, &client_header.ip, client_version.as_ref()).await } "authorization_code" => err!("SSO sign-in is not available"), t => err!("Invalid type", t), @@ -177,7 +177,7 @@ async fn _sso_login( user_id: &mut Option, conn: &DbConn, ip: &ClientIp, - client_version: &Option, + client_version: Option<&ClientVersion>, ) -> JsonResult { AuthMethod::Sso.check_scope(data.scope.as_ref())?; @@ -320,7 +320,7 @@ async fn _password_login( user_id: &mut Option, conn: &DbConn, ip: &ClientIp, - client_version: &Option, + client_version: Option<&ClientVersion>, ) -> JsonResult { // Validate scope AuthMethod::Password.check_scope(data.scope.as_ref())?; @@ -734,7 +734,7 @@ async fn twofactor_auth( data: &ConnectData, device: &mut Device, ip: &ClientIp, - client_version: &Option, + client_version: Option<&ClientVersion>, conn: &DbConn, ) -> ApiResult> { let twofactors = TwoFactor::find_by_user(&user.uuid, conn).await; @@ -879,7 +879,7 @@ async fn _json_err_twofactor( providers: &[i32], user_id: &UserId, data: &ConnectData, - client_version: &Option, + client_version: Option<&ClientVersion>, conn: &DbConn, ) -> ApiResult { let mut result = json!({ @@ -1114,7 +1114,7 @@ struct ConnectData { #[field(name = uncased("code_verifier"))] code_verifier: Option, } -fn _check_is_some(value: &Option, msg: &str) -> EmptyResult { +fn _check_is_some(value: Option<&T>, msg: &str) -> EmptyResult { if value.is_none() { err!(msg) } diff --git a/src/api/notifications.rs b/src/api/notifications.rs index 492fdb19..b1d64472 100644 --- a/src/api/notifications.rs +++ b/src/api/notifications.rs @@ -338,7 +338,7 @@ impl WebSocketUsers { } // NOTE: The last modified date needs to be updated before calling these methods - pub async fn send_user_update(&self, ut: UpdateType, user: &User, push_uuid: &Option, conn: &DbConn) { + pub async fn send_user_update(&self, ut: UpdateType, user: &User, push_uuid: Option<&PushId>, conn: &DbConn) { // Skip any processing if both WebSockets and Push are not active if *NOTIFICATIONS_DISABLED { return; diff --git a/src/api/push.rs b/src/api/push.rs index 5000869d..e3ff1383 100644 --- a/src/api/push.rs +++ b/src/api/push.rs @@ -135,7 +135,7 @@ pub async fn register_push_device(device: &mut Device, conn: &DbConn) -> EmptyRe Ok(()) } -pub async fn unregister_push_device(push_id: &Option) -> EmptyResult { +pub async fn unregister_push_device(push_id: Option<&PushId>) -> EmptyResult { if !CONFIG.push_enabled() || push_id.is_none() { return Ok(()); } @@ -206,7 +206,7 @@ pub async fn push_logout(user: &User, acting_device: Option<&Device>, conn: &DbC } } -pub async fn push_user_update(ut: UpdateType, user: &User, push_uuid: &Option, conn: &DbConn) { +pub async fn push_user_update(ut: UpdateType, user: &User, push_uuid: Option<&PushId>, conn: &DbConn) { if Device::check_user_has_push_device(&user.uuid, conn).await { tokio::task::spawn(send_to_push_relay(json!({ "userId": user.uuid, diff --git a/src/config.rs b/src/config.rs index 6ff09467..ae995f69 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1076,7 +1076,7 @@ fn validate_config(cfg: &ConfigItems, on_update: bool) -> Result<(), Error> { validate_internal_sso_issuer_url(&cfg.sso_authority)?; validate_internal_sso_redirect_url(&cfg.sso_callback_path)?; - validate_sso_master_password_policy(&cfg.sso_master_password_policy)?; + validate_sso_master_password_policy(cfg.sso_master_password_policy.as_ref())?; } if cfg._enable_yubico { @@ -1271,7 +1271,7 @@ fn validate_internal_sso_redirect_url(sso_callback_path: &String) -> Result, + sso_master_password_policy: Option<&String>, ) -> Result, Error> { let policy = sso_master_password_policy.as_ref().map(|mpp| serde_json::from_str::(mpp)); @@ -1725,7 +1725,7 @@ impl Config { } pub fn sso_master_password_policy_value(&self) -> Option { - validate_sso_master_password_policy(&self.sso_master_password_policy()).ok().flatten() + validate_sso_master_password_policy(self.sso_master_password_policy().as_ref()).ok().flatten() } pub fn sso_scopes_vec(&self) -> Vec { From 62748100f04178b9b2df7f9fd8f9884fab59fbd8 Mon Sep 17 00:00:00 2001 From: Timshel Date: Tue, 28 Apr 2026 17:09:47 +0000 Subject: [PATCH 68/89] Fix hardcoded sso identifier (#7157) Co-authored-by: Timshel --- src/api/core/organizations.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/api/core/organizations.rs b/src/api/core/organizations.rs index 434f0f9d..cbff2099 100644 --- a/src/api/core/organizations.rs +++ b/src/api/core/organizations.rs @@ -1979,7 +1979,7 @@ async fn list_policies_token(org_id: OrganizationId, token: &str, conn: DbConn) } // Called during the SSO enrollment return the default policy -#[get("/organizations/vaultwarden-dummy-oidc-identifier/policies/master-password", rank = 1)] +#[get("/organizations/00000000-01DC-01DC-01DC-000000000000/policies/master-password", rank = 1)] fn get_dummy_master_password_policy() -> JsonResult { let (enabled, data) = match CONFIG.sso_master_password_policy_value() { Some(policy) if CONFIG.sso_enabled() => (true, policy.to_string()), From 5cc7360816eba3b8a9898218e066d46abd2d5625 Mon Sep 17 00:00:00 2001 From: Mathijs van Veluw Date: Wed, 29 Apr 2026 22:10:26 +0200 Subject: [PATCH 69/89] Update crates and fix a nightly lint (#7161) Updated all the crates including two which reported a possible CVE Updated Typos Signed-off-by: BlackDex --- .github/workflows/typos.yml | 2 +- Cargo.lock | 116 +++++++++++++++--------------- Cargo.toml | 12 ++-- src/db/models/emergency_access.rs | 5 +- 4 files changed, 67 insertions(+), 68 deletions(-) diff --git a/.github/workflows/typos.yml b/.github/workflows/typos.yml index b3ee311b..375600ed 100644 --- a/.github/workflows/typos.yml +++ b/.github/workflows/typos.yml @@ -23,4 +23,4 @@ jobs: # When this version is updated, do not forget to update this in `.pre-commit-config.yaml` too - name: Spell Check Repo - uses: crate-ci/typos@cf5f1c29a8ac336af8568821ec41919923b05a83 # v1.45.1 + uses: crate-ci/typos@7c572958218557a3272c2d6719629443b5cc26fd # v1.45.2 diff --git a/Cargo.lock b/Cargo.lock index 298a8d80..b9fcceef 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -152,9 +152,9 @@ dependencies = [ [[package]] name = "async-compression" -version = "0.4.41" +version = "0.4.42" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0f9ee0f6e02ffd7ad5816e9464499fba7b3effd01123b515c41d1697c43dad1" +checksum = "e79b3f8a79cccc2898f31920fc69f304859b3bd567490f75ebf51ae1c792a9ac" dependencies = [ "compression-codecs", "compression-core", @@ -903,9 +903,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.60" +version = "1.2.61" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43c5703da9466b66a946814e1adf53ea2c90f10063b86290cc9eb67ce3478a20" +checksum = "d16d90359e986641506914ba71350897565610e87ce0ad9e6f28569db3dd5c6d" dependencies = [ "find-msvc-tools", "jobserver", @@ -994,9 +994,9 @@ dependencies = [ [[package]] name = "compression-codecs" -version = "0.4.37" +version = "0.4.38" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb7b51a7d9c967fc26773061ba86150f19c50c0d65c887cb1fbe295fd16619b7" +checksum = "ce2548391e9c1929c21bf6aa2680af86fe4c1b33e6cea9ac1cfeec0bd11218cf" dependencies = [ "brotli", "compression-core", @@ -1008,9 +1008,9 @@ dependencies = [ [[package]] name = "compression-core" -version = "0.4.31" +version = "0.4.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75984efb6ed102a0d42db99afb6c1948f0380d1d91808d5529916e6c08b49d8d" +checksum = "cc14f565cf027a105f7a44ccf9e5b424348421a1d8952a8fc9d499d313107789" [[package]] name = "concurrent-queue" @@ -1387,9 +1387,9 @@ dependencies = [ [[package]] name = "data-encoding" -version = "2.10.0" +version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7a1e2f27636f116493b8b860f5546edb47c8d8f8ea73e1d2a20be88e28d1fea" +checksum = "a4ae5f15dda3c708c0ade84bfee31ccab44a3da4f88015ed22f63732abe300c8" [[package]] name = "data-url" @@ -1521,9 +1521,9 @@ dependencies = [ [[package]] name = "diesel" -version = "2.3.7" +version = "2.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4ae09a41a4b89f94ec1e053623da8340d996bc32c6517d325a9daad9b239358" +checksum = "78df0e4e8c596662edb07fbfbb7f23769cca35049827df5f909084d956b6aeaf" dependencies = [ "bigdecimal", "bitflags", @@ -1558,9 +1558,9 @@ dependencies = [ [[package]] name = "diesel_derives" -version = "2.3.7" +version = "2.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47618bf0fac06bb670c036e48404c26a865e6a71af4114dfd97dfe89936e404e" +checksum = "0b79402bd1cfb25b65650f0f4901d0e79c095729e2139c8ab779d025968c7099" dependencies = [ "diesel_table_macro_syntax", "dsl_auto_type", @@ -1571,9 +1571,9 @@ dependencies = [ [[package]] name = "diesel_migrations" -version = "2.3.1" +version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "745fd255645f0f1135f9ec55c7b00e0882192af9683ab4731e4bba3da82b8f9c" +checksum = "28d0f4a98124ba6d4ca75da535f65984badec16a003b6e2f94a01e31a79490b8" dependencies = [ "diesel", "migrations_internals", @@ -2455,9 +2455,9 @@ checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" [[package]] name = "hybrid-array" -version = "0.4.10" +version = "0.4.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3944cf8cf766b40e2a1a333ee5e9b563f854d5fa49d6a8ca2764e97c6eddb214" +checksum = "08d46837a0ed51fe95bd3b05de33cd64a1ee88fc797477ca48446872504507c5" dependencies = [ "typenum", ] @@ -2515,7 +2515,7 @@ dependencies = [ "http 1.4.0", "hyper 1.9.0", "hyper-util", - "rustls 0.23.38", + "rustls 0.23.40", "rustls-native-certs", "tokio", "tokio-rustls 0.26.4", @@ -2679,9 +2679,9 @@ dependencies = [ [[package]] name = "idna_adapter" -version = "1.2.1" +version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" dependencies = [ "icu_normalizer", "icu_properties", @@ -2792,9 +2792,9 @@ checksum = "47f142fe24a9c9944451e8349de0a56af5f3e7226dc46f3ed4d4ecc0b85af75e" [[package]] name = "jiff" -version = "0.2.23" +version = "0.2.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a3546dc96b6d42c5f24902af9e2538e82e39ad350b0c766eb3fbf2d8f3d8359" +checksum = "f00b5dbd620d61dfdcb6007c9c1f6054ebd75319f163d886a9055cec1155073d" dependencies = [ "jiff-static", "jiff-tzdb-platform", @@ -2807,9 +2807,9 @@ dependencies = [ [[package]] name = "jiff-static" -version = "0.2.23" +version = "0.2.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a8c8b344124222efd714b73bb41f8b5120b27a7cc1c75593a6ff768d9d05aa4" +checksum = "e000de030ff8022ea1da3f466fbb0f3a809f5e51ed31f6dd931c35181ad8e6d7" dependencies = [ "proc-macro2", "quote", @@ -2903,9 +2903,9 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.95" +version = "0.3.97" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2964e92d1d9dc3364cae4d718d93f227e3abb088e747d92e0395bfdedf1c12ca" +checksum = "a1840c94c045fbcf8ba2812c95db44499f7c64910a912551aaaa541decebcacf" dependencies = [ "cfg-if", "futures-util", @@ -3005,7 +3005,7 @@ dependencies = [ "nom 8.0.0", "percent-encoding", "quoted_printable", - "rustls 0.23.38", + "rustls 0.23.40", "rustls-native-certs", "serde", "socket2 0.6.3", @@ -3017,9 +3017,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.185" +version = "0.2.186" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52ff2c0fe9bc6cb6b14a0592c2ff4fa9ceb83eea9db979b0487cd054946a2b8f" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" [[package]] name = "libm" @@ -3038,9 +3038,9 @@ dependencies = [ [[package]] name = "libsqlite3-sys" -version = "0.36.0" +version = "0.37.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95b4103cffefa72eb8428cb6b47d6627161e51c2739fc5e3b734584157bc642a" +checksum = "b1f111c8c41e7c61a49cd34e44c7619462967221a6443b0ec299e0ac30cfb9b1" dependencies = [ "cc", "pkg-config", @@ -3647,9 +3647,9 @@ checksum = "35fb2e5f958ec131621fdd531e9fc186ed768cbe395337403ae56c17a74c68ec" [[package]] name = "pastey" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b867cad97c0791bbd3aaa6472142568c6c9e8f71937e98379f584cfb0cf35bec" +checksum = "c5a797f0e07bdf071d15742978fc3128ec6c22891c31a3a931513263904c982a" [[package]] name = "pbkdf2" @@ -4070,7 +4070,7 @@ dependencies = [ "quinn-proto", "quinn-udp", "rustc-hash", - "rustls 0.23.38", + "rustls 0.23.40", "socket2 0.6.3", "thiserror 2.0.18", "tokio", @@ -4090,7 +4090,7 @@ dependencies = [ "rand 0.9.4", "ring", "rustc-hash", - "rustls 0.23.38", + "rustls 0.23.40", "rustls-pki-types", "slab", "thiserror 2.0.18", @@ -4371,7 +4371,7 @@ dependencies = [ "percent-encoding", "pin-project-lite", "quinn", - "rustls 0.23.38", + "rustls 0.23.40", "rustls-native-certs", "rustls-pki-types", "serde", @@ -4537,13 +4537,13 @@ dependencies = [ [[package]] name = "rpassword" -version = "7.4.0" +version = "7.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "66d4c8b64f049c6721ec8ccec37ddfc3d641c4a7fca57e8f2a89de509c73df39" +checksum = "2501c67132bd19c3005b0111fba298907ef002c8c1cf68e25634707e38bf66fe" dependencies = [ "libc", "rtoolbox", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -4648,9 +4648,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.38" +version = "0.23.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69f9466fb2c14ea04357e91413efb882e2a6d4a406e625449bc0a5d360d53a21" +checksum = "ef86cd5876211988985292b91c96a8f2d298df24e75989a43a3c73f2d4d8168b" dependencies = [ "log", "once_cell", @@ -4684,9 +4684,9 @@ dependencies = [ [[package]] name = "rustls-pki-types" -version = "1.14.0" +version = "1.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd" +checksum = "30a7197ae7eb376e574fe940d068c30fe0462554a3ddbe4eca7838e049c937a9" dependencies = [ "web-time", "zeroize", @@ -5493,7 +5493,7 @@ version = "0.26.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" dependencies = [ - "rustls 0.23.38", + "rustls 0.23.40", "tokio", ] @@ -5911,7 +5911,7 @@ dependencies = [ "opendal", "openidconnect", "openssl", - "pastey 0.2.1", + "pastey 0.2.2", "percent-encoding", "pico-args", "rand 0.10.1", @@ -6006,9 +6006,9 @@ dependencies = [ [[package]] name = "wasm-bindgen" -version = "0.2.118" +version = "0.2.120" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bf938a0bacb0469e83c1e148908bd7d5a6010354cf4fb73279b7447422e3a89" +checksum = "df52b6d9b87e0c74c9edfa1eb2d9bf85e5d63515474513aa50fa181b3c4f5db1" dependencies = [ "cfg-if", "once_cell", @@ -6019,9 +6019,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.68" +version = "0.4.70" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f371d383f2fb139252e0bfac3b81b265689bf45b6874af544ffa4c975ac1ebf8" +checksum = "af934872acec734c2d80e6617bbb5ff4f12b052dd8e6332b0817bce889516084" dependencies = [ "js-sys", "wasm-bindgen", @@ -6029,9 +6029,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.118" +version = "0.2.120" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eeff24f84126c0ec2db7a449f0c2ec963c6a49efe0698c4242929da037ca28ed" +checksum = "78b1041f495fb322e64aca85f5756b2172e35cd459376e67f2a6c9dffcedb103" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -6039,9 +6039,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.118" +version = "0.2.120" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d08065faf983b2b80a79fd87d8254c409281cf7de75fc4b773019824196c904" +checksum = "9dcd0ff20416988a18ac686d4d4d0f6aae9ebf08a389ff5d29012b05af2a1b41" dependencies = [ "bumpalo", "proc-macro2", @@ -6052,9 +6052,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.118" +version = "0.2.120" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5fd04d9e306f1907bd13c6361b5c6bfc7b3b3c095ed3f8a9246390f8dbdee129" +checksum = "49757b3c82ebf16c57d69365a142940b384176c24df52a087fb748e2085359ea" dependencies = [ "unicode-ident", ] @@ -6108,9 +6108,9 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.95" +version = "0.3.97" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f2dfbb17949fa2088e5d39408c48368947b86f7834484e87b73de55bc14d97d" +checksum = "2eadbac71025cd7b0834f20d1fe8472e8495821b4e9801eb0a60bd1f19827602" dependencies = [ "js-sys", "wasm-bindgen", diff --git a/Cargo.toml b/Cargo.toml index 1d8a6ca0..aba94374 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -88,14 +88,14 @@ serde_json = "1.0.149" # A safe, extensible ORM and Query builder # Currently pinned diesel to v2.3.3 as newer version break MySQL/MariaDB compatibility -diesel = { version = "2.3.7", features = ["chrono", "r2d2", "numeric"] } -diesel_migrations = "2.3.1" +diesel = { version = "2.3.8", features = ["chrono", "r2d2", "numeric"] } +diesel_migrations = "2.3.2" derive_more = { version = "2.1.1", features = ["from", "into", "as_ref", "deref", "display"] } diesel-derive-newtype = "2.1.2" # Bundled/Static SQLite -libsqlite3-sys = { version = "0.36.0", features = ["bundled"], optional = true } +libsqlite3-sys = { version = "0.37.0", features = ["bundled"], optional = true } # Crypto-related libraries rand = "0.10.1" @@ -114,7 +114,7 @@ time = "0.3.47" job_scheduler_ng = "2.4.0" # Data encoding library Hex/Base32/Base64 -data-encoding = "2.10.0" +data-encoding = "2.11.0" # JWT library jsonwebtoken = { version = "10.3.0", features = ["use_pem", "rust_crypto"], default-features = false } @@ -168,7 +168,7 @@ openssl = "0.10.78" pico-args = "0.5.0" # Macro ident concatenation -pastey = "0.2.1" +pastey = "0.2.2" governor = "0.10.4" # OIDC for SSO @@ -188,7 +188,7 @@ which = "8.0.2" argon2 = "0.5.3" # Reading a password from the cli for generating the Argon2id ADMIN_TOKEN -rpassword = "7.4.0" +rpassword = "7.5.1" # Loading a dynamic CSS Stylesheet grass_compiler = { version = "0.13.4", default-features = false } diff --git a/src/db/models/emergency_access.rs b/src/db/models/emergency_access.rs index cf7f5385..5ea334a4 100644 --- a/src/db/models/emergency_access.rs +++ b/src/db/models/emergency_access.rs @@ -85,7 +85,8 @@ impl EmergencyAccess { pub async fn to_json_grantee_details(&self, conn: &DbConn) -> Option { let grantee_user = if let Some(grantee_uuid) = &self.grantee_uuid { User::find_by_uuid(grantee_uuid, conn).await.expect("Grantee user not found.") - } else if let Some(email) = self.email.as_deref() { + } else { + let email = self.email.as_deref()?; match User::find_by_mail(email, conn).await { Some(user) => user, None => { @@ -94,8 +95,6 @@ impl EmergencyAccess { return None; } } - } else { - return None; }; Some(json!({ From a354e57659d26149fde0d91b76f83fce94e8f277 Mon Sep 17 00:00:00 2001 From: Mathijs van Veluw Date: Wed, 29 Apr 2026 22:20:59 +0200 Subject: [PATCH 70/89] Fix Host/IP resolving (#7162) IPv4 addresses can also be in decimal or hex formats. These were not checked during the Global IP check, and could bypass it. We now convert everything to the right format before running this check and it will catch these formats. Also updated the `is_global()` function to match Rust's still unstable version. And updated the Image Magic checks to be more precise and filter out any possible broken or invalid formats. While at it, also added several checks to ensure these special formatted IPv4 addresses are still blocked and punycode domains are also correctly resolved. Signed-off-by: BlackDex --- .pre-commit-config.yaml | 2 +- .typos.toml | 2 + src/api/icons.rs | 107 ++++++--------- src/http_client.rs | 282 +++++++++++++++++++++++++++++++++++++--- src/util.rs | 24 +++- 5 files changed, 323 insertions(+), 94 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index b16ae4c6..f10cef65 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -18,7 +18,7 @@ repos: # When this version is updated, do not forget to update this in `.github/workflows/typos.yaml` too - repo: https://github.com/crate-ci/typos - rev: cf5f1c29a8ac336af8568821ec41919923b05a83 # v1.45.1 + rev: 7c572958218557a3272c2d6719629443b5cc26fd # v1.45.2 hooks: - id: typos diff --git a/.typos.toml b/.typos.toml index 59f6d7d6..87c0c4a6 100644 --- a/.typos.toml +++ b/.typos.toml @@ -23,4 +23,6 @@ extend-ignore-re = [ # https://github.com/bitwarden/server/blob/dff9f1cf538198819911cf2c20f8cda3307701c5/src/Notifications/HubHelpers.cs#L86 # https://github.com/bitwarden/clients/blob/9612a4ac45063e372a6fbe87eb253c7cb3c588fb/libs/common/src/auth/services/anonymous-hub.service.ts#L45 "AuthRequestResponseRecieved", + # Ignore Punycode/IDN tests + "xn--.+" ] diff --git a/src/api/icons.rs b/src/api/icons.rs index da83d0c4..b3a66f4d 100644 --- a/src/api/icons.rs +++ b/src/api/icons.rs @@ -19,7 +19,7 @@ use svg_hush::{data_url_filter, Filter}; use crate::{ config::PathType, error::Error, - http_client::{get_reqwest_client_builder, should_block_address, CustomHttpClientError}, + http_client::{get_reqwest_client_builder, get_valid_host, should_block_host, CustomHttpClientError}, util::Cached, CONFIG, }; @@ -81,19 +81,19 @@ static ICON_SIZE_REGEX: LazyLock = LazyLock::new(|| Regex::new(r"(?x)(\d+ // The function name `icon_external` is checked in the `on_response` function in `AppHeaders` // It is used to prevent sending a specific header which breaks icon downloads. // If this function needs to be renamed, also adjust the code in `util.rs` -#[get("//icon.png")] -fn icon_external(domain: &str) -> Cached> { - if !is_valid_domain(domain) { - warn!("Invalid domain: {domain}"); +#[get("//icon.png")] +fn icon_external(host: &str) -> Cached> { + let Ok(host) = get_valid_host(host) else { + warn!("Invalid host: {host}"); + return Cached::ttl(None, CONFIG.icon_cache_negttl(), true); + }; + + if should_block_host(&host).is_err() { + warn!("Blocked address: {host}"); return Cached::ttl(None, CONFIG.icon_cache_negttl(), true); } - if should_block_address(domain) { - warn!("Blocked address: {domain}"); - return Cached::ttl(None, CONFIG.icon_cache_negttl(), true); - } - - let url = CONFIG._icon_service_url().replace("{}", domain); + let url = CONFIG._icon_service_url().replace("{}", &host.to_string()); let redir = match CONFIG.icon_redirect_code() { 301 => Some(Redirect::moved(url)), // legacy permanent redirect 302 => Some(Redirect::found(url)), // legacy temporary redirect @@ -107,12 +107,21 @@ fn icon_external(domain: &str) -> Cached> { Cached::ttl(redir, CONFIG.icon_cache_ttl(), true) } -#[get("//icon.png")] -async fn icon_internal(domain: &str) -> Cached<(ContentType, Vec)> { +#[get("//icon.png")] +async fn icon_internal(host: &str) -> Cached<(ContentType, Vec)> { const FALLBACK_ICON: &[u8] = include_bytes!("../static/images/fallback-icon.png"); - if !is_valid_domain(domain) { - warn!("Invalid domain: {domain}"); + let Ok(host) = get_valid_host(host) else { + warn!("Invalid host: {host}"); + return Cached::ttl( + (ContentType::new("image", "png"), FALLBACK_ICON.to_vec()), + CONFIG.icon_cache_negttl(), + true, + ); + }; + + if should_block_host(&host).is_err() { + warn!("Blocked address: {host}"); return Cached::ttl( (ContentType::new("image", "png"), FALLBACK_ICON.to_vec()), CONFIG.icon_cache_negttl(), @@ -120,16 +129,7 @@ async fn icon_internal(domain: &str) -> Cached<(ContentType, Vec)> { ); } - if should_block_address(domain) { - warn!("Blocked address: {domain}"); - return Cached::ttl( - (ContentType::new("image", "png"), FALLBACK_ICON.to_vec()), - CONFIG.icon_cache_negttl(), - true, - ); - } - - match get_icon(domain).await { + match get_icon(&host.to_string()).await { Some((icon, icon_type)) => { Cached::ttl((ContentType::new("image", icon_type), icon), CONFIG.icon_cache_ttl(), true) } @@ -137,42 +137,6 @@ async fn icon_internal(domain: &str) -> Cached<(ContentType, Vec)> { } } -/// Returns if the domain provided is valid or not. -/// -/// This does some manual checks and makes use of Url to do some basic checking. -/// domains can't be larger then 63 characters (not counting multiple subdomains) according to the RFC's, but we limit the total size to 255. -fn is_valid_domain(domain: &str) -> bool { - const ALLOWED_CHARS: &str = "-."; - - // If parsing the domain fails using Url, it will not work with reqwest. - if let Err(parse_error) = url::Url::parse(format!("https://{domain}").as_str()) { - debug!("Domain parse error: '{domain}' - {parse_error:?}"); - return false; - } else if domain.is_empty() - || domain.contains("..") - || domain.starts_with('.') - || domain.starts_with('-') - || domain.ends_with('-') - { - debug!( - "Domain validation error: '{domain}' is either empty, contains '..', starts with an '.', starts or ends with a '-'" - ); - return false; - } else if domain.len() > 255 { - debug!("Domain validation error: '{domain}' exceeds 255 characters"); - return false; - } - - for c in domain.chars() { - if !c.is_alphanumeric() && !ALLOWED_CHARS.contains(c) { - debug!("Domain validation error: '{domain}' contains an invalid character '{c}'"); - return false; - } - } - - true -} - async fn get_icon(domain: &str) -> Option<(Vec, String)> { let path = format!("{domain}.png"); @@ -367,7 +331,7 @@ async fn get_icon_url(domain: &str) -> Result { tld = domain_parts.next_back().unwrap(), base = domain_parts.next_back().unwrap() ); - if is_valid_domain(&base_domain) { + if get_valid_host(&base_domain).is_ok() { let sslbase = format!("https://{base_domain}"); let httpbase = format!("http://{base_domain}"); debug!("[get_icon_url]: Trying without subdomains '{base_domain}'"); @@ -378,7 +342,7 @@ async fn get_icon_url(domain: &str) -> Result { // When the domain is not an IP, and has less then 2 dots, try to add www. infront of it. } else if is_ip.is_err() && domain.matches('.').count() < 2 { let www_domain = format!("www.{domain}"); - if is_valid_domain(&www_domain) { + if get_valid_host(&www_domain).is_ok() { let sslwww = format!("https://{www_domain}"); let httpwww = format!("http://{www_domain}"); debug!("[get_icon_url]: Trying with www. prefix '{www_domain}'"); @@ -618,14 +582,17 @@ fn get_icon_type(bytes: &[u8]) -> Option<&'static str> { None } + // Some details can be found here: + // - https://www.garykessler.net/library/file_sigs_GCK_latest.html + // - https://en.wikipedia.org/wiki/List_of_file_signatures match bytes { - [137, 80, 78, 71, ..] => Some("png"), - [0, 0, 1, 0, ..] => Some("x-icon"), - [82, 73, 70, 70, ..] => Some("webp"), - [255, 216, 255, ..] => Some("jpeg"), - [71, 73, 70, 56, ..] => Some("gif"), - [66, 77, ..] => Some("bmp"), - [60, 115, 118, 103, ..] => Some("svg+xml"), // Normal svg + [137, 80, 78, 71, 13, 10, 26, 10, ..] => Some("png"), + [0, 0, 1, 0, n1, n2, ..] if u16::from_le_bytes([*n1, *n2]) > 0 => Some("x-icon"), // https://en.wikipedia.org/wiki/ICO_(file_format) + [82, 73, 70, 70, _, _, _, _, 87, 69, 66, 80, ..] => Some("webp"), // Only match WebP Images + [255, 216, 255, b, ..] if *b >= 0xC0 => Some("jpeg"), + [71, 73, 70, 56, 55 | 57, 97, ..] => Some("gif"), + [66, 77, _, _, _, _, 0, 0, 0, 0, ..] => Some("bmp"), // https://en.wikipedia.org/wiki/BMP_file_format + [60, 115, 118, 103, ..] => Some("svg+xml"), // Normal svg [60, 63, 120, 109, 108, ..] => check_svg_after_xml_declaration(bytes), // An svg starting with None, } diff --git a/src/http_client.rs b/src/http_client.rs index df52e2bc..d39b884d 100644 --- a/src/http_client.rs +++ b/src/http_client.rs @@ -1,7 +1,6 @@ use std::{ fmt, net::{IpAddr, SocketAddr}, - str::FromStr, sync::{Arc, LazyLock, Mutex}, time::Duration, }; @@ -59,16 +58,6 @@ pub fn get_reqwest_client_builder() -> ClientBuilder { .timeout(Duration::from_secs(10)) } -pub fn should_block_address(domain_or_ip: &str) -> bool { - if let Ok(ip) = IpAddr::from_str(domain_or_ip) { - if should_block_ip(ip) { - return true; - } - } - - should_block_address_regex(domain_or_ip) -} - fn should_block_ip(ip: IpAddr) -> bool { if !CONFIG.http_request_block_non_global_ips() { return false; @@ -100,11 +89,54 @@ fn should_block_address_regex(domain_or_ip: &str) -> bool { is_match } -fn should_block_host(host: &Host<&str>) -> Result<(), CustomHttpClientError> { +pub fn get_valid_host(host: &str) -> Result { + let Ok(host) = Host::parse(host) else { + return Err(CustomHttpClientError::Invalid { + domain: host.to_string(), + }); + }; + + // Some extra checks to validate hosts + match host { + Host::Domain(ref domain) => { + // Host::parse() does not verify length or all possible invalid characters + // We do some extra checks here to prevent issues + if domain.len() > 253 { + debug!("Domain validation error: '{domain}' exceeds 253 characters"); + return Err(CustomHttpClientError::Invalid { + domain: host.to_string(), + }); + } + if !domain.split('.').all(|label| { + !label.is_empty() + // Labels can't be longer than 63 chars + && label.len() <= 63 + // Labels are not allowed to start or end with a hyphen `-` + && !label.starts_with('-') + && !label.ends_with('-') + // Only ASCII Alphanumeric characters are allowed + // We already received a punycoded domain back, so no unicode should exists here + && label.chars().all(|c| c.is_ascii_alphanumeric() || c == '-') + }) { + debug!( + "Domain validation error: '{domain}' labels contain invalid characters or exceed the maximum length" + ); + return Err(CustomHttpClientError::Invalid { + domain: host.to_string(), + }); + } + } + Host::Ipv4(_) | Host::Ipv6(_) => {} + } + + Ok(host) +} + +pub fn should_block_host>(host: &Host) -> Result<(), CustomHttpClientError> { let (ip, host_str): (Option, String) = match host { Host::Ipv4(ip) => (Some(IpAddr::V4(*ip)), ip.to_string()), Host::Ipv6(ip) => (Some(IpAddr::V6(*ip)), ip.to_string()), - Host::Domain(d) => (None, (*d).to_string()), + Host::Domain(d) => (None, d.as_ref().to_string()), }; if let Some(ip) = ip { @@ -134,6 +166,9 @@ pub enum CustomHttpClientError { domain: Option, ip: IpAddr, }, + Invalid { + domain: String, + }, } impl CustomHttpClientError { @@ -155,7 +190,7 @@ impl fmt::Display for CustomHttpClientError { match self { Self::Blocked { domain, - } => write!(f, "Blocked domain: {domain} matched HTTP_REQUEST_BLOCK_REGEX"), + } => write!(f, "Blocked domain: '{domain}' matched HTTP_REQUEST_BLOCK_REGEX"), Self::NonGlobalIp { domain: Some(domain), ip, @@ -163,7 +198,10 @@ impl fmt::Display for CustomHttpClientError { Self::NonGlobalIp { domain: None, ip, - } => write!(f, "IP {ip} is not a global IP!"), + } => write!(f, "IP '{ip}' is not a global IP!"), + Self::Invalid { + domain, + } => write!(f, "Invalid host: '{domain}' contains invalid characters or exceeds the maximum length"), } } } @@ -217,7 +255,13 @@ impl CustomDnsResolver { } fn pre_resolve(name: &str) -> Result<(), CustomHttpClientError> { - if should_block_address(name) { + let Ok(host) = get_valid_host(name) else { + return Err(CustomHttpClientError::Invalid { + domain: name.to_string(), + }); + }; + + if should_block_host(&host).is_err() { return Err(CustomHttpClientError::Blocked { domain: name.to_string(), }); @@ -308,3 +352,209 @@ pub(crate) mod aws { } } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::util::is_global_hardcoded; + use std::net::Ipv4Addr; + use url::Host; + + // === + // IPv4 numeric-format normalization + fn parse_to_ip(s: &str) -> Option { + match Host::parse(s).ok()? { + Host::Ipv4(v4) => Some(IpAddr::V4(v4)), + Host::Ipv6(v6) => Some(IpAddr::V6(v6)), + Host::Domain(_) => None, + } + } + + #[test] + fn dotted_decimal_loopback_normalizes() { + let ip = parse_to_ip("127.0.0.1").unwrap(); + assert_eq!(ip, IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1))); + assert!(!is_global_hardcoded(ip)); + } + + #[test] + fn single_decimal_loopback_normalizes() { + // 127.0.0.1 == 2130706433 + let ip = parse_to_ip("2130706433").unwrap(); + assert_eq!(ip, IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1))); + assert!(!is_global_hardcoded(ip)); + } + + #[test] + fn hex_loopback_normalizes() { + let ip = parse_to_ip("0x7f000001").unwrap(); + assert_eq!(ip, IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1))); + assert!(!is_global_hardcoded(ip)); + } + + #[test] + fn dotted_hex_loopback_normalizes() { + let ip = parse_to_ip("0x7f.0.0.1").unwrap(); + assert_eq!(ip, IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1))); + assert!(!is_global_hardcoded(ip)); + } + + #[test] + fn octal_loopback_normalizes() { + // 017700000001 == 127.0.0.1 + let ip = parse_to_ip("017700000001").unwrap(); + assert_eq!(ip, IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1))); + assert!(!is_global_hardcoded(ip)); + } + + #[test] + fn dotted_octal_loopback_normalizes() { + let ip = parse_to_ip("0177.0.0.01").unwrap(); + assert_eq!(ip, IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1))); + assert!(!is_global_hardcoded(ip)); + } + + #[test] + fn aws_metadata_decimal_blocked() { + // 169.254.169.254 == 2852039166 (link-local, AWS IMDS) + let ip = parse_to_ip("2852039166").unwrap(); + assert_eq!(ip, IpAddr::V4(Ipv4Addr::new(169, 254, 169, 254))); + assert!(!is_global_hardcoded(ip)); + } + + #[test] + fn rfc1918_hex_blocked() { + // 10.0.0.1 + let ip = parse_to_ip("0x0a000001").unwrap(); + assert!(!is_global_hardcoded(ip)); + } + + #[test] + fn public_ip_decimal_allowed() { + // 8.8.8.8 == 134744072 + let ip = parse_to_ip("134744072").unwrap(); + assert_eq!(ip, IpAddr::V4(Ipv4Addr::new(8, 8, 8, 8))); + assert!(is_global_hardcoded(ip)); + } + + // === + // get_valid_host integration: numeric forms become Host::Ipv4 + #[test] + fn get_valid_host_normalizes_decimal_int() { + let h = get_valid_host("2130706433").expect("valid"); + assert!(matches!(h, Host::Ipv4(ip) if ip == Ipv4Addr::new(127, 0, 0, 1))); + } + + #[test] + fn get_valid_host_normalizes_hex() { + let h = get_valid_host("0x7f000001").expect("valid"); + assert!(matches!(h, Host::Ipv4(ip) if ip == Ipv4Addr::new(127, 0, 0, 1))); + } + + #[test] + fn get_valid_host_normalizes_octal() { + let h = get_valid_host("017700000001").expect("valid"); + assert!(matches!(h, Host::Ipv4(ip) if ip == Ipv4Addr::new(127, 0, 0, 1))); + } + + // === + // IPv6 formats + #[test] + fn ipv6_loopback_blocked() { + let h = get_valid_host("[::1]").expect("valid"); + let Host::Ipv6(ip) = h else { + panic!("expected v6") + }; + assert!(!is_global_hardcoded(IpAddr::V6(ip))); + } + + #[test] + fn ipv4_mapped_in_ipv6_loopback_blocked() { + // ::ffff:127.0.0.1 — v4-mapped form; is_global_hardcoded blocks via ::ffff:0:0/96 + let h = get_valid_host("[::ffff:127.0.0.1]").expect("valid"); + let Host::Ipv6(ip) = h else { + panic!("expected v6") + }; + assert!(!is_global_hardcoded(IpAddr::V6(ip))); + } + + #[test] + fn ipv6_unique_local_blocked() { + let h = get_valid_host("[fc00::1]").expect("valid"); + let Host::Ipv6(ip) = h else { + panic!("expected v6") + }; + assert!(!is_global_hardcoded(IpAddr::V6(ip))); + } + + // === + // Punycode / IDN + #[test] + fn punycode_passthrough() { + let h = get_valid_host("xn--deadbeafcaf-lbb.test").expect("valid"); + match h { + Host::Domain(d) => assert_eq!(d, "xn--deadbeafcaf-lbb.test"), + _ => panic!("expected domain"), + } + } + + #[test] + fn idn_unicode_gets_punycoded() { + let h = get_valid_host("deadbeafcafé.test").expect("valid"); + match h { + Host::Domain(d) => assert_eq!(d, "xn--deadbeafcaf-lbb.test"), + _ => panic!("expected domain"), + } + } + + #[test] + fn idn_unicode_gets_punycoded_tld() { + let h = get_valid_host("deadbeaf.café").expect("valid"); + match h { + Host::Domain(d) => assert_eq!(d, "deadbeaf.xn--caf-dma"), + _ => panic!("expected domain"), + } + } + + #[test] + fn idn_emoji_gets_punycoded() { + let h = get_valid_host("xn--t88h.test").expect("valid"); // 🛡️.test + match h { + Host::Domain(d) => assert_eq!(d, "xn--t88h.test"), + _ => panic!("expected domain"), + } + } + + #[test] + fn idn_unicode_to_punycode_roundtrip() { + let from_unicode = get_valid_host("🛡️.test").expect("valid"); + let from_puny = get_valid_host("xn--t88h.test").expect("valid"); + match (from_unicode, from_puny) { + (Host::Domain(a), Host::Domain(b)) => assert_eq!(a, b), + _ => panic!("expected domains"), + } + } + + #[test] + fn invalid_punycode_rejected() { + // bare invalid punycode + assert!(get_valid_host("xn--").is_err()); + } + + #[test] + fn underscore_in_label_rejected() { + assert!(get_valid_host("dead_beaf.cafe").is_err()); + } + + #[test] + fn label_too_long_rejected() { + let label = "a".repeat(64); + assert!(get_valid_host(&format!("{label}.test")).is_err()); + } + + #[test] + fn domain_too_long_rejected() { + let big = "a.".repeat(130) + "test"; // > 253 + assert!(get_valid_host(&big).is_err()); + } +} diff --git a/src/util.rs b/src/util.rs index 06f00b98..5cd78eed 100644 --- a/src/util.rs +++ b/src/util.rs @@ -818,14 +818,18 @@ pub fn is_global_hardcoded(ip: std::net::IpAddr) -> bool { std::net::IpAddr::V4(ip) => { !(ip.octets()[0] == 0 // "This network" || ip.is_private() - || (ip.octets()[0] == 100 && (ip.octets()[1] & 0b1100_0000 == 0b0100_0000)) //ip.is_shared() + || (ip.octets()[0] == 100 && (ip.octets()[1] & 0b1100_0000 == 0b0100_0000)) // ip.is_shared() || ip.is_loopback() || ip.is_link_local() // addresses reserved for future protocols (`192.0.0.0/24`) - ||(ip.octets()[0] == 192 && ip.octets()[1] == 0 && ip.octets()[2] == 0) + // .9 and .10 are documented as globally reachable so they're excluded + || ( + ip.octets()[0] == 192 && ip.octets()[1] == 0 && ip.octets()[2] == 0 + && ip.octets()[3] != 9 && ip.octets()[3] != 10 + ) || ip.is_documentation() || (ip.octets()[0] == 198 && (ip.octets()[1] & 0xfe) == 18) // ip.is_benchmarking() - || (ip.octets()[0] & 240 == 240 && !ip.is_broadcast()) //ip.is_reserved() + || (ip.octets()[0] & 240 == 240 && !ip.is_broadcast()) // ip.is_reserved() || ip.is_broadcast()) } std::net::IpAddr::V6(ip) => { @@ -849,11 +853,17 @@ pub fn is_global_hardcoded(ip: std::net::IpAddr) -> bool { // AS112-v6 (`2001:4:112::/48`) || matches!(ip.segments(), [0x2001, 4, 0x112, _, _, _, _, _]) // ORCHIDv2 (`2001:20::/28`) - || matches!(ip.segments(), [0x2001, b, _, _, _, _, _, _] if (0x20..=0x2F).contains(&b)) + // Drone Remote ID Protocol Entity Tags (DETs) Prefix (`2001:30::/28`)` + || matches!(ip.segments(), [0x2001, b, _, _, _, _, _, _] if (0x20..=0x3F).contains(&b)) )) - || ((ip.segments()[0] == 0x2001) && (ip.segments()[1] == 0xdb8)) // ip.is_documentation() - || ((ip.segments()[0] & 0xfe00) == 0xfc00) //ip.is_unique_local() - || ((ip.segments()[0] & 0xffc0) == 0xfe80)) //ip.is_unicast_link_local() + // 6to4 (`2002::/16`) – it's not explicitly documented as globally reachable, + // IANA says N/A. + || matches!(ip.segments(), [0x2002, _, _, _, _, _, _, _]) + || matches!(ip.segments(), [0x2001, 0xdb8, ..] | [0x3fff, 0..=0x0fff, ..]) // ip.is_documentation() + // Segment Routing (SRv6) SIDs (`5f00::/16`) + || matches!(ip.segments(), [0x5f00, ..]) + || ip.is_unique_local() + || ip.is_unicast_link_local()) } } } From d297e274a35dccd0f5d935e9d5934e0f7e9c0a87 Mon Sep 17 00:00:00 2001 From: Mathijs van Veluw Date: Wed, 29 Apr 2026 22:25:36 +0200 Subject: [PATCH 71/89] Several SSO Fixes (#7163) * Ensure SSO token is only usable on the same client This commit adds an extra check via cookies to ensure the same browser/client is used to request and provide the SSO token. Previously it would be able to provide a custom link which attackers could use to steal data. While an attacker would still need the Master Password to be able to decrypt or execute specific actions, they were able to fetch encrypted data. Solved with some help of Claude Code. Signed-off-by: BlackDex * Check email-verified on SSO login/create This commit prevents possible account takeover via SSO which doesn't check/validate or provide validated status of the email. It was checked at other locations, but was skipped here. Signed-off-by: BlackDex * Prevent data disclosure via SSO endpoints This commit prevents some data disclosure and user enumeration by only returning the fake SSO identifier. Since we do not check the identifier anywhere useful, returning the fake one is just fine. During an invite to an org, that link contains the correct UUID and will be used for the master password requirements. For anything else, server admins should set the `SSO_MASTER_PASSWORD_POLICY` env variable. Signed-off-by: BlackDex * Adjust admin layout to fix issues when SSO is enabled Signed-off-by: BlackDex --------- Signed-off-by: BlackDex --- .../down.sql | 1 + .../2026-04-25-120000_sso_auth_binding/up.sql | 1 + .../down.sql | 1 + .../2026-04-25-120000_sso_auth_binding/up.sql | 1 + .../down.sql | 1 + .../2026-04-25-120000_sso_auth_binding/up.sql | 1 + src/api/core/organizations.rs | 42 ++++------- src/api/identity.rs | 71 +++++++++++++++++-- src/crypto.rs | 7 ++ src/db/models/sso_auth.rs | 10 ++- src/db/schema.rs | 1 + src/sso.rs | 3 +- src/sso_client.rs | 3 +- src/static/scripts/admin.css | 15 +++- src/static/templates/admin/base.hbs | 2 +- src/static/templates/admin/diagnostics.hbs | 2 +- src/static/templates/admin/login.hbs | 2 +- src/static/templates/admin/organizations.hbs | 2 +- src/static/templates/admin/settings.hbs | 2 +- src/static/templates/admin/users.hbs | 4 +- 20 files changed, 125 insertions(+), 47 deletions(-) create mode 100644 migrations/mysql/2026-04-25-120000_sso_auth_binding/down.sql create mode 100644 migrations/mysql/2026-04-25-120000_sso_auth_binding/up.sql create mode 100644 migrations/postgresql/2026-04-25-120000_sso_auth_binding/down.sql create mode 100644 migrations/postgresql/2026-04-25-120000_sso_auth_binding/up.sql create mode 100644 migrations/sqlite/2026-04-25-120000_sso_auth_binding/down.sql create mode 100644 migrations/sqlite/2026-04-25-120000_sso_auth_binding/up.sql diff --git a/migrations/mysql/2026-04-25-120000_sso_auth_binding/down.sql b/migrations/mysql/2026-04-25-120000_sso_auth_binding/down.sql new file mode 100644 index 00000000..17e3d8c7 --- /dev/null +++ b/migrations/mysql/2026-04-25-120000_sso_auth_binding/down.sql @@ -0,0 +1 @@ +ALTER TABLE sso_auth DROP COLUMN binding_hash; diff --git a/migrations/mysql/2026-04-25-120000_sso_auth_binding/up.sql b/migrations/mysql/2026-04-25-120000_sso_auth_binding/up.sql new file mode 100644 index 00000000..53ee8063 --- /dev/null +++ b/migrations/mysql/2026-04-25-120000_sso_auth_binding/up.sql @@ -0,0 +1 @@ +ALTER TABLE sso_auth ADD COLUMN binding_hash TEXT; diff --git a/migrations/postgresql/2026-04-25-120000_sso_auth_binding/down.sql b/migrations/postgresql/2026-04-25-120000_sso_auth_binding/down.sql new file mode 100644 index 00000000..17e3d8c7 --- /dev/null +++ b/migrations/postgresql/2026-04-25-120000_sso_auth_binding/down.sql @@ -0,0 +1 @@ +ALTER TABLE sso_auth DROP COLUMN binding_hash; diff --git a/migrations/postgresql/2026-04-25-120000_sso_auth_binding/up.sql b/migrations/postgresql/2026-04-25-120000_sso_auth_binding/up.sql new file mode 100644 index 00000000..53ee8063 --- /dev/null +++ b/migrations/postgresql/2026-04-25-120000_sso_auth_binding/up.sql @@ -0,0 +1 @@ +ALTER TABLE sso_auth ADD COLUMN binding_hash TEXT; diff --git a/migrations/sqlite/2026-04-25-120000_sso_auth_binding/down.sql b/migrations/sqlite/2026-04-25-120000_sso_auth_binding/down.sql new file mode 100644 index 00000000..17e3d8c7 --- /dev/null +++ b/migrations/sqlite/2026-04-25-120000_sso_auth_binding/down.sql @@ -0,0 +1 @@ +ALTER TABLE sso_auth DROP COLUMN binding_hash; diff --git a/migrations/sqlite/2026-04-25-120000_sso_auth_binding/up.sql b/migrations/sqlite/2026-04-25-120000_sso_auth_binding/up.sql new file mode 100644 index 00000000..53ee8063 --- /dev/null +++ b/migrations/sqlite/2026-04-25-120000_sso_auth_binding/up.sql @@ -0,0 +1 @@ +ALTER TABLE sso_auth ADD COLUMN binding_hash TEXT; diff --git a/src/api/core/organizations.rs b/src/api/core/organizations.rs index cbff2099..31311a65 100644 --- a/src/api/core/organizations.rs +++ b/src/api/core/organizations.rs @@ -907,36 +907,21 @@ async fn _get_org_details( Ok(json!(ciphers_json)) } -#[derive(Deserialize)] -#[serde(rename_all = "camelCase")] -struct OrgDomainDetails { - email: String, -} - // Returning a Domain/Organization here allow to prefill it and prevent prompting the user -// So we either return an Org name associated to the user or a dummy value. +// So we return a dummy value, since we only support a single SSO integration, and do not use the response anywhere // In use since `v2025.6.0`, appears to use only the first `organizationIdentifier` -#[post("/organizations/domain/sso/verified", data = "")] -async fn get_org_domain_sso_verified(data: Json, conn: DbConn) -> JsonResult { - let data: OrgDomainDetails = data.into_inner(); - - let identifiers = match Organization::find_org_user_email(&data.email, &conn) - .await - .into_iter() - .map(|o| (o.name, o.uuid.to_string())) - .collect::>() - { - v if !v.is_empty() => v, - _ => vec![(FAKE_SSO_IDENTIFIER.to_string(), FAKE_SSO_IDENTIFIER.to_string())], - }; - +#[post("/organizations/domain/sso/verified")] +fn get_org_domain_sso_verified() -> JsonResult { + // Always return a dummy value, no matter if SSO is enabled or not Ok(Json(json!({ "object": "list", - "data": identifiers.into_iter().map(|(name, identifier)| json!({ - "organizationName": name, // appear unused - "organizationIdentifier": identifier, - "domainName": CONFIG.domain(), // appear unused - })).collect::>() + "data": [{ + "organizationIdentifier": FAKE_SSO_IDENTIFIER, + // These appear to be unused + "organizationName": FAKE_SSO_IDENTIFIER, + "domainName": CONFIG.domain() + }], + "continuationToken": null }))) } @@ -3049,10 +3034,7 @@ async fn put_reset_password_enrollment( err!("User to enroll isn't member of required organization", "The user_id and acting user do not match"); } - let Some(mut membership) = Membership::find_confirmed_by_user_and_org(&headers.user.uuid, &org_id, &conn).await - else { - err!("User to enroll isn't member of required organization") - }; + let mut membership = headers.membership; check_reset_password_applicable(&org_id, &conn).await?; diff --git a/src/api/identity.rs b/src/api/identity.rs index 72323f73..569deaf9 100644 --- a/src/api/identity.rs +++ b/src/api/identity.rs @@ -2,6 +2,7 @@ use chrono::Utc; use num_traits::FromPrimitive; use rocket::{ form::{Form, FromForm}, + http::{Cookie, CookieJar, SameSite}, response::Redirect, serde::json::Json, Route, @@ -23,7 +24,8 @@ use crate::{ ApiResult, EmptyResult, JsonResult, }, auth, - auth::{generate_organization_api_key_login_claims, AuthMethod, ClientHeaders, ClientIp, ClientVersion}, + auth::{generate_organization_api_key_login_claims, AuthMethod, ClientHeaders, ClientIp, ClientVersion, Secure}, + crypto, db::{ models::{ AuthRequest, AuthRequestId, Device, DeviceId, EventType, Invitation, OIDCCodeWrapper, OrganizationApiKey, @@ -228,7 +230,33 @@ async fn _sso_login( } ) } - Some((user, None)) => Some((user, None)), + Some((user, None)) => match user_infos.email_verified { + None if !CONFIG.sso_allow_unknown_email_verification() => { + error!( + "Login failure ({}), existing non SSO user ({}) with same email ({}) and email verification status is unknown", + user_infos.identifier, user.uuid, user.email + ); + err_silent!( + "Email verification status is unknown", + ErrorEvent { + event: EventType::UserFailedLogIn + } + ) + } + Some(false) => { + error!( + "Login failure ({}), existing non SSO user ({}) with same email ({}) and email is not verified", + user_infos.identifier, user.uuid, user.email + ); + err_silent!( + "Email is not verified by the SSO provider", + ErrorEvent { + event: EventType::UserFailedLogIn + } + ) + } + _ => Some((user, None)), + }, }, Some((user, sso_user)) => Some((user, Some(sso_user))), }; @@ -1133,13 +1161,16 @@ fn prevalidate() -> JsonResult { } } +const SSO_BINDING_COOKIE: &str = "VW_SSO_BINDING"; + #[get("/connect/oidc-signin?&", rank = 1)] -async fn oidcsignin(code: OIDCCode, state: String, mut conn: DbConn) -> ApiResult { +async fn oidcsignin(code: OIDCCode, state: String, cookies: &CookieJar<'_>, mut conn: DbConn) -> ApiResult { _oidcsignin_redirect( state, OIDCCodeWrapper::Ok { code, }, + cookies, &mut conn, ) .await @@ -1152,6 +1183,7 @@ async fn oidcsignin_error( state: String, error: String, error_description: Option, + cookies: &CookieJar<'_>, mut conn: DbConn, ) -> ApiResult { _oidcsignin_redirect( @@ -1160,6 +1192,7 @@ async fn oidcsignin_error( error, error_description, }, + cookies, &mut conn, ) .await @@ -1171,6 +1204,7 @@ async fn oidcsignin_error( async fn _oidcsignin_redirect( base64_state: String, code_response: OIDCCodeWrapper, + cookies: &CookieJar<'_>, conn: &mut DbConn, ) -> ApiResult { let state = sso::decode_state(&base64_state)?; @@ -1179,6 +1213,17 @@ async fn _oidcsignin_redirect( None => err!(format!("Cannot retrieve sso_auth for {state}")), Some(sso_auth) => sso_auth, }; + + // Browser-binding check + // The cookie was set on /connect/authorize and must come from the same browser that initiated the flow. + let cookie_value = cookies.get(SSO_BINDING_COOKIE).map(|c| c.value().to_string()); + let provided_hash = cookie_value.as_deref().map(|v| crypto::sha256_hex(v.as_bytes())); + match (sso_auth.binding_hash.as_deref(), provided_hash.as_deref()) { + (Some(expected), Some(actual)) if crypto::ct_eq(expected, actual) => {} + _ => err!(format!("SSO session binding mismatch for {state}")), + } + cookies.remove(Cookie::build(SSO_BINDING_COOKIE).path("/identity/connect/").build()); + sso_auth.code_response = Some(code_response); sso_auth.updated_at = Utc::now().naive_utc(); sso_auth.save(conn).await?; @@ -1225,7 +1270,7 @@ struct AuthorizeData { // The `redirect_uri` will change depending of the client (web, android, ios ..) #[get("/connect/authorize?")] -async fn authorize(data: AuthorizeData, conn: DbConn) -> ApiResult { +async fn authorize(data: AuthorizeData, cookies: &CookieJar<'_>, secure: Secure, conn: DbConn) -> ApiResult { let AuthorizeData { client_id, redirect_uri, @@ -1239,7 +1284,23 @@ async fn authorize(data: AuthorizeData, conn: DbConn) -> ApiResult { err!("Unsupported code challenge method"); } - let auth_url = sso::authorize_url(state, code_challenge, &client_id, &redirect_uri, conn).await?; + // Generate browser-binding token. Stored hashed in DB; raw value handed to the browser as a cookie. + // Validated on /connect/oidc-signin + let binding_token = data_encoding::BASE64URL_NOPAD.encode(&crypto::get_random_bytes::<32>()); + let binding_hash = crypto::sha256_hex(binding_token.as_bytes()); + + let auth_url = + sso::authorize_url(state, code_challenge, &client_id, &redirect_uri, Some(binding_hash), conn).await?; + + cookies.add( + Cookie::build((SSO_BINDING_COOKIE, binding_token)) + .path("/identity/connect/") + .max_age(time::Duration::seconds(sso::SSO_AUTH_EXPIRATION.num_seconds())) + .same_site(SameSite::Lax) // Lax is needed because the IdP runs on a different FQDN + .http_only(true) + .secure(secure.https) + .build(), + ); Ok(Redirect::temporary(String::from(auth_url))) } diff --git a/src/crypto.rs b/src/crypto.rs index 1930f380..46d305a5 100644 --- a/src/crypto.rs +++ b/src/crypto.rs @@ -113,3 +113,10 @@ pub fn ct_eq, U: AsRef<[u8]>>(a: T, b: U) -> bool { use subtle::ConstantTimeEq; a.as_ref().ct_eq(b.as_ref()).into() } + +// +// SHA256 +// +pub fn sha256_hex(data: &[u8]) -> String { + HEXLOWER.encode(digest::digest(&digest::SHA256, data).as_ref()) +} diff --git a/src/db/models/sso_auth.rs b/src/db/models/sso_auth.rs index fec0433a..2c6eec6d 100644 --- a/src/db/models/sso_auth.rs +++ b/src/db/models/sso_auth.rs @@ -54,11 +54,18 @@ pub struct SsoAuth { pub auth_response: Option, pub created_at: NaiveDateTime, pub updated_at: NaiveDateTime, + pub binding_hash: Option, } /// Local methods impl SsoAuth { - pub fn new(state: OIDCState, client_challenge: OIDCCodeChallenge, nonce: String, redirect_uri: String) -> Self { + pub fn new( + state: OIDCState, + client_challenge: OIDCCodeChallenge, + nonce: String, + redirect_uri: String, + binding_hash: Option, + ) -> Self { let now = Utc::now().naive_utc(); SsoAuth { @@ -70,6 +77,7 @@ impl SsoAuth { updated_at: now, code_response: None, auth_response: None, + binding_hash, } } } diff --git a/src/db/schema.rs b/src/db/schema.rs index 914b4fe9..147440e5 100644 --- a/src/db/schema.rs +++ b/src/db/schema.rs @@ -265,6 +265,7 @@ table! { auth_response -> Nullable, created_at -> Timestamp, updated_at -> Timestamp, + binding_hash -> Nullable, } } diff --git a/src/sso.rs b/src/sso.rs index 26ea7375..7505f84f 100644 --- a/src/sso.rs +++ b/src/sso.rs @@ -188,6 +188,7 @@ pub async fn authorize_url( client_challenge: OIDCCodeChallenge, client_id: &str, raw_redirect_uri: &str, + binding_hash: Option, conn: DbConn, ) -> ApiResult { let redirect_uri = match client_id { @@ -203,7 +204,7 @@ pub async fn authorize_url( _ => err!(format!("Unsupported client {client_id}")), }; - let (auth_url, sso_auth) = Client::authorize_url(state, client_challenge, redirect_uri).await?; + let (auth_url, sso_auth) = Client::authorize_url(state, client_challenge, redirect_uri, binding_hash).await?; sso_auth.save(&conn).await?; Ok(auth_url) } diff --git a/src/sso_client.rs b/src/sso_client.rs index 6204ab48..abff6bcb 100644 --- a/src/sso_client.rs +++ b/src/sso_client.rs @@ -117,6 +117,7 @@ impl Client { state: OIDCState, client_challenge: OIDCCodeChallenge, redirect_uri: String, + binding_hash: Option, ) -> ApiResult<(Url, SsoAuth)> { let scopes = CONFIG.sso_scopes_vec().into_iter().map(Scope::new); let base64_state = data_encoding::BASE64.encode(state.to_string().as_bytes()); @@ -139,7 +140,7 @@ impl Client { } let (auth_url, _, nonce) = auth_req.url(); - Ok((auth_url, SsoAuth::new(state, client_challenge, nonce.secret().clone(), redirect_uri))) + Ok((auth_url, SsoAuth::new(state, client_challenge, nonce.secret().clone(), redirect_uri, binding_hash))) } pub async fn exchange_code( diff --git a/src/static/scripts/admin.css b/src/static/scripts/admin.css index 0df56771..c7c6f443 100644 --- a/src/static/scripts/admin.css +++ b/src/static/scripts/admin.css @@ -1,6 +1,17 @@ body { padding-top: 75px; } +/* Some extra width's for the main layout */ +@media (min-width: 1600px) { + .container-xxl { + max-width: 1520px; + } +} +@media (min-width: 1800px) { + .container-xxl { + max-width: 1720px; + } +} img { width: 48px; height: 48px; @@ -38,8 +49,8 @@ img { max-width: 130px; } #users-table .vw-actions, #orgs-table .vw-actions { - min-width: 155px; - max-width: 160px; + min-width: 170px; + max-width: 180px; } #users-table .vw-org-cell { max-height: 120px; diff --git a/src/static/templates/admin/base.hbs b/src/static/templates/admin/base.hbs index f56d8262..e1dcacb5 100644 --- a/src/static/templates/admin/base.hbs +++ b/src/static/templates/admin/base.hbs @@ -27,7 +27,7 @@
{{/if}}
- {{sso_identifier}} + {{sso_identifier}} From 38a6850b8d0aac9d56dc2978e5bfa33bdd0e3732 Mon Sep 17 00:00:00 2001 From: Matt Aaron <13080357+matt-aaron@users.noreply.github.com> Date: Wed, 29 Apr 2026 16:29:42 -0400 Subject: [PATCH 72/89] Add support for archiving items (#6916) * Add archiving * Update Diesel macros and remove unnecessary SUPPORTED_FEATURE_FLAG * Add IF EXISTS to down.sql migratinos * Rename migration folders, separate logic based on PR threads --- .../2026-03-09-005927_add_archives/down.sql | 1 + .../2026-03-09-005927_add_archives/up.sql | 10 + .../2026-03-09-005927_add_archives/down.sql | 1 + .../2026-03-09-005927_add_archives/up.sql | 8 + .../2026-03-09-005927_add_archives/down.sql | 1 + .../2026-03-09-005927_add_archives/up.sql | 8 + src/api/core/ciphers.rs | 175 +++++++++++++++++- src/api/core/mod.rs | 4 +- src/db/models/archive.rs | 91 +++++++++ src/db/models/cipher.rs | 21 ++- src/db/models/mod.rs | 2 + src/db/schema.rs | 11 ++ 12 files changed, 324 insertions(+), 9 deletions(-) create mode 100644 migrations/mysql/2026-03-09-005927_add_archives/down.sql create mode 100644 migrations/mysql/2026-03-09-005927_add_archives/up.sql create mode 100644 migrations/postgresql/2026-03-09-005927_add_archives/down.sql create mode 100644 migrations/postgresql/2026-03-09-005927_add_archives/up.sql create mode 100644 migrations/sqlite/2026-03-09-005927_add_archives/down.sql create mode 100644 migrations/sqlite/2026-03-09-005927_add_archives/up.sql create mode 100644 src/db/models/archive.rs diff --git a/migrations/mysql/2026-03-09-005927_add_archives/down.sql b/migrations/mysql/2026-03-09-005927_add_archives/down.sql new file mode 100644 index 00000000..a3ef20c3 --- /dev/null +++ b/migrations/mysql/2026-03-09-005927_add_archives/down.sql @@ -0,0 +1 @@ +DROP TABLE IF EXISTS archives; diff --git a/migrations/mysql/2026-03-09-005927_add_archives/up.sql b/migrations/mysql/2026-03-09-005927_add_archives/up.sql new file mode 100644 index 00000000..6d7a7024 --- /dev/null +++ b/migrations/mysql/2026-03-09-005927_add_archives/up.sql @@ -0,0 +1,10 @@ +DROP TABLE IF EXISTS archives; + +CREATE TABLE archives ( + user_uuid CHAR(36) NOT NULL, + cipher_uuid CHAR(36) NOT NULL, + archived_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (user_uuid, cipher_uuid), + FOREIGN KEY (user_uuid) REFERENCES users (uuid) ON DELETE CASCADE, + FOREIGN KEY (cipher_uuid) REFERENCES ciphers (uuid) ON DELETE CASCADE +); diff --git a/migrations/postgresql/2026-03-09-005927_add_archives/down.sql b/migrations/postgresql/2026-03-09-005927_add_archives/down.sql new file mode 100644 index 00000000..a3ef20c3 --- /dev/null +++ b/migrations/postgresql/2026-03-09-005927_add_archives/down.sql @@ -0,0 +1 @@ +DROP TABLE IF EXISTS archives; diff --git a/migrations/postgresql/2026-03-09-005927_add_archives/up.sql b/migrations/postgresql/2026-03-09-005927_add_archives/up.sql new file mode 100644 index 00000000..c56d01a0 --- /dev/null +++ b/migrations/postgresql/2026-03-09-005927_add_archives/up.sql @@ -0,0 +1,8 @@ +DROP TABLE IF EXISTS archives; + +CREATE TABLE archives ( + user_uuid CHAR(36) NOT NULL REFERENCES users (uuid) ON DELETE CASCADE, + cipher_uuid CHAR(36) NOT NULL REFERENCES ciphers (uuid) ON DELETE CASCADE, + archived_at TIMESTAMP NOT NULL DEFAULT now(), + PRIMARY KEY (user_uuid, cipher_uuid) +); diff --git a/migrations/sqlite/2026-03-09-005927_add_archives/down.sql b/migrations/sqlite/2026-03-09-005927_add_archives/down.sql new file mode 100644 index 00000000..a3ef20c3 --- /dev/null +++ b/migrations/sqlite/2026-03-09-005927_add_archives/down.sql @@ -0,0 +1 @@ +DROP TABLE IF EXISTS archives; diff --git a/migrations/sqlite/2026-03-09-005927_add_archives/up.sql b/migrations/sqlite/2026-03-09-005927_add_archives/up.sql new file mode 100644 index 00000000..d624f57b --- /dev/null +++ b/migrations/sqlite/2026-03-09-005927_add_archives/up.sql @@ -0,0 +1,8 @@ +DROP TABLE IF EXISTS archives; + +CREATE TABLE archives ( + user_uuid CHAR(36) NOT NULL REFERENCES users (uuid) ON DELETE CASCADE, + cipher_uuid CHAR(36) NOT NULL REFERENCES ciphers (uuid) ON DELETE CASCADE, + archived_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (user_uuid, cipher_uuid) +); diff --git a/src/api/core/ciphers.rs b/src/api/core/ciphers.rs index 29aa859e..fab88b98 100644 --- a/src/api/core/ciphers.rs +++ b/src/api/core/ciphers.rs @@ -19,9 +19,9 @@ use crate::{ crypto, db::{ models::{ - Attachment, AttachmentId, Cipher, CipherId, Collection, CollectionCipher, CollectionGroup, CollectionId, - CollectionUser, EventType, Favorite, Folder, FolderCipher, FolderId, Group, Membership, MembershipType, - OrgPolicy, OrgPolicyType, OrganizationId, RepromptType, Send, UserId, + Archive, Attachment, AttachmentId, Cipher, CipherId, Collection, CollectionCipher, CollectionGroup, + CollectionId, CollectionUser, EventType, Favorite, Folder, FolderCipher, FolderId, Group, Membership, + MembershipType, OrgPolicy, OrgPolicyType, OrganizationId, RepromptType, Send, UserId, }, DbConn, DbPool, }, @@ -96,6 +96,10 @@ pub fn routes() -> Vec { post_collections_update, post_collections_admin, put_collections_admin, + archive_cipher_put, + archive_cipher_selected, + unarchive_cipher_put, + unarchive_cipher_selected, ] } @@ -293,6 +297,7 @@ pub struct CipherData { // when using older client versions, or if the operation doesn't involve // updating an existing cipher. last_known_revision_date: Option, + archived_date: Option, } #[derive(Debug, Deserialize)] @@ -534,6 +539,13 @@ pub async fn update_cipher_from_data( cipher.move_to_folder(data.folder_id, &headers.user.uuid, conn).await?; cipher.set_favorite(data.favorite, &headers.user.uuid, conn).await?; + if let Some(dt_str) = data.archived_date { + match NaiveDateTime::parse_from_str(&dt_str, "%+") { + Ok(dt) => cipher.set_archived_at(dt, &headers.user.uuid, conn).await?, + Err(err) => warn!("Error parsing ArchivedDate '{dt_str}': {err}"), + } + } + if ut != UpdateType::None { // Only log events for organizational ciphers if let Some(org_id) = &cipher.organization_uuid { @@ -1715,6 +1727,36 @@ async fn purge_personal_vault( Ok(()) } +#[put("/ciphers//archive")] +async fn archive_cipher_put(cipher_id: CipherId, headers: Headers, conn: DbConn, nt: Notify<'_>) -> JsonResult { + archive_cipher(&cipher_id, &headers, false, &conn, &nt).await +} + +#[put("/ciphers/archive", data = "")] +async fn archive_cipher_selected( + data: Json, + headers: Headers, + conn: DbConn, + nt: Notify<'_>, +) -> JsonResult { + archive_multiple_ciphers(data, &headers, &conn, &nt).await +} + +#[put("/ciphers//unarchive")] +async fn unarchive_cipher_put(cipher_id: CipherId, headers: Headers, conn: DbConn, nt: Notify<'_>) -> JsonResult { + unarchive_cipher(&cipher_id, &headers, false, &conn, &nt).await +} + +#[put("/ciphers/unarchive", data = "")] +async fn unarchive_cipher_selected( + data: Json, + headers: Headers, + conn: DbConn, + nt: Notify<'_>, +) -> JsonResult { + unarchive_multiple_ciphers(data, &headers, &conn, &nt).await +} + #[derive(PartialEq)] pub enum CipherDeleteOptions { SoftSingle, @@ -1933,6 +1975,122 @@ async fn _delete_cipher_attachment_by_id( Ok(Json(json!({"cipher":cipher_json}))) } +async fn archive_cipher( + cipher_id: &CipherId, + headers: &Headers, + multi_archive: bool, + conn: &DbConn, + nt: &Notify<'_>, +) -> JsonResult { + let Some(cipher) = Cipher::find_by_uuid(cipher_id, conn).await else { + err!("Cipher doesn't exist") + }; + + if !cipher.is_accessible_to_user(&headers.user.uuid, conn).await { + err!("Cipher is not accessible for the current user") + } + + cipher.set_archived_at(Utc::now().naive_utc(), &headers.user.uuid, conn).await?; + + if !multi_archive { + nt.send_cipher_update( + UpdateType::SyncCipherUpdate, + &cipher, + &cipher.update_users_revision(conn).await, + &headers.device, + None, + conn, + ) + .await; + } + + Ok(Json(cipher.to_json(&headers.host, &headers.user.uuid, None, CipherSyncType::User, conn).await?)) +} + +async fn unarchive_cipher( + cipher_id: &CipherId, + headers: &Headers, + multi_unarchive: bool, + conn: &DbConn, + nt: &Notify<'_>, +) -> JsonResult { + let Some(cipher) = Cipher::find_by_uuid(cipher_id, conn).await else { + err!("Cipher doesn't exist") + }; + + if !cipher.is_accessible_to_user(&headers.user.uuid, conn).await { + err!("Cipher is not accessible for the current user") + } + + cipher.unarchive(&headers.user.uuid, conn).await?; + + if !multi_unarchive { + nt.send_cipher_update( + UpdateType::SyncCipherUpdate, + &cipher, + &cipher.update_users_revision(conn).await, + &headers.device, + None, + conn, + ) + .await; + } + + Ok(Json(cipher.to_json(&headers.host, &headers.user.uuid, None, CipherSyncType::User, conn).await?)) +} + +async fn archive_multiple_ciphers( + data: Json, + headers: &Headers, + conn: &DbConn, + nt: &Notify<'_>, +) -> JsonResult { + let data = data.into_inner(); + + let mut ciphers: Vec = Vec::new(); + for cipher_id in data.ids { + match archive_cipher(&cipher_id, headers, true, conn, nt).await { + Ok(json) => ciphers.push(json.into_inner()), + err => return err, + } + } + + // Multi archive does not send out a push for each cipher, we need to send a general sync here + nt.send_user_update(UpdateType::SyncCiphers, &headers.user, &headers.device.push_uuid, conn).await; + + Ok(Json(json!({ + "data": ciphers, + "object": "list", + "continuationToken": null + }))) +} + +async fn unarchive_multiple_ciphers( + data: Json, + headers: &Headers, + conn: &DbConn, + nt: &Notify<'_>, +) -> JsonResult { + let data = data.into_inner(); + + let mut ciphers: Vec = Vec::new(); + for cipher_id in data.ids { + match unarchive_cipher(&cipher_id, headers, true, conn, nt).await { + Ok(json) => ciphers.push(json.into_inner()), + err => return err, + } + } + + // Multi unarchive does not send out a push for each cipher, we need to send a general sync here + nt.send_user_update(UpdateType::SyncCiphers, &headers.user, &headers.device.push_uuid, conn).await; + + Ok(Json(json!({ + "data": ciphers, + "object": "list", + "continuationToken": null + }))) +} + /// This will hold all the necessary data to improve a full sync of all the ciphers /// It can be used during the `Cipher::to_json()` call. /// It will prevent the so called N+1 SQL issue by running just a few queries which will hold all the data needed. @@ -1942,6 +2100,7 @@ pub struct CipherSyncData { pub cipher_folders: HashMap, pub cipher_favorites: HashSet, pub cipher_collections: HashMap>, + pub cipher_archives: HashMap, pub members: HashMap, pub user_collections: HashMap, pub user_collections_groups: HashMap, @@ -1958,20 +2117,25 @@ impl CipherSyncData { pub async fn new(user_id: &UserId, sync_type: CipherSyncType, conn: &DbConn) -> Self { let cipher_folders: HashMap; let cipher_favorites: HashSet; + let cipher_archives: HashMap; match sync_type { - // User Sync supports Folders and Favorites + // User Sync supports Folders, Favorites, and Archives CipherSyncType::User => { // Generate a HashMap with the Cipher UUID as key and the Folder UUID as value cipher_folders = FolderCipher::find_by_user(user_id, conn).await.into_iter().collect(); // Generate a HashSet of all the Cipher UUID's which are marked as favorite cipher_favorites = Favorite::get_all_cipher_uuid_by_user(user_id, conn).await.into_iter().collect(); + + // Generate a HashMap with the Cipher UUID as key and the archived date time as value + cipher_archives = Archive::find_by_user(user_id, conn).await.into_iter().collect(); } - // Organization Sync does not support Folders and Favorites. + // Organization Sync does not support Folders, Favorites, or Archives. // If these are set, it will cause issues in the web-vault. CipherSyncType::Organization => { cipher_folders = HashMap::with_capacity(0); cipher_favorites = HashSet::with_capacity(0); + cipher_archives = HashMap::with_capacity(0); } } @@ -2034,6 +2198,7 @@ impl CipherSyncData { }; Self { + cipher_archives, cipher_attachments, cipher_folders, cipher_favorites, diff --git a/src/api/core/mod.rs b/src/api/core/mod.rs index e0a56e4d..ad9002fd 100644 --- a/src/api/core/mod.rs +++ b/src/api/core/mod.rs @@ -204,11 +204,11 @@ fn config() -> Json { // Client (v2026.2.1): https://github.com/bitwarden/clients/blob/f96380c3138291a028bdd2c7a5fee540d5c98ba5/libs/common/src/enums/feature-flag.enum.ts#L12 // Android (v2026.2.1): https://github.com/bitwarden/android/blob/6902c19c0093fa476bbf74ccaa70c9f14afbb82f/core/src/main/kotlin/com/bitwarden/core/data/manager/model/FlagKey.kt#L31 // iOS (v2026.2.1): https://github.com/bitwarden/ios/blob/cdd9ba1770ca2ffc098d02d12cc3208e3a830454/BitwardenShared/Core/Platform/Models/Enum/FeatureFlag.swift#L7 - let feature_states = parse_experimental_client_feature_flags( + let mut feature_states = parse_experimental_client_feature_flags( &CONFIG.experimental_client_feature_flags(), FeatureFlagFilter::ValidOnly, ); - // Add default feature_states here if needed, currently no features are needed by default. + feature_states.insert("pm-19148-innovation-archive".to_string(), true); Json(json!({ // Note: The clients use this version to handle backwards compatibility concerns diff --git a/src/db/models/archive.rs b/src/db/models/archive.rs new file mode 100644 index 00000000..f576e7ed --- /dev/null +++ b/src/db/models/archive.rs @@ -0,0 +1,91 @@ +use chrono::NaiveDateTime; +use diesel::prelude::*; + +use super::{CipherId, User, UserId}; +use crate::api::EmptyResult; +use crate::db::schema::archives; +use crate::db::DbConn; +use crate::error::MapResult; + +#[derive(Identifiable, Queryable, Insertable)] +#[diesel(table_name = archives)] +#[diesel(primary_key(user_uuid, cipher_uuid))] +pub struct Archive { + pub user_uuid: UserId, + pub cipher_uuid: CipherId, + pub archived_at: NaiveDateTime, +} + +impl Archive { + // Returns the date the specified cipher was archived + pub async fn get_archived_at(cipher_uuid: &CipherId, user_uuid: &UserId, conn: &DbConn) -> Option { + db_run! { conn: { + archives::table + .filter(archives::cipher_uuid.eq(cipher_uuid)) + .filter(archives::user_uuid.eq(user_uuid)) + .select(archives::archived_at) + .first::(conn).ok() + }} + } + + // Saves (inserts or updates) an archive record with the provided timestamp + pub async fn save( + user_uuid: &UserId, + cipher_uuid: &CipherId, + archived_at: NaiveDateTime, + conn: &DbConn, + ) -> EmptyResult { + User::update_uuid_revision(user_uuid, conn).await; + db_run! { conn: + sqlite, mysql { + diesel::replace_into(archives::table) + .values(( + archives::user_uuid.eq(user_uuid), + archives::cipher_uuid.eq(cipher_uuid), + archives::archived_at.eq(archived_at), + )) + .execute(conn) + .map_res("Error saving archive") + } + postgresql { + diesel::insert_into(archives::table) + .values(( + archives::user_uuid.eq(user_uuid), + archives::cipher_uuid.eq(cipher_uuid), + archives::archived_at.eq(archived_at), + )) + .on_conflict((archives::user_uuid, archives::cipher_uuid)) + .do_update() + .set(archives::archived_at.eq(archived_at)) + .execute(conn) + .map_res("Error saving archive") + } + } + } + + // Deletes an archive record for a specific cipher + pub async fn delete_by_cipher(user_uuid: &UserId, cipher_uuid: &CipherId, conn: &DbConn) -> EmptyResult { + User::update_uuid_revision(user_uuid, conn).await; + db_run! { conn: { + diesel::delete( + archives::table + .filter(archives::user_uuid.eq(user_uuid)) + .filter(archives::cipher_uuid.eq(cipher_uuid)) + ) + .execute(conn) + .map_res("Error deleting archive") + }} + } + + /// Return a vec with (cipher_uuid, archived_at) + /// This is used during a full sync so we only need one query for all archive matches + pub async fn find_by_user(user_uuid: &UserId, conn: &DbConn) -> Vec<(CipherId, NaiveDateTime)> { + db_run! { conn: { + archives::table + .filter(archives::user_uuid.eq(user_uuid)) + .select((archives::cipher_uuid, archives::archived_at)) + .load::<(CipherId, NaiveDateTime)>(conn) + .unwrap_or_default() + }} + } +} diff --git a/src/db/models/cipher.rs b/src/db/models/cipher.rs index edc5f8c9..87f3e415 100644 --- a/src/db/models/cipher.rs +++ b/src/db/models/cipher.rs @@ -10,8 +10,8 @@ use diesel::prelude::*; use serde_json::Value; use super::{ - Attachment, CollectionCipher, CollectionId, Favorite, FolderCipher, FolderId, Group, Membership, MembershipStatus, - MembershipType, OrganizationId, User, UserId, + Archive, Attachment, CollectionCipher, CollectionId, Favorite, FolderCipher, FolderId, Group, Membership, + MembershipStatus, MembershipType, OrganizationId, User, UserId, }; use crate::api::core::{CipherData, CipherSyncData, CipherSyncType}; use macros::UuidFromParam; @@ -380,6 +380,11 @@ impl Cipher { } else { self.is_favorite(user_uuid, conn).await }); + json_object["archivedDate"] = json!(if let Some(cipher_sync_data) = cipher_sync_data { + cipher_sync_data.cipher_archives.get(&self.uuid).map_or(Value::Null, |d| Value::String(format_date(d))) + } else { + self.get_archived_at(user_uuid, conn).await.map_or(Value::Null, |d| Value::String(format_date(&d))) + }); // These values are true by default, but can be false if the // cipher belongs to a collection or group where the org owner has enabled // the "Read Only" or "Hide Passwords" restrictions for the user. @@ -742,6 +747,18 @@ impl Cipher { } } + pub async fn get_archived_at(&self, user_uuid: &UserId, conn: &DbConn) -> Option { + Archive::get_archived_at(&self.uuid, user_uuid, conn).await + } + + pub async fn set_archived_at(&self, archived_at: NaiveDateTime, user_uuid: &UserId, conn: &DbConn) -> EmptyResult { + Archive::save(user_uuid, &self.uuid, archived_at, conn).await + } + + pub async fn unarchive(&self, user_uuid: &UserId, conn: &DbConn) -> EmptyResult { + Archive::delete_by_cipher(user_uuid, &self.uuid, conn).await + } + pub async fn get_folder_uuid(&self, user_uuid: &UserId, conn: &DbConn) -> Option { db_run! { conn: { folders_ciphers::table diff --git a/src/db/models/mod.rs b/src/db/models/mod.rs index b4fcf658..2d31259c 100644 --- a/src/db/models/mod.rs +++ b/src/db/models/mod.rs @@ -1,3 +1,4 @@ +mod archive; mod attachment; mod auth_request; mod cipher; @@ -17,6 +18,7 @@ mod two_factor_duo_context; mod two_factor_incomplete; mod user; +pub use self::archive::Archive; pub use self::attachment::{Attachment, AttachmentId}; pub use self::auth_request::{AuthRequest, AuthRequestId}; pub use self::cipher::{Cipher, CipherId, RepromptType}; diff --git a/src/db/schema.rs b/src/db/schema.rs index 147440e5..bf79ceac 100644 --- a/src/db/schema.rs +++ b/src/db/schema.rs @@ -342,6 +342,16 @@ table! { } } +table! { + archives (user_uuid, cipher_uuid) { + user_uuid -> Text, + cipher_uuid -> Text, + archived_at -> Timestamp, + } +} + +joinable!(archives -> users (user_uuid)); +joinable!(archives -> ciphers (cipher_uuid)); joinable!(attachments -> ciphers (cipher_uuid)); joinable!(ciphers -> organizations (organization_uuid)); joinable!(ciphers -> users (user_uuid)); @@ -373,6 +383,7 @@ joinable!(auth_requests -> users (user_uuid)); joinable!(sso_users -> users (user_uuid)); allow_tables_to_appear_in_same_query!( + archives, attachments, ciphers, ciphers_collections, From 8c3c969938d55eeec62c33a639da5d801aef5e7a Mon Sep 17 00:00:00 2001 From: Shocker Date: Wed, 29 Apr 2026 23:32:48 +0300 Subject: [PATCH 73/89] Fix favicon fetching to check all icon links instead of just the first one (#6880) * Fix favicon fetching to check all icon links instead of just the first one * revert max icons limit removal * optimize code * code formatting --- src/api/icons.rs | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/src/api/icons.rs b/src/api/icons.rs index b3a66f4d..5c9ed113 100644 --- a/src/api/icons.rs +++ b/src/api/icons.rs @@ -496,7 +496,8 @@ async fn download_icon(domain: &str) -> Result<(Bytes, Option<&str>), Error> { use data_url::DataUrl; - for icon in icon_result.iconlist.iter().take(5) { + let mut icons = icon_result.iconlist.iter().take(5).peekable(); + while let Some(icon) = icons.next() { if icon.href.starts_with("data:image") { let Ok(datauri) = DataUrl::process(&icon.href) else { continue; @@ -524,11 +525,23 @@ async fn download_icon(domain: &str) -> Result<(Bytes, Option<&str>), Error> { _ => debug!("Extracted icon from data:image uri is invalid"), }; } else { - let res = get_page_with_referer(&icon.href, &icon_result.referer).await?; + debug!("Trying {}", icon.href); + // Make sure all icons are checked before returning error + let res = match get_page_with_referer(&icon.href, &icon_result.referer).await { + Ok(r) => r, + Err(e) if icons.peek().is_none() => return Err(e), + Err(e) if CustomHttpClientError::downcast_ref(&e).is_some() => return Err(e), // If blacklisted stop immediately instead of checking the rest of the icons. see explanation and actual handling inside get_icon() + Err(e) => { + warn!("Unable to download icon: {e:?}"); + + // Continue to next icon + continue; + } + }; buffer = stream_to_bytes_limit(res, 5120 * 1024).await?; // 5120KB/5MB for each icon max (Same as icons.bitwarden.net) - // Check if the icon type is allowed, else try an icon from the list. + // Check if the icon type is allowed, else try another icon from the list. icon_type = get_icon_type(&buffer); if icon_type.is_none() { buffer.clear(); From c3bd1eb5652cfba9577ea022f818c27e92e4f527 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Garc=C3=ADa?= Date: Wed, 29 Apr 2026 22:47:42 +0200 Subject: [PATCH 74/89] Fix merge conflict (#7164) --- src/api/core/ciphers.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/api/core/ciphers.rs b/src/api/core/ciphers.rs index fab88b98..f2fc9fe3 100644 --- a/src/api/core/ciphers.rs +++ b/src/api/core/ciphers.rs @@ -2056,7 +2056,7 @@ async fn archive_multiple_ciphers( } // Multi archive does not send out a push for each cipher, we need to send a general sync here - nt.send_user_update(UpdateType::SyncCiphers, &headers.user, &headers.device.push_uuid, conn).await; + nt.send_user_update(UpdateType::SyncCiphers, &headers.user, headers.device.push_uuid.as_ref(), conn).await; Ok(Json(json!({ "data": ciphers, @@ -2082,7 +2082,7 @@ async fn unarchive_multiple_ciphers( } // Multi unarchive does not send out a push for each cipher, we need to send a general sync here - nt.send_user_update(UpdateType::SyncCiphers, &headers.user, &headers.device.push_uuid, conn).await; + nt.send_user_update(UpdateType::SyncCiphers, &headers.user, headers.device.push_uuid.as_ref(), conn).await; Ok(Json(json!({ "data": ciphers, From b89648a1363f805b10e5d7d1838720446aca01f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johny=20Jim=C3=A9nez?= Date: Wed, 29 Apr 2026 15:58:39 -0500 Subject: [PATCH 75/89] Replace organization_uuid unwrap with proper error handling (#6936) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The collection update endpoints (post_collections_update and post_collections_admin) call .unwrap() on cipher.organization_uuid in four places. If a user-owned cipher without an organization somehow reaches these code paths, the server would panic. Extract the organization UUID early with a descriptive error message instead of relying on .unwrap(), preventing potential panics and providing a clear API error response. Co-authored-by: Daniel GarcĂ­a --- src/api/core/ciphers.rs | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/src/api/core/ciphers.rs b/src/api/core/ciphers.rs index f2fc9fe3..43e555e2 100644 --- a/src/api/core/ciphers.rs +++ b/src/api/core/ciphers.rs @@ -814,12 +814,16 @@ async fn post_collections_update( err!("Collection cannot be changed") } + let Some(ref org_uuid) = cipher.organization_uuid else { + err!("Cipher is not owned by an organization") + }; + let posted_collections = HashSet::::from_iter(data.collection_ids); let current_collections = HashSet::::from_iter(cipher.get_collections(headers.user.uuid.clone(), &conn).await); for collection in posted_collections.symmetric_difference(¤t_collections) { - match Collection::find_by_uuid_and_org(collection, cipher.organization_uuid.as_ref().unwrap(), &conn).await { + match Collection::find_by_uuid_and_org(collection, org_uuid, &conn).await { None => err!("Invalid collection ID provided"), Some(collection) => { if collection.is_writable_by_user(&headers.user.uuid, &conn).await { @@ -850,7 +854,7 @@ async fn post_collections_update( log_event( EventType::CipherUpdatedCollections as i32, &cipher.uuid, - &cipher.organization_uuid.clone().unwrap(), + org_uuid, &headers.user.uuid, headers.device.atype, &headers.ip.ip, @@ -890,12 +894,16 @@ async fn post_collections_admin( err!("Collection cannot be changed") } + let Some(ref org_uuid) = cipher.organization_uuid else { + err!("Cipher is not owned by an organization") + }; + let posted_collections = HashSet::::from_iter(data.collection_ids); let current_collections = HashSet::::from_iter(cipher.get_admin_collections(headers.user.uuid.clone(), &conn).await); for collection in posted_collections.symmetric_difference(¤t_collections) { - match Collection::find_by_uuid_and_org(collection, cipher.organization_uuid.as_ref().unwrap(), &conn).await { + match Collection::find_by_uuid_and_org(collection, org_uuid, &conn).await { None => err!("Invalid collection ID provided"), Some(collection) => { if collection.is_writable_by_user(&headers.user.uuid, &conn).await { @@ -926,7 +934,7 @@ async fn post_collections_admin( log_event( EventType::CipherUpdatedCollections as i32, &cipher.uuid, - &cipher.organization_uuid.unwrap(), + org_uuid, &headers.user.uuid, headers.device.atype, &headers.ip.ip, From cb46fcb9483a31a9b08a9b95ec893e0a68edf714 Mon Sep 17 00:00:00 2001 From: eason <85663565+mango766@users.noreply.github.com> Date: Thu, 30 Apr 2026 04:58:50 +0800 Subject: [PATCH 76/89] fix: return Err instead of panic on unknown cipher atype in to_json() (#7068) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `Cipher::to_json()` returns `Result` but its match arm for unknown `atype` values called `panic!("Wrong type")` instead of propagating an error. This means if a cipher with an invalid/unknown type ends up in the database (via direct DB edits, data migration issues, or future type additions in the upstream Bitwarden protocol), the entire server process would crash on the next sync request. Replace the `panic!` with `err!()` so callers receive a proper `Err` and can handle or log it gracefully without taking down the server. Co-authored-by: easonysliu Co-authored-by: Claude Opus 4.6 (1M context) Co-authored-by: Daniel GarcĂ­a --- src/db/models/cipher.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/db/models/cipher.rs b/src/db/models/cipher.rs index 87f3e415..db906179 100644 --- a/src/db/models/cipher.rs +++ b/src/db/models/cipher.rs @@ -403,7 +403,7 @@ impl Cipher { 3 => "card", 4 => "identity", 5 => "sshKey", - _ => panic!("Wrong type"), + _ => err!(format!("Cipher {} has an invalid type {}", self.uuid, self.atype)), }; json_object[key] = type_data_json; From 14258caec902a7a4364c02c7a393353c8826d2db Mon Sep 17 00:00:00 2001 From: Eldred Habert Date: Wed, 29 Apr 2026 22:59:18 +0200 Subject: [PATCH 77/89] Allow SQLite to be linked against dynamically (#7057) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Keeping the default behaviour of SQLite being built statically, so as not to break anyone's workflow, but allowing for downstream packagers to link dynamically against SQLite (where it's fine because that's the point of package managers). Note that SQLite is still *not* enabled by default, thanks to the `?` operator. Co-authored-by: Daniel GarcĂ­a --- Cargo.toml | 10 ++++++---- build.rs | 12 ++++++------ 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index aba94374..6ed8d1b8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,15 +23,17 @@ publish.workspace = true [features] default = [ - # "sqlite", + # "sqlite" or "sqlite_system", # "mysql", # "postgresql", ] # Empty to keep compatibility, prefer to set USE_SYSLOG=true enable_syslog = [] +# Please enable at least one of these DB backends. mysql = ["diesel/mysql", "diesel_migrations/mysql"] postgresql = ["diesel/postgres", "diesel_migrations/postgres"] -sqlite = ["diesel/sqlite", "diesel_migrations/sqlite", "dep:libsqlite3-sys"] +sqlite_system = ["diesel/sqlite", "diesel_migrations/sqlite"] +sqlite = ["sqlite_system", "libsqlite3-sys/bundled"] # Alternative to the above, statically linked SQLite into the binary instead of dynamically. # Enable to use a vendored and statically linked openssl vendored_openssl = ["openssl/vendored"] # Enable MiMalloc memory allocator to replace the default malloc @@ -94,8 +96,8 @@ diesel_migrations = "2.3.2" derive_more = { version = "2.1.1", features = ["from", "into", "as_ref", "deref", "display"] } diesel-derive-newtype = "2.1.2" -# Bundled/Static SQLite -libsqlite3-sys = { version = "0.37.0", features = ["bundled"], optional = true } +# SQLite, statically bundled unless the `sqlite_system` feature is enabled +libsqlite3-sys = { version = "0.37.0", optional = true } # Crypto-related libraries rand = "0.10.1" diff --git a/build.rs b/build.rs index 4a831737..2d1106c2 100644 --- a/build.rs +++ b/build.rs @@ -2,21 +2,21 @@ use std::env; use std::process::Command; fn main() { - // This allow using #[cfg(sqlite)] instead of #[cfg(feature = "sqlite")], which helps when trying to add them through macros - #[cfg(feature = "sqlite")] + // These allow using e.g. #[cfg(mysql)] instead of #[cfg(feature = "mysql")], which helps when trying to add them through macros + #[cfg(feature = "sqlite_system")] // The `sqlite` feature implies this one. println!("cargo:rustc-cfg=sqlite"); #[cfg(feature = "mysql")] println!("cargo:rustc-cfg=mysql"); #[cfg(feature = "postgresql")] println!("cargo:rustc-cfg=postgresql"); - #[cfg(feature = "s3")] - println!("cargo:rustc-cfg=s3"); - - #[cfg(not(any(feature = "sqlite", feature = "mysql", feature = "postgresql")))] + #[cfg(not(any(feature = "sqlite_system", feature = "mysql", feature = "postgresql")))] compile_error!( "You need to enable one DB backend. To build with previous defaults do: cargo build --features sqlite" ); + #[cfg(feature = "s3")] + println!("cargo:rustc-cfg=s3"); + // Use check-cfg to let cargo know which cfg's we define, // and avoid warnings when they are used in the code. println!("cargo::rustc-check-cfg=cfg(sqlite)"); From 07aa377af77ea03c464fcba243b62e743c98ce4e Mon Sep 17 00:00:00 2001 From: Mathijs van Veluw Date: Thu, 30 Apr 2026 21:45:45 +0200 Subject: [PATCH 78/89] Update crates and web-vault (#7171) - Update crates including fixing a regression of Diesel - Update web-vault to v2026.4.1 - Adjusted the README to address the secure context and needing HTTPS Fixes #7132 Closes #7137 Signed-off-by: BlackDex --- Cargo.lock | 28 ++++++++++++++-------------- Cargo.toml | 8 ++++---- README.md | 5 +++-- docker/DockerSettings.yaml | 4 ++-- docker/Dockerfile.alpine | 12 ++++++------ docker/Dockerfile.debian | 12 ++++++------ 6 files changed, 35 insertions(+), 34 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b9fcceef..a2011056 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -717,9 +717,9 @@ checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" [[package]] name = "base64urlsafedata" -version = "0.5.4" +version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42f7f6be94fa637132933fd0a68b9140bcb60e3d46164cb68e82a2bb8d102b3a" +checksum = "b08e33815c87d8cadcddb1e74ac307368a3751fbe40c961538afa21a1899f21c" dependencies = [ "base64 0.21.7", "pastey 0.1.1", @@ -1521,9 +1521,9 @@ dependencies = [ [[package]] name = "diesel" -version = "2.3.8" +version = "2.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78df0e4e8c596662edb07fbfbb7f23769cca35049827df5f909084d956b6aeaf" +checksum = "9940fb8467a0a06312218ed384185cb8536aa10d8ec017d0ce7fad2c1bd882d5" dependencies = [ "bigdecimal", "bitflags", @@ -1558,9 +1558,9 @@ dependencies = [ [[package]] name = "diesel_derives" -version = "2.3.8" +version = "2.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b79402bd1cfb25b65650f0f4901d0e79c095729e2139c8ab779d025968c7099" +checksum = "d1817b7f4279b947fc4cafddec12b0e5f8727141706561ce3ac94a60bddd1cf5" dependencies = [ "diesel_table_macro_syntax", "dsl_auto_type", @@ -6128,9 +6128,9 @@ dependencies = [ [[package]] name = "webauthn-attestation-ca" -version = "0.5.4" +version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fafcf13f7dc1fb292ed4aea22cdd3757c285d7559e9748950ee390249da4da6b" +checksum = "6475c0bbd1a3f04afaa3e98880408c5be61680c5e6bd3c6f8c250990d5d3e18e" dependencies = [ "base64urlsafedata", "openssl", @@ -6142,9 +6142,9 @@ dependencies = [ [[package]] name = "webauthn-rs" -version = "0.5.4" +version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b24d082d3360258fefb6ffe56123beef7d6868c765c779f97b7a2fcf06727f8" +checksum = "6c548915e0e92ee946bbf2aecf01ea21bef53d974b0793cc6732ba81a03fc422" dependencies = [ "base64urlsafedata", "serde", @@ -6156,9 +6156,9 @@ dependencies = [ [[package]] name = "webauthn-rs-core" -version = "0.5.4" +version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15784340a24c170ce60567282fb956a0938742dbfbf9eff5df793a686a009b8b" +checksum = "296d2d501feb715d80b8e186fb88bab1073bca17f460303a1013d17b673bea6a" dependencies = [ "base64 0.21.7", "base64urlsafedata", @@ -6183,9 +6183,9 @@ dependencies = [ [[package]] name = "webauthn-rs-proto" -version = "0.5.4" +version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16a1fb2580ce73baa42d3011a24de2ceab0d428de1879ece06e02e8c416e497c" +checksum = "c37393beac9c1ed1ca6dbb30b1e01783fb316ab3a45d90ecd48c99052dd7ef1e" dependencies = [ "base64 0.21.7", "base64urlsafedata", diff --git a/Cargo.toml b/Cargo.toml index 6ed8d1b8..6ec40f58 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -90,7 +90,7 @@ serde_json = "1.0.149" # A safe, extensible ORM and Query builder # Currently pinned diesel to v2.3.3 as newer version break MySQL/MariaDB compatibility -diesel = { version = "2.3.8", features = ["chrono", "r2d2", "numeric"] } +diesel = { version = "2.3.9", features = ["chrono", "r2d2", "numeric"] } diesel_migrations = "2.3.2" derive_more = { version = "2.1.1", features = ["from", "into", "as_ref", "deref", "display"] } @@ -130,9 +130,9 @@ yubico = { package = "yubico_ng", version = "0.14.1", features = ["online-tokio" # WebAuthn libraries # danger-allow-state-serialisation is needed to save the state in the db # danger-credential-internals is needed to support U2F to Webauthn migration -webauthn-rs = { version = "0.5.4", features = ["danger-allow-state-serialisation", "danger-credential-internals"] } -webauthn-rs-proto = "0.5.4" -webauthn-rs-core = "0.5.4" +webauthn-rs = { version = "0.5.5", features = ["danger-allow-state-serialisation", "danger-credential-internals"] } +webauthn-rs-proto = "0.5.5" +webauthn-rs-core = "0.5.5" # Handling of URL's for WebAuthn and favicons url = "2.5.8" diff --git a/README.md b/README.md index c84a9c40..0b24ba69 100644 --- a/README.md +++ b/README.md @@ -59,8 +59,9 @@ A nearly complete implementation of the Bitwarden Client API is provided, includ ## Usage > [!IMPORTANT] -> The web-vault requires the use a secure context for the [Web Crypto API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Crypto_API). -> That means it will only work via `http://localhost:8000` (using the port from the example below) or if you [enable HTTPS](https://github.com/dani-garcia/vaultwarden/wiki/Enabling-HTTPS). +> The web-vault requires the use of HTTPS and a secure context for the [Web Crypto API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Crypto_API).
+> That means it will only work if you [enable HTTPS](https://github.com/dani-garcia/vaultwarden/wiki/Enabling-HTTPS).
+> We also suggest to use a [reverse proxy](https://github.com/dani-garcia/vaultwarden/wiki/Proxy-examples). The recommended way to install and use Vaultwarden is via our container images which are published to [ghcr.io](https://github.com/dani-garcia/vaultwarden/pkgs/container/vaultwarden), [docker.io](https://hub.docker.com/r/vaultwarden/server) and [quay.io](https://quay.io/repository/vaultwarden/server). See [which container image to use](https://github.com/dani-garcia/vaultwarden/wiki/Which-container-image-to-use) for an explanation of the provided tags. diff --git a/docker/DockerSettings.yaml b/docker/DockerSettings.yaml index 9a5d7f02..9d4a563a 100644 --- a/docker/DockerSettings.yaml +++ b/docker/DockerSettings.yaml @@ -1,6 +1,6 @@ --- -vault_version: "v2026.3.1" -vault_image_digest: "sha256:c1b1f212333f95bff4ef8d00e8e3589c4ae8eda018691f28f8bddc7e971dd767" +vault_version: "v2026.4.1" +vault_image_digest: "sha256:ca2a4251c4e63c9ad428262b4dd452789a1b9f6fce71da351e93dceed0d2edbe" # Cross Compile Docker Helper Scripts v1.9.0 # We use the linux/amd64 platform shell scripts since there is no difference between the different platform scripts # https://github.com/tonistiigi/xx | https://hub.docker.com/r/tonistiigi/xx/tags diff --git a/docker/Dockerfile.alpine b/docker/Dockerfile.alpine index 44517aa3..cbb18e2b 100644 --- a/docker/Dockerfile.alpine +++ b/docker/Dockerfile.alpine @@ -19,15 +19,15 @@ # - From https://hub.docker.com/r/vaultwarden/web-vault/tags, # click the tag name to view the digest of the image it currently points to. # - From the command line: -# $ docker pull docker.io/vaultwarden/web-vault:v2026.3.1 -# $ docker image inspect --format "{{.RepoDigests}}" docker.io/vaultwarden/web-vault:v2026.3.1 -# [docker.io/vaultwarden/web-vault@sha256:c1b1f212333f95bff4ef8d00e8e3589c4ae8eda018691f28f8bddc7e971dd767] +# $ docker pull docker.io/vaultwarden/web-vault:v2026.4.1 +# $ docker image inspect --format "{{.RepoDigests}}" docker.io/vaultwarden/web-vault:v2026.4.1 +# [docker.io/vaultwarden/web-vault@sha256:ca2a4251c4e63c9ad428262b4dd452789a1b9f6fce71da351e93dceed0d2edbe] # # - Conversely, to get the tag name from the digest: -# $ docker image inspect --format "{{.RepoTags}}" docker.io/vaultwarden/web-vault@sha256:c1b1f212333f95bff4ef8d00e8e3589c4ae8eda018691f28f8bddc7e971dd767 -# [docker.io/vaultwarden/web-vault:v2026.3.1] +# $ docker image inspect --format "{{.RepoTags}}" docker.io/vaultwarden/web-vault@sha256:ca2a4251c4e63c9ad428262b4dd452789a1b9f6fce71da351e93dceed0d2edbe +# [docker.io/vaultwarden/web-vault:v2026.4.1] # -FROM --platform=linux/amd64 docker.io/vaultwarden/web-vault@sha256:c1b1f212333f95bff4ef8d00e8e3589c4ae8eda018691f28f8bddc7e971dd767 AS vault +FROM --platform=linux/amd64 docker.io/vaultwarden/web-vault@sha256:ca2a4251c4e63c9ad428262b4dd452789a1b9f6fce71da351e93dceed0d2edbe AS vault ########################## ALPINE BUILD IMAGES ########################## ## NOTE: The Alpine Base Images do not support other platforms then linux/amd64 and linux/arm64 diff --git a/docker/Dockerfile.debian b/docker/Dockerfile.debian index d472cbb6..829f59d2 100644 --- a/docker/Dockerfile.debian +++ b/docker/Dockerfile.debian @@ -19,15 +19,15 @@ # - From https://hub.docker.com/r/vaultwarden/web-vault/tags, # click the tag name to view the digest of the image it currently points to. # - From the command line: -# $ docker pull docker.io/vaultwarden/web-vault:v2026.3.1 -# $ docker image inspect --format "{{.RepoDigests}}" docker.io/vaultwarden/web-vault:v2026.3.1 -# [docker.io/vaultwarden/web-vault@sha256:c1b1f212333f95bff4ef8d00e8e3589c4ae8eda018691f28f8bddc7e971dd767] +# $ docker pull docker.io/vaultwarden/web-vault:v2026.4.1 +# $ docker image inspect --format "{{.RepoDigests}}" docker.io/vaultwarden/web-vault:v2026.4.1 +# [docker.io/vaultwarden/web-vault@sha256:ca2a4251c4e63c9ad428262b4dd452789a1b9f6fce71da351e93dceed0d2edbe] # # - Conversely, to get the tag name from the digest: -# $ docker image inspect --format "{{.RepoTags}}" docker.io/vaultwarden/web-vault@sha256:c1b1f212333f95bff4ef8d00e8e3589c4ae8eda018691f28f8bddc7e971dd767 -# [docker.io/vaultwarden/web-vault:v2026.3.1] +# $ docker image inspect --format "{{.RepoTags}}" docker.io/vaultwarden/web-vault@sha256:ca2a4251c4e63c9ad428262b4dd452789a1b9f6fce71da351e93dceed0d2edbe +# [docker.io/vaultwarden/web-vault:v2026.4.1] # -FROM --platform=linux/amd64 docker.io/vaultwarden/web-vault@sha256:c1b1f212333f95bff4ef8d00e8e3589c4ae8eda018691f28f8bddc7e971dd767 AS vault +FROM --platform=linux/amd64 docker.io/vaultwarden/web-vault@sha256:ca2a4251c4e63c9ad428262b4dd452789a1b9f6fce71da351e93dceed0d2edbe AS vault ########################## Cross Compile Docker Helper Scripts ########################## ## We use the linux/amd64 no matter which Build Platform, since these are all bash scripts From f21a3adae2fbb8582b60b121783c597fe6895ff4 Mon Sep 17 00:00:00 2001 From: Mathijs van Veluw Date: Sat, 2 May 2026 18:56:15 +0200 Subject: [PATCH 79/89] Update hickory (#7175) Signed-off-by: BlackDex --- Cargo.lock | 12 ++++++------ Cargo.toml | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a2011056..ac84b501 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2262,9 +2262,9 @@ checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" [[package]] name = "hickory-net" -version = "0.26.0" +version = "0.26.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c61c8db47fae51ba9f8f2a2748bd87542acfbe22f2ec9cf9c8ec72d1ee6e9a6" +checksum = "e2295ed2f9c31e471e1428a8f88a3f0e1f4b27c15049592138d1eebe9c35b183" dependencies = [ "async-trait", "cfg-if", @@ -2286,9 +2286,9 @@ dependencies = [ [[package]] name = "hickory-proto" -version = "0.26.0" +version = "0.26.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a916d0494600d99ecb15aadfab677ad97c4de559e8f1af0c129353a733ac1fcc" +checksum = "0bab31817bfb44672a252e97fe81cd0c18d1b2cf892108922f6818820df8c643" dependencies = [ "data-encoding", "idna", @@ -2306,9 +2306,9 @@ dependencies = [ [[package]] name = "hickory-resolver" -version = "0.26.0" +version = "0.26.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a10bd64d950b4d38ca21e25c8ae230712e4955fb8290cfcb29a5e5dc6017e544" +checksum = "f0d58d28879ceecde6607729660c2667a081ccdc082e082675042793960f178c" dependencies = [ "cfg-if", "futures-util", diff --git a/Cargo.toml b/Cargo.toml index 6ec40f58..e7fd5ade 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -147,7 +147,7 @@ handlebars = { version = "6.4.0", features = ["dir_source"] } # HTTP client (Used for favicons, version check, DUO and HIBP API) reqwest = { version = "0.12.28", features = ["rustls-tls", "rustls-tls-native-roots", "stream", "json", "deflate", "gzip", "brotli", "zstd", "socks", "cookies", "charset", "http2", "system-proxy"], default-features = false} -hickory-resolver = "0.26.0" +hickory-resolver = "0.26.1" # Favicon extraction libraries html5gum = "0.8.3" From cdf711bb30e12dc347d5e8674b37575531e7b126 Mon Sep 17 00:00:00 2001 From: Chase Douglas Date: Fri, 15 May 2026 11:30:31 -0700 Subject: [PATCH 80/89] OpenDAL S3 parameter support (#6127) * deps: upgrade the reqwest stack to 0.13 The reqwest 0.13 rustls feature selects the aws-lc provider. Use rustls-no-provider instead, add rustls 0.23 with the ring provider, and install that provider at process startup. This keeps Vaultwarden on the existing ring crypto provider while giving reqwest, OpenDAL and lettre a process-wide rustls provider. Disable openidconnect default features and provide a small AsyncHttpClient wrapper around Vaultwarden's shared reqwest client builder. This preserves custom DNS, request blocking, timeouts and the no-redirect OIDC behavior without openidconnect enabling its own reqwest stack. Upgrade yubico_ng to 0.15.0 and OpenDAL to 0.56.0. OpenDAL 0.56 also moves S3 signing to reqsign 3, so switch the optional S3 dependencies from reqsign/anyhow to reqsign-core and reqsign-aws-v4 and adapt the AWS SDK credential bridge to the new ProvideCredential API. Adjust the local OpenDAL call sites for the 0.56 API: use the FS_SCHEME constant for filesystem checks and replace deprecated remove_all() with delete_with(...).recursive(true) for Send file cleanup. * storage: add OpenDAL S3 URI options OpenDAL S3 storage accepts bucket and root path data today, but serverless deployments also need URI query parameters to describe provider behavior in one DATA_FOLDER value. Update OpenDAL to 0.56.0 and build S3 operators with S3Config::from_uri(). Keep Vaultwarden's AWS SDK credential chain by installing a reqsign provider when the URI does not explicitly request OpenDAL-native credential handling. Move path handling and operator construction into storage.rs so S3-specific parsing, credential setup, and URI path manipulation stay out of configuration handling. Local filesystem behavior is unchanged, and S3 child paths are derived before query strings. --- Cargo.lock | 482 ++++++++++++------------------------ Cargo.toml | 15 +- src/api/core/sends.rs | 2 +- src/auth.rs | 8 +- src/config.rs | 131 ++-------- src/db/models/attachment.rs | 2 +- src/db/models/send.rs | 2 +- src/main.rs | 10 + src/sso_client.rs | 41 ++- src/storage.rs | 297 ++++++++++++++++++++++ 10 files changed, 533 insertions(+), 457 deletions(-) create mode 100644 src/storage.rs diff --git a/Cargo.lock b/Cargo.lock index ac84b501..9a44e0b2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8,17 +8,6 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" -[[package]] -name = "aes" -version = "0.8.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" -dependencies = [ - "cfg-if", - "cipher", - "cpufeatures 0.2.17", -] - [[package]] name = "ahash" version = "0.8.12" @@ -670,17 +659,6 @@ dependencies = [ "tracing", ] -[[package]] -name = "backon" -version = "1.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cffb0e931875b666fc4fcb20fee52e9bbd1ef836fd9e9e04ec21555f9f85f7ef" -dependencies = [ - "fastrand", - "gloo-timers", - "tokio", -] - [[package]] name = "base16ct" version = "0.2.0" @@ -778,15 +756,6 @@ dependencies = [ "hybrid-array", ] -[[package]] -name = "block-padding" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8894febbff9f758034a5b8e12d87918f56dfc64a8e1fe757d65e29041538d93" -dependencies = [ - "generic-array", -] - [[package]] name = "blocking" version = "1.6.2" @@ -892,15 +861,6 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ade8366b8bd5ba243f0a58f036cc0ca8a2f069cff1a2351ef1cac6b083e16fc0" -[[package]] -name = "cbc" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26b52a9543ae338f279b96b0b9fed9c8093744685043739079ce85cd58f289a6" -dependencies = [ - "cipher", -] - [[package]] name = "cc" version = "1.2.61" @@ -919,12 +879,6 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" -[[package]] -name = "cfg_aliases" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" - [[package]] name = "chacha20" version = "0.10.0" @@ -960,16 +914,6 @@ dependencies = [ "phf 0.12.1", ] -[[package]] -name = "cipher" -version = "0.4.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" -dependencies = [ - "crypto-common 0.1.6", - "inout", -] - [[package]] name = "cmov" version = "0.5.3" @@ -2357,15 +2301,6 @@ dependencies = [ "digest 0.11.2", ] -[[package]] -name = "home" -version = "0.5.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc627f471c528ff0c4a49e1d5e60450c8f6461dd6d10ba9dcd3a61d3dff7728d" -dependencies = [ - "windows-sys 0.61.2", -] - [[package]] name = "hostname" version = "0.4.2" @@ -2516,11 +2451,9 @@ dependencies = [ "hyper 1.9.0", "hyper-util", "rustls 0.23.40", - "rustls-native-certs", "tokio", "tokio-rustls 0.26.4", "tower-service", - "webpki-roots", ] [[package]] @@ -2716,16 +2649,6 @@ version = "0.1.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c8fae54786f62fb2918dcfae3d568594e50eb9b5c25bf04371af6fe7516452fb" -[[package]] -name = "inout" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" -dependencies = [ - "block-padding", - "generic-array", -] - [[package]] name = "ipconfig" version = "0.3.4" @@ -2798,10 +2721,12 @@ checksum = "f00b5dbd620d61dfdcb6007c9c1f6054ebd75319f163d886a9055cec1155073d" dependencies = [ "jiff-static", "jiff-tzdb-platform", + "js-sys", "log", "portable-atomic", "portable-atomic-util", "serde_core", + "wasm-bindgen", "windows-sys 0.61.2", ] @@ -2913,21 +2838,6 @@ dependencies = [ "wasm-bindgen", ] -[[package]] -name = "jsonwebtoken" -version = "9.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a87cc7a48537badeae96744432de36f4be2b4a34a05a5ef32e9dd8a1c169dde" -dependencies = [ - "base64 0.22.1", - "js-sys", - "pem", - "ring", - "serde", - "serde_json", - "simple_asn1", -] - [[package]] name = "jsonwebtoken" version = "10.3.0" @@ -3098,12 +3008,6 @@ dependencies = [ "tracing-subscriber", ] -[[package]] -name = "lru-slab" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" - [[package]] name = "macros" version = "0.1.0" @@ -3131,6 +3035,15 @@ dependencies = [ "digest 0.10.7", ] +[[package]] +name = "mea" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6747f54621d156e1b47eb6b25f39a941b9fc347f98f67d25d8881ff99e8ed832" +dependencies = [ + "slab", +] + [[package]] name = "memchr" version = "2.8.0" @@ -3408,7 +3321,6 @@ dependencies = [ "getrandom 0.2.17", "http 1.4.0", "rand 0.8.6", - "reqwest", "serde", "serde_json", "serde_path_to_error", @@ -3438,31 +3350,76 @@ dependencies = [ [[package]] name = "opendal" -version = "0.55.0" +version = "0.56.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d075ab8a203a6ab4bc1bce0a4b9fe486a72bf8b939037f4b78d95386384bc80a" +checksum = "97b31d3d8e99a85d83b73ec26647f5607b80578ed9375810b6e44ffa3590a236" +dependencies = [ + "opendal-core", + "opendal-service-fs", + "opendal-service-s3", +] + +[[package]] +name = "opendal-core" +version = "0.56.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1849dd2687e173e776d3af5fce1ba3ae47b9dd37a09d1c4deba850ef45fe00ca" dependencies = [ "anyhow", - "backon", "base64 0.22.1", "bytes", - "crc32c", "futures", - "getrandom 0.2.17", "http 1.4.0", "http-body 1.0.1", "jiff", "log", "md-5", + "mea", "percent-encoding", "quick-xml 0.38.4", - "reqsign", + "reqsign-core", "reqwest", "serde", "serde_json", "tokio", "url", "uuid", + "web-time", +] + +[[package]] +name = "opendal-service-fs" +version = "0.56.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf0be0417abeeb0053376d816b90fceb9ca98f20dfb54ebf1f2a282729f83663" +dependencies = [ + "bytes", + "log", + "opendal-core", + "serde", + "tokio", + "xattr", +] + +[[package]] +name = "opendal-service-s3" +version = "0.56.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dadddeb9bb50b0d30927dd914c298c4ddca47e4c1cfa7674d311f0cf9b051c8" +dependencies = [ + "base64 0.22.1", + "bytes", + "crc32c", + "http 1.4.0", + "log", + "md-5", + "opendal-core", + "quick-xml 0.38.4", + "reqsign-aws-v4", + "reqsign-core", + "reqsign-file-read-tokio", + "serde", + "url", ] [[package]] @@ -3651,16 +3608,6 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c5a797f0e07bdf071d15742978fc3128ec6c22891c31a3a931513263904c982a" -[[package]] -name = "pbkdf2" -version = "0.12.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2" -dependencies = [ - "digest 0.10.7", - "hmac 0.12.1", -] - [[package]] name = "pear" version = "0.2.9" @@ -3852,21 +3799,6 @@ dependencies = [ "spki", ] -[[package]] -name = "pkcs5" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e847e2c91a18bfa887dd028ec33f2fe6f25db77db3619024764914affe8b69a6" -dependencies = [ - "aes", - "cbc", - "der", - "pbkdf2", - "scrypt", - "sha2 0.10.9", - "spki", -] - [[package]] name = "pkcs8" version = "0.10.2" @@ -3874,8 +3806,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" dependencies = [ "der", - "pkcs5", - "rand_core 0.6.4", "spki", ] @@ -4038,16 +3968,6 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" -[[package]] -name = "quick-xml" -version = "0.37.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "331e97a1af0bf59823e6eadffe373d7b27f485be8748f71471c662c1f269b7fb" -dependencies = [ - "memchr", - "serde", -] - [[package]] name = "quick-xml" version = "0.38.4" @@ -4059,58 +3979,13 @@ dependencies = [ ] [[package]] -name = "quinn" -version = "0.11.9" +name = "quick-xml" +version = "0.39.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20" +checksum = "cdcc8dd4e2f670d309a5f0e83fe36dfdc05af317008fea29144da1a2ac858e5e" dependencies = [ - "bytes", - "cfg_aliases", - "pin-project-lite", - "quinn-proto", - "quinn-udp", - "rustc-hash", - "rustls 0.23.40", - "socket2 0.6.3", - "thiserror 2.0.18", - "tokio", - "tracing", - "web-time", -] - -[[package]] -name = "quinn-proto" -version = "0.11.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "434b42fec591c96ef50e21e886936e66d3cc3f737104fdb9b737c40ffb94c098" -dependencies = [ - "bytes", - "getrandom 0.3.4", - "lru-slab", - "rand 0.9.4", - "ring", - "rustc-hash", - "rustls 0.23.40", - "rustls-pki-types", - "slab", - "thiserror 2.0.18", - "tinyvec", - "tracing", - "web-time", -] - -[[package]] -name = "quinn-udp" -version = "0.5.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd" -dependencies = [ - "cfg_aliases", - "libc", - "once_cell", - "socket2 0.6.3", - "tracing", - "windows-sys 0.60.2", + "memchr", + "serde", ] [[package]] @@ -4312,43 +4187,64 @@ dependencies = [ ] [[package]] -name = "reqsign" -version = "0.16.5" +name = "reqsign-aws-v4" +version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43451dbf3590a7590684c25fb8d12ecdcc90ed3ac123433e500447c7d77ed701" +checksum = "44eaca382e94505a49f1a4849658d153aebf79d9c1a58e5dd3b10361511e9f43" dependencies = [ "anyhow", - "async-trait", - "base64 0.22.1", - "chrono", + "bytes", "form_urlencoded", - "getrandom 0.2.17", - "hex", - "hmac 0.12.1", - "home", "http 1.4.0", - "jsonwebtoken 9.3.1", "log", - "once_cell", "percent-encoding", - "quick-xml 0.37.5", - "rand 0.8.6", - "reqwest", - "rsa", + "quick-xml 0.39.4", + "reqsign-core", "rust-ini", "serde", "serde_json", + "serde_urlencoded", + "sha1", +] + +[[package]] +name = "reqsign-core" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b10302cf0a7d7e7352ba211fc92c3c5bebf1286153e49cc5aa87348078a8e102" +dependencies = [ + "anyhow", + "base64 0.22.1", + "bytes", + "form_urlencoded", + "futures", + "hex", + "hmac 0.12.1", + "http 1.4.0", + "jiff", + "log", + "percent-encoding", "sha1", "sha2 0.10.9", + "windows-sys 0.61.2", +] + +[[package]] +name = "reqsign-file-read-tokio" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2d89295b3d17abea31851cc8de55d843d89c52132c864963c38d41920613dc5" +dependencies = [ + "anyhow", + "reqsign-core", "tokio", - "toml 0.8.23", ] [[package]] name = "reqwest" -version = "0.12.28" +version = "0.13.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" +checksum = "62e0021ea2c22aed41653bc7e1419abb2c97e038ff2c33d0e1309e49a97deec0" dependencies = [ "base64 0.22.1", "bytes", @@ -4370,10 +4266,9 @@ dependencies = [ "mime", "percent-encoding", "pin-project-lite", - "quinn", "rustls 0.23.40", - "rustls-native-certs", "rustls-pki-types", + "rustls-platform-verifier", "serde", "serde_json", "serde_urlencoded", @@ -4389,7 +4284,6 @@ dependencies = [ "wasm-bindgen-futures", "wasm-streams", "web-sys", - "webpki-roots", ] [[package]] @@ -4560,7 +4454,6 @@ dependencies = [ "pkcs1", "pkcs8", "rand_core 0.6.4", - "sha2 0.10.9", "signature", "spki", "subtle", @@ -4597,12 +4490,6 @@ dependencies = [ "ordered-multimap", ] -[[package]] -name = "rustc-hash" -version = "2.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe" - [[package]] name = "rustc_version" version = "0.4.1" @@ -4688,10 +4575,36 @@ version = "1.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "30a7197ae7eb376e574fe940d068c30fe0462554a3ddbe4eca7838e049c937a9" dependencies = [ - "web-time", "zeroize", ] +[[package]] +name = "rustls-platform-verifier" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d1e2536ce4f35f4846aa13bff16bd0ff40157cdb14cc056c7b14ba41233ba0" +dependencies = [ + "core-foundation 0.10.1", + "core-foundation-sys", + "jni", + "log", + "once_cell", + "rustls 0.23.40", + "rustls-native-certs", + "rustls-platform-verifier-android", + "rustls-webpki 0.103.13", + "security-framework", + "security-framework-sys", + "webpki-root-certs", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls-platform-verifier-android" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f" + [[package]] name = "rustls-webpki" version = "0.101.7" @@ -4725,15 +4638,6 @@ version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" -[[package]] -name = "salsa20" -version = "0.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97a22f5af31f73a954c10289c93e8a50cc23d971e80ee446f1f6f7137a088213" -dependencies = [ - "cipher", -] - [[package]] name = "same-file" version = "1.0.6" @@ -4797,17 +4701,6 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" -[[package]] -name = "scrypt" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0516a385866c09368f0b5bcd1caff3366aace790fcd46e2bb032697bb172fd1f" -dependencies = [ - "pbkdf2", - "salsa20", - "sha2 0.10.9", -] - [[package]] name = "sct" version = "0.7.1" @@ -5869,7 +5762,6 @@ checksum = "7ba6f5989077681266825251a52748b8c1d8a4ad098cc37e440103d0ea717fc0" name = "vaultwarden" version = "1.0.0" dependencies = [ - "anyhow", "argon2", "aws-config", "aws-credential-types", @@ -5899,7 +5791,7 @@ dependencies = [ "html5gum", "http 1.4.0", "job_scheduler_ng", - "jsonwebtoken 10.3.0", + "jsonwebtoken", "lettre", "libsqlite3-sys", "log", @@ -5916,13 +5808,15 @@ dependencies = [ "pico-args", "rand 0.10.1", "regex", - "reqsign", + "reqsign-aws-v4", + "reqsign-core", "reqwest", "ring", "rmpv", "rocket", "rocket_ws", "rpassword", + "rustls 0.23.40", "semver", "serde", "serde_json", @@ -6083,9 +5977,9 @@ dependencies = [ [[package]] name = "wasm-streams" -version = "0.4.2" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15053d8d85c7eccdbefef60f06769760a563c7f0a9d6902a13d35c7800b0ad65" +checksum = "9d1ec4f6517c9e11ae630e200b2b65d193279042e28edd4a2cda233e46670bbb" dependencies = [ "futures-util", "js-sys", @@ -6195,10 +6089,10 @@ dependencies = [ ] [[package]] -name = "webpki-roots" +name = "webpki-root-certs" version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52f5ee44c96cf55f1b349600768e3ece3a8f26010c05265ab73f945bb1a2eb9d" +checksum = "f31141ce3fc3e300ae89b78c0dd67f9708061d1d2eda54b8209346fd6be9a92c" dependencies = [ "rustls-pki-types", ] @@ -6346,15 +6240,6 @@ dependencies = [ "windows-targets 0.52.6", ] -[[package]] -name = "windows-sys" -version = "0.60.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" -dependencies = [ - "windows-targets 0.53.5", -] - [[package]] name = "windows-sys" version = "0.61.2" @@ -6388,30 +6273,13 @@ dependencies = [ "windows_aarch64_gnullvm 0.52.6", "windows_aarch64_msvc 0.52.6", "windows_i686_gnu 0.52.6", - "windows_i686_gnullvm 0.52.6", + "windows_i686_gnullvm", "windows_i686_msvc 0.52.6", "windows_x86_64_gnu 0.52.6", "windows_x86_64_gnullvm 0.52.6", "windows_x86_64_msvc 0.52.6", ] -[[package]] -name = "windows-targets" -version = "0.53.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" -dependencies = [ - "windows-link", - "windows_aarch64_gnullvm 0.53.1", - "windows_aarch64_msvc 0.53.1", - "windows_i686_gnu 0.53.1", - "windows_i686_gnullvm 0.53.1", - "windows_i686_msvc 0.53.1", - "windows_x86_64_gnu 0.53.1", - "windows_x86_64_gnullvm 0.53.1", - "windows_x86_64_msvc 0.53.1", -] - [[package]] name = "windows_aarch64_gnullvm" version = "0.48.5" @@ -6424,12 +6292,6 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" - [[package]] name = "windows_aarch64_msvc" version = "0.48.5" @@ -6442,12 +6304,6 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" -[[package]] -name = "windows_aarch64_msvc" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" - [[package]] name = "windows_i686_gnu" version = "0.48.5" @@ -6460,24 +6316,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" -[[package]] -name = "windows_i686_gnu" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" - [[package]] name = "windows_i686_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" -[[package]] -name = "windows_i686_gnullvm" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" - [[package]] name = "windows_i686_msvc" version = "0.48.5" @@ -6490,12 +6334,6 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" -[[package]] -name = "windows_i686_msvc" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" - [[package]] name = "windows_x86_64_gnu" version = "0.48.5" @@ -6508,12 +6346,6 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" -[[package]] -name = "windows_x86_64_gnu" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" - [[package]] name = "windows_x86_64_gnullvm" version = "0.48.5" @@ -6526,12 +6358,6 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" - [[package]] name = "windows_x86_64_msvc" version = "0.48.5" @@ -6544,12 +6370,6 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" -[[package]] -name = "windows_x86_64_msvc" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" - [[package]] name = "winnow" version = "0.6.26" @@ -6691,6 +6511,16 @@ dependencies = [ "time", ] +[[package]] +name = "xattr" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32e45ad4206f6d2479085147f02bc2ef834ac85886624a23575ae137c8aa8156" +dependencies = [ + "libc", + "rustix", +] + [[package]] name = "xml" version = "1.2.1" @@ -6737,9 +6567,9 @@ dependencies = [ [[package]] name = "yubico_ng" -version = "0.14.1" +version = "0.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "929981f5b46b8fb8ee54b144de6b55c3a94fbe26635ee25b0e126e184250867c" +checksum = "228e2862e3c66f3224102d9a00d9d3646b271a05cc6c4819fea195fa8b5c00e0" dependencies = [ "base64 0.22.1", "form_urlencoded", diff --git a/Cargo.toml b/Cargo.toml index e7fd5ade..271d102d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -39,7 +39,7 @@ vendored_openssl = ["openssl/vendored"] # Enable MiMalloc memory allocator to replace the default malloc # This can improve performance for Alpine builds enable_mimalloc = ["dep:mimalloc"] -s3 = ["opendal/services-s3", "dep:aws-config", "dep:aws-credential-types", "dep:aws-smithy-runtime-api", "dep:anyhow", "dep:http", "dep:reqsign"] +s3 = ["opendal/services-s3", "dep:aws-config", "dep:aws-credential-types", "dep:aws-smithy-runtime-api", "dep:http", "dep:reqsign-aws-v4", "dep:reqsign-core"] # OIDC specific features oidc-accept-rfc3339-timestamps = ["openidconnect/accept-rfc3339-timestamps"] @@ -102,6 +102,7 @@ libsqlite3-sys = { version = "0.37.0", optional = true } # Crypto-related libraries rand = "0.10.1" ring = "0.17.14" +rustls = { version = "0.23.40", features = ["ring", "std"], default-features = false } subtle = "2.6.1" # UUID generation @@ -125,7 +126,7 @@ jsonwebtoken = { version = "10.3.0", features = ["use_pem", "rust_crypto"], defa totp-lite = "2.0.1" # Yubico Library -yubico = { package = "yubico_ng", version = "0.14.1", features = ["online-tokio"], default-features = false } +yubico = { package = "yubico_ng", version = "0.15.0", features = ["online-tokio"], default-features = false } # WebAuthn libraries # danger-allow-state-serialisation is needed to save the state in the db @@ -146,7 +147,7 @@ email_address = "0.2.9" handlebars = { version = "6.4.0", features = ["dir_source"] } # HTTP client (Used for favicons, version check, DUO and HIBP API) -reqwest = { version = "0.12.28", features = ["rustls-tls", "rustls-tls-native-roots", "stream", "json", "deflate", "gzip", "brotli", "zstd", "socks", "cookies", "charset", "http2", "system-proxy"], default-features = false} +reqwest = { version = "0.13.3", features = ["rustls-no-provider", "stream", "json", "form", "deflate", "gzip", "brotli", "zstd", "socks", "cookies", "charset", "http2", "system-proxy"], default-features = false} hickory-resolver = "0.26.1" # Favicon extraction libraries @@ -174,7 +175,7 @@ pastey = "0.2.2" governor = "0.10.4" # OIDC for SSO -openidconnect = { version = "4.0.1", features = ["reqwest", "rustls-tls"] } +openidconnect = { version = "4.0.1", default-features = false } moka = { version = "0.12.15", features = ["future"] } # Check client versions for specific features. @@ -196,15 +197,15 @@ rpassword = "7.5.1" grass_compiler = { version = "0.13.4", default-features = false } # File are accessed through Apache OpenDAL -opendal = { version = "0.55.0", features = ["services-fs"], default-features = false } +opendal = { version = "0.56.0", features = ["services-fs"], default-features = false } # For retrieving AWS credentials, including temporary SSO credentials -anyhow = { version = "1.0.102", optional = true } aws-config = { version = "1.8.16", features = ["behavior-version-latest", "rt-tokio", "credentials-process", "sso"], default-features = false, optional = true } aws-credential-types = { version = "1.2.14", optional = true } aws-smithy-runtime-api = { version = "1.12.0", optional = true } http = { version = "1.4.0", optional = true } -reqsign = { version = "0.16.5", optional = true } +reqsign-aws-v4 = { version = "3.0.0", optional = true } +reqsign-core = { version = "3.0.0", optional = true } # Strip debuginfo from the release builds # The debug symbols are to provide better panic traces diff --git a/src/api/core/sends.rs b/src/api/core/sends.rs index 22abb396..45ead810 100644 --- a/src/api/core/sends.rs +++ b/src/api/core/sends.rs @@ -568,7 +568,7 @@ async fn post_access_file( async fn download_url(host: &Host, send_id: &SendId, file_id: &SendFileId) -> Result { let operator = CONFIG.opendal_operator_for_path_type(&PathType::Sends)?; - if operator.info().scheme() == <&'static str>::from(opendal::Scheme::Fs) { + if crate::storage::is_fs_operator(&operator) { let token_claims = crate::auth::generate_send_claims(send_id, file_id); let token = crate::auth::encode_jwt(&token_claims); diff --git a/src/auth.rs b/src/auth.rs index 43184369..06bd9c22 100644 --- a/src/auth.rs +++ b/src/auth.rs @@ -54,12 +54,8 @@ static PUBLIC_RSA_KEY: OnceLock = OnceLock::new(); pub async fn initialize_keys() -> Result<(), Error> { use std::io::Error; - let rsa_key_filename = std::path::PathBuf::from(CONFIG.private_rsa_key()) - .file_name() - .ok_or_else(|| Error::other("Private RSA key path missing filename"))? - .to_str() - .ok_or_else(|| Error::other("Private RSA key path filename is not valid UTF-8"))? - .to_string(); + let rsa_key_filename = crate::storage::file_name(&CONFIG.private_rsa_key()) + .ok_or_else(|| Error::other("Private RSA key path missing filename"))?; let operator = CONFIG.opendal_operator_for_path_type(&PathType::RsaKey).map_err(Error::other)?; diff --git a/src/config.rs b/src/config.rs index ae995f69..b6d0ce8a 100644 --- a/src/config.rs +++ b/src/config.rs @@ -14,6 +14,7 @@ use serde::de::{self, Deserialize, Deserializer, MapAccess, Visitor}; use crate::{ error::Error, + storage, util::{ get_active_web_release, get_env, get_env_bool, is_valid_email, parse_experimental_client_feature_flags, FeatureFlagFilter, @@ -22,18 +23,14 @@ use crate::{ static CONFIG_FILE: LazyLock = LazyLock::new(|| { let data_folder = get_env("DATA_FOLDER").unwrap_or_else(|| String::from("data")); - get_env("CONFIG_FILE").unwrap_or_else(|| format!("{data_folder}/config.json")) + get_env("CONFIG_FILE").unwrap_or_else(|| storage::join_path(&data_folder, "config.json")) }); -static CONFIG_FILE_PARENT_DIR: LazyLock = LazyLock::new(|| { - let path = std::path::PathBuf::from(&*CONFIG_FILE); - path.parent().unwrap_or(std::path::Path::new("data")).to_str().unwrap_or("data").to_string() -}); +static CONFIG_FILE_PARENT_DIR: LazyLock = + LazyLock::new(|| storage::parent(&CONFIG_FILE).unwrap_or_else(|| "data".to_string())); -static CONFIG_FILENAME: LazyLock = LazyLock::new(|| { - let path = std::path::PathBuf::from(&*CONFIG_FILE); - path.file_name().unwrap_or(std::ffi::OsStr::new("config.json")).to_str().unwrap_or("config.json").to_string() -}); +static CONFIG_FILENAME: LazyLock = + LazyLock::new(|| storage::file_name(&CONFIG_FILE).unwrap_or_else(|| "config.json".to_string())); pub static SKIP_CONFIG_VALIDATION: AtomicBool = AtomicBool::new(false); @@ -263,7 +260,7 @@ macro_rules! make_config { } async fn from_file() -> Result { - let operator = opendal_operator_for_path(&CONFIG_FILE_PARENT_DIR)?; + let operator = storage::operator_for_path(&CONFIG_FILE_PARENT_DIR)?; let config_bytes = operator.read(&CONFIG_FILENAME).await?; println!("[INFO] Using saved config from `{}` for configuration.\n", *CONFIG_FILE); serde_json::from_slice(&config_bytes.to_vec()).map_err(Into::into) @@ -507,19 +504,19 @@ make_config! { /// Data folder |> Main data folder data_folder: String, false, def, "data".to_string(); /// Database URL - database_url: String, false, auto, |c| format!("{}/db.sqlite3", c.data_folder); + database_url: String, false, auto, |c| storage::join_path(&c.data_folder, "db.sqlite3"); /// Icon cache folder - icon_cache_folder: String, false, auto, |c| format!("{}/icon_cache", c.data_folder); + icon_cache_folder: String, false, auto, |c| storage::join_path(&c.data_folder, "icon_cache"); /// Attachments folder - attachments_folder: String, false, auto, |c| format!("{}/attachments", c.data_folder); + attachments_folder: String, false, auto, |c| storage::join_path(&c.data_folder, "attachments"); /// Sends folder - sends_folder: String, false, auto, |c| format!("{}/sends", c.data_folder); + sends_folder: String, false, auto, |c| storage::join_path(&c.data_folder, "sends"); /// Temp folder |> Used for storing temporary file uploads - tmp_folder: String, false, auto, |c| format!("{}/tmp", c.data_folder); + tmp_folder: String, false, auto, |c| storage::join_path(&c.data_folder, "tmp"); /// Templates folder - templates_folder: String, false, auto, |c| format!("{}/templates", c.data_folder); + templates_folder: String, false, auto, |c| storage::join_path(&c.data_folder, "templates"); /// Session JWT key - rsa_key_filename: String, false, auto, |c| format!("{}/rsa_key", c.data_folder); + rsa_key_filename: String, false, auto, |c| storage::join_path(&c.data_folder, "rsa_key"); /// Web vault folder web_vault_folder: String, false, def, "web-vault/".to_string(); }, @@ -1366,90 +1363,6 @@ fn smtp_convert_deprecated_ssl_options(smtp_ssl: Option, smtp_explicit_tls "starttls".to_string() } -fn opendal_operator_for_path(path: &str) -> Result { - // Cache of previously built operators by path - static OPERATORS_BY_PATH: LazyLock> = - LazyLock::new(dashmap::DashMap::new); - - if let Some(operator) = OPERATORS_BY_PATH.get(path) { - return Ok(operator.clone()); - } - - let operator = if path.starts_with("s3://") { - #[cfg(not(s3))] - return Err(opendal::Error::new(opendal::ErrorKind::ConfigInvalid, "S3 support is not enabled").into()); - - #[cfg(s3)] - opendal_s3_operator_for_path(path)? - } else { - let builder = opendal::services::Fs::default().root(path); - opendal::Operator::new(builder)?.finish() - }; - - OPERATORS_BY_PATH.insert(path.to_string(), operator.clone()); - - Ok(operator) -} - -#[cfg(s3)] -fn opendal_s3_operator_for_path(path: &str) -> Result { - use crate::http_client::aws::AwsReqwestConnector; - use aws_config::{default_provider::credentials::DefaultCredentialsChain, provider_config::ProviderConfig}; - - // This is a custom AWS credential loader that uses the official AWS Rust - // SDK config crate to load credentials. This ensures maximum compatibility - // with AWS credential configurations. For example, OpenDAL doesn't support - // AWS SSO temporary credentials yet. - struct OpenDALS3CredentialLoader {} - - #[async_trait] - impl reqsign::AwsCredentialLoad for OpenDALS3CredentialLoader { - async fn load_credential(&self, _client: reqwest::Client) -> anyhow::Result> { - use aws_credential_types::provider::ProvideCredentials as _; - use tokio::sync::OnceCell; - - static DEFAULT_CREDENTIAL_CHAIN: OnceCell = OnceCell::const_new(); - - let chain = DEFAULT_CREDENTIAL_CHAIN - .get_or_init(|| { - let reqwest_client = reqwest::Client::builder().build().unwrap(); - let connector = AwsReqwestConnector { - client: reqwest_client, - }; - - let conf = ProviderConfig::default().with_http_client(connector); - - DefaultCredentialsChain::builder().configure(conf).build() - }) - .await; - - let creds = chain.provide_credentials().await?; - - Ok(Some(reqsign::AwsCredential { - access_key_id: creds.access_key_id().to_string(), - secret_access_key: creds.secret_access_key().to_string(), - session_token: creds.session_token().map(|s| s.to_string()), - expires_in: creds.expiry().map(|expiration| expiration.into()), - })) - } - } - - const OPEN_DAL_S3_CREDENTIAL_LOADER: OpenDALS3CredentialLoader = OpenDALS3CredentialLoader {}; - - let url = Url::parse(path).map_err(|e| format!("Invalid path S3 URL path {path:?}: {e}"))?; - - let bucket = url.host_str().ok_or_else(|| format!("Missing Bucket name in data folder S3 URL {path:?}"))?; - - let builder = opendal::services::S3::default() - .customized_credential_load(Box::new(OPEN_DAL_S3_CREDENTIAL_LOADER)) - .enable_virtual_host_style() - .bucket(bucket) - .root(url.path()) - .default_storage_class("INTELLIGENT_TIERING"); - - Ok(opendal::Operator::new(builder)?.finish()) -} - pub enum PathType { Data, IconCache, @@ -1547,7 +1460,7 @@ impl Config { } //Save to file - let operator = opendal_operator_for_path(&CONFIG_FILE_PARENT_DIR)?; + let operator = storage::operator_for_path(&CONFIG_FILE_PARENT_DIR)?; operator.write(&CONFIG_FILENAME, config_str).await?; Ok(()) @@ -1612,7 +1525,7 @@ impl Config { } pub async fn delete_user_config(&self) -> Result<(), Error> { - let operator = opendal_operator_for_path(&CONFIG_FILE_PARENT_DIR)?; + let operator = storage::operator_for_path(&CONFIG_FILE_PARENT_DIR)?; operator.delete(&CONFIG_FILENAME).await?; // Empty user config @@ -1636,7 +1549,7 @@ impl Config { } pub fn private_rsa_key(&self) -> String { - format!("{}.pem", self.rsa_key_filename()) + storage::with_extension(&self.rsa_key_filename(), "pem") } pub fn mail_enabled(&self) -> bool { let inner = &self.inner.read().unwrap().config; @@ -1677,15 +1590,11 @@ impl Config { PathType::IconCache => self.icon_cache_folder(), PathType::Attachments => self.attachments_folder(), PathType::Sends => self.sends_folder(), - PathType::RsaKey => std::path::Path::new(&self.rsa_key_filename()) - .parent() - .ok_or_else(|| std::io::Error::other("Failed to get directory of RSA key file"))? - .to_str() - .ok_or_else(|| std::io::Error::other("Failed to convert RSA key file directory to UTF-8 string"))? - .to_string(), + PathType::RsaKey => storage::parent(&self.private_rsa_key()) + .ok_or_else(|| std::io::Error::other("Failed to get directory of RSA key file"))?, }; - opendal_operator_for_path(&path) + storage::operator_for_path(&path) } pub fn render_template(&self, name: &str, data: &T) -> Result { diff --git a/src/db/models/attachment.rs b/src/db/models/attachment.rs index 7611b927..dad081bd 100644 --- a/src/db/models/attachment.rs +++ b/src/db/models/attachment.rs @@ -46,7 +46,7 @@ impl Attachment { pub async fn get_url(&self, host: &str) -> Result { let operator = CONFIG.opendal_operator_for_path_type(&PathType::Attachments)?; - if operator.info().scheme() == <&'static str>::from(opendal::Scheme::Fs) { + if crate::storage::is_fs_operator(&operator) { let token = encode_jwt(&generate_file_download_claims(self.cipher_uuid.clone(), self.id.clone())); Ok(format!("{host}/attachments/{}/{}?token={token}", self.cipher_uuid, self.id)) } else { diff --git a/src/db/models/send.rs b/src/db/models/send.rs index 84802c54..5b6611fa 100644 --- a/src/db/models/send.rs +++ b/src/db/models/send.rs @@ -237,7 +237,7 @@ impl Send { if self.atype == SendType::File as i32 { let operator = CONFIG.opendal_operator_for_path_type(&PathType::Sends)?; - operator.remove_all(&self.uuid).await.ok(); + operator.delete_with(&self.uuid).recursive(true).await.ok(); } db_run! { conn: { diff --git a/src/main.rs b/src/main.rs index 60c5a593..4ffeacc1 100644 --- a/src/main.rs +++ b/src/main.rs @@ -57,6 +57,7 @@ mod mail; mod ratelimit; mod sso; mod sso_client; +mod storage; mod util; use crate::api::core::two_factor::duo_oidc::purge_duo_contexts; @@ -70,6 +71,7 @@ pub use util::is_running_in_container; #[rocket::main] async fn main() -> Result<(), Error> { + install_rustls_crypto_provider(); parse_args(); launch_info(); @@ -202,6 +204,14 @@ fn parse_args() { } } +fn install_rustls_crypto_provider() { + if rustls::crypto::CryptoProvider::get_default().is_none() { + rustls::crypto::ring::default_provider() + .install_default() + .expect("failed to install rustls ring crypto provider"); + } +} + fn launch_info() { println!( "\ diff --git a/src/sso_client.rs b/src/sso_client.rs index abff6bcb..68e171c6 100644 --- a/src/sso_client.rs +++ b/src/sso_client.rs @@ -1,12 +1,13 @@ -use std::{borrow::Cow, sync::LazyLock, time::Duration}; +use std::{borrow::Cow, future::Future, pin::Pin, sync::LazyLock, time::Duration}; -use openidconnect::{core::*, reqwest, *}; +use openidconnect::{core::*, *}; use regex::Regex; use url::Url; use crate::{ api::{ApiResult, EmptyResult}, db::models::SsoAuth, + http_client::get_reqwest_client_builder, sso::{OIDCCode, OIDCCodeChallenge, OIDCCodeVerifier, OIDCState}, CONFIG, }; @@ -46,10 +47,42 @@ pub type RefreshTokenResponse = (Option, String, Option); #[derive(Clone)] pub struct Client { - pub http_client: reqwest::Client, + pub http_client: OidcHttpClient, pub core_client: CustomClient, } +#[derive(Clone)] +pub struct OidcHttpClient { + client: reqwest::Client, +} + +impl OidcHttpClient { + fn new() -> Result { + get_reqwest_client_builder().redirect(reqwest::redirect::Policy::none()).build().map(|client| Self { + client, + }) + } +} + +impl<'c> AsyncHttpClient<'c> for OidcHttpClient { + type Error = HttpClientError; + type Future = Pin> + Send + Sync + 'c>>; + + fn call(&'c self, request: HttpRequest) -> Self::Future { + Box::pin(async move { + let response = self.client.execute(request.try_into().map_err(Box::new)?).await.map_err(Box::new)?; + + let mut builder = http::Response::builder().status(response.status()).version(response.version()); + + for (name, value) in response.headers() { + builder = builder.header(name, value); + } + + builder.body(response.bytes().await.map_err(Box::new)?.to_vec()).map_err(HttpClientError::Http) + }) + } +} + impl Client { // Call the OpenId discovery endpoint to retrieve configuration async fn _get_client() -> ApiResult { @@ -58,7 +91,7 @@ impl Client { let issuer_url = CONFIG.sso_issuer_url()?; - let http_client = match reqwest::ClientBuilder::new().redirect(reqwest::redirect::Policy::none()).build() { + let http_client = match OidcHttpClient::new() { Err(err) => err!(format!("Failed to build http client: {err}")), Ok(client) => client, }; diff --git a/src/storage.rs b/src/storage.rs new file mode 100644 index 00000000..ada2a951 --- /dev/null +++ b/src/storage.rs @@ -0,0 +1,297 @@ +use std::sync::LazyLock; + +pub(crate) fn join_path(base: &str, child: &str) -> String { + #[cfg(s3)] + if s3::is_uri(base) { + return s3::join_path(base, child); + } + + let base = base.trim_end_matches('/'); + let child = child.trim_start_matches('/'); + if base.is_empty() { + child.to_string() + } else if child.is_empty() { + base.to_string() + } else { + format!("{base}/{child}") + } +} + +pub(crate) fn with_extension(path: &str, extension: &str) -> String { + let extension = extension.trim_start_matches('.'); + + #[cfg(s3)] + if s3::is_uri(path) { + return s3::with_extension(path, extension); + } + + format!("{path}.{extension}") +} + +pub(crate) fn parent(path: &str) -> Option { + #[cfg(s3)] + if s3::is_uri(path) { + return s3::parent(path); + } + + std::path::Path::new(path).parent()?.to_str().map(ToString::to_string) +} + +pub(crate) fn file_name(path: &str) -> Option { + #[cfg(s3)] + if s3::is_uri(path) { + return s3::file_name(path); + } + + std::path::Path::new(path).file_name()?.to_str().map(ToString::to_string) +} + +pub(crate) fn is_fs_operator(operator: &opendal::Operator) -> bool { + operator.info().scheme() == opendal::services::FS_SCHEME +} + +pub(crate) fn operator_for_path(path: &str) -> Result { + // Cache of previously built operators by path + static OPERATORS_BY_PATH: LazyLock> = + LazyLock::new(dashmap::DashMap::new); + + if let Some(operator) = OPERATORS_BY_PATH.get(path) { + return Ok(operator.clone()); + } + + let operator = if path.starts_with("s3://") { + #[cfg(not(s3))] + return Err(opendal::Error::new(opendal::ErrorKind::ConfigInvalid, "S3 support is not enabled").into()); + + #[cfg(s3)] + s3::operator_for_path(path)? + } else { + let builder = opendal::services::Fs::default().root(path); + opendal::Operator::new(builder)?.finish() + }; + + OPERATORS_BY_PATH.insert(path.to_string(), operator.clone()); + + Ok(operator) +} + +#[cfg(s3)] +mod s3 { + use reqwest::Url; + + use crate::error::Error; + + pub(super) fn is_uri(path: &str) -> bool { + path.starts_with("s3://") + } + + pub(super) fn join_path(base: &str, child: &str) -> String { + if let Ok(mut url) = Url::parse(base) { + let mut segments = path_segments(&url); + segments.extend(child.split('/').filter(|segment| !segment.is_empty()).map(ToString::to_string)); + set_path_segments(&mut url, &segments); + return url.to_string(); + } + + let base = base.trim_end_matches('/'); + let child = child.trim_start_matches('/'); + if base.is_empty() { + child.to_string() + } else if child.is_empty() { + base.to_string() + } else { + format!("{base}/{child}") + } + } + + pub(super) fn with_extension(path: &str, extension: &str) -> String { + if let Ok(mut url) = Url::parse(path) { + let mut segments = path_segments(&url); + if let Some(file_name) = segments.last_mut() { + file_name.push('.'); + file_name.push_str(extension); + set_path_segments(&mut url, &segments); + return url.to_string(); + } + } + + format!("{path}.{extension}") + } + + pub(super) fn parent(path: &str) -> Option { + if let Ok(mut url) = Url::parse(path) { + let mut segments = path_segments(&url); + segments.pop()?; + set_path_segments(&mut url, &segments); + return Some(url.to_string()); + } + + std::path::Path::new(path).parent()?.to_str().map(ToString::to_string) + } + + pub(super) fn file_name(path: &str) -> Option { + if let Ok(url) = Url::parse(path) { + return path_segments(&url).pop(); + } + + std::path::Path::new(path).file_name()?.to_str().map(ToString::to_string) + } + + fn path_segments(url: &Url) -> Vec { + url.path_segments() + .map(|segments| segments.filter(|segment| !segment.is_empty()).map(ToString::to_string).collect()) + .unwrap_or_default() + } + + fn set_path_segments(url: &mut Url, segments: &[String]) { + if segments.is_empty() { + url.set_path(""); + } else { + url.set_path(&format!("/{}", segments.join("/"))); + } + } + + pub(super) fn operator_for_path(path: &str) -> Result { + use crate::http_client::aws::AwsReqwestConnector; + use aws_config::{default_provider::credentials::DefaultCredentialsChain, provider_config::ProviderConfig}; + use opendal::Configurator; + use reqsign_aws_v4::Credential; + use reqsign_core::{Context, ProvideCredential, ProvideCredentialChain}; + + // This is a custom AWS credential loader that uses the official AWS Rust + // SDK config crate to load credentials. This ensures maximum compatibility + // with AWS credential configurations. For example, OpenDAL doesn't support + // AWS SSO temporary credentials yet. + #[derive(Debug)] + struct OpenDALS3CredentialProvider; + + impl ProvideCredential for OpenDALS3CredentialProvider { + type Credential = Credential; + + async fn provide_credential(&self, _ctx: &Context) -> reqsign_core::Result> { + use aws_credential_types::provider::ProvideCredentials as _; + use reqsign_core::time::Timestamp; + use tokio::sync::OnceCell; + + static DEFAULT_CREDENTIAL_CHAIN: OnceCell = OnceCell::const_new(); + + let chain = DEFAULT_CREDENTIAL_CHAIN + .get_or_init(|| { + let reqwest_client = reqwest::Client::builder().build().unwrap(); + let connector = AwsReqwestConnector { + client: reqwest_client, + }; + + let conf = ProviderConfig::default().with_http_client(connector); + + DefaultCredentialsChain::builder().configure(conf).build() + }) + .await; + + let creds = chain.provide_credentials().await.map_err(|e| { + reqsign_core::Error::unexpected("failed to load AWS credentials via AWS SDK").with_source(e) + })?; + + let expires_in = if let Some(expiration) = creds.expiry() { + let duration = expiration.duration_since(std::time::UNIX_EPOCH).map_err(|e| { + reqsign_core::Error::unexpected("AWS credential expiration is before the Unix epoch") + .with_source(e) + })?; + let seconds = i64::try_from(duration.as_secs()).map_err(|e| { + reqsign_core::Error::unexpected("AWS credential expiration is too large").with_source(e) + })?; + Some(Timestamp::from_second(seconds)?) + } else { + None + }; + + Ok(Some(Credential { + access_key_id: creds.access_key_id().to_string(), + secret_access_key: creds.secret_access_key().to_string(), + session_token: creds.session_token().map(|s| s.to_string()), + expires_in, + })) + } + } + + let uri = opendal::OperatorUri::new(path, std::iter::empty::<(String, String)>())?; + let mut config = opendal::services::S3Config::from_uri(&uri)?; + + if !uri_has_option(&uri, &["default_storage_class"]) { + config.default_storage_class = Some("INTELLIGENT_TIERING".to_string()); + } + + if !uri_has_option( + &uri, + &["enable_virtual_host_style", "aws_virtual_hosted_style_request", "virtual_hosted_style_request"], + ) { + config.enable_virtual_host_style = true; + } + + let use_aws_sdk_credentials = !uri_has_credential_options(&uri, &config); + let mut builder = config.into_builder(); + + if use_aws_sdk_credentials { + builder = + builder.credential_provider_chain(ProvideCredentialChain::new().push(OpenDALS3CredentialProvider)); + } + + Ok(opendal::Operator::new(builder)?.finish()) + } + + fn uri_has_option(uri: &opendal::OperatorUri, names: &[&str]) -> bool { + names.iter().any(|name| uri.options().contains_key(*name)) + } + + fn uri_has_credential_options(uri: &opendal::OperatorUri, config: &opendal::services::S3Config) -> bool { + config.access_key_id.is_some() + || config.secret_access_key.is_some() + || config.session_token.is_some() + || config.role_arn.is_some() + || config.external_id.is_some() + || config.role_session_name.is_some() + || uri_has_option(uri, &["allow_anonymous", "disable_config_load", "disable_ec2_metadata"]) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn handles_local_paths() { + assert_eq!(join_path("data", "attachments"), "data/attachments"); + assert_eq!(with_extension("data/rsa_key", "pem"), "data/rsa_key.pem"); + assert_eq!(parent("data/rsa_key.pem").as_deref(), Some("data")); + assert_eq!(file_name("data/rsa_key.pem").as_deref(), Some("rsa_key.pem")); + } +} + +#[cfg(all(test, s3))] +mod s3_tests { + use super::*; + + #[test] + fn joins_s3_path_before_query_string() { + assert_eq!( + join_path("s3://bucket/base?region=us-west-2", "attachments"), + "s3://bucket/base/attachments?region=us-west-2" + ); + } + + #[test] + fn appends_extension_before_s3_query_string() { + assert_eq!( + with_extension("s3://bucket/base/rsa_key?region=us-west-2", "pem"), + "s3://bucket/base/rsa_key.pem?region=us-west-2" + ); + } + + #[test] + fn splits_s3_parent_and_file_name_without_query_string() { + let path = "s3://bucket/base/config.json?region=us-west-2"; + + assert_eq!(parent(path).as_deref(), Some("s3://bucket/base?region=us-west-2")); + assert_eq!(file_name(path).as_deref(), Some("config.json")); + } +} From 9bc14e6e7790f21e3ddf62b1b8bad6e7da9274e6 Mon Sep 17 00:00:00 2001 From: Mathijs van Veluw Date: Fri, 15 May 2026 20:31:58 +0200 Subject: [PATCH 81/89] Fix SSO Cookie path (#7187) Signed-off-by: BlackDex --- src/api/identity.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/api/identity.rs b/src/api/identity.rs index 569deaf9..9f64e560 100644 --- a/src/api/identity.rs +++ b/src/api/identity.rs @@ -1222,7 +1222,8 @@ async fn _oidcsignin_redirect( (Some(expected), Some(actual)) if crypto::ct_eq(expected, actual) => {} _ => err!(format!("SSO session binding mismatch for {state}")), } - cookies.remove(Cookie::build(SSO_BINDING_COOKIE).path("/identity/connect/").build()); + cookies + .remove(Cookie::build(SSO_BINDING_COOKIE).path(format!("{}/identity/connect/", CONFIG.domain_path())).build()); sso_auth.code_response = Some(code_response); sso_auth.updated_at = Utc::now().naive_utc(); @@ -1294,7 +1295,7 @@ async fn authorize(data: AuthorizeData, cookies: &CookieJar<'_>, secure: Secure, cookies.add( Cookie::build((SSO_BINDING_COOKIE, binding_token)) - .path("/identity/connect/") + .path(format!("{}/identity/connect/", CONFIG.domain_path())) .max_age(time::Duration::seconds(sso::SSO_AUTH_EXPIRATION.num_seconds())) .same_site(SameSite::Lax) // Lax is needed because the IdP runs on a different FQDN .http_only(true) From 2f85b62d2f2d27505fa2262dce096a20d01afe84 Mon Sep 17 00:00:00 2001 From: Stefan Melmuk <509385+stefan0xC@users.noreply.github.com> Date: Sat, 16 May 2026 21:18:39 +0200 Subject: [PATCH 82/89] fix email 2fa for bw cli (#7225) --- src/api/core/two_factor/email.rs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/api/core/two_factor/email.rs b/src/api/core/two_factor/email.rs index e7d1aed2..7fa350de 100644 --- a/src/api/core/two_factor/email.rs +++ b/src/api/core/two_factor/email.rs @@ -25,7 +25,7 @@ pub fn routes() -> Vec { #[serde(rename_all = "camelCase")] struct SendEmailLoginData { #[serde(alias = "DeviceIdentifier")] - device_identifier: DeviceId, + device_identifier: Option, #[serde(alias = "Email")] email: Option, #[serde(alias = "MasterPasswordHash")] @@ -91,8 +91,11 @@ async fn send_email_login(data: Json, client_headers: Client user } else { + let Some(device_identifier) = &data.device_identifier else { + err!("No device identifier has been submitted.") + }; // SSO login only sends device id, so we get the user by the most recently used device - let Some(user) = User::find_by_device_for_email2fa(&data.device_identifier, &conn).await else { + let Some(user) = User::find_by_device_for_email2fa(device_identifier, &conn).await else { err!("Username or password is incorrect. Try again.") }; From a057c7deae78b9f47f6d91372b7ffc0b397d849e Mon Sep 17 00:00:00 2001 From: Timshel Date: Sat, 16 May 2026 19:18:46 +0000 Subject: [PATCH 83/89] sso_auth improvements (#7197) Co-authored-by: Timshel --- .../2026-05-05-120000_sso_auth_error/down.sql | 1 + .../2026-05-05-120000_sso_auth_error/up.sql | 1 + .../2026-05-05-120000_sso_auth_error/down.sql | 1 + .../2026-05-05-120000_sso_auth_error/up.sql | 1 + .../2026-05-05-120000_sso_auth_error/down.sql | 1 + .../2026-05-05-120000_sso_auth_error/up.sql | 1 + src/api/identity.rs | 42 +++++++++---------- src/db/models/mod.rs | 2 +- src/db/models/sso_auth.rs | 28 ++++++++----- src/db/schema.rs | 1 + src/sso.rs | 28 ++++++------- 11 files changed, 59 insertions(+), 48 deletions(-) create mode 100644 migrations/mysql/2026-05-05-120000_sso_auth_error/down.sql create mode 100644 migrations/mysql/2026-05-05-120000_sso_auth_error/up.sql create mode 100644 migrations/postgresql/2026-05-05-120000_sso_auth_error/down.sql create mode 100644 migrations/postgresql/2026-05-05-120000_sso_auth_error/up.sql create mode 100644 migrations/sqlite/2026-05-05-120000_sso_auth_error/down.sql create mode 100644 migrations/sqlite/2026-05-05-120000_sso_auth_error/up.sql diff --git a/migrations/mysql/2026-05-05-120000_sso_auth_error/down.sql b/migrations/mysql/2026-05-05-120000_sso_auth_error/down.sql new file mode 100644 index 00000000..98a6d836 --- /dev/null +++ b/migrations/mysql/2026-05-05-120000_sso_auth_error/down.sql @@ -0,0 +1 @@ +ALTER TABLE sso_auth DROP COLUMN code_response_error; diff --git a/migrations/mysql/2026-05-05-120000_sso_auth_error/up.sql b/migrations/mysql/2026-05-05-120000_sso_auth_error/up.sql new file mode 100644 index 00000000..6042a7d4 --- /dev/null +++ b/migrations/mysql/2026-05-05-120000_sso_auth_error/up.sql @@ -0,0 +1 @@ +ALTER TABLE sso_auth ADD COLUMN code_response_error TEXT; diff --git a/migrations/postgresql/2026-05-05-120000_sso_auth_error/down.sql b/migrations/postgresql/2026-05-05-120000_sso_auth_error/down.sql new file mode 100644 index 00000000..fae11ae3 --- /dev/null +++ b/migrations/postgresql/2026-05-05-120000_sso_auth_error/down.sql @@ -0,0 +1 @@ +ALTER TABLE sso_auth DROP COLUMN IF EXISTS code_response_error; diff --git a/migrations/postgresql/2026-05-05-120000_sso_auth_error/up.sql b/migrations/postgresql/2026-05-05-120000_sso_auth_error/up.sql new file mode 100644 index 00000000..d4524898 --- /dev/null +++ b/migrations/postgresql/2026-05-05-120000_sso_auth_error/up.sql @@ -0,0 +1 @@ +ALTER TABLE sso_auth ADD COLUMN IF NOT EXISTS code_response_error TEXT; diff --git a/migrations/sqlite/2026-05-05-120000_sso_auth_error/down.sql b/migrations/sqlite/2026-05-05-120000_sso_auth_error/down.sql new file mode 100644 index 00000000..98a6d836 --- /dev/null +++ b/migrations/sqlite/2026-05-05-120000_sso_auth_error/down.sql @@ -0,0 +1 @@ +ALTER TABLE sso_auth DROP COLUMN code_response_error; diff --git a/migrations/sqlite/2026-05-05-120000_sso_auth_error/up.sql b/migrations/sqlite/2026-05-05-120000_sso_auth_error/up.sql new file mode 100644 index 00000000..6042a7d4 --- /dev/null +++ b/migrations/sqlite/2026-05-05-120000_sso_auth_error/up.sql @@ -0,0 +1 @@ +ALTER TABLE sso_auth ADD COLUMN code_response_error TEXT; diff --git a/src/api/identity.rs b/src/api/identity.rs index 9f64e560..34078529 100644 --- a/src/api/identity.rs +++ b/src/api/identity.rs @@ -28,8 +28,9 @@ use crate::{ crypto, db::{ models::{ - AuthRequest, AuthRequestId, Device, DeviceId, EventType, Invitation, OIDCCodeWrapper, OrganizationApiKey, - OrganizationId, SsoAuth, SsoUser, TwoFactor, TwoFactorIncomplete, TwoFactorType, User, UserId, + AuthRequest, AuthRequestId, Device, DeviceId, EventType, Invitation, OIDCCodeResponseError, + OrganizationApiKey, OrganizationId, SsoAuth, SsoUser, TwoFactor, TwoFactorIncomplete, TwoFactorType, User, + UserId, }, DbConn, }, @@ -186,7 +187,7 @@ async fn _sso_login( // Ratelimit the login crate::ratelimit::check_limit_login(&ip.ip)?; - let (state, code_verifier) = match (data.code.as_ref(), data.code_verifier.as_ref()) { + let (code, code_verifier) = match (data.code.as_ref(), data.code_verifier.as_ref()) { (None, _) => err!( "Got no code in OIDC data", ErrorEvent { @@ -202,7 +203,7 @@ async fn _sso_login( (Some(code), Some(code_verifier)) => (code, code_verifier.clone()), }; - let (sso_auth, user_infos) = sso::exchange_code(state, code_verifier, conn).await?; + let (sso_auth, user_infos) = sso::exchange_code(code, code_verifier, conn).await?; let user_with_sso = match SsoUser::find_by_identifier(&user_infos.identifier, conn).await { None => match SsoUser::find_by_mail(&user_infos.email, conn).await { None => None, @@ -1138,7 +1139,7 @@ struct ConnectData { // Needed for authorization code #[field(name = uncased("code"))] - code: Option, + code: Option, #[field(name = uncased("code_verifier"))] code_verifier: Option, } @@ -1165,19 +1166,12 @@ const SSO_BINDING_COOKIE: &str = "VW_SSO_BINDING"; #[get("/connect/oidc-signin?&", rank = 1)] async fn oidcsignin(code: OIDCCode, state: String, cookies: &CookieJar<'_>, mut conn: DbConn) -> ApiResult { - _oidcsignin_redirect( - state, - OIDCCodeWrapper::Ok { - code, - }, - cookies, - &mut conn, - ) - .await + _oidcsignin_redirect(state, code, None, cookies, &mut conn).await } -// Bitwarden client appear to only care for code and state so we pipe it through -// cf: https://github.com/bitwarden/clients/blob/80b74b3300e15b4ae414dc06044cc9b02b6c10a6/libs/auth/src/angular/sso/sso.component.ts#L141 +// Bitwarden client appear to only care for code and state +// We save the error in the database and set the encoded state as the code to be able to retrieve them later on +// cf: https://github.com/bitwarden/clients/blob/afd36d290ce18fb0048e0575e7d5a8f78b5dbffc/libs/auth/src/angular/sso/sso.component.ts#L156 #[get("/connect/oidc-signin?&&", rank = 2)] async fn oidcsignin_error( state: String, @@ -1187,11 +1181,12 @@ async fn oidcsignin_error( mut conn: DbConn, ) -> ApiResult { _oidcsignin_redirect( - state, - OIDCCodeWrapper::Error { + state.clone(), + state.into(), + Some(OIDCCodeResponseError { error, error_description, - }, + }), cookies, &mut conn, ) @@ -1200,10 +1195,10 @@ async fn oidcsignin_error( // The state was encoded using Base64 to ensure no issue with providers. // iss and scope parameters are needed for redirection to work on IOS. -// We pass the state as the code to get it back later on. async fn _oidcsignin_redirect( base64_state: String, - code_response: OIDCCodeWrapper, + code: OIDCCode, + error: Option, cookies: &CookieJar<'_>, conn: &mut DbConn, ) -> ApiResult { @@ -1225,7 +1220,8 @@ async fn _oidcsignin_redirect( cookies .remove(Cookie::build(SSO_BINDING_COOKIE).path(format!("{}/identity/connect/", CONFIG.domain_path())).build()); - sso_auth.code_response = Some(code_response); + sso_auth.code_response = Some(code.clone()); + sso_auth.code_response_error = error; sso_auth.updated_at = Utc::now().naive_utc(); sso_auth.save(conn).await?; @@ -1235,7 +1231,7 @@ async fn _oidcsignin_redirect( }; url.query_pairs_mut() - .append_pair("code", &state) + .append_pair("code", &code) .append_pair("state", &state) .append_pair("scope", &AuthMethod::Sso.scope()) .append_pair("iss", &CONFIG.domain()); diff --git a/src/db/models/mod.rs b/src/db/models/mod.rs index 2d31259c..7cc81852 100644 --- a/src/db/models/mod.rs +++ b/src/db/models/mod.rs @@ -38,7 +38,7 @@ pub use self::send::{ id::{SendFileId, SendId}, Send, SendType, }; -pub use self::sso_auth::{OIDCAuthenticatedUser, OIDCCodeWrapper, SsoAuth}; +pub use self::sso_auth::{OIDCAuthenticatedUser, OIDCCodeResponseError, SsoAuth}; pub use self::two_factor::{TwoFactor, TwoFactorType}; pub use self::two_factor_duo_context::TwoFactorDuoContext; pub use self::two_factor_incomplete::TwoFactorIncomplete; diff --git a/src/db/models/sso_auth.rs b/src/db/models/sso_auth.rs index 2c6eec6d..3ecaf9f7 100644 --- a/src/db/models/sso_auth.rs +++ b/src/db/models/sso_auth.rs @@ -15,17 +15,12 @@ use diesel::sql_types::Text; #[derive(AsExpression, Clone, Debug, Serialize, Deserialize, FromSqlRow)] #[diesel(sql_type = Text)] -pub enum OIDCCodeWrapper { - Ok { - code: OIDCCode, - }, - Error { - error: String, - error_description: Option, - }, +pub struct OIDCCodeResponseError { + pub error: String, + pub error_description: Option, } -impl_FromToSqlText!(OIDCCodeWrapper); +impl_FromToSqlText!(OIDCCodeResponseError); #[derive(AsExpression, Clone, Debug, Serialize, Deserialize, FromSqlRow)] #[diesel(sql_type = Text)] @@ -50,7 +45,8 @@ pub struct SsoAuth { pub client_challenge: OIDCCodeChallenge, pub nonce: String, pub redirect_uri: String, - pub code_response: Option, + pub code_response: Option, + pub code_response_error: Option, pub auth_response: Option, pub created_at: NaiveDateTime, pub updated_at: NaiveDateTime, @@ -76,6 +72,7 @@ impl SsoAuth { created_at: now, updated_at: now, code_response: None, + code_response_error: None, auth_response: None, binding_hash, } @@ -118,6 +115,17 @@ impl SsoAuth { }} } + pub async fn find_by_code(code: &OIDCCode, conn: &DbConn) -> Option { + let oldest = Utc::now().naive_utc() - *SSO_AUTH_EXPIRATION; + db_run! { conn: { + sso_auth::table + .filter(sso_auth::code_response.eq(code)) + .filter(sso_auth::created_at.ge(oldest)) + .first::(conn) + .ok() + }} + } + pub async fn delete(self, conn: &DbConn) -> EmptyResult { db_run! {conn: { diesel::delete(sso_auth::table.filter(sso_auth::state.eq(self.state))) diff --git a/src/db/schema.rs b/src/db/schema.rs index bf79ceac..af342186 100644 --- a/src/db/schema.rs +++ b/src/db/schema.rs @@ -262,6 +262,7 @@ table! { nonce -> Text, redirect_uri -> Text, code_response -> Nullable, + code_response_error -> Nullable, auth_response -> Nullable, created_at -> Timestamp, updated_at -> Timestamp, diff --git a/src/sso.rs b/src/sso.rs index 7505f84f..56e9a534 100644 --- a/src/sso.rs +++ b/src/sso.rs @@ -10,7 +10,7 @@ use crate::{ auth, auth::{AuthMethod, AuthTokens, TokenWrapper, BW_EXPIRATION, DEFAULT_REFRESH_VALIDITY}, db::{ - models::{Device, OIDCAuthenticatedUser, OIDCCodeWrapper, SsoAuth, SsoUser, User}, + models::{Device, OIDCAuthenticatedUser, SsoAuth, SsoUser, User}, DbConn, }, sso_client::Client, @@ -240,14 +240,14 @@ impl OIDCIdentifier { // - second time we will rely on `SsoAuth.auth_response` since the `code` has already been exchanged. // The `SsoAuth` will ensure that the user is authorized only once. pub async fn exchange_code( - state: &OIDCState, + code: &OIDCCode, client_verifier: OIDCCodeVerifier, conn: &DbConn, ) -> ApiResult<(SsoAuth, OIDCAuthenticatedUser)> { use openidconnect::OAuth2TokenResponse; - let mut sso_auth = match SsoAuth::find(state, conn).await { - None => err!(format!("Invalid state cannot retrieve sso auth")), + let mut sso_auth = match SsoAuth::find_by_code(code, conn).await { + None => err!(format!("Invalid code cannot retrieve sso auth")), Some(sso_auth) => sso_auth, }; @@ -255,18 +255,18 @@ pub async fn exchange_code( return Ok((sso_auth, authenticated_user)); } - let code = match sso_auth.code_response.clone() { - Some(OIDCCodeWrapper::Ok { - code, - }) => code.clone(), - Some(OIDCCodeWrapper::Error { - error, - error_description, - }) => { + let code = match (sso_auth.code_response.clone(), sso_auth.code_response_error.as_ref()) { + (Some(code), None) => code, + (_, Some(re)) => { + let error_msg = format!( + "SSO authorization failed: {}, {}", + re.error, + re.error_description.as_ref().unwrap_or(&String::new()) + ); sso_auth.delete(conn).await?; - err!(format!("SSO authorization failed: {error}, {}", error_description.as_ref().unwrap_or(&String::new()))) + err!(error_msg); } - None => { + (None, _) => { sso_auth.delete(conn).await?; err!("Missing authorization provider return"); } From 54895ad4bed4c12e07cce0dedfb0498830d6f991 Mon Sep 17 00:00:00 2001 From: mfw78 Date: Sat, 16 May 2026 19:18:53 +0000 Subject: [PATCH 84/89] Reject unrecognised DATABASE_URL instead of silent SQLite fallback (#7061) * Panic on unrecognised DATABASE_URL instead of silent SQLite fallback Previously, any DATABASE_URL that did not match the mysql: or postgresql: prefix was silently treated as a SQLite file path. This caused data loss in containerised environments when the URL was misconfigured (typos, quoting issues), as vaultwarden would create an ephemeral SQLite database that was wiped on restart. Now, an explicit sqlite:// prefix is supported and used as the default. Bare paths without a recognised scheme are still accepted for backwards compatibility, but only if the database file already exists. If not, the process panics with a clear error message. Relates to #2835, #1910, #860. * Use err!() instead of panic!() for unrecognised DATABASE_URL Follow the established codebase convention where configuration validation errors use err!() to propagate gracefully, rather than panic!(). The error propagates through from_config() and is caught by create_db_pool() which logs and calls exit(1). * Use 'scheme' instead of 'prefix' in DATABASE_URL messages Per review feedback, 'scheme' is the more accurate term for the sqlite:// portion of the URL. --- .env.template | 9 +++++---- src/config.rs | 21 ++++++++++++--------- src/db/mod.rs | 34 +++++++++++++++++++++++++++------- 3 files changed, 44 insertions(+), 20 deletions(-) diff --git a/.env.template b/.env.template index 03990820..a12559ad 100644 --- a/.env.template +++ b/.env.template @@ -50,10 +50,11 @@ ######################### ## Database URL -## When using SQLite, this is the path to the DB file, and it defaults to -## %DATA_FOLDER%/db.sqlite3. If DATA_FOLDER is set to an external location, this -## must be set to a local sqlite3 file path. -# DATABASE_URL=data/db.sqlite3 +## When using SQLite, this should use the sqlite:// scheme followed by the path +## to the DB file. It defaults to sqlite://%DATA_FOLDER%/db.sqlite3. +## Bare paths without the sqlite:// scheme are supported for backwards compatibility, +## but only if the database file already exists. +# DATABASE_URL=sqlite://data/db.sqlite3 ## When using MySQL, specify an appropriate connection URI. ## Details: https://docs.diesel.rs/2.1.x/diesel/mysql/struct.MysqlConnection.html # DATABASE_URL=mysql://user:password@host[:port]/database_name diff --git a/src/config.rs b/src/config.rs index b6d0ce8a..770fb553 100644 --- a/src/config.rs +++ b/src/config.rs @@ -504,7 +504,7 @@ make_config! { /// Data folder |> Main data folder data_folder: String, false, def, "data".to_string(); /// Database URL - database_url: String, false, auto, |c| storage::join_path(&c.data_folder, "db.sqlite3"); + database_url: String, false, auto, |c| format!("sqlite://{}", storage::join_path(&c.data_folder, "db.sqlite3")); /// Icon cache folder icon_cache_folder: String, false, auto, |c| storage::join_path(&c.data_folder, "icon_cache"); /// Attachments folder @@ -926,14 +926,17 @@ fn validate_config(cfg: &ConfigItems, on_update: bool) -> Result<(), Error> { { use crate::db::DbConnType; let url = &cfg.database_url; - if DbConnType::from_url(url)? == DbConnType::Sqlite && url.contains('/') { - let path = std::path::Path::new(&url); - if let Some(parent) = path.parent() { - if !parent.is_dir() { - err!(format!( - "SQLite database directory `{}` does not exist or is not a directory", - parent.display() - )); + if DbConnType::from_url(url)? == DbConnType::Sqlite { + let file_path = url.strip_prefix("sqlite://").unwrap_or(url); + if file_path.contains('/') { + let path = std::path::Path::new(file_path); + if let Some(parent) = path.parent() { + if !parent.is_dir() { + err!(format!( + "SQLite database directory `{}` does not exist or is not a directory", + parent.display() + )); + } } } } diff --git a/src/db/mod.rs b/src/db/mod.rs index d2ed9479..4aafe995 100644 --- a/src/db/mod.rs +++ b/src/db/mod.rs @@ -272,13 +272,32 @@ impl DbConnType { #[cfg(not(postgresql))] err!("`DATABASE_URL` is a PostgreSQL URL, but the 'postgresql' feature is not enabled") - //Sqlite - } else { + // Sqlite (explicit) + } else if url.len() > 7 && &url[..7] == "sqlite:" { #[cfg(sqlite)] return Ok(DbConnType::Sqlite); #[cfg(not(sqlite))] - err!("`DATABASE_URL` looks like a SQLite URL, but 'sqlite' feature is not enabled") + err!("`DATABASE_URL` is a SQLite URL, but the 'sqlite' feature is not enabled") + + // No recognized scheme — assume legacy bare-path SQLite, but the database file must already exist. + // This prevents misconfigured URLs (typos, quoted strings) from silently creating a new empty SQLite database. + } else { + #[cfg(sqlite)] + { + if std::path::Path::new(url).exists() { + return Ok(DbConnType::Sqlite); + } + err!(format!( + "`DATABASE_URL` does not match any known database scheme (mysql://, postgresql://, sqlite://) \ + and no existing SQLite database was found at '{url}'. \ + If you intend to use SQLite, use an explicit `sqlite://` scheme in your `DATABASE_URL`. \ + Otherwise, check your DATABASE_URL for typos or quoting issues." + )) + } + + #[cfg(not(sqlite))] + err!("`DATABASE_URL` does not match any known database scheme (mysql://, postgresql://, sqlite://)") } } @@ -390,11 +409,12 @@ pub fn backup_sqlite() -> Result { let db_url = CONFIG.database_url(); if DbConnType::from_url(&CONFIG.database_url()).map(|t| t == DbConnType::Sqlite).unwrap_or(false) { - // Since we do not allow any schema for sqlite database_url's like `file:` or `sqlite:` to be set, we can assume here it isn't - // This way we can set a readonly flag on the opening mode without issues. - let mut conn = diesel::sqlite::SqliteConnection::establish(&format!("sqlite://{db_url}?mode=ro"))?; + // Strip the sqlite:// prefix if present to get the raw file path + let file_path = db_url.strip_prefix("sqlite://").unwrap_or(&db_url); + // Open a read-only connection for the backup + let mut conn = diesel::sqlite::SqliteConnection::establish(&format!("sqlite://{file_path}?mode=ro"))?; - let db_path = std::path::Path::new(&db_url).parent().unwrap(); + let db_path = std::path::Path::new(file_path).parent().unwrap(); let backup_file = db_path .join(format!("db_{}.sqlite3", chrono::Utc::now().format("%Y%m%d_%H%M%S"))) .to_string_lossy() From 70f9dfbe8b5b82b05cd8cb552a51b4de23a0f292 Mon Sep 17 00:00:00 2001 From: Daniel Date: Sat, 16 May 2026 22:19:01 +0300 Subject: [PATCH 85/89] Switch to `xx-cargo` (#6640) - removes a lot of additional configuration lines from the Dockerfile - includes the workaround for the `openssl-sys` build issues with improper `pkg-config` setup - for reference: https://github.com/tonistiigi/xx/pull/108#issuecomment-3700635977 --- docker/Dockerfile.alpine | 1 - docker/Dockerfile.debian | 44 ++++++++------------------------ docker/Dockerfile.j2 | 54 +++++++++++++++------------------------- 3 files changed, 31 insertions(+), 68 deletions(-) diff --git a/docker/Dockerfile.alpine b/docker/Dockerfile.alpine index cbb18e2b..88492d94 100644 --- a/docker/Dockerfile.alpine +++ b/docker/Dockerfile.alpine @@ -57,7 +57,6 @@ ENV DEBIAN_FRONTEND=noninteractive \ # Debian Trixie uses libpq v17 PQ_LIB_DIR="/usr/local/musl/pq17/lib" - # Create CARGO_HOME folder and don't download rust docs RUN mkdir -pv "${CARGO_HOME}" && \ rustup set profile minimal diff --git a/docker/Dockerfile.debian b/docker/Dockerfile.debian index 829f59d2..04f93361 100644 --- a/docker/Dockerfile.debian +++ b/docker/Dockerfile.debian @@ -51,7 +51,7 @@ ENV DEBIAN_FRONTEND=noninteractive \ TERM=xterm-256color \ CARGO_HOME="/root/.cargo" \ USER="root" -# Install clang to get `xx-cargo` working +# Install clang && xx-c-essentials to get `xx-cargo` working # Install pkg-config to allow amd64 builds to find all libraries # Install git so build.rs can determine the correct version # Install the libc cross packages based upon the debian-arch @@ -59,19 +59,16 @@ RUN apt-get update && \ apt-get install -y \ --no-install-recommends \ clang \ - pkg-config \ - git \ - "libc6-$(xx-info debian-arch)-cross" \ - "libc6-dev-$(xx-info debian-arch)-cross" \ - "linux-libc-dev-$(xx-info debian-arch)-cross" && \ + git && \ xx-apt-get install -y \ --no-install-recommends \ - gcc \ libpq-dev \ libpq5 \ libssl-dev \ libmariadb-dev \ - zlib1g-dev && \ + pkg-config \ + zlib1g-dev \ + xx-c-essentials && \ # Run xx-cargo early, since it sometimes seems to break when run at a later stage echo "export CARGO_TARGET=$(xx-cargo --print-target-triple)" >> /env-cargo @@ -83,29 +80,6 @@ RUN mkdir -pv "${CARGO_HOME}" && \ RUN USER=root cargo new --bin /app WORKDIR /app -# Environment variables for Cargo on Debian based builds -ARG TARGET_PKG_CONFIG_PATH - -RUN source /env-cargo && \ - if xx-info is-cross ; then \ - # We can't use xx-cargo since that uses clang, which doesn't work for our libraries. - # Because of this we generate the needed environment variables here which we can load in the needed steps. - echo "export CC_$(echo "${CARGO_TARGET}" | tr '[:upper:]' '[:lower:]' | tr - _)=/usr/bin/$(xx-info)-gcc" >> /env-cargo && \ - echo "export CARGO_TARGET_$(echo "${CARGO_TARGET}" | tr '[:lower:]' '[:upper:]' | tr - _)_LINKER=/usr/bin/$(xx-info)-gcc" >> /env-cargo && \ - echo "export CROSS_COMPILE=1" >> /env-cargo && \ - echo "export PKG_CONFIG_ALLOW_CROSS=1" >> /env-cargo && \ - # For some architectures `xx-info` returns a triple which doesn't matches the path on disk - # In those cases you can override this by setting the `TARGET_PKG_CONFIG_PATH` build-arg - if [[ -n "${TARGET_PKG_CONFIG_PATH}" ]]; then \ - echo "export TARGET_PKG_CONFIG_PATH=${TARGET_PKG_CONFIG_PATH}" >> /env-cargo ; \ - else \ - echo "export PKG_CONFIG_PATH=/usr/lib/$(xx-info)/pkgconfig" >> /env-cargo ; \ - fi && \ - echo "# End of env-cargo" >> /env-cargo ; \ - fi && \ - # Output the current contents of the file - cat /env-cargo - RUN source /env-cargo && \ rustup target add "${CARGO_TARGET}" @@ -122,7 +96,9 @@ ARG DB=sqlite,mysql,postgresql # dummy project, except the target folder # This folder contains the compiled dependencies RUN source /env-cargo && \ - cargo build --features ${DB} --profile "${CARGO_PROFILE}" --target="${CARGO_TARGET}" && \ + # Workaround for xx related build issues + # https://github.com/tonistiigi/xx/pull/108#issuecomment-3700635977 + PKG_CONFIG="$(command -v "$(xx-info)-pkg-config")" xx-cargo build --features ${DB} --profile "${CARGO_PROFILE}" && \ find . -not -path "./target*" -delete # Copies the complete project @@ -137,7 +113,9 @@ RUN source /env-cargo && \ # Also do this for build.rs to ensure the version is rechecked touch build.rs src/main.rs && \ # Create a symlink to the binary target folder to easy copy the binary in the final stage - cargo build --features ${DB} --profile "${CARGO_PROFILE}" --target="${CARGO_TARGET}" && \ + # Workaround for xx related build issues + # https://github.com/tonistiigi/xx/pull/108#issuecomment-3700635977 + PKG_CONFIG="$(command -v "$(xx-info)-pkg-config")" xx-cargo build --features ${DB} --profile "${CARGO_PROFILE}" && \ if [[ "${CARGO_PROFILE}" == "dev" ]] ; then \ ln -vfsr "/app/target/${CARGO_TARGET}/debug" /app/target/final ; \ else \ diff --git a/docker/Dockerfile.j2 b/docker/Dockerfile.j2 index f745780e..f7a056ff 100644 --- a/docker/Dockerfile.j2 +++ b/docker/Dockerfile.j2 @@ -27,6 +27,11 @@ # $ docker image inspect --format "{{ '{{' }}.RepoTags}}" docker.io/vaultwarden/web-vault@{{ vault_image_digest }} # [docker.io/vaultwarden/web-vault:{{ vault_version | replace('+', '_') }}] # +{% macro xx_cargo_config() -%} +# Workaround for xx related build issues + # https://github.com/tonistiigi/xx/pull/108#issuecomment-3700635977 + PKG_CONFIG="$(command -v "$(xx-info)-pkg-config")" xx-cargo build --features ${DB} --profile "${CARGO_PROFILE}" +{%- endmacro %} FROM --platform=linux/amd64 docker.io/vaultwarden/web-vault@{{ vault_image_digest }} AS vault {% if base == "debian" %} @@ -66,10 +71,10 @@ ENV DEBIAN_FRONTEND=noninteractive \ # Use PostgreSQL v17 during Alpine/MUSL builds instead of the default v16 # Debian Trixie uses libpq v17 PQ_LIB_DIR="/usr/local/musl/pq17/lib" -{% endif %} +{%- endif %} {% if base == "debian" %} -# Install clang to get `xx-cargo` working +# Install clang && xx-c-essentials to get `xx-cargo` working # Install pkg-config to allow amd64 builds to find all libraries # Install git so build.rs can determine the correct version # Install the libc cross packages based upon the debian-arch @@ -77,19 +82,16 @@ RUN apt-get update && \ apt-get install -y \ --no-install-recommends \ clang \ - pkg-config \ - git \ - "libc6-$(xx-info debian-arch)-cross" \ - "libc6-dev-$(xx-info debian-arch)-cross" \ - "linux-libc-dev-$(xx-info debian-arch)-cross" && \ + git && \ xx-apt-get install -y \ --no-install-recommends \ - gcc \ libpq-dev \ libpq5 \ libssl-dev \ libmariadb-dev \ - zlib1g-dev && \ + pkg-config \ + zlib1g-dev \ + xx-c-essentials && \ # Run xx-cargo early, since it sometimes seems to break when run at a later stage echo "export CARGO_TARGET=$(xx-cargo --print-target-triple)" >> /env-cargo {% endif %} @@ -102,31 +104,7 @@ RUN mkdir -pv "${CARGO_HOME}" && \ RUN USER=root cargo new --bin /app WORKDIR /app -{% if base == "debian" %} -# Environment variables for Cargo on Debian based builds -ARG TARGET_PKG_CONFIG_PATH - -RUN source /env-cargo && \ - if xx-info is-cross ; then \ - # We can't use xx-cargo since that uses clang, which doesn't work for our libraries. - # Because of this we generate the needed environment variables here which we can load in the needed steps. - echo "export CC_$(echo "${CARGO_TARGET}" | tr '[:upper:]' '[:lower:]' | tr - _)=/usr/bin/$(xx-info)-gcc" >> /env-cargo && \ - echo "export CARGO_TARGET_$(echo "${CARGO_TARGET}" | tr '[:lower:]' '[:upper:]' | tr - _)_LINKER=/usr/bin/$(xx-info)-gcc" >> /env-cargo && \ - echo "export CROSS_COMPILE=1" >> /env-cargo && \ - echo "export PKG_CONFIG_ALLOW_CROSS=1" >> /env-cargo && \ - # For some architectures `xx-info` returns a triple which doesn't matches the path on disk - # In those cases you can override this by setting the `TARGET_PKG_CONFIG_PATH` build-arg - if [[ -n "${TARGET_PKG_CONFIG_PATH}" ]]; then \ - echo "export TARGET_PKG_CONFIG_PATH=${TARGET_PKG_CONFIG_PATH}" >> /env-cargo ; \ - else \ - echo "export PKG_CONFIG_PATH=/usr/lib/$(xx-info)/pkgconfig" >> /env-cargo ; \ - fi && \ - echo "# End of env-cargo" >> /env-cargo ; \ - fi && \ - # Output the current contents of the file - cat /env-cargo - -{% elif base == "alpine" %} +{% if base == "alpine" %} # Environment variables for Cargo on Alpine based builds RUN echo "export CARGO_TARGET=${RUST_MUSL_CROSS_TARGET}" >> /env-cargo && \ # Output the current contents of the file @@ -154,7 +132,11 @@ ARG DB=sqlite,mysql,postgresql,enable_mimalloc # dummy project, except the target folder # This folder contains the compiled dependencies RUN source /env-cargo && \ +{% if base == "debian" %} + {{ xx_cargo_config() }} && \ +{% elif base == "alpine" %} cargo build --features ${DB} --profile "${CARGO_PROFILE}" --target="${CARGO_TARGET}" && \ +{% endif %} find . -not -path "./target*" -delete # Copies the complete project @@ -169,7 +151,11 @@ RUN source /env-cargo && \ # Also do this for build.rs to ensure the version is rechecked touch build.rs src/main.rs && \ # Create a symlink to the binary target folder to easy copy the binary in the final stage +{% if base == "debian" %} + {{ xx_cargo_config() }} && \ +{% elif base == "alpine" %} cargo build --features ${DB} --profile "${CARGO_PROFILE}" --target="${CARGO_TARGET}" && \ +{% endif %} if [[ "${CARGO_PROFILE}" == "dev" ]] ; then \ ln -vfsr "/app/target/${CARGO_TARGET}/debug" /app/target/final ; \ else \ From 22f5e0496cca369af378c4f6379a0260b521fcbb Mon Sep 17 00:00:00 2001 From: Mathijs van Veluw Date: Sun, 17 May 2026 00:43:58 +0200 Subject: [PATCH 86/89] Updates and fixes (#7235) * Update crates and gha Updated all the crates Updated GitHub Actions Signed-off-by: BlackDex * Fix restoring revoked user A new endpoint is used to restore a revoked user. This commit fixes that. Fixes #7224 Signed-off-by: BlackDex * Update datatables Signed-off-by: BlackDex --------- Signed-off-by: BlackDex --- .github/workflows/trivy.yml | 2 +- .github/workflows/typos.yml | 2 +- .github/workflows/zizmor.yml | 2 +- .pre-commit-config.yaml | 2 +- Cargo.lock | 150 ++++++++++++++++-------------- Cargo.toml | 10 +- src/api/core/organizations.rs | 13 +++ src/static/scripts/datatables.css | 4 +- src/static/scripts/datatables.js | 52 ++++++++--- 9 files changed, 147 insertions(+), 90 deletions(-) diff --git a/.github/workflows/trivy.yml b/.github/workflows/trivy.yml index 26f64aed..83d00210 100644 --- a/.github/workflows/trivy.yml +++ b/.github/workflows/trivy.yml @@ -50,6 +50,6 @@ jobs: severity: CRITICAL,HIGH - name: Upload Trivy scan results to GitHub Security tab - uses: github/codeql-action/upload-sarif@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4.35.2 + uses: github/codeql-action/upload-sarif@9e0d7b8d25671d64c341c19c0152d693099fb5ba # v4.35.5 with: sarif_file: 'trivy-results.sarif' diff --git a/.github/workflows/typos.yml b/.github/workflows/typos.yml index 375600ed..eaf77896 100644 --- a/.github/workflows/typos.yml +++ b/.github/workflows/typos.yml @@ -23,4 +23,4 @@ jobs: # When this version is updated, do not forget to update this in `.pre-commit-config.yaml` too - name: Spell Check Repo - uses: crate-ci/typos@7c572958218557a3272c2d6719629443b5cc26fd # v1.45.2 + uses: crate-ci/typos@5374cbf686e897b15713110e233094e2874de7ef # v1.46.1 diff --git a/.github/workflows/zizmor.yml b/.github/workflows/zizmor.yml index 2350ec61..d5bc7464 100644 --- a/.github/workflows/zizmor.yml +++ b/.github/workflows/zizmor.yml @@ -24,7 +24,7 @@ jobs: persist-credentials: false - name: Run zizmor - uses: zizmorcore/zizmor-action@b1d7e1fb5de872772f31590499237e7cce841e8e # v0.5.3 + uses: zizmorcore/zizmor-action@b572f7b1a1c2d41efaab43d504f68d215c3cd727 # v0.5.4 with: # intentionally not scanning the entire repository, # since it contains integration tests. diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index f10cef65..11f0b439 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -18,7 +18,7 @@ repos: # When this version is updated, do not forget to update this in `.github/workflows/typos.yaml` too - repo: https://github.com/crate-ci/typos - rev: 7c572958218557a3272c2d6719629443b5cc26fd # v1.45.2 + rev: 5374cbf686e897b15713110e233094e2874de7ef # v1.46.1 hooks: - id: typos diff --git a/Cargo.lock b/Cargo.lock index 9a44e0b2..f4d87f9b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -790,6 +790,15 @@ dependencies = [ "alloc-stdlib", ] +[[package]] +name = "bs58" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf88ba1141d185c399bee5288d850d63b8369520c1eafc32a0430b5b6c287bf4" +dependencies = [ + "tinyvec", +] + [[package]] name = "bumpalo" version = "3.20.2" @@ -863,9 +872,9 @@ checksum = "ade8366b8bd5ba243f0a58f036cc0ca8a2f069cff1a2351ef1cac6b083e16fc0" [[package]] name = "cc" -version = "1.2.61" +version = "1.2.62" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d16d90359e986641506914ba71350897565610e87ce0ad9e6f28569db3dd5c6d" +checksum = "a1dce859f0832a7d088c4f1119888ab94ef4b5d6795d1ce05afb7fe159d79f98" dependencies = [ "find-msvc-tools", "jobserver", @@ -1547,9 +1556,9 @@ dependencies = [ [[package]] name = "digest" -version = "0.11.2" +version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4850db49bf08e663084f7fb5c87d202ef91a3907271aff24a94eb97ff039153c" +checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2" dependencies = [ "block-buffer 0.12.0", "const-oid 0.10.2", @@ -2099,9 +2108,9 @@ dependencies = [ [[package]] name = "h2" -version = "0.4.13" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f44da3a8150a6703ed5d34e164b875fd14c2cdab9af1252a9a1020bde2bdc54" +checksum = "171fefbc92fe4a4de27e0698d6a5b392d6a0e333506bc49133760b3bcf948733" dependencies = [ "atomic-waker", "bytes", @@ -2182,9 +2191,9 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.17.0" +version = "0.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" [[package]] name = "heck" @@ -2298,7 +2307,7 @@ version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6303bc9732ae41b04cb554b844a762b4115a61bfaa81e3e83050991eeb56863f" dependencies = [ - "digest 0.11.2", + "digest 0.11.3", ] [[package]] @@ -2390,9 +2399,9 @@ checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" [[package]] name = "hybrid-array" -version = "0.4.11" +version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08d46837a0ed51fe95bd3b05de33cd64a1ee88fc797477ca48446872504507c5" +checksum = "9155a582abd142abc056962c29e3ce5ff2ad5469f4246b537ed42c5deba857da" dependencies = [ "typenum", ] @@ -2638,7 +2647,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" dependencies = [ "equivalent", - "hashbrown 0.17.0", + "hashbrown 0.17.1", "serde", "serde_core", ] @@ -2671,16 +2680,6 @@ dependencies = [ "serde", ] -[[package]] -name = "iri-string" -version = "0.7.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25e659a4bb38e810ebc252e53b5814ff908a8c58c2a9ce2fae1bbec24cbf4e20" -dependencies = [ - "memchr", - "serde", -] - [[package]] name = "is-terminal" version = "0.4.17" @@ -2828,9 +2827,9 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.97" +version = "0.3.98" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1840c94c045fbcf8ba2812c95db44499f7c64910a912551aaaa541decebcacf" +checksum = "67df7112613f8bfd9150013a0314e196f4800d3201ae742489d999db2f979f08" dependencies = [ "cfg-if", "futures-util", @@ -2840,9 +2839,9 @@ dependencies = [ [[package]] name = "jsonwebtoken" -version = "10.3.0" +version = "10.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0529410abe238729a60b108898784df8984c87f6054c9c4fcacc47e4803c1ce1" +checksum = "eba32bfb4ffdeaca3e34431072faf01745c9b26d25504aa7a6cf5684334fc4fc" dependencies = [ "base64 0.22.1", "ed25519-dalek", @@ -2859,6 +2858,7 @@ dependencies = [ "sha2 0.10.9", "signature", "simple_asn1", + "zeroize", ] [[package]] @@ -2896,9 +2896,9 @@ checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" [[package]] name = "lettre" -version = "0.11.21" +version = "0.11.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dabda5859ee7c06b995b9d1165aa52c39110e079ef609db97178d86aeb051fa7" +checksum = "0da65617f6cb926332d039cb578aad56178da86e128db6a1b09f4c94fa5b3349" dependencies = [ "async-std", "async-trait", @@ -3455,15 +3455,14 @@ dependencies = [ [[package]] name = "openssl" -version = "0.10.78" +version = "0.10.79" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f38c4372413cdaaf3cc79dd92d29d7d9f5ab09b51b10dded508fb90bb70b9222" +checksum = "bf0b434746ee2832f4f0baf10137e1cabb18cbe6912c69e2e33263c45250f542" dependencies = [ "bitflags", "cfg-if", "foreign-types", "libc", - "once_cell", "openssl-macros", "openssl-sys", ] @@ -3496,9 +3495,9 @@ dependencies = [ [[package]] name = "openssl-sys" -version = "0.9.114" +version = "0.9.115" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13ce1245cd07fcc4cfdb438f7507b0c7e4f3849a69fd84d52374c66d83741bb6" +checksum = "158fe5b292746440aa6e7a7e690e55aeb72d41505e2804c23c6973ad0e9c9781" dependencies = [ "cc", "libc", @@ -3881,9 +3880,9 @@ dependencies = [ [[package]] name = "prefix-trie" -version = "0.8.2" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23370be78b7e5bcbb0cab4a02047eb040279a693c78daad04c2c5f1c24a83503" +checksum = "4cf6e3177f0684016a5c209b00882e15f8bdd3f3bb48f0491df10cd102d0c6e7" dependencies = [ "either", "ipnet", @@ -4431,9 +4430,9 @@ dependencies = [ [[package]] name = "rpassword" -version = "7.5.1" +version = "7.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2501c67132bd19c3005b0111fba298907ef002c8c1cf68e25634707e38bf66fe" +checksum = "5ac5b223d9738ef56e0b98305410be40fa0941bf6036c56f1506751e43552d64" dependencies = [ "libc", "rtoolbox", @@ -4869,11 +4868,12 @@ dependencies = [ [[package]] name = "serde_with" -version = "3.18.0" +version = "3.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd5414fad8e6907dbdd5bc441a50ae8d6e26151a03b1de04d89a5576de61d01f" +checksum = "e72c1c2cb7b223fafb600a619537a871c2818583d619401b785e7c0b746ccde2" dependencies = [ "base64 0.22.1", + "bs58", "chrono", "hex", "indexmap 1.9.3", @@ -4888,9 +4888,9 @@ dependencies = [ [[package]] name = "serde_with_macros" -version = "3.18.0" +version = "3.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3db8978e608f1fe7357e211969fd9abdcae80bac1ba7a3369bb7eb6b404eb65" +checksum = "b90c488738ecb4fb0262f41f43bc40efc5868d9fb744319ddf5f5317f417bfac" dependencies = [ "darling 0.23.0", "proc-macro2", @@ -4928,7 +4928,7 @@ checksum = "446ba717509524cb3f22f17ecc096f10f4822d76ab5c0b9822c5f9c284e825f4" dependencies = [ "cfg-if", "cpufeatures 0.3.0", - "digest 0.11.2", + "digest 0.11.3", ] [[package]] @@ -5012,9 +5012,9 @@ dependencies = [ [[package]] name = "siphasher" -version = "1.0.2" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2aa850e253778c88a04c3d7323b043aeda9d3e30d5971937c1855769763678e" +checksum = "8ee5873ec9cce0195efcb7a4e9507a04cd49aec9c83d0389df45b1ef7ba2e649" [[package]] name = "slab" @@ -5344,9 +5344,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.52.1" +version = "1.52.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b67dee974fe86fd92cc45b7a95fdd2f99a36a6d7b0d431a231178d3d670bbcc6" +checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe" dependencies = [ "bytes", "libc", @@ -5490,7 +5490,7 @@ version = "1.1.2+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" dependencies = [ - "winnow 1.0.2", + "winnow 1.0.3", ] [[package]] @@ -5528,9 +5528,9 @@ dependencies = [ [[package]] name = "tower-http" -version = "0.6.8" +version = "0.6.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" +checksum = "68d6fdd9f81c2819c9a8b0e0cd91660e7746a8e6ea2ba7c6b2b057985f6bcb51" dependencies = [ "async-compression", "bitflags", @@ -5540,13 +5540,13 @@ dependencies = [ "http 1.4.0", "http-body 1.0.1", "http-body-util", - "iri-string", "pin-project-lite", "tokio", "tokio-util", "tower", "tower-layer", "tower-service", + "url", ] [[package]] @@ -5900,9 +5900,9 @@ dependencies = [ [[package]] name = "wasm-bindgen" -version = "0.2.120" +version = "0.2.121" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df52b6d9b87e0c74c9edfa1eb2d9bf85e5d63515474513aa50fa181b3c4f5db1" +checksum = "49ace1d07c165b0864824eee619580c4689389afa9dc9ed3a4c75040d82e6790" dependencies = [ "cfg-if", "once_cell", @@ -5913,9 +5913,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.70" +version = "0.4.71" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af934872acec734c2d80e6617bbb5ff4f12b052dd8e6332b0817bce889516084" +checksum = "96492d0d3ffba25305a7dc88720d250b1401d7edca02cc3bcd50633b424673b8" dependencies = [ "js-sys", "wasm-bindgen", @@ -5923,9 +5923,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.120" +version = "0.2.121" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78b1041f495fb322e64aca85f5756b2172e35cd459376e67f2a6c9dffcedb103" +checksum = "8e68e6f4afd367a562002c05637acb8578ff2dea1943df76afb9e83d177c8578" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -5933,9 +5933,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.120" +version = "0.2.121" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9dcd0ff20416988a18ac686d4d4d0f6aae9ebf08a389ff5d29012b05af2a1b41" +checksum = "d95a9ec35c64b2a7cb35d3fead40c4238d0940c86d107136999567a4703259f2" dependencies = [ "bumpalo", "proc-macro2", @@ -5946,9 +5946,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.120" +version = "0.2.121" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49757b3c82ebf16c57d69365a142940b384176c24df52a087fb748e2085359ea" +checksum = "c4e0100b01e9f0d03189a92b96772a1fb998639d981193d7dbab487302513441" dependencies = [ "unicode-ident", ] @@ -6002,9 +6002,9 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.97" +version = "0.3.98" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2eadbac71025cd7b0834f20d1fe8472e8495821b4e9801eb0a60bd1f19827602" +checksum = "4b572dff8bcf38bad0fa19729c89bb5748b2b9b1d8be70cf90df697e3a8f32aa" dependencies = [ "js-sys", "wasm-bindgen", @@ -6390,9 +6390,9 @@ dependencies = [ [[package]] name = "winnow" -version = "1.0.2" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ee1708bef14716a11bae175f579062d4554d95be2c6829f518df847b7b3fdd0" +checksum = "0592e1c9d151f854e6fd382574c3a0855250e1d9b2f99d9281c6e6391af352f1" [[package]] name = "wit-bindgen" @@ -6523,9 +6523,9 @@ dependencies = [ [[package]] name = "xml" -version = "1.2.1" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8aa498d22c9bbaf482329839bc5620c46be275a19a812e9a22a2b07529a642a" +checksum = "636f85e5ca6488e96401b61eb7de54f4e44755c988af0f52cf90230c312a1a89" [[package]] name = "xmlparser" @@ -6603,9 +6603,9 @@ dependencies = [ [[package]] name = "zerofrom" -version = "0.1.7" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69faa1f2a1ea75661980b013019ed6687ed0e83d069bc1114e2cc74c6c04c4df" +checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" dependencies = [ "zerofrom-derive", ] @@ -6627,6 +6627,20 @@ name = "zeroize" version = "1.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85a5b4158499876c763cb03bc4e49185d3cccbabb15b33c627f7884f43db852e" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] [[package]] name = "zerotrie" diff --git a/Cargo.toml b/Cargo.toml index 271d102d..61910302 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -81,7 +81,7 @@ dashmap = "6.1.0" # Async futures futures = "0.3.32" -tokio = { version = "1.52.1", features = ["rt-multi-thread", "fs", "io-util", "parking_lot", "time", "signal", "net"] } +tokio = { version = "1.52.3", features = ["rt-multi-thread", "fs", "io-util", "parking_lot", "time", "signal", "net"] } tokio-util = { version = "0.7.18", features = ["compat"]} # A generic serialization/deserialization framework @@ -120,7 +120,7 @@ job_scheduler_ng = "2.4.0" data-encoding = "2.11.0" # JWT library -jsonwebtoken = { version = "10.3.0", features = ["use_pem", "rust_crypto"], default-features = false } +jsonwebtoken = { version = "10.4.0", features = ["use_pem", "rust_crypto"], default-features = false } # TOTP library totp-lite = "2.0.1" @@ -139,7 +139,7 @@ webauthn-rs-core = "0.5.5" url = "2.5.8" # Email libraries -lettre = { version = "0.11.21", features = ["smtp-transport", "sendmail-transport", "builder", "serde", "hostname", "tracing", "tokio1-rustls", "ring", "rustls-native-certs"], default-features = false } +lettre = { version = "0.11.22", features = ["smtp-transport", "sendmail-transport", "builder", "serde", "hostname", "tracing", "tokio1-rustls", "ring", "rustls-native-certs"], default-features = false } percent-encoding = "2.3.2" # URL encoding library used for URL's in the emails email_address = "0.2.9" @@ -165,7 +165,7 @@ cookie = "0.18.1" cookie_store = "0.22.1" # Used by U2F, JWT and PostgreSQL -openssl = "0.10.78" +openssl = "0.10.79" # CLI argument parsing pico-args = "0.5.0" @@ -191,7 +191,7 @@ which = "8.0.2" argon2 = "0.5.3" # Reading a password from the cli for generating the Argon2id ADMIN_TOKEN -rpassword = "7.5.1" +rpassword = "7.5.2" # Loading a dynamic CSS Stylesheet grass_compiler = { version = "0.13.4", default-features = false } diff --git a/src/api/core/organizations.rs b/src/api/core/organizations.rs index 31311a65..3e6eb767 100644 --- a/src/api/core/organizations.rs +++ b/src/api/core/organizations.rs @@ -78,6 +78,7 @@ pub fn routes() -> Vec { revoke_member, bulk_revoke_members, restore_member, + restore_member_vnext, bulk_restore_members, get_groups, get_groups_details, @@ -2315,6 +2316,18 @@ async fn _revoke_member( Ok(()) } +#[put("/organizations//users//restore/vnext")] +async fn restore_member_vnext( + org_id: OrganizationId, + member_id: MembershipId, + headers: AdminHeaders, + conn: DbConn, +) -> EmptyResult { + // Vaultwarden does not (yet) support the per User Collection linked to the `Enforce organization data ownership` policy. + // Therefor we ignore the `defaultUserCollectionName` data sent and just call restore_member + _restore_member(&org_id, &member_id, &headers, &conn).await +} + #[put("/organizations//users//restore")] async fn restore_member( org_id: OrganizationId, diff --git a/src/static/scripts/datatables.css b/src/static/scripts/datatables.css index d91ea601..e518c143 100644 --- a/src/static/scripts/datatables.css +++ b/src/static/scripts/datatables.css @@ -4,10 +4,10 @@ * * To rebuild or modify this file with the latest versions of the included * software please visit: - * https://datatables.net/download/#bs5/dt-2.3.7 + * https://datatables.net/download/#bs5/dt-2.3.8 * * Included libraries: - * DataTables 2.3.7 + * DataTables 2.3.8 */ :root { diff --git a/src/static/scripts/datatables.js b/src/static/scripts/datatables.js index 9c7fa042..c9f9ea56 100644 --- a/src/static/scripts/datatables.js +++ b/src/static/scripts/datatables.js @@ -4,13 +4,13 @@ * * To rebuild or modify this file with the latest versions of the included * software please visit: - * https://datatables.net/download/#bs5/dt-2.3.7 + * https://datatables.net/download/#bs5/dt-2.3.8 * * Included libraries: - * DataTables 2.3.7 + * DataTables 2.3.8 */ -/*! DataTables 2.3.7 +/*! DataTables 2.3.8 * © SpryMedia Ltd - datatables.net/license */ @@ -525,7 +525,7 @@ * * @type string */ - builder: "bs5/dt-2.3.7", + builder: "bs5/dt-2.3.8", /** * Buttons. For use with the Buttons extension for DataTables. This is @@ -3607,6 +3607,11 @@ if ( holdPosition !== true ) { settings._iDisplayStart = 0; } + else { + // Keep position, but make sure that there is actually data to display, + // otherwise we need to rewind a bit (e.g. if rows were deleted) + _fnLengthOverflow(settings); + } // Let any modules know about the draw hold position state (used by // scrolling internally) @@ -4920,6 +4925,12 @@ var args = [settings, settings.json]; + // If the footer element is empty after initialisation, then remove it + let tfoot = $(settings.tfoot); + if (tfoot.children().length === 0) { + tfoot.remove(); + } + settings._bInitComplete = true; // Table is fully set up and we have data, so calculate the @@ -5376,12 +5387,12 @@ // the content of the cell so that the width applied to the header and body // both match, but we want to hide it completely. $('th, td', headerCopy).each(function () { - $(this.childNodes).wrapAll('
'); + $(this.childNodes).wrapAll('
'); }); if ( footer ) { $('th, td', footerCopy).each(function () { - $(this.childNodes).wrapAll('
'); + $(this.childNodes).wrapAll('
'); }); } @@ -5409,6 +5420,10 @@ // Correct DOM ordering for colgroup - comes before the thead table.children('colgroup').prependTo(table); + // Remove tabindex from the hidden row elements + table.find('thead, tfoot').find('[tabindex]').removeAttr('tabindex'); + table.find('thead, tfoot').find('role').removeAttr('role'); + // Adjust the position of the header in case we loose the y-scrollbar divBody.trigger('scroll'); @@ -5732,8 +5747,12 @@ .replace(/id=".*?"/g, '') .replace(/name=".*?"/g, ''); - // Don't want Javascript at all in these calculation cells. - cellString = cellString.replace(//gi, ' '); + // Don't want script, dialog or template tags in the width + // calculations as they are hidden content + cellString = cellString + .replace(//gi, ' ') + .replace(//gi, ' ') + .replace(//gi, ' '); var noHtml = _stripHtml(cellString, ' ') .replace( / /g, ' ' ); @@ -10304,7 +10323,7 @@ * @type string * @default Version number */ - DataTable.version = "2.3.7"; + DataTable.version = "2.3.8"; /** * Private data store, containing all of the settings objects that are @@ -12586,6 +12605,7 @@ var __mlWarning = false; var __luxon; // Can be assigned in DateTable.use() var __moment; // Can be assigned in DateTable.use() + var __reIsoTimezone = /[T\s]\d{2}.*?(Z|[+-]\d{2}(?::?\d{2})?)$/; /** * @@ -12606,7 +12626,7 @@ resolveWindowLibs(); if (__moment) { - dt = __moment.utc( d, format, locale, true ); + dt = __moment( d, format, locale, true ); if (! dt.isValid()) { return null; @@ -12716,6 +12736,16 @@ return d; } + // Determine if there is a timezone. If there is, we want to reuse + // it for the output, so the timezone doesn't change between the + // input and output. + let options = {}; + let tzMatch = typeof d === 'string' ? d.match(__reIsoTimezone) : null; + + if (tzMatch) { + options.timeZone = tzMatch[1] === 'Z' ? 'UTC' : tzMatch[1]; + } + var dt = __mldObj(d, from, locale); if (dt === null) { @@ -12729,7 +12759,7 @@ var formatted = to === null ? __mld(dt, 'toDate', 'toJSDate', '')[localeString]( navigator.language, - { timeZone: "UTC" } + options ) : __mld(dt, 'format', 'toFormat', 'toISOString', to); From 1ba2c6a26c3097f87afbf17d172918c30817896f Mon Sep 17 00:00:00 2001 From: Mathijs van Veluw Date: Sun, 17 May 2026 19:38:49 +0200 Subject: [PATCH 87/89] Switch to Edition 2024, more clippy lints, and less macro calls (#7200) * Update to Rust 2024 Edition Updated to the Rust 2024 Edition and added and fixed several lint checks. This is a large change which, because of the extra lints, added some possible fixes for issues. Signed-off-by: BlackDex * Reorder and merge imports Signed-off-by: BlackDex * Remove "db_run!" macro calls where possible Signed-off-by: BlackDex --------- Signed-off-by: BlackDex --- Cargo.toml | 169 +++-- build.rs | 10 +- macros/src/lib.rs | 9 +- rustfmt.toml | 2 +- src/api/admin.rs | 133 ++-- src/api/core/accounts.rs | 172 +++-- src/api/core/ciphers.rs | 222 +++--- src/api/core/emergency_access.rs | 52 +- src/api/core/events.rs | 114 ++- src/api/core/folders.rs | 9 +- src/api/core/mod.rs | 86 ++- src/api/core/organizations.rs | 268 +++---- src/api/core/public.rs | 129 ++-- src/api/core/sends.rs | 66 +- src/api/core/two_factor/authenticator.rs | 22 +- src/api/core/two_factor/duo.rs | 33 +- src/api/core/two_factor/duo_oidc.rs | 44 +- src/api/core/two_factor/email.rs | 40 +- src/api/core/two_factor/mod.rs | 35 +- src/api/core/two_factor/protected_actions.rs | 26 +- src/api/core/two_factor/webauthn.rs | 88 +-- src/api/core/two_factor/yubikey.rs | 20 +- src/api/icons.rs | 75 +- src/api/identity.rs | 270 +++---- src/api/mod.rs | 11 +- src/api/notifications.rs | 37 +- src/api/push.rs | 48 +- src/api/web.rs | 20 +- src/auth.rs | 148 ++-- src/config.rs | 261 +++---- src/db/mod.rs | 55 +- src/db/models/archive.rs | 32 +- src/db/models/attachment.rs | 77 +- src/db/models/auth_request.rs | 52 +- src/db/models/cipher.rs | 684 +++++++++--------- src/db/models/collection.rs | 711 +++++++++++-------- src/db/models/device.rs | 83 +-- src/db/models/emergency_access.rs | 116 +-- src/db/models/event.rs | 66 +- src/db/models/favorite.rs | 62 +- src/db/models/folder.rs | 71 +- src/db/models/group.rs | 265 +++---- src/db/models/mod.rs | 4 +- src/db/models/org_policy.rs | 144 ++-- src/db/models/organization.rs | 387 +++++----- src/db/models/send.rs | 135 ++-- src/db/models/sso_auth.rs | 40 +- src/db/models/two_factor.rs | 67 +- src/db/models/two_factor_duo_context.rs | 48 +- src/db/models/two_factor_incomplete.rs | 45 +- src/db/models/user.rs | 143 ++-- src/db/query_logger.rs | 11 +- src/error.rs | 92 +-- src/http_client.rs | 53 +- src/mail.rs | 56 +- src/main.rs | 60 +- src/ratelimit.rs | 8 +- src/sso.rs | 62 +- src/sso_client.rs | 50 +- src/storage.rs | 30 +- src/util.rs | 111 ++- 61 files changed, 3352 insertions(+), 3087 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 61910302..599af5c8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,5 @@ [workspace.package] -edition = "2021" +edition = "2024" rust-version = "1.93.0" license = "AGPL-3.0-only" repository = "https://github.com/dani-garcia/vaultwarden" @@ -23,7 +23,8 @@ publish.workspace = true [features] default = [ - # "sqlite" or "sqlite_system", + # "sqlite", + # "sqlite_system", # "mysql", # "postgresql", ] @@ -32,14 +33,22 @@ enable_syslog = [] # Please enable at least one of these DB backends. mysql = ["diesel/mysql", "diesel_migrations/mysql"] postgresql = ["diesel/postgres", "diesel_migrations/postgres"] -sqlite_system = ["diesel/sqlite", "diesel_migrations/sqlite"] -sqlite = ["sqlite_system", "libsqlite3-sys/bundled"] # Alternative to the above, statically linked SQLite into the binary instead of dynamically. +sqlite_system = ["diesel/sqlite", "diesel_migrations/sqlite"] # Dynamically link SQLite +sqlite = ["sqlite_system", "libsqlite3-sys/bundled"] # Statically link SQLite into the binary instead of dynamically. # Enable to use a vendored and statically linked openssl vendored_openssl = ["openssl/vendored"] # Enable MiMalloc memory allocator to replace the default malloc # This can improve performance for Alpine builds enable_mimalloc = ["dep:mimalloc"] -s3 = ["opendal/services-s3", "dep:aws-config", "dep:aws-credential-types", "dep:aws-smithy-runtime-api", "dep:http", "dep:reqsign-aws-v4", "dep:reqsign-core"] +s3 = [ + "opendal/services-s3", + "dep:aws-config", + "dep:aws-credential-types", + "dep:aws-smithy-runtime-api", + "dep:http", + "dep:reqsign-aws-v4", + "dep:reqsign-core", +] # OIDC specific features oidc-accept-rfc3339-timestamps = ["openidconnect/accept-rfc3339-timestamps"] @@ -59,7 +68,8 @@ macros = { path = "./macros" } # Logging log = "0.4.29" fern = { version = "0.7.1", features = ["syslog-7", "reopen-1"] } -tracing = { version = "0.1.44", features = ["log"] } # Needed to have lettre and webauthn-rs trace logging to work +# We need the `log` feature for `tracing` to enable logging for several crates to work, like lettre or webauthn-rs +tracing = { version = "0.1.44", features = ["log"] } # A `dotenv` implementation for Rust dotenvy = { version = "0.15.7", default-features = false } @@ -70,8 +80,8 @@ num-derive = "0.4.2" bigdecimal = "0.4.10" # Web framework -rocket = { version = "0.5.1", features = ["tls", "json"], default-features = false } -rocket_ws = { version ="0.1.1" } +rocket = { version = "0.5.1", default-features = false, features = ["json", "tls"] } +rocket_ws = { version = "0.1.1" } # WebSockets libraries rmpv = "1.3.1" # MessagePack library @@ -81,19 +91,32 @@ dashmap = "6.1.0" # Async futures futures = "0.3.32" -tokio = { version = "1.52.3", features = ["rt-multi-thread", "fs", "io-util", "parking_lot", "time", "signal", "net"] } -tokio-util = { version = "0.7.18", features = ["compat"]} +tokio = { version = "1.52.3", features = [ + "fs", + "io-util", + "net", + "parking_lot", + "rt-multi-thread", + "signal", + "time", +] } +tokio-util = { version = "0.7.18", features = ["compat"] } # A generic serialization/deserialization framework serde = { version = "1.0.228", features = ["derive"] } serde_json = "1.0.149" # A safe, extensible ORM and Query builder -# Currently pinned diesel to v2.3.3 as newer version break MySQL/MariaDB compatibility diesel = { version = "2.3.9", features = ["chrono", "r2d2", "numeric"] } diesel_migrations = "2.3.2" -derive_more = { version = "2.1.1", features = ["from", "into", "as_ref", "deref", "display"] } +derive_more = { version = "2.1.1", features = [ + "as_ref", + "deref", + "display", + "from", + "into", +] } diesel-derive-newtype = "2.1.2" # SQLite, statically bundled unless the `sqlite_system` feature is enabled @@ -109,7 +132,7 @@ subtle = "2.6.1" uuid = { version = "1.23.1", features = ["v4"] } # Date and time libraries -chrono = { version = "0.4.44", features = ["clock", "serde"], default-features = false } +chrono = { version = "0.4.44", default-features = false, features = ["clock", "serde"] } chrono-tz = "0.10.4" time = "0.3.47" @@ -120,13 +143,13 @@ job_scheduler_ng = "2.4.0" data-encoding = "2.11.0" # JWT library -jsonwebtoken = { version = "10.4.0", features = ["use_pem", "rust_crypto"], default-features = false } +jsonwebtoken = { version = "10.4.0", default-features = false, features = ["rust_crypto", "use_pem"] } # TOTP library totp-lite = "2.0.1" # Yubico Library -yubico = { package = "yubico_ng", version = "0.15.0", features = ["online-tokio"], default-features = false } +yubico = { package = "yubico_ng", version = "0.15.0", default-features = false, features = ["online-tokio"] } # WebAuthn libraries # danger-allow-state-serialisation is needed to save the state in the db @@ -139,7 +162,20 @@ webauthn-rs-core = "0.5.5" url = "2.5.8" # Email libraries -lettre = { version = "0.11.22", features = ["smtp-transport", "sendmail-transport", "builder", "serde", "hostname", "tracing", "tokio1-rustls", "ring", "rustls-native-certs"], default-features = false } +lettre = { version = "0.11.22", default-features = false, features = [ + # Misc + "tracing", + "serde", + "builder", + "hostname", + # TLS/Security + "ring", + "rustls-native-certs", + "tokio1-rustls", + # Transport + "smtp-transport", + "sendmail-transport", +] } percent-encoding = "2.3.2" # URL encoding library used for URL's in the emails email_address = "0.2.9" @@ -147,12 +183,33 @@ email_address = "0.2.9" handlebars = { version = "6.4.0", features = ["dir_source"] } # HTTP client (Used for favicons, version check, DUO and HIBP API) -reqwest = { version = "0.13.3", features = ["rustls-no-provider", "stream", "json", "form", "deflate", "gzip", "brotli", "zstd", "socks", "cookies", "charset", "http2", "system-proxy"], default-features = false} +reqwest = { version = "0.13.3", default-features = false, features = [ + # Misc + "charset", + "cookies", + "http2", + "json", + "form", + "rustls-no-provider", + "stream", + # Compression + "brotli", + "deflate", + "gzip", + "zstd", + # Proxy + "socks", + "system-proxy", +] } hickory-resolver = "0.26.1" # Favicon extraction libraries html5gum = "0.8.3" -regex = { version = "1.12.3", features = ["std", "perf", "unicode-perl"], default-features = false } +regex = { version = "1.12.3", default-features = false, features = [ + "perf", + "std", + "unicode-perl", +] } data-url = "0.3.2" bytes = "1.11.1" svg-hush = "0.9.6" @@ -183,7 +240,7 @@ semver = "1.0.28" # Allow overriding the default memory allocator # Mainly used for the musl builds, since the default musl malloc is very slow -mimalloc = { version = "0.1.50", features = ["secure"], default-features = false, optional = true } +mimalloc = { version = "0.1.50", optional = true, default-features = false, features = ["secure"] } which = "8.0.2" @@ -197,10 +254,15 @@ rpassword = "7.5.2" grass_compiler = { version = "0.13.4", default-features = false } # File are accessed through Apache OpenDAL -opendal = { version = "0.56.0", features = ["services-fs"], default-features = false } +opendal = { version = "0.56.0", default-features = false, features = ["services-fs"] } # For retrieving AWS credentials, including temporary SSO credentials -aws-config = { version = "1.8.16", features = ["behavior-version-latest", "rt-tokio", "credentials-process", "sso"], default-features = false, optional = true } +aws-config = { version = "1.8.16", optional = true, default-features = false, features = [ + "behavior-version-latest", + "credentials-process", + "rt-tokio", + "sso", +] } aws-credential-types = { version = "1.2.14", optional = true } aws-smithy-runtime-api = { version = "1.12.0", optional = true } http = { version = "1.4.0", optional = true } @@ -265,77 +327,74 @@ unsafe_code = "forbid" non_ascii_idents = "forbid" # Deny -deprecated_in_future = "deny" +warnings = "deny" # Explicitly deny all warnings since we deny all warnings in the end + +# Deny lint groups deprecated_safe = { level = "deny", priority = -1 } future_incompatible = { level = "deny", priority = -1 } keyword_idents = { level = "deny", priority = -1 } let_underscore = { level = "deny", priority = -1 } nonstandard_style = { level = "deny", priority = -1 } -noop_method_call = "deny" refining_impl_trait = { level = "deny", priority = -1 } rust_2018_idioms = { level = "deny", priority = -1 } rust_2021_compatibility = { level = "deny", priority = -1 } rust_2024_compatibility = { level = "deny", priority = -1 } +unused = { level = "deny", priority = -1 } + +# Deny individual lints +closure_returning_async_block = "deny" +deprecated_in_future = "deny" single_use_lifetimes = "deny" trivial_casts = "deny" trivial_numeric_casts = "deny" -unused = { level = "deny", priority = -1 } unused_import_braces = "deny" unused_lifetimes = "deny" unused_qualifications = "deny" variant_size_differences = "deny" -# Allow the following lints since these cause issues with Rust v1.84.0 or newer -# Building Vaultwarden with Rust v1.85.0 with edition 2024 also works without issues -edition_2024_expr_fragment_specifier = "allow" # Once changed to Rust 2024 this should be removed and macro's should be validated again -if_let_rescope = "allow" -tail_expr_drop_order = "allow" # https://rust-lang.github.io/rust-clippy/stable/index.html [workspace.lints.clippy] -# Warn +# Warn only so you can still use these during development, but not in the final code dbg_macro = "warn" todo = "warn" # Ignore/Allow result_large_err = "allow" -# Deny +# Warn on these lint group (Some might be warn by default already though) +# Will be denied during CI! +complexity = { level = "warn", priority = -1 } +pedantic = { level = "warn", priority = -1 } +perf = { level = "warn", priority = -1 } +style = { level = "warn", priority = -1 } +suspicious = { level = "warn", priority = -1 } + +# Deny individual lints branches_sharing_code = "deny" -case_sensitive_file_extension_comparisons = "deny" -cast_lossless = "deny" clone_on_ref_ptr = "deny" -duration_suboptimal_units = "deny" equatable_if_let = "deny" -excessive_precision = "deny" -filter_map_next = "deny" float_cmp_const = "deny" -implicit_clone = "deny" -inefficient_to_string = "deny" iter_on_empty_collections = "deny" iter_on_single_items = "deny" -linkedlist = "deny" -macro_use_imports = "deny" -manual_assert = "deny" -manual_instant_elapsed = "deny" -manual_string_new = "deny" -match_wildcard_for_single_variants = "deny" mem_forget = "deny" -needless_borrow = "deny" needless_collect = "deny" -needless_continue = "deny" -needless_lifetimes = "deny" -option_option = "deny" redundant_clone = "deny" -ref_option = "deny" -string_add_assign = "deny" -unnecessary_join = "deny" unnecessary_self_imports = "deny" -unnested_or_patterns = "deny" -unused_async = "deny" -unused_self = "deny" useless_let_if_seq = "deny" verbose_file_reads = "deny" -zero_sized_map_values = "deny" +str_to_string = "deny" + +# Pedantic Opt-Outs +inline_always = "allow" # We use this sparsely +struct_field_names = "allow" # Noisy and some items are Bitwarden controlled +large_futures = "allow" # Causes a fail in some Rocket macro's, since we experience no issues, allow it +too_many_lines = "allow" # For now, allow this, good to enable in the future and see if we can refactor +unnecessary_wraps = "allow" # Too much false positives because of Rocket integrations +# We do not use these doc items +doc_link_with_quotes = "allow" +doc_markdown = "allow" +missing_errors_doc = "allow" +missing_panics_doc = "allow" [lints] workspace = true diff --git a/build.rs b/build.rs index 2d1106c2..32fcf845 100644 --- a/build.rs +++ b/build.rs @@ -1,5 +1,4 @@ -use std::env; -use std::process::Command; +use std::{env, io::Error, process::Command}; fn main() { // These allow using e.g. #[cfg(mysql)] instead of #[cfg(feature = "mysql")], which helps when trying to add them through macros @@ -42,13 +41,12 @@ fn main() { } } -fn run(args: &[&str]) -> Result { +fn run(args: &[&str]) -> Result { let out = Command::new(args[0]).args(&args[1..]).output()?; if !out.status.success() { - use std::io::Error; return Err(Error::other("Command not successful")); } - Ok(String::from_utf8(out.stdout).unwrap().trim().to_string()) + Ok(String::from_utf8(out.stdout).unwrap().trim().to_owned()) } /// This method reads info from Git, namely tags, branch, and revision @@ -58,7 +56,7 @@ fn run(args: &[&str]) -> Result { /// - `env!("GIT_BRANCH")` /// - `env!("GIT_REV")` /// - `env!("VW_VERSION")` -fn version_from_git_info() -> Result { +fn version_from_git_info() -> Result { // The exact tag for the current commit, can be empty when // the current commit doesn't have an associated tag let exact_tag = run(&["git", "describe", "--abbrev=0", "--tags", "--exact-match"]).ok(); diff --git a/macros/src/lib.rs b/macros/src/lib.rs index 2d923ce1..73b23a22 100644 --- a/macros/src/lib.rs +++ b/macros/src/lib.rs @@ -1,14 +1,15 @@ use proc_macro::TokenStream; use quote::quote; +use syn::{DeriveInput, parse_macro_input}; #[proc_macro_derive(UuidFromParam)] pub fn derive_uuid_from_param(input: TokenStream) -> TokenStream { - let ast = syn::parse(input).unwrap(); + let ast = parse_macro_input!(input as DeriveInput); impl_derive_uuid_macro(&ast) } -fn impl_derive_uuid_macro(ast: &syn::DeriveInput) -> TokenStream { +fn impl_derive_uuid_macro(ast: &DeriveInput) -> TokenStream { let name = &ast.ident; let gen_derive = quote! { #[automatically_derived] @@ -30,12 +31,12 @@ fn impl_derive_uuid_macro(ast: &syn::DeriveInput) -> TokenStream { #[proc_macro_derive(IdFromParam)] pub fn derive_id_from_param(input: TokenStream) -> TokenStream { - let ast = syn::parse(input).unwrap(); + let ast = parse_macro_input!(input as DeriveInput); impl_derive_safestring_macro(&ast) } -fn impl_derive_safestring_macro(ast: &syn::DeriveInput) -> TokenStream { +fn impl_derive_safestring_macro(ast: &DeriveInput) -> TokenStream { let name = &ast.ident; let gen_derive = quote! { #[automatically_derived] diff --git a/rustfmt.toml b/rustfmt.toml index 1d5e440f..a00d27e0 100644 --- a/rustfmt.toml +++ b/rustfmt.toml @@ -1,4 +1,4 @@ -edition = "2021" +edition = "2024" max_width = 120 newline_style = "Unix" use_small_heuristics = "Off" diff --git a/src/api/admin.rs b/src/api/admin.rs index 02c976cc..cb31a353 100644 --- a/src/api/admin.rs +++ b/src/api/admin.rs @@ -2,40 +2,40 @@ use std::{env, sync::LazyLock}; use reqwest::Method; use rocket::{ + Catcher, Route, form::Form, http::{Cookie, CookieJar, MediaType, SameSite, Status}, request::{FromRequest, Outcome, Request}, - response::{content::RawHtml as Html, Redirect}, + response::{Redirect, content::RawHtml as Html}, serde::json::Json, - Catcher, Route, }; use serde::de::DeserializeOwned; use serde_json::Value; use crate::{ + CONFIG, VERSION, api::{ + ApiResult, EmptyResult, JsonResult, Notify, core::{log_event, two_factor}, - unregister_push_device, ApiResult, EmptyResult, JsonResult, Notify, + unregister_push_device, }, - auth::{decode_admin, encode_jwt, generate_admin_claims, ClientIp, Secure}, + auth::{ClientIp, Secure, decode_admin, encode_jwt, generate_admin_claims}, config::ConfigBuilder, db::{ - backup_sqlite, get_sql_server_version, + ACTIVE_DB_TYPE, DbConn, DbConnType, backup_sqlite, get_sql_server_version, models::{ Attachment, Cipher, Collection, Device, Event, EventType, Group, Invitation, Membership, MembershipId, MembershipType, OrgPolicy, Organization, OrganizationId, SsoUser, TwoFactor, User, UserId, }, - DbConn, DbConnType, ACTIVE_DB_TYPE, }, error::{Error, MapResult}, http_client::make_http_request, mail, sso::FAKE_SSO_IDENTIFIER, util::{ - container_base_image, format_naive_datetime_local, get_active_web_release, get_display_size, - is_running_in_container, parse_experimental_client_feature_flags, FeatureFlagFilter, NumberOrString, + FeatureFlagFilter, NumberOrString, container_base_image, format_naive_datetime_local, get_active_web_release, + get_display_size, is_running_in_container, parse_experimental_client_feature_flags, }, - CONFIG, VERSION, }; pub fn routes() -> Vec { @@ -93,8 +93,7 @@ static DB_TYPE: LazyLock<&str> = LazyLock::new(|| match ACTIVE_DB_TYPE.get() { }); #[cfg(sqlite)] -static CAN_BACKUP: LazyLock = - LazyLock::new(|| ACTIVE_DB_TYPE.get().map(|t| *t == DbConnType::Sqlite).unwrap_or(false)); +static CAN_BACKUP: LazyLock = LazyLock::new(|| ACTIVE_DB_TYPE.get().is_some_and(|t| *t == DbConnType::Sqlite)); #[cfg(not(sqlite))] static CAN_BACKUP: LazyLock = LazyLock::new(|| false); @@ -200,13 +199,7 @@ fn post_admin_login( } // If the token is invalid, redirect to login page - if !_validate_token(&data.token) { - error!("Invalid admin token. IP: {}", ip.ip); - Err(AdminResponse::Unauthorized(render_admin_login( - Some("Invalid admin token, please try again."), - redirect.as_deref(), - ))) - } else { + if validate_token(&data.token) { // If the token received is valid, generate JWT and save it as a cookie let claims = generate_admin_claims(); let jwt = encode_jwt(&claims); @@ -224,10 +217,16 @@ fn post_admin_login( } else { Err(AdminResponse::Ok(render_admin_page())) } + } else { + error!("Invalid admin token. IP: {}", ip.ip); + Err(AdminResponse::Unauthorized(render_admin_login( + Some("Invalid admin token, please try again."), + redirect.as_deref(), + ))) } } -fn _validate_token(token: &str) -> bool { +fn validate_token(token: &str) -> bool { match CONFIG.admin_token().as_ref() { None => false, Some(t) if t.starts_with("$argon2") => { @@ -307,21 +306,14 @@ async fn get_user_or_404(user_id: &UserId, conn: &DbConn) -> ApiResult { #[post("/invite", format = "application/json", data = "")] async fn invite_user(data: Json, _token: AdminToken, conn: DbConn) -> JsonResult { - let data: InviteData = data.into_inner(); - if User::find_by_mail(&data.email, &conn).await.is_some() { - err_code!("User already exists", Status::Conflict.code) - } - - let mut user = User::new(&data.email, None); - - async fn _generate_invite(user: &User, conn: &DbConn) -> EmptyResult { + async fn generate_invite(user: &User, conn: &DbConn) -> EmptyResult { if CONFIG.mail_enabled() { let org_id: OrganizationId = if CONFIG.sso_enabled() { FAKE_SSO_IDENTIFIER.into() } else { FAKE_ADMIN_UUID.into() }; - let member_id: MembershipId = FAKE_ADMIN_UUID.to_string().into(); + let member_id: MembershipId = FAKE_ADMIN_UUID.to_owned().into(); mail::send_invite(user, org_id, member_id, &CONFIG.invitation_org_name(), None).await } else { let invitation = Invitation::new(&user.email); @@ -329,7 +321,14 @@ async fn invite_user(data: Json, _token: AdminToken, conn: DbConn) - } } - _generate_invite(&user, &conn).await.map_err(|e| e.with_code(Status::InternalServerError.code))?; + let data: InviteData = data.into_inner(); + if User::find_by_mail(&data.email, &conn).await.is_some() { + err_code!("User already exists", Status::Conflict.code) + } + + let mut user = User::new(&data.email, None); + + generate_invite(&user, &conn).await.map_err(|e| e.with_code(Status::InternalServerError.code))?; user.save(&conn).await.map_err(|e| e.with_code(Status::InternalServerError.code))?; Ok(Json(user.to_json(&conn).await)) @@ -386,7 +385,7 @@ async fn users_overview(_token: AdminToken, conn: DbConn) -> ApiResult json!("Never"), }; - usr["sso_identifier"] = json!(sso_u.map(|u| u.identifier.to_string()).unwrap_or(String::new())); + usr["sso_identifier"] = json!(sso_u.map_or(String::new(), |u| u.identifier.to_string())); users_json.push(usr); } @@ -472,7 +471,7 @@ async fn deauth_user(user_id: UserId, _token: AdminToken, conn: DbConn, nt: Noti match unregister_push_device(device.push_uuid.as_ref()).await { Ok(r) => r, Err(e) => error!("Unable to unregister devices from Bitwarden server: {e}"), - }; + } } } @@ -528,7 +527,7 @@ async fn resend_user_invite(user_id: UserId, _token: AdminToken, conn: DbConn) - } else { FAKE_ADMIN_UUID.into() }; - let member_id: MembershipId = FAKE_ADMIN_UUID.to_string().into(); + let member_id: MembershipId = FAKE_ADMIN_UUID.to_owned().into(); mail::send_invite(&user, org_id, member_id, &CONFIG.invitation_org_name(), None).await } else { Ok(()) @@ -554,9 +553,10 @@ async fn update_membership_type(data: Json, token: AdminToke err!("The specified user isn't member of the organization") }; - let new_type = match MembershipType::from_str(&data.user_type.into_string()) { - Some(new_type) => new_type as i32, - None => err!("Invalid type"), + let new_type = if let Some(new_type) = MembershipType::from_str(&data.user_type.into_string()) { + new_type as i32 + } else { + err!("Invalid type") }; if member_to_edit.atype == MembershipType::Owner && new_type != MembershipType::Owner { @@ -656,42 +656,40 @@ async fn get_release_info(has_http_access: bool) -> (String, String, String) { .await { Ok(r) => r.tag_name, - _ => "-".to_string(), + _ => "-".to_owned(), }, match get_json_api::("https://api.github.com/repos/dani-garcia/vaultwarden/commits/main").await { Ok(mut c) => { c.sha.truncate(8); c.sha } - _ => "-".to_string(), + _ => "-".to_owned(), }, // Do not fetch the web-vault version when running within a container // The web-vault version is embedded within the container it self, and should not be updated manually match get_json_api::("https://api.github.com/repos/dani-garcia/bw_web_builds/releases/latest") .await { - Ok(r) => r.tag_name.trim_start_matches('v').to_string(), - _ => "-".to_string(), + Ok(r) => r.tag_name.trim_start_matches('v').to_owned(), + _ => "-".to_owned(), }, ) } else { - ("-".to_string(), "-".to_string(), "-".to_string()) + ("-".to_owned(), "-".to_owned(), "-".to_owned()) } } async fn get_ntp_time(has_http_access: bool) -> String { - if has_http_access { - if let Ok(cf_trace) = get_text_api("https://cloudflare.com/cdn-cgi/trace").await { - for line in cf_trace.lines() { - if let Some((key, value)) = line.split_once('=') { - if key == "ts" { - let ts = value.split_once('.').map_or(value, |(s, _)| s); - if let Ok(dt) = chrono::DateTime::parse_from_str(ts, "%s") { - return dt.format("%Y-%m-%d %H:%M:%S UTC").to_string(); - } - break; - } + if has_http_access && let Ok(cf_trace) = get_text_api("https://cloudflare.com/cdn-cgi/trace").await { + for line in cf_trace.lines() { + if let Some((key, value)) = line.split_once('=') + && key == "ts" + { + let ts = value.split_once('.').map_or(value, |(s, _)| s); + if let Ok(dt) = chrono::DateTime::parse_from_str(ts, "%s") { + return dt.format("%Y-%m-%d %H:%M:%S UTC").to_string(); } + break; } } } @@ -734,7 +732,7 @@ async fn diagnostics(_token: AdminToken, ip_header: IpHeader, conn: DbConn) -> A // Check if we are able to resolve DNS entries let dns_resolved = match ("github.com", 0).to_socket_addrs().map(|mut i| i.next()) { Ok(Some(a)) => a.ip().to_string(), - _ => "Unable to resolve domain name.".to_string(), + _ => "Unable to resolve domain name.".to_owned(), }; let (latest_vw_release, latest_vw_commit, latest_web_release) = get_release_info(has_http_access).await; @@ -745,7 +743,7 @@ async fn diagnostics(_token: AdminToken, ip_header: IpHeader, conn: DbConn) -> A let invalid_feature_flags: Vec = parse_experimental_client_feature_flags( &CONFIG.experimental_client_feature_flags(), - FeatureFlagFilter::InvalidOnly, + &FeatureFlagFilter::InvalidOnly, ) .into_keys() .collect(); @@ -834,33 +832,30 @@ impl<'r> FromRequest<'r> for AdminToken { type Error = &'static str; async fn from_request(request: &'r Request<'_>) -> Outcome { - let ip = match ClientIp::from_request(request).await { - Outcome::Success(ip) => ip, - _ => err_handler!("Error getting Client IP"), + let Outcome::Success(ip) = ClientIp::from_request(request).await else { + err_handler!("Error getting Client IP") }; if !CONFIG.disable_admin_token() { let cookies = request.cookies(); - let access_token = match cookies.get(COOKIE_NAME) { - Some(cookie) => cookie.value(), - None => { - let requested_page = - request.segments::(0..).unwrap_or_default().display().to_string(); - // When the requested page is empty, it is `/admin`, in that case, Forward, so it will render the login page - // Else, return a 401 failure, which will be caught - if requested_page.is_empty() { - return Outcome::Forward(Status::Unauthorized); - } else { - return Outcome::Error((Status::Unauthorized, "Unauthorized")); - } + let access_token = if let Some(cookie) = cookies.get(COOKIE_NAME) { + cookie.value() + } else { + let requested_page = + request.segments::(0..).unwrap_or_default().display().to_string(); + // When the requested page is empty, it is `/admin`, in that case, Forward, so it will render the login page + // Else, return a 401 failure, which will be caught + if requested_page.is_empty() { + return Outcome::Forward(Status::Unauthorized); } + return Outcome::Error((Status::Unauthorized, "Unauthorized")); }; if decode_admin(access_token).is_err() { // Remove admin cookie cookies.remove(Cookie::build(COOKIE_NAME).path(admin_path())); - error!("Invalid or expired admin JWT. IP: {}.", &ip.ip); + error!("Invalid or expired admin JWT. IP: {}.", ip.ip); return Outcome::Error((Status::Unauthorized, "Session expired")); } } diff --git a/src/api/core/accounts.rs b/src/api/core/accounts.rs index a8f9768e..954b35bd 100644 --- a/src/api/core/accounts.rs +++ b/src/api/core/accounts.rs @@ -1,34 +1,37 @@ use std::collections::HashSet; -use crate::db::DbPool; use chrono::Utc; -use rocket::serde::json::Json; -use serde_json::Value; - -use crate::{ - api::{ - core::{accept_org_invite, log_user_event, two_factor::email}, - master_password_policy, register_push_device, unregister_push_device, AnonymousNotify, ApiResult, EmptyResult, - JsonResult, Notify, PasswordOrOtpData, UpdateType, - }, - auth::{decode_delete, decode_invite, decode_verify_email, ClientHeaders, Headers}, - crypto, - db::{ - models::{ - AuthRequest, AuthRequestId, Cipher, CipherId, Device, DeviceId, DeviceType, EmergencyAccess, - EmergencyAccessId, EventType, Folder, FolderId, Invitation, Membership, MembershipId, OrgPolicy, - OrgPolicyType, Organization, OrganizationId, Send, SendId, User, UserId, UserKdfType, - }, - DbConn, - }, - mail, - util::{deser_opt_nonempty_str, format_date, NumberOrString}, - CONFIG, -}; - use rocket::{ http::Status, request::{FromRequest, Outcome, Request}, + serde::json::Json, +}; +use serde_json::Value; + +use crate::{ + CONFIG, + api::{ + AnonymousNotify, ApiResult, EmptyResult, JsonResult, Notify, PasswordOrOtpData, UpdateType, + core::{accept_org_invite, log_user_event, two_factor::email}, + master_password_policy, register_push_device, unregister_push_device, + }, + auth::{ClientHeaders, Headers, decode_delete, decode_invite, decode_verify_email}, + crypto, + db::{ + DbConn, DbPool, + models::{ + AuthRequest, AuthRequestId, Cipher, CipherId, Device, DeviceId, DeviceType, DeviceWithAuthRequest, + EmergencyAccess, EmergencyAccessId, EventType, Folder, FolderId, Invitation, Membership, MembershipId, + OrgPolicy, OrgPolicyType, Organization, OrganizationId, Send, SendId, User, UserId, UserKdfType, + }, + }, + mail, + util::{NumberOrString, deser_opt_nonempty_str, format_date}, +}; + +use super::{ + ciphers::{CipherData, update_cipher_from_data}, + sends::{SendData, update_send_from_data}, }; pub fn routes() -> Vec { @@ -54,9 +57,9 @@ pub fn routes() -> Vec { delete_account, revision_date, password_hint, - prelogin, + post_prelogin, verify_password, - api_key, + post_api_key, rotate_api_key, get_known_device, get_all_devices, @@ -142,7 +145,7 @@ fn clean_password_hint(password_hint: Option<&String>) -> Option { None => None, Some(h) => match h.trim() { "" => None, - ht => Some(ht.to_string()), + ht => Some(ht.to_owned()), }, } } @@ -166,7 +169,7 @@ async fn is_email_2fa_required(member_id: Option, conn: &DbConn) - false } -pub async fn _register(data: Json, email_verification: bool, conn: DbConn) -> JsonResult { +pub async fn register(data: Json, email_verification: bool, conn: DbConn) -> JsonResult { let mut data: RegisterData = data.into_inner(); let email = data.email.to_lowercase(); @@ -237,10 +240,10 @@ pub async fn _register(data: Json, email_verification: bool, conn: // Check if the length of the username exceeds 50 characters (Same is Upstream Bitwarden) // This also prevents issues with very long usernames causing to large JWT's. See #2419 - if let Some(ref name) = data.name { - if name.len() > 50 { - err!("The field Name must be a string with a maximum length of 50."); - } + if let Some(ref name) = data.name + && name.len() > 50 + { + err!("The field Name must be a string with a maximum length of 50."); } // Check against the password hint setting here so if it fails, the user @@ -373,18 +376,19 @@ async fn post_set_password(data: Json, headers: Headers, conn: user.public_key = Some(keys.public_key); } - if let Some(identifier) = data.org_identifier { - if identifier != crate::sso::FAKE_SSO_IDENTIFIER && identifier != crate::api::admin::FAKE_ADMIN_UUID { - let Some(org) = Organization::find_by_uuid(&identifier.into(), &conn).await else { - err!("Failed to retrieve the associated organization") - }; + if let Some(identifier) = data.org_identifier + && identifier != crate::sso::FAKE_SSO_IDENTIFIER + && identifier != crate::api::admin::FAKE_ADMIN_UUID + { + let Some(org) = Organization::find_by_uuid(&identifier.into(), &conn).await else { + err!("Failed to retrieve the associated organization") + }; - let Some(membership) = Membership::find_by_user_and_org(&user.uuid, &org.uuid, &conn).await else { - err!("Failed to retrieve the invitation") - }; + let Some(membership) = Membership::find_by_user_and_org(&user.uuid, &org.uuid, &conn).await else { + err!("Failed to retrieve the invitation") + }; - accept_org_invite(&user, membership, None, &conn).await?; - } + accept_org_invite(&user, membership, None, &conn).await?; } if CONFIG.mail_enabled() { @@ -451,10 +455,10 @@ async fn put_avatar(data: Json, headers: Headers, conn: DbConn) -> J // It looks like it only supports the 6 hex color format. // If you try to add the short value it will not show that color. // Check and force 7 chars, including the #. - if let Some(color) = &data.avatar_color { - if color.len() != 7 { - err!("The field AvatarColor must be a HTML/Hex color code with a length of 7 characters") - } + if let Some(color) = &data.avatar_color + && color.len() != 7 + { + err!("The field AvatarColor must be a HTML/Hex color code with a length of 7 characters") } let mut user = headers.user; @@ -668,9 +672,6 @@ struct UpdateResetPasswordData { reset_password_key: String, } -use super::ciphers::CipherData; -use super::sends::{update_send_from_data, SendData}; - #[derive(Deserialize)] #[serde(rename_all = "camelCase")] struct KeyData { @@ -840,7 +841,7 @@ async fn post_rotatekey(data: Json, headers: Headers, conn: DbConn, nt: }; saved_folder.name = folder_data.name; - saved_folder.save(&conn).await? + saved_folder.save(&conn).await?; } } @@ -853,7 +854,7 @@ async fn post_rotatekey(data: Json, headers: Headers, conn: DbConn, nt: }; saved_emergency_access.key_encrypted = Some(emergency_access_data.key_encrypted); - saved_emergency_access.save(&conn).await? + saved_emergency_access.save(&conn).await?; } // Update reset password data @@ -865,7 +866,7 @@ async fn post_rotatekey(data: Json, headers: Headers, conn: DbConn, nt: }; membership.reset_password_key = Some(reset_password_data.reset_password_key); - membership.save(&conn).await? + membership.save(&conn).await?; } // Update send data @@ -878,8 +879,6 @@ async fn post_rotatekey(data: Json, headers: Headers, conn: DbConn, nt: } // Update cipher data - use super::ciphers::update_cipher_from_data; - for cipher_data in data.account_data.ciphers { if cipher_data.organization_id.is_none() { let Some(saved_cipher) = existing_ciphers.iter_mut().find(|c| &c.uuid == cipher_data.id.as_ref().unwrap()) @@ -890,7 +889,7 @@ async fn post_rotatekey(data: Json, headers: Headers, conn: DbConn, nt: // 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. // We force the users to logout after the user has been saved to try and prevent these issues. - update_cipher_from_data(saved_cipher, cipher_data, &headers, None, &conn, &nt, UpdateType::None).await? + update_cipher_from_data(saved_cipher, cipher_data, &headers, None, &conn, &nt, UpdateType::None).await?; } } @@ -1020,24 +1019,22 @@ async fn post_email(data: Json, headers: Headers, conn: DbConn, err!("Email already in use"); } - match user.email_new { - Some(ref val) => { - if val != &data.new_email { - err!("Email change mismatch"); - } + if let Some(ref val) = user.email_new { + if val != &data.new_email { + err!("Email change mismatch"); } - None => err!("No email change pending"), + } else { + err!("No email change pending") } if CONFIG.mail_enabled() { // Only check the token if we sent out an email... - match user.email_new_token { - Some(ref val) => { - if *val != data.token.into_string() { - err!("Token mismatch"); - } + if let Some(ref val) = user.email_new_token { + if *val != data.token.into_string() { + err!("Token mismatch"); } - None => err!("No email change pending"), + } else { + err!("No email change pending") } user.verified_at = Some(Utc::now().naive_utc()); } else { @@ -1114,10 +1111,10 @@ async fn post_delete_recover(data: Json, conn: DbConn) -> Emp let data: DeleteRecoverData = data.into_inner(); if CONFIG.mail_enabled() { - if let Some(user) = User::find_by_mail(&data.email, &conn).await { - if let Err(e) = mail::send_delete_account(&user.email, &user.uuid).await { - error!("Error sending delete account email: {e:#?}"); - } + if let Some(user) = User::find_by_mail(&data.email, &conn).await + && let Err(e) = mail::send_delete_account(&user.email, &user.uuid).await + { + error!("Error sending delete account email: {e:#?}"); } Ok(()) } else { @@ -1169,6 +1166,7 @@ async fn delete_account(data: Json, headers: Headers, conn: D user.delete(&conn).await } +#[expect(clippy::needless_pass_by_value, reason = "Not beneficial for Headers")] #[get("/accounts/revision-date")] fn revision_date(headers: Headers) -> JsonResult { let revision_date = headers.user.updated_at.and_utc().timestamp_millis(); @@ -1183,12 +1181,12 @@ struct PasswordHintData { #[post("/accounts/password-hint", data = "")] async fn password_hint(data: Json, conn: DbConn) -> EmptyResult { + const NO_HINT: &str = "Sorry, you have no password hint..."; + if !CONFIG.password_hints_allowed() || (!CONFIG.mail_enabled() && !CONFIG.show_password_hint()) { err!("This server is not configured to provide password hints."); } - const NO_HINT: &str = "Sorry, you have no password hint..."; - let data: PasswordHintData = data.into_inner(); let email = &data.email; @@ -1199,9 +1197,9 @@ async fn password_hint(data: Json, conn: DbConn) -> EmptyResul // There is still a timing side channel here in that the code // paths that send mail take noticeably longer than ones that // don't. Add a randomized sleep to mitigate this somewhat. - use rand::{rngs::SmallRng, RngExt}; + use rand::{RngExt, rngs::SmallRng}; let mut rng: SmallRng = rand::make_rng(); - let sleep_ms = rng.random_range(900..=1100) as u64; + let sleep_ms: u64 = rng.random_range(900..=1100); tokio::time::sleep(tokio::time::Duration::from_millis(sleep_ms)).await; Ok(()) } else { @@ -1229,11 +1227,11 @@ pub struct PreloginData { } #[post("/accounts/prelogin", data = "")] -async fn prelogin(data: Json, conn: DbConn) -> Json { - _prelogin(data, conn).await +async fn post_prelogin(data: Json, conn: DbConn) -> Json { + prelogin(data, conn).await } -pub async fn _prelogin(data: Json, conn: DbConn) -> Json { +pub async fn prelogin(data: Json, conn: DbConn) -> Json { let data: PreloginData = data.into_inner(); let (kdf_type, kdf_iter, kdf_mem, kdf_para) = match User::find_by_mail(&data.email, &conn).await { @@ -1283,9 +1281,7 @@ async fn verify_password(data: Json, headers: Headers Ok(Json(master_password_policy(&user, &conn).await)) } -async fn _api_key(data: Json, rotate: bool, headers: Headers, conn: DbConn) -> JsonResult { - use crate::util::format_date; - +async fn update_api_key(data: Json, rotate: bool, headers: Headers, conn: DbConn) -> JsonResult { let data: PasswordOrOtpData = data.into_inner(); let mut user = headers.user; @@ -1304,13 +1300,13 @@ async fn _api_key(data: Json, rotate: bool, headers: Headers, } #[post("/accounts/api-key", data = "")] -async fn api_key(data: Json, headers: Headers, conn: DbConn) -> JsonResult { - _api_key(data, false, headers, conn).await +async fn post_api_key(data: Json, headers: Headers, conn: DbConn) -> JsonResult { + update_api_key(data, false, headers, conn).await } #[post("/accounts/rotate-api-key", data = "")] async fn rotate_api_key(data: Json, headers: Headers, conn: DbConn) -> JsonResult { - _api_key(data, true, headers, conn).await + update_api_key(data, true, headers, conn).await } #[get("/devices/knowndevice")] @@ -1353,7 +1349,7 @@ impl<'r> FromRequest<'r> for KnownDevice { }; let uuid = if let Some(uuid) = req.headers().get_one("X-Device-Identifier") { - uuid.to_string().into() + uuid.to_owned().into() } else { return Outcome::Error((Status::BadRequest, "X-Device-Identifier value is required")); }; @@ -1368,7 +1364,7 @@ impl<'r> FromRequest<'r> for KnownDevice { #[get("/devices")] async fn get_all_devices(headers: Headers, conn: DbConn) -> JsonResult { let devices = Device::find_with_auth_request_by_user(&headers.user.uuid, &conn).await; - let devices = devices.iter().map(|device| device.to_json()).collect::>(); + let devices = devices.iter().map(DeviceWithAuthRequest::to_json).collect::>(); Ok(Json(json!({ "data": devices, @@ -1708,6 +1704,6 @@ pub async fn purge_auth_requests(pool: DbPool) { if let Ok(conn) = pool.get().await { AuthRequest::purge_expired_auth_requests(&conn).await; } else { - error!("Failed to get DB connection while purging auth requests") + error!("Failed to get DB connection while purging auth requests"); } } diff --git a/src/api/core/ciphers.rs b/src/api/core/ciphers.rs index 43e555e2..6b9994cf 100644 --- a/src/api/core/ciphers.rs +++ b/src/api/core/ciphers.rs @@ -2,30 +2,30 @@ use std::collections::{HashMap, HashSet}; use chrono::{NaiveDateTime, Utc}; use num_traits::ToPrimitive; -use rocket::fs::TempFile; -use rocket::serde::json::Json; use rocket::{ - form::{Form, FromForm}, Route, + form::{Form, FromForm}, + fs::TempFile, + serde::json::Json, }; use serde_json::Value; -use crate::auth::ClientVersion; -use crate::util::{deser_opt_nonempty_str, save_temp_file, NumberOrString}; use crate::{ - api::{self, core::log_event, EmptyResult, JsonResult, Notify, PasswordOrOtpData, UpdateType}, + CONFIG, + api::{self, EmptyResult, JsonResult, Notify, PasswordOrOtpData, UpdateType, core::log_event}, + auth::ClientVersion, auth::{Headers, OrgIdGuard, OwnerHeaders}, config::PathType, crypto, db::{ + DbConn, DbPool, models::{ Archive, Attachment, AttachmentId, Cipher, CipherId, Collection, CollectionCipher, CollectionGroup, CollectionId, CollectionUser, EventType, Favorite, Folder, FolderCipher, FolderId, Group, Membership, MembershipType, OrgPolicy, OrgPolicyType, OrganizationId, RepromptType, Send, UserId, }, - DbConn, DbPool, }, - CONFIG, + util::{NumberOrString, deser_opt_nonempty_str, save_temp_file}, }; use super::folders::FolderData; @@ -108,7 +108,7 @@ pub async fn purge_trashed_ciphers(pool: DbPool) { if let Ok(conn) = pool.get().await { Cipher::purge_trash(&conn).await; } else { - error!("Failed to get DB connection while purging trashed ciphers") + error!("Failed to get DB connection while purging trashed ciphers"); } } @@ -164,7 +164,7 @@ async fn sync(data: SyncData, headers: Headers, client_version: Option, ut: UpdateType, ) -> EmptyResult { + // Cleanup cipher data, like removing the 'Response' key. + // This key is somewhere generated during Javascript so no way for us this fix this. + // Also, upstream only retrieves keys they actually want to store, and thus skip the 'Response' key. + // We do not mind which data is in it, the keep our model more flexible when there are upstream changes. + // But, we at least know we do not need to store and return this specific key. + fn clean_cipher_data(mut json_data: Value) -> Value { + if json_data.is_array() { + json_data.as_array_mut().unwrap().iter_mut().for_each(|ref mut f| { + f.as_object_mut().unwrap().remove("response"); + }); + } + json_data + } + enforce_personal_ownership_policy(Some(&data), headers, conn).await?; // Check that the client isn't updating an existing cipher with stale data. // And only perform this check when not importing ciphers, else the date/time check will fail. - if ut != UpdateType::None { - if let Some(dt) = data.last_known_revision_date { - match NaiveDateTime::parse_from_str(&dt, "%+") { - // ISO 8601 format - Err(err) => warn!("Error parsing LastKnownRevisionDate '{dt}': {err}"), - Ok(dt) if cipher.updated_at.signed_duration_since(dt).num_seconds() > 1 => { - err!("The client copy of this cipher is out of date. Resync the client and try again.") - } - Ok(_) => (), + if ut != UpdateType::None + && let Some(dt) = data.last_known_revision_date + { + match NaiveDateTime::parse_from_str(&dt, "%+") { + // ISO 8601 format + Err(err) => warn!("Error parsing LastKnownRevisionDate '{dt}': {err}"), + Ok(dt) if cipher.updated_at.signed_duration_since(dt).num_seconds() > 1 => { + err!("The client copy of this cipher is out of date. Resync the client and try again.") } + Ok(_) => (), } } @@ -456,25 +470,22 @@ pub async fn update_cipher_from_data( cipher.user_uuid = Some(headers.user.uuid.clone()); } - if let Some(ref folder_id) = data.folder_id { - if Folder::find_by_uuid_and_user(folder_id, &headers.user.uuid, conn).await.is_none() { - err!("Invalid folder", "Folder does not exist or belongs to another user"); - } + if let Some(ref folder_id) = data.folder_id + && Folder::find_by_uuid_and_user(folder_id, &headers.user.uuid, conn).await.is_none() + { + err!("Invalid folder", "Folder does not exist or belongs to another user"); } // Modify attachments name and keys when rotating if let Some(attachments) = data.attachments2 { for (id, attachment) in attachments { - let mut saved_att = match Attachment::find_by_id(&id, conn).await { - Some(att) => att, - None => { - // Warn and continue here. - // A missing attachment means it was removed via an other client. - // Also the Desktop Client supports removing attachments and save an update afterwards. - // Bitwarden it self ignores these mismatches server side. - warn!("Attachment {id} doesn't exist"); - continue; - } + let Some(mut saved_att) = Attachment::find_by_id(&id, conn).await else { + // Warn and continue here. + // A missing attachment means it was removed via an other client. + // Also the Desktop Client supports removing attachments and save an update afterwards. + // Bitwarden it self ignores these mismatches server side. + warn!("Attachment {id} doesn't exist"); + continue; }; if saved_att.cipher_uuid != cipher.uuid { @@ -491,20 +502,6 @@ pub async fn update_cipher_from_data( } } - // Cleanup cipher data, like removing the 'Response' key. - // This key is somewhere generated during Javascript so no way for us this fix this. - // Also, upstream only retrieves keys they actually want to store, and thus skip the 'Response' key. - // We do not mind which data is in it, the keep our model more flexible when there are upstream changes. - // But, we at least know we do not need to store and return this specific key. - fn _clean_cipher_data(mut json_data: Value) -> Value { - if json_data.is_array() { - json_data.as_array_mut().unwrap().iter_mut().for_each(|ref mut f| { - f.as_object_mut().unwrap().remove("response"); - }); - }; - json_data - } - let type_data_opt = match data.r#type { 1 => data.login, 2 => data.secure_note, @@ -514,23 +511,22 @@ pub async fn update_cipher_from_data( _ => err!("Invalid type"), }; - let type_data = match type_data_opt { - Some(mut data) => { - // Remove the 'Response' key from the base object. - data.as_object_mut().unwrap().remove("response"); - // Remove the 'Response' key from every Uri. - if data["uris"].is_array() { - data["uris"] = _clean_cipher_data(data["uris"].clone()); - } - data + let type_data = if let Some(mut data) = type_data_opt { + // Remove the 'Response' key from the base object. + data.as_object_mut().unwrap().remove("response"); + // Remove the 'Response' key from every Uri. + if data["uris"].is_array() { + data["uris"] = clean_cipher_data(data["uris"].clone()); } - None => err!("Data missing"), + data + } else { + err!("Data missing") }; cipher.key = data.key; cipher.name = data.name; cipher.notes = data.notes; - 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.password_history = data.password_history.map(|f| f.to_string()); cipher.reprompt = data.reprompt.filter(|r| *r == RepromptType::None as i32 || *r == RepromptType::Password as i32); @@ -612,7 +608,7 @@ async fn post_ciphers_import(data: Json, headers: Headers, conn: DbC let existing_folders: HashSet> = Folder::find_by_user(&headers.user.uuid, &conn).await.into_iter().map(|f| Some(f.uuid)).collect(); let mut folders: Vec = Vec::with_capacity(data.folders.len()); - for folder in data.folders.into_iter() { + for folder in data.folders { let folder_id = if existing_folders.contains(&folder.id) { folder.id.unwrap() } else { @@ -737,10 +733,10 @@ async fn put_cipher_partial( err!("Cipher does not exist", "Cipher is not accessible for the current user") } - if let Some(ref folder_id) = data.folder_id { - if Folder::find_by_uuid_and_user(folder_id, &headers.user.uuid, &conn).await.is_none() { - err!("Invalid folder", "Folder does not exist or belongs to another user"); - } + if let Some(ref folder_id) = data.folder_id + && Folder::find_by_uuid_and_user(folder_id, &headers.user.uuid, &conn).await.is_none() + { + err!("Invalid folder", "Folder does not exist or belongs to another user"); } // Move cipher @@ -1004,7 +1000,7 @@ async fn put_cipher_share_selected( err!("You must select at least one collection.") } - for cipher in data.ciphers.iter() { + for cipher in &data.ciphers { if cipher.id.is_none() { err!("Request missing ids field") } @@ -1016,11 +1012,10 @@ async fn put_cipher_share_selected( collection_ids: data.collection_ids.clone(), }; - match shared_cipher_data.cipher.id.take() { - Some(id) => { - share_cipher_by_uuid(&id, shared_cipher_data, &headers, &conn, &nt, Some(UpdateType::None)).await? - } - None => err!("Request missing ids field"), + if let Some(id) = shared_cipher_data.cipher.id.take() { + share_cipher_by_uuid(&id, shared_cipher_data, &headers, &conn, &nt, Some(UpdateType::None)).await? + } else { + err!("Request missing ids field") }; } @@ -1038,15 +1033,14 @@ async fn share_cipher_by_uuid( nt: &Notify<'_>, override_ut: Option, ) -> JsonResult { - let mut cipher = match Cipher::find_by_uuid(cipher_id, conn).await { - Some(cipher) => { - if cipher.is_write_accessible_to_user(&headers.user.uuid, conn).await { - cipher - } else { - err!("Cipher is not write accessible") - } + let mut cipher = if let Some(cipher) = Cipher::find_by_uuid(cipher_id, conn).await { + if cipher.is_write_accessible_to_user(&headers.user.uuid, conn).await { + cipher + } else { + err!("Cipher is not write accessible") } - None => err!("Cipher doesn't exist"), + } else { + err!("Cipher doesn't exist") }; let mut shared_to_collections = vec![]; @@ -1065,7 +1059,7 @@ async fn share_cipher_by_uuid( } } } - }; + } // When LastKnownRevisionDate is None, it is a new cipher, so send CipherCreate. // If there is an override, like when handling multiple items, we want to prevent a push notification for every single item @@ -1263,10 +1257,10 @@ async fn save_attachment( err!("Cipher is neither owned by a user nor an organization"); }; - if let Some(size_limit) = size_limit { - if size > size_limit { - err!("Attachment storage limit exceeded with this file"); - } + if let Some(size_limit) = size_limit + && size > size_limit + { + err!("Attachment storage limit exceeded with this file"); } let file_id = match &attachment { @@ -1408,7 +1402,7 @@ async fn post_attachment_share( conn: DbConn, nt: Notify<'_>, ) -> JsonResult { - _delete_cipher_attachment_by_id(&cipher_id, &attachment_id, &headers, &conn, &nt).await?; + delete_cipher_attachment_by_id(&cipher_id, &attachment_id, &headers, &conn, &nt).await?; post_attachment(cipher_id, data, headers, conn, nt).await } @@ -1442,7 +1436,7 @@ async fn delete_attachment( conn: DbConn, nt: Notify<'_>, ) -> JsonResult { - _delete_cipher_attachment_by_id(&cipher_id, &attachment_id, &headers, &conn, &nt).await + delete_cipher_attachment_by_id(&cipher_id, &attachment_id, &headers, &conn, &nt).await } #[delete("/ciphers//attachment//admin")] @@ -1453,42 +1447,42 @@ async fn delete_attachment_admin( conn: DbConn, nt: Notify<'_>, ) -> JsonResult { - _delete_cipher_attachment_by_id(&cipher_id, &attachment_id, &headers, &conn, &nt).await + delete_cipher_attachment_by_id(&cipher_id, &attachment_id, &headers, &conn, &nt).await } #[post("/ciphers//delete")] async fn delete_cipher_post(cipher_id: CipherId, headers: Headers, conn: DbConn, nt: Notify<'_>) -> EmptyResult { - _delete_cipher_by_uuid(&cipher_id, &headers, &conn, &CipherDeleteOptions::HardSingle, &nt).await + delete_cipher_by_uuid(&cipher_id, &headers, &conn, &CipherDeleteOptions::HardSingle, &nt).await // permanent delete } #[post("/ciphers//delete-admin")] async fn delete_cipher_post_admin(cipher_id: CipherId, headers: Headers, conn: DbConn, nt: Notify<'_>) -> EmptyResult { - _delete_cipher_by_uuid(&cipher_id, &headers, &conn, &CipherDeleteOptions::HardSingle, &nt).await + delete_cipher_by_uuid(&cipher_id, &headers, &conn, &CipherDeleteOptions::HardSingle, &nt).await // permanent delete } #[put("/ciphers//delete")] async fn delete_cipher_put(cipher_id: CipherId, headers: Headers, conn: DbConn, nt: Notify<'_>) -> EmptyResult { - _delete_cipher_by_uuid(&cipher_id, &headers, &conn, &CipherDeleteOptions::SoftSingle, &nt).await + delete_cipher_by_uuid(&cipher_id, &headers, &conn, &CipherDeleteOptions::SoftSingle, &nt).await // soft delete } #[put("/ciphers//delete-admin")] async fn delete_cipher_put_admin(cipher_id: CipherId, headers: Headers, conn: DbConn, nt: Notify<'_>) -> EmptyResult { - _delete_cipher_by_uuid(&cipher_id, &headers, &conn, &CipherDeleteOptions::SoftSingle, &nt).await + delete_cipher_by_uuid(&cipher_id, &headers, &conn, &CipherDeleteOptions::SoftSingle, &nt).await // soft delete } #[delete("/ciphers/")] async fn delete_cipher(cipher_id: CipherId, headers: Headers, conn: DbConn, nt: Notify<'_>) -> EmptyResult { - _delete_cipher_by_uuid(&cipher_id, &headers, &conn, &CipherDeleteOptions::HardSingle, &nt).await + delete_cipher_by_uuid(&cipher_id, &headers, &conn, &CipherDeleteOptions::HardSingle, &nt).await // permanent delete } #[delete("/ciphers//admin")] async fn delete_cipher_admin(cipher_id: CipherId, headers: Headers, conn: DbConn, nt: Notify<'_>) -> EmptyResult { - _delete_cipher_by_uuid(&cipher_id, &headers, &conn, &CipherDeleteOptions::HardSingle, &nt).await + delete_cipher_by_uuid(&cipher_id, &headers, &conn, &CipherDeleteOptions::HardSingle, &nt).await // permanent delete } @@ -1499,7 +1493,7 @@ async fn delete_cipher_selected( conn: DbConn, nt: Notify<'_>, ) -> EmptyResult { - _delete_multiple_ciphers(data, headers, conn, CipherDeleteOptions::HardMulti, nt).await + delete_multiple_ciphers(data, headers, conn, CipherDeleteOptions::HardMulti, nt).await // permanent delete } @@ -1510,7 +1504,7 @@ async fn delete_cipher_selected_post( conn: DbConn, nt: Notify<'_>, ) -> EmptyResult { - _delete_multiple_ciphers(data, headers, conn, CipherDeleteOptions::HardMulti, nt).await + delete_multiple_ciphers(data, headers, conn, CipherDeleteOptions::HardMulti, nt).await // permanent delete } @@ -1521,7 +1515,7 @@ async fn delete_cipher_selected_put( conn: DbConn, nt: Notify<'_>, ) -> EmptyResult { - _delete_multiple_ciphers(data, headers, conn, CipherDeleteOptions::SoftMulti, nt).await + delete_multiple_ciphers(data, headers, conn, CipherDeleteOptions::SoftMulti, nt).await // soft delete } @@ -1532,7 +1526,7 @@ async fn delete_cipher_selected_admin( conn: DbConn, nt: Notify<'_>, ) -> EmptyResult { - _delete_multiple_ciphers(data, headers, conn, CipherDeleteOptions::HardMulti, nt).await + delete_multiple_ciphers(data, headers, conn, CipherDeleteOptions::HardMulti, nt).await // permanent delete } @@ -1543,7 +1537,7 @@ async fn delete_cipher_selected_post_admin( conn: DbConn, nt: Notify<'_>, ) -> EmptyResult { - _delete_multiple_ciphers(data, headers, conn, CipherDeleteOptions::HardMulti, nt).await + delete_multiple_ciphers(data, headers, conn, CipherDeleteOptions::HardMulti, nt).await // permanent delete } @@ -1554,18 +1548,18 @@ async fn delete_cipher_selected_put_admin( conn: DbConn, nt: Notify<'_>, ) -> EmptyResult { - _delete_multiple_ciphers(data, headers, conn, CipherDeleteOptions::SoftMulti, nt).await + delete_multiple_ciphers(data, headers, conn, CipherDeleteOptions::SoftMulti, nt).await // soft delete } #[put("/ciphers//restore")] async fn restore_cipher_put(cipher_id: CipherId, headers: Headers, conn: DbConn, nt: Notify<'_>) -> JsonResult { - _restore_cipher_by_uuid(&cipher_id, &headers, false, &conn, &nt).await + restore_cipher_by_uuid(&cipher_id, &headers, false, &conn, &nt).await } #[put("/ciphers//restore-admin")] async fn restore_cipher_put_admin(cipher_id: CipherId, headers: Headers, conn: DbConn, nt: Notify<'_>) -> JsonResult { - _restore_cipher_by_uuid(&cipher_id, &headers, false, &conn, &nt).await + restore_cipher_by_uuid(&cipher_id, &headers, false, &conn, &nt).await } #[put("/ciphers/restore-admin", data = "")] @@ -1575,7 +1569,7 @@ async fn restore_cipher_selected_admin( conn: DbConn, nt: Notify<'_>, ) -> JsonResult { - _restore_multiple_ciphers(data, &headers, &conn, &nt).await + restore_multiple_ciphers(data, &headers, &conn, &nt).await } #[put("/ciphers/restore", data = "")] @@ -1585,7 +1579,7 @@ async fn restore_cipher_selected( conn: DbConn, nt: Notify<'_>, ) -> JsonResult { - _restore_multiple_ciphers(data, &headers, &conn, &nt).await + restore_multiple_ciphers(data, &headers, &conn, &nt).await } #[derive(Deserialize)] @@ -1606,10 +1600,10 @@ async fn move_cipher_selected( let data = data.into_inner(); let user_id = &headers.user.uuid; - if let Some(ref folder_id) = data.folder_id { - if Folder::find_by_uuid_and_user(folder_id, user_id, &conn).await.is_none() { - err!("Invalid folder", "Folder does not exist or belongs to another user"); - } + if let Some(ref folder_id) = data.folder_id + && Folder::find_by_uuid_and_user(folder_id, user_id, &conn).await.is_none() + { + err!("Invalid folder", "Folder does not exist or belongs to another user"); } let cipher_count = data.ids.len(); @@ -1773,7 +1767,7 @@ pub enum CipherDeleteOptions { HardMulti, } -async fn _delete_cipher_by_uuid( +async fn delete_cipher_by_uuid( cipher_id: &CipherId, headers: &Headers, conn: &DbConn, @@ -1839,7 +1833,7 @@ struct CipherIdsData { ids: Vec, } -async fn _delete_multiple_ciphers( +async fn delete_multiple_ciphers( data: Json, headers: Headers, conn: DbConn, @@ -1849,9 +1843,9 @@ async fn _delete_multiple_ciphers( let data = data.into_inner(); for cipher_id in data.ids { - if let error @ Err(_) = _delete_cipher_by_uuid(&cipher_id, &headers, &conn, &delete_options, &nt).await { + if let error @ Err(_) = delete_cipher_by_uuid(&cipher_id, &headers, &conn, &delete_options, &nt).await { return error; - }; + } } // Multi delete actions do not send out a push for each cipher, we need to send a general sync here @@ -1860,7 +1854,7 @@ async fn _delete_multiple_ciphers( Ok(()) } -async fn _restore_cipher_by_uuid( +async fn restore_cipher_by_uuid( cipher_id: &CipherId, headers: &Headers, multi_restore: bool, @@ -1906,7 +1900,7 @@ async fn _restore_cipher_by_uuid( Ok(Json(cipher.to_json(&headers.host, &headers.user.uuid, None, CipherSyncType::User, conn).await?)) } -async fn _restore_multiple_ciphers( +async fn restore_multiple_ciphers( data: Json, headers: &Headers, conn: &DbConn, @@ -1916,7 +1910,7 @@ async fn _restore_multiple_ciphers( let mut ciphers: Vec = Vec::new(); for cipher_id in data.ids { - match _restore_cipher_by_uuid(&cipher_id, headers, true, conn, nt).await { + match restore_cipher_by_uuid(&cipher_id, headers, true, conn, nt).await { Ok(json) => ciphers.push(json.into_inner()), err => return err, } @@ -1932,7 +1926,7 @@ async fn _restore_multiple_ciphers( }))) } -async fn _delete_cipher_attachment_by_id( +async fn delete_cipher_attachment_by_id( cipher_id: &CipherId, attachment_id: &AttachmentId, headers: &Headers, @@ -2206,11 +2200,11 @@ impl CipherSyncData { }; Self { - cipher_archives, cipher_attachments, cipher_folders, cipher_favorites, cipher_collections, + cipher_archives, members, user_collections, user_collections_groups, diff --git a/src/api/core/emergency_access.rs b/src/api/core/emergency_access.rs index 29a15c8d..2eb95502 100644 --- a/src/api/core/emergency_access.rs +++ b/src/api/core/emergency_access.rs @@ -1,23 +1,23 @@ use chrono::{TimeDelta, Utc}; -use rocket::{serde::json::Json, Route}; +use rocket::{Route, serde::json::Json}; use serde_json::Value; use crate::{ + CONFIG, api::{ - core::{CipherSyncData, CipherSyncType}, EmptyResult, JsonResult, + core::{CipherSyncData, CipherSyncType}, }, - auth::{decode_emergency_access_invite, Headers}, + auth::{Headers, decode_emergency_access_invite}, db::{ + DbConn, DbPool, models::{ Cipher, EmergencyAccess, EmergencyAccessId, EmergencyAccessStatus, EmergencyAccessType, Invitation, Membership, MembershipType, OrgPolicy, TwoFactor, User, UserId, }, - DbConn, DbPool, }, mail, util::NumberOrString, - CONFIG, }; pub fn routes() -> Vec { @@ -55,7 +55,7 @@ async fn get_contacts(headers: Headers, conn: DbConn) -> Json { let mut emergency_access_list_json = Vec::with_capacity(emergency_access_list.len()); for ea in emergency_access_list { if let Some(grantee) = ea.to_json_grantee_details(&conn).await { - emergency_access_list_json.push(grantee) + emergency_access_list_json.push(grantee); } } @@ -89,11 +89,14 @@ async fn get_grantees(headers: Headers, conn: DbConn) -> Json { async fn get_emergency_access(emer_id: EmergencyAccessId, headers: Headers, conn: DbConn) -> JsonResult { check_emergency_access_enabled()?; - match EmergencyAccess::find_by_uuid_and_grantor_uuid(&emer_id, &headers.user.uuid, &conn).await { - Some(emergency_access) => Ok(Json( + if let Some(emergency_access) = + EmergencyAccess::find_by_uuid_and_grantor_uuid(&emer_id, &headers.user.uuid, &conn).await + { + Ok(Json( emergency_access.to_json_grantee_details(&conn).await.expect("Grantee user should exist but does not!"), - )), - None => err!("Emergency access not valid."), + )) + } else { + err!("Emergency access not valid.") } } @@ -136,9 +139,10 @@ async fn post_emergency_access( err!("Emergency access not valid.") }; - let new_type = match EmergencyAccessType::from_str(&data.r#type.into_string()) { - Some(new_type) => new_type as i32, - None => err!("Invalid emergency access type."), + let new_type = if let Some(new_type) = EmergencyAccessType::from_str(&data.r#type.into_string()) { + new_type as i32 + } else { + err!("Invalid emergency access type.") }; emergency_access.atype = new_type; @@ -205,9 +209,10 @@ async fn send_invite(data: Json, headers: Headers, co let emergency_access_status = EmergencyAccessStatus::Invited as i32; - let new_type = match EmergencyAccessType::from_str(&data.r#type.into_string()) { - Some(new_type) => new_type as i32, - None => err!("Invalid emergency access type."), + let new_type = if let Some(new_type) = EmergencyAccessType::from_str(&data.r#type.into_string()) { + new_type as i32 + } else { + err!("Invalid emergency access type.") }; let grantor_user = headers.user; @@ -342,12 +347,11 @@ async fn accept_invite( err!("Claim email does not match current users email") } - let grantee_user = match User::find_by_mail(&claims.email, &conn).await { - Some(user) => { - Invitation::take(&claims.email, &conn).await; - user - } - None => err!("Invited user not found"), + let grantee_user = if let Some(user) = User::find_by_mail(&claims.email, &conn).await { + Invitation::take(&claims.email, &conn).await; + user + } else { + err!("Invited user not found") }; // We need to search for the uuid in combination with the email, since we do not yet store the uuid of the grantee in the database. @@ -766,7 +770,7 @@ pub async fn emergency_request_timeout_job(pool: DbPool) { } } } else { - error!("Failed to get DB connection while searching emergency request timed out") + error!("Failed to get DB connection while searching emergency request timed out"); } } @@ -825,6 +829,6 @@ pub async fn emergency_notification_reminder_job(pool: DbPool) { } } } else { - error!("Failed to get DB connection while searching emergency notification reminder") + error!("Failed to get DB connection while searching emergency notification reminder"); } } diff --git a/src/api/core/events.rs b/src/api/core/events.rs index d1612255..b6e2bacd 100644 --- a/src/api/core/events.rs +++ b/src/api/core/events.rs @@ -1,18 +1,18 @@ use std::net::IpAddr; use chrono::NaiveDateTime; -use rocket::{form::FromForm, serde::json::Json, Route}; +use rocket::{Route, form::FromForm, serde::json::Json}; use serde_json::Value; use crate::{ + CONFIG, api::{EmptyResult, JsonResult}, auth::{AdminHeaders, Headers}, db::{ - models::{Cipher, CipherId, Event, Membership, MembershipId, OrganizationId, UserId}, DbConn, DbPool, + models::{Cipher, CipherId, Event, Membership, MembershipId, OrganizationId, UserId}, }, util::parse_date, - CONFIG, }; /// ############################################################################################################### @@ -38,9 +38,7 @@ async fn get_org_events(org_id: OrganizationId, data: EventRange, headers: Admin // Return an empty vec when we org events are disabled. // This prevents client errors - let events_json: Vec = if !CONFIG.org_events_enabled() { - Vec::with_capacity(0) - } else { + let events_json: Vec = if CONFIG.org_events_enabled() { let start_date = parse_date(&data.start); let end_date = if let Some(before_date) = &data.continuation_token { parse_date(before_date) @@ -51,8 +49,10 @@ async fn get_org_events(org_id: OrganizationId, data: EventRange, headers: Admin Event::find_by_organization_uuid(&org_id, &start_date, &end_date, &conn) .await .iter() - .map(|e| e.to_json()) + .map(Event::to_json) .collect() + } else { + Vec::with_capacity(0) }; Ok(Json(json!({ @@ -64,27 +64,21 @@ async fn get_org_events(org_id: OrganizationId, data: EventRange, headers: Admin #[get("/ciphers//events?")] async fn get_cipher_events(cipher_id: CipherId, data: EventRange, headers: Headers, conn: DbConn) -> JsonResult { - // Return an empty vec when we org events are disabled. + // Return an empty vec when org events are disabled. // This prevents client errors - let events_json: Vec = if !CONFIG.org_events_enabled() { - Vec::with_capacity(0) - } else { - let mut events_json = Vec::with_capacity(0); - if Membership::user_has_ge_admin_access_to_cipher(&headers.user.uuid, &cipher_id, &conn).await { - let start_date = parse_date(&data.start); - let end_date = if let Some(before_date) = &data.continuation_token { - parse_date(before_date) - } else { - parse_date(&data.end) - }; + let events_json: Vec = if CONFIG.org_events_enabled() + && Membership::user_has_ge_admin_access_to_cipher(&headers.user.uuid, &cipher_id, &conn).await + { + let start_date = parse_date(&data.start); + let end_date = if let Some(before_date) = &data.continuation_token { + parse_date(before_date) + } else { + parse_date(&data.end) + }; - events_json = Event::find_by_cipher_uuid(&cipher_id, &start_date, &end_date, &conn) - .await - .iter() - .map(|e| e.to_json()) - .collect() - } - events_json + Event::find_by_cipher_uuid(&cipher_id, &start_date, &end_date, &conn).await.iter().map(Event::to_json).collect() + } else { + Vec::with_capacity(0) }; Ok(Json(json!({ @@ -107,9 +101,7 @@ async fn get_user_events( } // Return an empty vec when we org events are disabled. // This prevents client errors - let events_json: Vec = if !CONFIG.org_events_enabled() { - Vec::with_capacity(0) - } else { + let events_json: Vec = if CONFIG.org_events_enabled() { let start_date = parse_date(&data.start); let end_date = if let Some(before_date) = &data.continuation_token { parse_date(before_date) @@ -120,8 +112,10 @@ async fn get_user_events( Event::find_by_org_and_member(&org_id, &member_id, &start_date, &end_date, &conn) .await .iter() - .map(|e| e.to_json()) + .map(Event::to_json) .collect() + } else { + Vec::with_capacity(0) }; Ok(Json(json!({ @@ -134,7 +128,8 @@ async fn get_user_events( fn get_continuation_token(events_json: &[Value]) -> Option<&str> { // When the length of the vec equals the max page_size there probably is more data // When it is less, then all events are loaded. - if events_json.len() as i64 == Event::PAGE_SIZE { + #[expect(clippy::cast_possible_truncation, reason = "PAGE_SIZE fits within usize")] + if events_json.len() == Event::PAGE_SIZE as usize { if let Some(last_event) = events_json.last() { last_event["date"].as_str() } else { @@ -176,7 +171,7 @@ async fn post_events_collect(data: Json>, headers: Headers, let event_date = parse_date(&event.date); match event.r#type { 1000..=1099 => { - _log_user_event( + log_user_event_impl( event.r#type, &headers.user.uuid, headers.device.atype, @@ -188,7 +183,7 @@ async fn post_events_collect(data: Json>, headers: Headers, } 1600..=1699 => { if let Some(org_id) = &event.organization_id { - _log_event( + log_event_impl( event.r#type, org_id, org_id, @@ -202,22 +197,21 @@ async fn post_events_collect(data: Json>, headers: Headers, } } _ => { - if let Some(cipher_uuid) = &event.cipher_id { - if let Some(cipher) = Cipher::find_by_uuid(cipher_uuid, &conn).await { - if let Some(org_id) = cipher.organization_uuid { - _log_event( - event.r#type, - cipher_uuid, - &org_id, - &headers.user.uuid, - headers.device.atype, - Some(event_date), - &headers.ip.ip, - &conn, - ) - .await; - } - } + if let Some(cipher_uuid) = &event.cipher_id + && let Some(cipher) = Cipher::find_by_uuid(cipher_uuid, &conn).await + && let Some(org_id) = cipher.organization_uuid + { + log_event_impl( + event.r#type, + cipher_uuid, + &org_id, + &headers.user.uuid, + headers.device.atype, + Some(event_date), + &headers.ip.ip, + &conn, + ) + .await; } } } @@ -229,10 +223,10 @@ pub async fn log_user_event(event_type: i32, user_id: &UserId, device_type: i32, if !CONFIG.org_events_enabled() { return; } - _log_user_event(event_type, user_id, device_type, None, ip, conn).await; + log_user_event_impl(event_type, user_id, device_type, None, ip, conn).await; } -async fn _log_user_event( +async fn log_user_event_impl( event_type: i32, user_id: &UserId, device_type: i32, @@ -278,11 +272,11 @@ pub async fn log_event( if !CONFIG.org_events_enabled() { return; } - _log_event(event_type, source_uuid, org_id, act_user_id, device_type, None, ip, conn).await; + log_event_impl(event_type, source_uuid, org_id, act_user_id, device_type, None, ip, conn).await; } -#[allow(clippy::too_many_arguments)] -async fn _log_event( +#[expect(clippy::too_many_arguments)] +async fn log_event_impl( event_type: i32, source_uuid: &str, org_id: &OrganizationId, @@ -298,24 +292,24 @@ async fn _log_event( // 1000..=1099 Are user events, they need to be logged via log_user_event() // Cipher Events 1100..=1199 => { - event.cipher_uuid = Some(source_uuid.to_string().into()); + event.cipher_uuid = Some(source_uuid.to_owned().into()); } // Collection Events 1300..=1399 => { - event.collection_uuid = Some(source_uuid.to_string().into()); + event.collection_uuid = Some(source_uuid.to_owned().into()); } // Group Events 1400..=1499 => { - event.group_uuid = Some(source_uuid.to_string().into()); + event.group_uuid = Some(source_uuid.to_owned().into()); } // Org User Events 1500..=1599 => { - event.org_user_uuid = Some(source_uuid.to_string().into()); + event.org_user_uuid = Some(source_uuid.to_owned().into()); } // 1600..=1699 Are organizational events, and they do not need the source_uuid // Policy Events 1700..=1799 => { - event.policy_uuid = Some(source_uuid.to_string().into()); + event.policy_uuid = Some(source_uuid.to_owned().into()); } // Ignore others _ => {} @@ -338,6 +332,6 @@ pub async fn event_cleanup_job(pool: DbPool) { if let Ok(conn) = pool.get().await { Event::clean_events(&conn).await.ok(); } else { - error!("Failed to get DB connection while trying to cleanup the events table") + error!("Failed to get DB connection while trying to cleanup the events table"); } } diff --git a/src/api/core/folders.rs b/src/api/core/folders.rs index 1b3fd714..8c930093 100644 --- a/src/api/core/folders.rs +++ b/src/api/core/folders.rs @@ -5,8 +5,8 @@ use crate::{ api::{EmptyResult, JsonResult, Notify, UpdateType}, auth::Headers, db::{ - models::{Folder, FolderId}, DbConn, + models::{Folder, FolderId}, }, util::deser_opt_nonempty_str, }; @@ -29,9 +29,10 @@ async fn get_folders(headers: Headers, conn: DbConn) -> Json { #[get("/folders/")] async fn get_folder(folder_id: FolderId, headers: Headers, conn: DbConn) -> JsonResult { - match Folder::find_by_uuid_and_user(&folder_id, &headers.user.uuid, &conn).await { - Some(folder) => Ok(Json(folder.to_json())), - _ => err!("Invalid folder", "Folder does not exist or belongs to another user"), + if let Some(folder) = Folder::find_by_uuid_and_user(&folder_id, &headers.user.uuid, &conn).await { + Ok(Json(folder.to_json())) + } else { + err!("Invalid folder", "Folder does not exist or belongs to another user") } } diff --git a/src/api/core/mod.rs b/src/api/core/mod.rs index ad9002fd..2ea8ab21 100644 --- a/src/api/core/mod.rs +++ b/src/api/core/mod.rs @@ -1,4 +1,6 @@ pub mod accounts; +pub mod two_factor; + mod ciphers; mod emergency_access; mod events; @@ -6,17 +8,32 @@ mod folders; mod organizations; mod public; mod sends; -pub mod two_factor; pub use accounts::purge_auth_requests; -pub use ciphers::{purge_trashed_ciphers, CipherData, CipherSyncData, CipherSyncType}; +pub use ciphers::{CipherData, CipherSyncData, CipherSyncType, purge_trashed_ciphers}; pub use emergency_access::{emergency_notification_reminder_job, emergency_request_timeout_job}; pub use events::{event_cleanup_job, log_event, log_user_event}; -use reqwest::Method; pub use sends::purge_sends; +use reqwest::Method; +use rocket::{Catcher, Route, serde::json::Json, serde::json::Value}; + +use crate::{ + CONFIG, + api::{EmptyResult, JsonResult, Notify, UpdateType}, + auth::Headers, + db::{ + DbConn, + models::{Membership, MembershipStatus, OrgPolicy, Organization, User}, + }, + error::Error, + http_client::make_http_request, + mail, + util::{FeatureFlagFilter, parse_experimental_client_feature_flags}, +}; + pub fn routes() -> Vec { - let mut eq_domains_routes = routes![get_eq_domains, post_eq_domains, put_eq_domains]; + let mut eq_domains_routes = routes![get_settings_domains, post_settings_domains, put_settings_domains]; let mut hibp_routes = routes![hibp_breach]; let mut meta_routes = routes![alive, now, version, config, get_api_webauthn]; @@ -44,25 +61,6 @@ pub fn events_routes() -> Vec { routes } -// -// Move this somewhere else -// -use rocket::{serde::json::Json, serde::json::Value, Catcher, Route}; - -use crate::{ - api::{EmptyResult, JsonResult, Notify, UpdateType}, - auth::Headers, - db::{ - models::{Membership, MembershipStatus, OrgPolicy, Organization, User}, - DbConn, - }, - error::Error, - http_client::make_http_request, - mail, - util::{parse_experimental_client_feature_flags, FeatureFlagFilter}, - CONFIG, -}; - #[derive(Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] struct GlobalDomain { @@ -73,15 +71,17 @@ struct GlobalDomain { const GLOBAL_DOMAINS: &str = include_str!("../../static/global_domains.json"); +#[expect(clippy::needless_pass_by_value, reason = "Not beneficial for Headers")] #[get("/settings/domains")] -fn get_eq_domains(headers: Headers) -> Json { - _get_eq_domains(&headers, false) +fn get_settings_domains(headers: Headers) -> Json { + get_eq_domains(&headers, false) } -fn _get_eq_domains(headers: &Headers, no_excluded: bool) -> Json { - let user = &headers.user; +fn get_eq_domains(headers: &Headers, no_excluded: bool) -> Json { use serde_json::from_str; + let user = &headers.user; + let equivalent_domains: Vec> = from_str(&user.equivalent_domains).unwrap(); let excluded_globals: Vec = from_str(&user.excluded_globals).unwrap(); @@ -110,17 +110,23 @@ struct EquivDomainData { } #[post("/settings/domains", data = "")] -async fn post_eq_domains(data: Json, headers: Headers, conn: DbConn, nt: Notify<'_>) -> JsonResult { +async fn post_settings_domains( + data: Json, + headers: Headers, + conn: DbConn, + nt: Notify<'_>, +) -> JsonResult { + use serde_json::to_string; + let data: EquivDomainData = data.into_inner(); let excluded_globals = data.excluded_global_equivalent_domains.unwrap_or_default(); let equivalent_domains = data.equivalent_domains.unwrap_or_default(); let mut user = headers.user; - use serde_json::to_string; - user.excluded_globals = to_string(&excluded_globals).unwrap_or_else(|_| "[]".to_string()); - user.equivalent_domains = to_string(&equivalent_domains).unwrap_or_else(|_| "[]".to_string()); + user.excluded_globals = to_string(&excluded_globals).unwrap_or_else(|_| "[]".to_owned()); + user.equivalent_domains = to_string(&equivalent_domains).unwrap_or_else(|_| "[]".to_owned()); user.save(&conn).await?; @@ -130,8 +136,13 @@ async fn post_eq_domains(data: Json, headers: Headers, conn: Db } #[put("/settings/domains", data = "")] -async fn put_eq_domains(data: Json, headers: Headers, conn: DbConn, nt: Notify<'_>) -> JsonResult { - post_eq_domains(data, headers, conn, nt).await +async fn put_settings_domains( + data: Json, + headers: Headers, + conn: DbConn, + nt: Notify<'_>, +) -> JsonResult { + post_settings_domains(data, headers, conn, nt).await } #[get("/hibp/breach?")] @@ -206,9 +217,9 @@ fn config() -> Json { // iOS (v2026.2.1): https://github.com/bitwarden/ios/blob/cdd9ba1770ca2ffc098d02d12cc3208e3a830454/BitwardenShared/Core/Platform/Models/Enum/FeatureFlag.swift#L7 let mut feature_states = parse_experimental_client_feature_flags( &CONFIG.experimental_client_feature_flags(), - FeatureFlagFilter::ValidOnly, + &FeatureFlagFilter::ValidOnly, ); - feature_states.insert("pm-19148-innovation-archive".to_string(), true); + feature_states.insert("pm-19148-innovation-archive".to_owned(), true); Json(json!({ // Note: The clients use this version to handle backwards compatibility concerns @@ -278,9 +289,8 @@ async fn accept_org_invite( member.save(conn).await?; if CONFIG.mail_enabled() { - let org = match Organization::find_by_uuid(&member.org_uuid, conn).await { - Some(org) => org, - None => err!("Organization not found."), + let Some(org) = Organization::find_by_uuid(&member.org_uuid, conn).await else { + err!("Organization not found.") }; // User was invited to an organization, so they must be confirmed manually after acceptance mail::send_invite_accepted(&user.email, &member.invited_by_email.unwrap_or(org.billing_email), &org.name) diff --git a/src/api/core/organizations.rs b/src/api/core/organizations.rs index 3e6eb767..dd68cd5b 100644 --- a/src/api/core/organizations.rs +++ b/src/api/core/organizations.rs @@ -1,28 +1,28 @@ -use num_traits::FromPrimitive; -use rocket::serde::json::Json; -use rocket::Route; -use serde_json::Value; use std::collections::{HashMap, HashSet}; -use crate::api::admin::FAKE_ADMIN_UUID; +use num_traits::FromPrimitive; +use rocket::{Route, serde::json::Json}; +use serde_json::Value; + use crate::{ + CONFIG, + api::admin::FAKE_ADMIN_UUID, api::{ - core::{accept_org_invite, log_event, two_factor, CipherSyncData, CipherSyncType}, EmptyResult, JsonResult, Notify, PasswordOrOtpData, UpdateType, + core::{CipherSyncData, CipherSyncType, accept_org_invite, log_event, two_factor}, }, - auth::{decode_invite, AdminHeaders, Headers, ManagerHeaders, ManagerHeadersLoose, OrgMemberHeaders, OwnerHeaders}, + auth::{AdminHeaders, Headers, ManagerHeaders, ManagerHeadersLoose, OrgMemberHeaders, OwnerHeaders, decode_invite}, db::{ + DbConn, models::{ Cipher, CipherId, Collection, CollectionCipher, CollectionGroup, CollectionId, CollectionUser, EventType, Group, GroupId, GroupUser, Invitation, Membership, MembershipId, MembershipStatus, MembershipType, OrgPolicy, OrgPolicyType, Organization, OrganizationApiKey, OrganizationId, User, UserId, }, - DbConn, }, mail, sso::FAKE_SSO_IDENTIFIER, - util::{convert_json_key_lcase_first, NumberOrString}, - CONFIG, + util::{NumberOrString, convert_json_key_lcase_first}, }; pub fn routes() -> Vec { @@ -97,7 +97,7 @@ pub fn routes() -> Vec { get_reset_password_details, put_reset_password, get_org_export, - api_key, + post_api_key, rotate_api_key, get_billing_metadata, get_billing_warnings, @@ -286,9 +286,10 @@ async fn get_organization(org_id: OrganizationId, headers: OwnerHeaders, conn: D if org_id != headers.org_id { err!("Organization not found", "Organization id's do not match"); } - match Organization::find_by_uuid(&org_id, &conn).await { - Some(organization) => Ok(Json(organization.to_json())), - None => err!("Can't find organization details"), + if let Some(organization) = Organization::find_by_uuid(&org_id, &conn).await { + Ok(Json(organization.to_json())) + } else { + err!("Can't find organization details") } } @@ -367,7 +368,7 @@ async fn get_auto_enroll_status(identifier: &str, headers: Headers, conn: DbConn }; let (id, identifier, rp_auto_enroll) = match org { - None => (identifier.to_string(), identifier.to_string(), false), + None => (identifier.to_owned(), identifier.to_owned(), false), Some(org) => ( org.uuid.to_string(), org.uuid.to_string(), @@ -393,7 +394,7 @@ async fn get_org_collections(org_id: OrganizationId, headers: ManagerHeadersLoos } Ok(Json(json!({ - "data": _get_org_collections(&org_id, &conn).await, + "data": get_org_collections_impl(&org_id, &conn).await, "object": "list", "continuationToken": null, }))) @@ -465,7 +466,7 @@ async fn get_org_collections_details(org_id: OrganizationId, headers: ManagerHea CollectionGroup::find_by_collection(&col.uuid, &conn) .await .iter() - .map(|collection_group| collection_group.to_json_details_for_group()) + .map(CollectionGroup::to_json_details_for_group) .collect() } else { Vec::with_capacity(0) @@ -477,7 +478,7 @@ async fn get_org_collections_details(org_id: OrganizationId, headers: ManagerHea json_object["groups"] = json!(groups); json_object["object"] = json!("collectionAccessDetails"); json_object["unmanaged"] = json!(false); - data.push(json_object) + data.push(json_object); } Ok(Json(json!({ @@ -487,7 +488,7 @@ async fn get_org_collections_details(org_id: OrganizationId, headers: ManagerHea }))) } -async fn _get_org_collections(org_id: &OrganizationId, conn: &DbConn) -> Value { +async fn get_org_collections_impl(org_id: &OrganizationId, conn: &DbConn) -> Value { Collection::find_by_organization(org_id, conn).await.iter().map(Collection::to_json).collect::() } @@ -573,7 +574,7 @@ async fn post_bulk_access_collections( if Organization::find_by_uuid(&org_id, &conn).await.is_none() { err!("Can't find organization details") - }; + } for col_id in data.collection_ids { let Some(collection) = Collection::find_by_uuid_and_org(&col_id, &org_id, &conn).await else { @@ -650,7 +651,7 @@ async fn post_organization_collection_update( if Organization::find_by_uuid(&org_id, &conn).await.is_none() { err!("Can't find organization details") - }; + } let Some(mut collection) = Collection::find_by_uuid_and_org(&col_id, &org_id, &conn).await else { err!("Collection not found") @@ -701,7 +702,7 @@ async fn post_organization_collection_update( Ok(Json(collection.to_json_details(&headers.user.uuid, None, &conn).await)) } -async fn _delete_organization_collection( +async fn delete_organization_collection_impl( org_id: &OrganizationId, col_id: &CollectionId, headers: &ManagerHeaders, @@ -733,7 +734,7 @@ async fn delete_organization_collection( headers: ManagerHeaders, conn: DbConn, ) -> EmptyResult { - _delete_organization_collection(&org_id, &col_id, &headers, &conn).await + delete_organization_collection_impl(&org_id, &col_id, &headers, &conn).await } #[post("/organizations//collections//delete")] @@ -743,7 +744,7 @@ async fn post_organization_collection_delete( headers: ManagerHeaders, conn: DbConn, ) -> EmptyResult { - _delete_organization_collection(&org_id, &col_id, &headers, &conn).await + delete_organization_collection_impl(&org_id, &col_id, &headers, &conn).await } #[derive(Deserialize, Debug)] @@ -769,7 +770,7 @@ async fn bulk_delete_organization_collections( let headers = ManagerHeaders::from_loose(headers, &collections, &conn).await?; for col_id in collections { - _delete_organization_collection(&org_id, &col_id, &headers, &conn).await? + delete_organization_collection_impl(&org_id, &col_id, &headers, &conn).await?; } Ok(()) } @@ -799,7 +800,7 @@ async fn get_org_collection_detail( CollectionGroup::find_by_collection(&collection.uuid, &conn) .await .iter() - .map(|collection_group| collection_group.to_json_details_for_group()) + .map(CollectionGroup::to_json_details_for_group) .collect() } else { // The Bitwarden clients seem to call this API regardless of whether groups are enabled, @@ -886,13 +887,13 @@ async fn get_org_details(data: OrgIdData, headers: ManagerHeadersLoose, conn: Db } Ok(Json(json!({ - "data": _get_org_details(&data.organization_id, &headers.host, &headers.user.uuid, &conn).await?, + "data": get_org_details_impl(&data.organization_id, &headers.host, &headers.user.uuid, &conn).await?, "object": "list", "continuationToken": null, }))) } -async fn _get_org_details( +async fn get_org_details_impl( org_id: &OrganizationId, host: &str, user_id: &UserId, @@ -975,14 +976,13 @@ async fn post_org_keys( } let data: OrgKeyData = data.into_inner(); - let mut org = match Organization::find_by_uuid(&org_id, &conn).await { - Some(organization) => { - if organization.private_key.is_some() && organization.public_key.is_some() { - err!("Organization Keys already exist") - } - organization + let mut org = if let Some(organization) = Organization::find_by_uuid(&org_id, &conn).await { + if organization.private_key.is_some() && organization.public_key.is_some() { + err!("Organization Keys already exist") } - None => err!("Can't find organization details"), + organization + } else { + err!("Can't find organization details") }; org.private_key = Some(data.encrypted_private_key); @@ -1043,9 +1043,10 @@ async fn send_invite( // The from_str() will convert the custom role type into a manager role type let raw_type = &data.r#type.into_string(); // Membership::from_str will convert custom (4) to manager (3) - let new_type = match MembershipType::from_str(raw_type) { - Some(new_type) => new_type as i32, - None => err!("Invalid type"), + let new_type = if let Some(new_type) = MembershipType::from_str(raw_type) { + new_type as i32 + } else { + err!("Invalid type") }; if new_type != MembershipType::User && headers.membership_type != MembershipType::Owner { @@ -1062,7 +1063,7 @@ async fn send_invite( && data.permissions.get("createNewCollections") == Some(&json!(true))); let mut user_created: bool = false; - for email in data.emails.iter() { + for email in &data.emails { let mut member_status = MembershipStatus::Invited as i32; let user = match User::find_by_mail(email, &conn).await { None => { @@ -1086,13 +1087,13 @@ async fn send_invite( Some(user) => { if Membership::find_by_user_and_org(&user.uuid, &org_id, &conn).await.is_some() { err!(format!("User already in organization: {email}")) - } else { - // automatically accept existing users if mail is disabled - if !CONFIG.mail_enabled() && !user.password_hash.is_empty() { - member_status = MembershipStatus::Accepted as i32; - } - user } + + // automatically accept existing users if mail is disabled + if !CONFIG.mail_enabled() && !user.password_hash.is_empty() { + member_status = MembershipStatus::Accepted as i32; + } + user } }; @@ -1103,9 +1104,10 @@ async fn send_invite( new_member.save(&conn).await?; if CONFIG.mail_enabled() { - let org_name = match Organization::find_by_uuid(&org_id, &conn).await { - Some(org) => org.name, - None => err!("Error looking up organization"), + let org_name = if let Some(org) = Organization::find_by_uuid(&org_id, &conn).await { + org.name + } else { + err!("Error looking up organization") }; if let Err(e) = mail::send_invite( @@ -1159,7 +1161,7 @@ async fn send_invite( } } - for group_id in data.groups.iter() { + for group_id in &data.groups { let mut group_entry = GroupUser::new(group_id.clone(), new_member.uuid.clone()); group_entry.save(&conn).await?; } @@ -1182,8 +1184,8 @@ async fn bulk_reinvite_members( let mut bulk_response = Vec::new(); for member_id in data.ids { - let err_msg = match _reinvite_member(&org_id, &member_id, &headers.user.email, &conn).await { - Ok(_) => String::new(), + let err_msg = match reinvite_member_impl(&org_id, &member_id, &headers.user.email, &conn).await { + Ok(()) => String::new(), Err(e) => format!("{e:?}"), }; @@ -1193,7 +1195,7 @@ async fn bulk_reinvite_members( "id": member_id, "error": err_msg } - )) + )); } Ok(Json(json!({ @@ -1213,10 +1215,10 @@ async fn reinvite_member( if org_id != headers.org_id { err!("Organization not found", "Organization id's do not match"); } - _reinvite_member(&org_id, &member_id, &headers.user.email, &conn).await + reinvite_member_impl(&org_id, &member_id, &headers.user.email, &conn).await } -async fn _reinvite_member( +async fn reinvite_member_impl( org_id: &OrganizationId, member_id: &MembershipId, invited_by_email: &str, @@ -1238,13 +1240,14 @@ async fn _reinvite_member( err!("Invitations are not allowed.") } - let org_name = match Organization::find_by_uuid(org_id, conn).await { - Some(org) => org.name, - None => err!("Error looking up organization."), + let org_name = if let Some(org) = Organization::find_by_uuid(org_id, conn).await { + org.name + } else { + err!("Error looking up organization.") }; if CONFIG.mail_enabled() { - mail::send_invite(&user, org_id.clone(), member.uuid, &org_name, Some(invited_by_email.to_string())).await?; + mail::send_invite(&user, org_id.clone(), member.uuid, &org_name, Some(invited_by_email.to_owned())).await?; } else if user.password_hash.is_empty() { let invitation = Invitation::new(&user.email); invitation.save(conn).await?; @@ -1352,8 +1355,8 @@ async fn bulk_confirm_invite( for invite in keys { let member_id = invite.id.unwrap(); let user_key = invite.key.unwrap_or_default(); - let err_msg = match _confirm_invite(&org_id, &member_id, &user_key, &headers, &conn, &nt).await { - Ok(_) => String::new(), + let err_msg = match confirm_invite_impl(&org_id, &member_id, &user_key, &headers, &conn, &nt).await { + Ok(()) => String::new(), Err(e) => format!("{e:?}"), }; @@ -1387,10 +1390,10 @@ async fn confirm_invite( ) -> EmptyResult { let data = data.into_inner(); let user_key = data.key.unwrap_or_default(); - _confirm_invite(&org_id, &member_id, &user_key, &headers, &conn, &nt).await + confirm_invite_impl(&org_id, &member_id, &user_key, &headers, &conn, &nt).await } -async fn _confirm_invite( +async fn confirm_invite_impl( org_id: &OrganizationId, member_id: &MembershipId, key: &str, @@ -1418,7 +1421,7 @@ async fn _confirm_invite( } member_to_confirm.status = MembershipStatus::Confirmed as i32; - member_to_confirm.akey = key.to_string(); + member_to_confirm.akey = key.to_owned(); // This check is also done at accept_invite, _confirm_invite, _activate_member, edit_member, admin::update_membership_type OrgPolicy::check_user_allowed(&member_to_confirm, "confirm", conn).await?; @@ -1435,13 +1438,15 @@ async fn _confirm_invite( .await; if CONFIG.mail_enabled() { - let org_name = match Organization::find_by_uuid(org_id, conn).await { - Some(org) => org.name, - None => err!("Error looking up organization."), + let org_name = if let Some(org) = Organization::find_by_uuid(org_id, conn).await { + org.name + } else { + err!("Error looking up organization.") }; - let address = match User::find_by_uuid(&member_to_confirm.user_uuid, conn).await { - Some(user) => user.email, - None => err!("Error looking up user."), + let address = if let Some(user) = User::find_by_uuid(&member_to_confirm.user_uuid, conn).await { + user.email + } else { + err!("Error looking up user.") }; mail::send_invite_confirmed(&address, &org_name).await?; } @@ -1637,8 +1642,8 @@ async fn bulk_delete_member( let mut bulk_response = Vec::new(); for member_id in data.ids { - let err_msg = match _delete_member(&org_id, &member_id, &headers, &conn, &nt).await { - Ok(_) => String::new(), + let err_msg = match delete_member_impl(&org_id, &member_id, &headers, &conn, &nt).await { + Ok(()) => String::new(), Err(e) => format!("{e:?}"), }; @@ -1648,7 +1653,7 @@ async fn bulk_delete_member( "id": member_id, "error": err_msg } - )) + )); } Ok(Json(json!({ @@ -1666,10 +1671,10 @@ async fn delete_member( conn: DbConn, nt: Notify<'_>, ) -> EmptyResult { - _delete_member(&org_id, &member_id, &headers, &conn, &nt).await + delete_member_impl(&org_id, &member_id, &headers, &conn, &nt).await } -async fn _delete_member( +async fn delete_member_impl( org_id: &OrganizationId, member_id: &MembershipId, headers: &AdminHeaders, @@ -1753,8 +1758,8 @@ async fn bulk_public_keys( }))) } -use super::ciphers::update_cipher_from_data; use super::ciphers::CipherData; +use super::ciphers::update_cipher_from_data; #[derive(Deserialize)] #[serde(rename_all = "camelCase")] @@ -1902,24 +1907,24 @@ async fn post_bulk_collections(data: Json, headers: Headers } } - for cipher_id in data.cipher_ids.iter() { + for cipher_id in &data.cipher_ids { // Only act on existing cipher uuid's // Do not abort the operation just ignore it, it could be a cipher was just deleted for example - if let Some(cipher) = Cipher::find_by_uuid_and_org(cipher_id, &data.organization_id, &conn).await { - if cipher.is_write_accessible_to_user(&headers.user.uuid, &conn).await { - // When selecting a specific collection from the left filter list, and use the bulk option, you can remove an item from that collection - // In these cases the client will call this endpoint twice, once for adding the new collections and a second for deleting. - if data.remove_collections { - for collection in &data.collection_ids { - CollectionCipher::delete(&cipher.uuid, collection, &conn).await?; - } - } else { - for collection in &data.collection_ids { - CollectionCipher::save(&cipher.uuid, collection, &conn).await?; - } + if let Some(cipher) = Cipher::find_by_uuid_and_org(cipher_id, &data.organization_id, &conn).await + && cipher.is_write_accessible_to_user(&headers.user.uuid, &conn).await + { + // When selecting a specific collection from the left filter list, and use the bulk option, you can remove an item from that collection + // In these cases the client will call this endpoint twice, once for adding the new collections and a second for deleting. + if data.remove_collections { + for collection in &data.collection_ids { + CollectionCipher::delete(&cipher.uuid, collection, &conn).await?; + } + } else { + for collection in &data.collection_ids { + CollectionCipher::save(&cipher.uuid, collection, &conn).await?; } } - }; + } } Ok(()) @@ -1969,7 +1974,7 @@ async fn list_policies_token(org_id: OrganizationId, token: &str, conn: DbConn) fn get_dummy_master_password_policy() -> JsonResult { let (enabled, data) = match CONFIG.sso_master_password_policy_value() { Some(policy) if CONFIG.sso_enabled() => (true, policy.to_string()), - _ => (false, "null".to_string()), + _ => (false, "null".to_owned()), }; let policy = OrgPolicy::new(FAKE_SSO_IDENTIFIER.into(), OrgPolicyType::MasterPassword, enabled, data); Ok(Json(policy.to_json())) @@ -1982,7 +1987,7 @@ async fn get_master_password_policy(org_id: OrganizationId, _headers: OrgMemberH OrgPolicy::find_by_org_and_type(&org_id, OrgPolicyType::MasterPassword, &conn).await.unwrap_or_else(|| { let (enabled, data) = match CONFIG.sso_master_password_policy_value() { Some(policy) if CONFIG.sso_enabled() => (true, policy.to_string()), - _ => (false, "null".to_string()), + _ => (false, "null".to_owned()), }; OrgPolicy::new(org_id, OrgPolicyType::MasterPassword, enabled, data) @@ -2003,7 +2008,7 @@ async fn get_policy(org_id: OrganizationId, pol_type: i32, headers: AdminHeaders let policy = match OrgPolicy::find_by_org_and_type(&org_id, pol_type_enum, &conn).await { Some(p) => p, - None => OrgPolicy::new(org_id.clone(), pol_type_enum, false, "null".to_string()), + None => OrgPolicy::new(org_id.clone(), pol_type_enum, false, "null".to_owned()), }; Ok(Json(policy.to_json())) @@ -2078,7 +2083,7 @@ async fn put_policy( // When enabling the SingleOrg policy, remove this org's members that are members of other orgs if pol_type_enum == OrgPolicyType::SingleOrg && data.enabled { - for mut member in Membership::find_by_org(&org_id, &conn).await.into_iter() { + for mut member in Membership::find_by_org(&org_id, &conn).await { // Policy only applies to non-Owner/non-Admin members who have accepted joining the org // Exclude invited and revoked users when checking for this policy. // Those users will not be allowed to accept or be activated because of the policy checks done there. @@ -2113,7 +2118,7 @@ async fn put_policy( let mut policy = match OrgPolicy::find_by_org_and_type(&org_id, pol_type_enum, &conn).await { Some(p) => p, - None => OrgPolicy::new(org_id.clone(), pol_type_enum, false, "{}".to_string()), + None => OrgPolicy::new(org_id.clone(), pol_type_enum, false, "{}".to_owned()), }; policy.enabled = data.enabled; @@ -2187,7 +2192,7 @@ fn get_plans() -> Json { #[get("/organizations/<_org_id>/billing/metadata")] fn get_billing_metadata(_org_id: OrganizationId, _headers: OrgMemberHeaders) -> Json { // Prevent a 404 error, which also causes Javascript errors. - Json(_empty_data_json()) + Json(empty_data_json()) } #[get("/organizations/<_org_id>/billing/vnext/warnings")] @@ -2209,7 +2214,7 @@ fn get_self_host_billing_metadata(_org_id: OrganizationId, _headers: OrgMemberHe })) } -fn _empty_data_json() -> Value { +fn empty_data_json() -> Value { json!({ "object": "list", "data": [], @@ -2230,7 +2235,7 @@ async fn revoke_member( headers: AdminHeaders, conn: DbConn, ) -> EmptyResult { - _revoke_member(&org_id, &member_id, &headers, &conn).await + revoke_member_impl(&org_id, &member_id, &headers, &conn).await } #[put("/organizations//users/revoke", data = "")] @@ -2249,8 +2254,8 @@ async fn bulk_revoke_members( match data.ids { Some(members) => { for member_id in members { - let err_msg = match _revoke_member(&org_id, &member_id, &headers, &conn).await { - Ok(_) => String::new(), + let err_msg = match revoke_member_impl(&org_id, &member_id, &headers, &conn).await { + Ok(()) => String::new(), Err(e) => format!("{e:?}"), }; @@ -2273,7 +2278,7 @@ async fn bulk_revoke_members( }))) } -async fn _revoke_member( +async fn revoke_member_impl( org_id: &OrganizationId, member_id: &MembershipId, headers: &AdminHeaders, @@ -2325,7 +2330,7 @@ async fn restore_member_vnext( ) -> EmptyResult { // Vaultwarden does not (yet) support the per User Collection linked to the `Enforce organization data ownership` policy. // Therefor we ignore the `defaultUserCollectionName` data sent and just call restore_member - _restore_member(&org_id, &member_id, &headers, &conn).await + restore_member_impl(&org_id, &member_id, &headers, &conn).await } #[put("/organizations//users//restore")] @@ -2335,7 +2340,7 @@ async fn restore_member( headers: AdminHeaders, conn: DbConn, ) -> EmptyResult { - _restore_member(&org_id, &member_id, &headers, &conn).await + restore_member_impl(&org_id, &member_id, &headers, &conn).await } #[put("/organizations//users/restore", data = "")] @@ -2352,8 +2357,8 @@ async fn bulk_restore_members( let mut bulk_response = Vec::new(); for member_id in data.ids { - let err_msg = match _restore_member(&org_id, &member_id, &headers, &conn).await { - Ok(_) => String::new(), + let err_msg = match restore_member_impl(&org_id, &member_id, &headers, &conn).await { + Ok(()) => String::new(), Err(e) => format!("{e:?}"), }; @@ -2373,7 +2378,7 @@ async fn bulk_restore_members( }))) } -async fn _restore_member( +async fn restore_member_impl( org_id: &OrganizationId, member_id: &MembershipId, headers: &AdminHeaders, @@ -2429,11 +2434,11 @@ async fn get_groups_data( if details { for g in groups { - groups_json.push(g.to_json_details(&conn).await) + groups_json.push(g.to_json_details(&conn).await); } } else { for g in groups { - groups_json.push(g.to_json()) + groups_json.push(g.to_json()); } } groups_json @@ -2672,15 +2677,15 @@ async fn post_delete_group( headers: AdminHeaders, conn: DbConn, ) -> EmptyResult { - _delete_group(&org_id, &group_id, &headers, &conn).await + delete_group_impl(&org_id, &group_id, &headers, &conn).await } #[delete("/organizations//groups/")] async fn delete_group(org_id: OrganizationId, group_id: GroupId, headers: AdminHeaders, conn: DbConn) -> EmptyResult { - _delete_group(&org_id, &group_id, &headers, &conn).await + delete_group_impl(&org_id, &group_id, &headers, &conn).await } -async fn _delete_group( +async fn delete_group_impl( org_id: &OrganizationId, group_id: &GroupId, headers: &AdminHeaders, @@ -2728,7 +2733,7 @@ async fn bulk_delete_groups( let data: BulkGroupIds = data.into_inner(); for group_id in data.ids { - _delete_group(&org_id, &group_id, &headers, &conn).await? + delete_group_impl(&org_id, &group_id, &headers, &conn).await?; } Ok(()) } @@ -2765,7 +2770,7 @@ async fn get_group_members( if Group::find_by_uuid_and_org(&group_id, &org_id, &conn).await.is_none() { err!("Group could not be found!", "Group uuid is invalid or does not belong to the organization") - }; + } let group_members: Vec = GroupUser::find_by_group(&group_id, &org_id, &conn) .await @@ -2793,7 +2798,7 @@ async fn put_group_members( if Group::find_by_uuid_and_org(&group_id, &org_id, &conn).await.is_none() { err!("Group could not be found!", "Group uuid is invalid or does not belong to the organization") - }; + } let assigned_members = data.into_inner(); @@ -3100,12 +3105,12 @@ async fn get_org_export(org_id: OrganizationId, headers: AdminHeaders, conn: DbC } Ok(Json(json!({ - "collections": convert_json_key_lcase_first(_get_org_collections(&org_id, &conn).await), - "ciphers": convert_json_key_lcase_first(_get_org_details(&org_id, &headers.host, &headers.user.uuid, &conn).await?), + "collections": convert_json_key_lcase_first(get_org_collections_impl(&org_id, &conn).await), + "ciphers": convert_json_key_lcase_first(get_org_details_impl(&org_id, &headers.host, &headers.user.uuid, &conn).await?), }))) } -async fn _api_key( +async fn api_key( org_id: &OrganizationId, data: Json, rotate: bool, @@ -3121,21 +3126,18 @@ async fn _api_key( // Validate the admin users password/otp data.validate(&user, true, &conn).await?; - let org_api_key = match OrganizationApiKey::find_by_org_uuid(org_id, &conn).await { - Some(mut org_api_key) => { - if rotate { - org_api_key.api_key = crate::crypto::generate_api_key(); - org_api_key.revision_date = chrono::Utc::now().naive_utc(); - org_api_key.save(&conn).await.expect("Error rotating organization API Key"); - } - org_api_key - } - None => { - let api_key = crate::crypto::generate_api_key(); - let new_org_api_key = OrganizationApiKey::new(org_id.clone(), api_key); - new_org_api_key.save(&conn).await.expect("Error creating organization API Key"); - new_org_api_key + let org_api_key = if let Some(mut org_api_key) = OrganizationApiKey::find_by_org_uuid(org_id, &conn).await { + if rotate { + org_api_key.api_key = crate::crypto::generate_api_key(); + org_api_key.revision_date = chrono::Utc::now().naive_utc(); + org_api_key.save(&conn).await.expect("Error rotating organization API Key"); } + org_api_key + } else { + let api_key = crate::crypto::generate_api_key(); + let new_org_api_key = OrganizationApiKey::new(org_id.clone(), api_key); + new_org_api_key.save(&conn).await.expect("Error creating organization API Key"); + new_org_api_key }; Ok(Json(json!({ @@ -3146,13 +3148,13 @@ async fn _api_key( } #[post("/organizations//api-key", data = "")] -async fn api_key( +async fn post_api_key( org_id: OrganizationId, data: Json, headers: AdminHeaders, conn: DbConn, ) -> JsonResult { - _api_key(&org_id, data, false, headers, conn).await + api_key(&org_id, data, false, headers, conn).await } #[post("/organizations//rotate-api-key", data = "")] @@ -3162,5 +3164,5 @@ async fn rotate_api_key( headers: AdminHeaders, conn: DbConn, ) -> JsonResult { - _api_key(&org_id, data, true, headers, conn).await + api_key(&org_id, data, true, headers, conn).await } diff --git a/src/api/core/public.rs b/src/api/core/public.rs index d757d953..33189e78 100644 --- a/src/api/core/public.rs +++ b/src/api/core/public.rs @@ -1,23 +1,24 @@ -use chrono::Utc; -use rocket::{ - request::{FromRequest, Outcome}, - serde::json::Json, - Request, Route, -}; - use std::collections::HashSet; +use chrono::Utc; +use rocket::{ + Request, Route, + request::{FromRequest, Outcome}, + serde::json::Json, +}; + use crate::{ + CONFIG, api::EmptyResult, auth, db::{ + DbConn, models::{ Group, GroupUser, Invitation, Membership, MembershipStatus, MembershipType, Organization, OrganizationApiKey, OrganizationId, User, }, - DbConn, }, - mail, CONFIG, + mail, }; pub fn routes() -> Vec { @@ -90,19 +91,18 @@ async fn ldap_import(data: Json, token: PublicToken, conn: DbConn } } else { // If user is not part of the organization - let user = match User::find_by_mail(&user_data.email, &conn).await { - Some(user) => user, // exists in vaultwarden - None => { - // User does not exist yet - let mut new_user = User::new(&user_data.email, None); - new_user.save(&conn).await?; + let user = if let Some(user) = User::find_by_mail(&user_data.email, &conn).await { + user + } else { + // User does not exist yet + let mut new_user = User::new(&user_data.email, None); + new_user.save(&conn).await?; - if !CONFIG.mail_enabled() { - Invitation::new(&new_user.email).save(&conn).await?; - } - user_created = true; - new_user + if !CONFIG.mail_enabled() { + Invitation::new(&new_user.email).save(&conn).await?; } + user_created = true; + new_user }; let member_status = if CONFIG.mail_enabled() || user.password_hash.is_empty() { MembershipStatus::Invited as i32 @@ -110,9 +110,10 @@ async fn ldap_import(data: Json, token: PublicToken, conn: DbConn MembershipStatus::Accepted as i32 // Automatically mark user as accepted if no email invites }; - let (org_name, org_email) = match Organization::find_by_uuid(&org_id, &conn).await { - Some(org) => (org.name, org.billing_email), - None => err!("Error looking up organization"), + let (org_name, org_email) = if let Some(org) = Organization::find_by_uuid(&org_id, &conn).await { + (org.name, org.billing_email) + } else { + err!("Error looking up organization") }; let mut new_member = Membership::new(user.uuid.clone(), org_id.clone(), Some(org_email.clone())); @@ -123,37 +124,33 @@ async fn ldap_import(data: Json, token: PublicToken, conn: DbConn new_member.save(&conn).await?; - if CONFIG.mail_enabled() { - if let Err(e) = + if CONFIG.mail_enabled() + && let Err(e) = mail::send_invite(&user, org_id.clone(), new_member.uuid.clone(), &org_name, Some(org_email)).await - { - // Upon error delete the user, invite and org member records when needed - if user_created { - user.delete(&conn).await?; - } else { - new_member.delete(&conn).await?; - } - - err!(format!("Error sending invite: {e:?} ")); + { + // Upon error delete the user, invite and org member records when needed + if user_created { + user.delete(&conn).await?; + } else { + new_member.delete(&conn).await?; } + + err!(format!("Error sending invite: {e:?} ")); } } } if CONFIG.org_groups_enabled() { for group_data in &data.groups { - let group_uuid = match Group::find_by_external_id_and_org(&group_data.external_id, &org_id, &conn).await { - Some(group) => group.uuid, - None => { - let mut group = Group::new( - org_id.clone(), - group_data.name.clone(), - false, - Some(group_data.external_id.clone()), - ); - group.save(&conn).await?; - group.uuid - } + let group_uuid = if let Some(group) = + Group::find_by_external_id_and_org(&group_data.external_id, &org_id, &conn).await + { + group.uuid + } else { + let mut group = + Group::new(org_id.clone(), group_data.name.clone(), false, Some(group_data.external_id.clone())); + group.save(&conn).await?; + group.uuid }; GroupUser::delete_all_by_group(&group_uuid, &org_id, &conn).await?; @@ -174,18 +171,17 @@ async fn ldap_import(data: Json, token: PublicToken, conn: DbConn // Generate a HashSet to quickly verify if a member is listed or not. let sync_members: HashSet = data.members.into_iter().map(|m| m.external_id).collect(); for member in Membership::find_by_org(&org_id, &conn).await { - if let Some(ref user_external_id) = member.external_id { - if !sync_members.contains(user_external_id) { - if member.atype == MembershipType::Owner && member.status == MembershipStatus::Confirmed as i32 { - // Removing owner, check that there is at least one other confirmed owner - if Membership::count_confirmed_by_org_and_type(&org_id, MembershipType::Owner, &conn).await <= 1 - { - warn!("Can't delete the last owner"); - continue; - } + if let Some(ref user_external_id) = member.external_id + && !sync_members.contains(user_external_id) + { + if member.atype == MembershipType::Owner && member.status == MembershipStatus::Confirmed as i32 { + // Removing owner, check that there is at least one other confirmed owner + if Membership::count_confirmed_by_org_and_type(&org_id, MembershipType::Owner, &conn).await <= 1 { + warn!("Can't delete the last owner"); + continue; } - member.delete(&conn).await?; } + member.delete(&conn).await?; } } } @@ -202,12 +198,14 @@ impl<'r> FromRequest<'r> for PublicToken { async fn from_request(request: &'r Request<'_>) -> Outcome { let headers = request.headers(); // Get access_token - let access_token: &str = match headers.get_one("Authorization") { - Some(a) => match a.rsplit("Bearer ").next() { - Some(split) => split, - None => err_handler!("No access token provided"), - }, - None => err_handler!("No access token provided"), + let access_token: &str = if let Some(a) = headers.get_one("Authorization") { + if let Some(split) = a.rsplit("Bearer ").next() { + split + } else { + err_handler!("No access token provided") + } + } else { + err_handler!("No access token provided") }; // Check JWT token is valid and get device and user from it let Ok(claims) = auth::decode_api_org(access_token) else { @@ -229,14 +227,13 @@ impl<'r> FromRequest<'r> for PublicToken { // Check if claims.sub is org_api_key.uuid // Check if claims.client_sub is org_api_key.org_uuid - let conn = match DbConn::from_request(request).await { - Outcome::Success(conn) => conn, - _ => err_handler!("Error getting DB"), + let Outcome::Success(conn) = DbConn::from_request(request).await else { + err_handler!("Error getting DB") }; let Some(org_id) = claims.client_id.strip_prefix("organization.") else { err_handler!("Malformed client_id") }; - let org_id: OrganizationId = org_id.to_string().into(); + let org_id: OrganizationId = org_id.to_owned().into(); let Some(org_api_key) = OrganizationApiKey::find_by_org_uuid(&org_id, &conn).await else { err_handler!("Invalid client_id") }; diff --git a/src/api/core/sends.rs b/src/api/core/sends.rs index 45ead810..2a7e06c1 100644 --- a/src/api/core/sends.rs +++ b/src/api/core/sends.rs @@ -10,15 +10,15 @@ use rocket::{ use serde_json::Value; use crate::{ + CONFIG, api::{ApiResult, EmptyResult, JsonResult, Notify, UpdateType}, auth::{ClientIp, Headers, Host}, config::PathType, db::{ - models::{Device, OrgPolicy, OrgPolicyType, Send, SendFileId, SendId, SendType, UserId}, DbConn, DbPool, + models::{Device, OrgPolicy, OrgPolicyType, Send, SendFileId, SendId, SendType, UserId}, }, - util::{save_temp_file, NumberOrString}, - CONFIG, + util::{NumberOrString, save_temp_file}, }; const SEND_INACCESSIBLE_MSG: &str = "Send does not exist or is no longer available"; @@ -63,7 +63,7 @@ pub async fn purge_sends(pool: DbPool) { if let Ok(conn) = pool.get().await { Send::purge(&conn).await; } else { - error!("Failed to get DB connection while purging sends") + error!("Failed to get DB connection while purging sends"); } } @@ -168,7 +168,7 @@ fn create_send(data: SendData, user_id: UserId) -> ApiResult { #[get("/sends")] async fn get_sends(headers: Headers, conn: DbConn) -> Json { let sends = Send::find_by_user(&headers.user.uuid, &conn); - let sends_json: Vec = sends.await.iter().map(|s| s.to_json()).collect(); + let sends_json: Vec = sends.await.iter().map(Send::to_json).collect(); Json(json!({ "data": sends_json, @@ -179,9 +179,10 @@ async fn get_sends(headers: Headers, conn: DbConn) -> Json { #[get("/sends/")] async fn get_send(send_id: SendId, headers: Headers, conn: DbConn) -> JsonResult { - match Send::find_by_uuid_and_user(&send_id, &headers.user.uuid, &conn).await { - Some(send) => Ok(Json(send.to_json())), - None => err!("Send not found", "Invalid send uuid or does not belong to user"), + if let Some(send) = Send::find_by_uuid_and_user(&send_id, &headers.user.uuid, &conn).await { + Ok(Json(send.to_json())) + } else { + err!("Send not found", "Invalid send uuid or does not belong to user") } } @@ -310,9 +311,10 @@ async fn post_send_file_v2(data: Json, headers: Headers, conn: DbConn) enforce_disable_hide_email_policy(&data, &headers, &conn).await?; - let file_length = match &data.file_length { - Some(m) => m.into_i64()?, - _ => err!("Invalid send length"), + let file_length = if let Some(m) = &data.file_length { + m.into_i64()? + } else { + err!("Invalid send length") }; if file_length < 0 { err!("Send size can't be negative") @@ -457,16 +459,16 @@ async fn post_access( err_code!(SEND_INACCESSIBLE_MSG, 404) }; - if let Some(max_access_count) = send.max_access_count { - if send.access_count >= max_access_count { - err_code!(SEND_INACCESSIBLE_MSG, 404); - } + if let Some(max_access_count) = send.max_access_count + && send.access_count >= max_access_count + { + err_code!(SEND_INACCESSIBLE_MSG, 404); } - if let Some(expiration) = send.expiration_date { - if Utc::now().naive_utc() >= expiration { - err_code!(SEND_INACCESSIBLE_MSG, 404) - } + if let Some(expiration) = send.expiration_date + && Utc::now().naive_utc() >= expiration + { + err_code!(SEND_INACCESSIBLE_MSG, 404) } if Utc::now().naive_utc() >= send.deletion_date { @@ -517,16 +519,16 @@ async fn post_access_file( err_code!(SEND_INACCESSIBLE_MSG, 404) }; - if let Some(max_access_count) = send.max_access_count { - if send.access_count >= max_access_count { - err_code!(SEND_INACCESSIBLE_MSG, 404) - } + if let Some(max_access_count) = send.max_access_count + && send.access_count >= max_access_count + { + err_code!(SEND_INACCESSIBLE_MSG, 404) } - if let Some(expiration) = send.expiration_date { - if Utc::now().naive_utc() >= expiration { - err_code!(SEND_INACCESSIBLE_MSG, 404) - } + if let Some(expiration) = send.expiration_date + && Utc::now().naive_utc() >= expiration + { + err_code!(SEND_INACCESSIBLE_MSG, 404) } if Utc::now().naive_utc() >= send.deletion_date { @@ -572,7 +574,7 @@ async fn download_url(host: &Host, send_id: &SendId, file_id: &SendFileId) -> Re let token_claims = crate::auth::generate_send_claims(send_id, file_id); let token = crate::auth::encode_jwt(&token_claims); - Ok(format!("{}/api/sends/{send_id}/{file_id}?t={token}", &host.host)) + Ok(format!("{}/api/sends/{send_id}/{file_id}?t={token}", host.host)) } else { Ok(operator.presign_read(&format!("{send_id}/{file_id}"), Duration::from_mins(5)).await?.uri().to_string()) } @@ -580,10 +582,10 @@ async fn download_url(host: &Host, send_id: &SendId, file_id: &SendFileId) -> Re #[get("/sends//?")] async fn download_send(send_id: SendId, file_id: SendFileId, t: &str) -> Option { - if let Ok(claims) = crate::auth::decode_send(t) { - if claims.sub == format!("{send_id}/{file_id}") { - return NamedFile::open(Path::new(&CONFIG.sends_folder()).join(send_id).join(file_id)).await.ok(); - } + if let Ok(claims) = crate::auth::decode_send(t) + && claims.sub == format!("{send_id}/{file_id}") + { + return NamedFile::open(Path::new(&CONFIG.sends_folder()).join(send_id).join(file_id)).await.ok(); } None } diff --git a/src/api/core/two_factor/authenticator.rs b/src/api/core/two_factor/authenticator.rs index 4759aa3c..692e8248 100644 --- a/src/api/core/two_factor/authenticator.rs +++ b/src/api/core/two_factor/authenticator.rs @@ -1,14 +1,13 @@ use data_encoding::BASE32; -use rocket::serde::json::Json; -use rocket::Route; +use rocket::{Route, serde::json::Json}; use crate::{ - api::{core::log_user_event, core::two_factor::_generate_recover_code, EmptyResult, JsonResult, PasswordOrOtpData}, + api::{EmptyResult, JsonResult, PasswordOrOtpData, core::log_user_event, core::two_factor::generate_recover_code}, auth::{ClientIp, Headers}, crypto, db::{ - models::{EventType, TwoFactor, TwoFactorType, UserId}, DbConn, + models::{EventType, TwoFactor, TwoFactorType, UserId}, }, util::NumberOrString, }; @@ -70,9 +69,10 @@ async fn activate_authenticator(data: Json, headers: He .await?; // Validate key as base32 and 20 bytes length - let decoded_key: Vec = match BASE32.decode(key.as_bytes()) { - Ok(decoded) => decoded, - _ => err!("Invalid totp secret"), + let decoded_key: Vec = if let Ok(decoded) = BASE32.decode(key.as_bytes()) { + decoded + } else { + err!("Invalid totp secret") }; if decoded_key.len() != 20 { @@ -82,7 +82,7 @@ async fn activate_authenticator(data: Json, headers: He // Validate the token provided with the key, and save new twofactor validate_totp_code(&user.uuid, &token, &key.to_uppercase(), &headers.ip, &conn).await?; - _generate_recover_code(&mut user, &conn).await; + generate_recover_code(&mut user, &conn).await; log_user_event(EventType::UserUpdated2fa as i32, &user.uuid, headers.device.atype, &headers.ip.ip, &conn).await; @@ -119,7 +119,7 @@ pub async fn validate_totp_code( ip: &ClientIp, conn: &DbConn, ) -> EmptyResult { - use totp_lite::{totp_custom, Sha1}; + use totp_lite::{Sha1, totp_custom}; let Ok(decoded_secret) = BASE32.decode(secret.as_bytes()) else { err!("Invalid TOTP secret") @@ -128,7 +128,7 @@ pub async fn validate_totp_code( let mut twofactor = match TwoFactor::find_by_user_and_type(user_id, TwoFactorType::Authenticator as i32, conn).await { Some(tf) => tf, - _ => TwoFactor::new(user_id.clone(), TwoFactorType::Authenticator, secret.to_string()), + _ => TwoFactor::new(user_id.clone(), TwoFactorType::Authenticator, secret.to_owned()), }; // The amount of steps back and forward in time @@ -145,7 +145,7 @@ pub async fn validate_totp_code( // We need to calculate the time offsite and cast it as an u64. // Since we only have times into the future and the totp generator needs an u64 instead of the default i64. - let time = (current_timestamp + step * 30i64) as u64; + let time: u64 = (current_timestamp + step * 30i64).cast_unsigned(); let generated = totp_custom::(30, 6, &decoded_secret, time); // Check the given code equals the generated and if the time_step is larger then the one last used. diff --git a/src/api/core/two_factor/duo.rs b/src/api/core/two_factor/duo.rs index f2de50c3..ed112eb9 100644 --- a/src/api/core/two_factor/duo.rs +++ b/src/api/core/two_factor/duo.rs @@ -1,22 +1,21 @@ use chrono::Utc; use data_encoding::BASE64; -use rocket::serde::json::Json; -use rocket::Route; +use rocket::{Route, serde::json::Json}; use crate::{ + CONFIG, api::{ - core::log_user_event, core::two_factor::_generate_recover_code, ApiResult, EmptyResult, JsonResult, - PasswordOrOtpData, + ApiResult, EmptyResult, JsonResult, PasswordOrOtpData, core::log_user_event, + core::two_factor::generate_recover_code, }, auth::Headers, crypto, db::{ - models::{EventType, TwoFactor, TwoFactorType, User, UserId}, DbConn, + models::{EventType, TwoFactor, TwoFactorType, User, UserId}, }, error::MapResult, http_client::make_http_request, - CONFIG, }; pub fn routes() -> Vec { @@ -82,8 +81,7 @@ enum DuoStatus { impl DuoStatus { fn data(self) -> Option { match self { - DuoStatus::Global(data) => Some(data), - DuoStatus::User(data) => Some(data), + DuoStatus::Global(data) | DuoStatus::User(data) => Some(data), DuoStatus::Disabled(_) => None, } } @@ -182,7 +180,7 @@ async fn activate_duo(data: Json, headers: Headers, conn: DbConn) let twofactor = TwoFactor::new(user.uuid.clone(), type_, data_str); twofactor.save(&conn).await?; - _generate_recover_code(&mut user, &conn).await; + generate_recover_code(&mut user, &conn).await; log_user_event(EventType::UserUpdated2fa as i32, &user.uuid, headers.device.atype, &headers.ip.ip, &conn).await; @@ -201,14 +199,14 @@ async fn activate_duo_put(data: Json, headers: Headers, conn: DbC } async fn duo_api_request(method: &str, path: &str, params: &str, data: &DuoData) -> EmptyResult { - use reqwest::{header, Method}; + use reqwest::{Method, header}; use std::str::FromStr; // https://duo.com/docs/authapi#api-details - let url = format!("https://{}{path}", &data.host); - let date = Utc::now().to_rfc2822(); + let url = format!("https://{}{path}", data.host); + let dt = Utc::now().to_rfc2822(); let username = &data.ik; - let fields = [&date, method, &data.host, path, params]; + let fields = [&dt, method, &data.host, path, params]; let password = crypto::hmac_sign(&data.sk, &fields.join("\n")); let m = Method::from_str(method).unwrap_or_default(); @@ -216,7 +214,7 @@ async fn duo_api_request(method: &str, path: &str, params: &str, data: &DuoData) make_http_request(m, &url)? .basic_auth(username, Some(password)) .header(header::USER_AGENT, "vaultwarden:Duo/1.0 (Rust)") - .header(header::DATE, date) + .header(header::DATE, dt) .send() .await? .error_for_status()?; @@ -356,9 +354,10 @@ fn parse_duo_values(key: &str, val: &str, ikey: &str, prefix: &str, time: i64) - err!("Invalid ikey") } - let expire: i64 = match expire.parse() { - Ok(e) => e, - Err(_) => err!("Invalid expire time"), + let expire: i64 = if let Ok(e) = expire.parse() { + e + } else { + err!("Invalid expire time") }; if time >= expire { diff --git a/src/api/core/two_factor/duo_oidc.rs b/src/api/core/two_factor/duo_oidc.rs index 144ffe84..adb030af 100644 --- a/src/api/core/two_factor/duo_oidc.rs +++ b/src/api/core/two_factor/duo_oidc.rs @@ -1,23 +1,24 @@ +use std::collections::HashMap; + use chrono::Utc; use data_encoding::HEXLOWER; use jsonwebtoken::{Algorithm, DecodingKey, EncodingKey, Header, Validation}; -use reqwest::{header, StatusCode}; -use ring::digest::{digest, Digest, SHA512_256}; +use reqwest::{StatusCode, header}; +use ring::digest::{Digest, SHA512_256, digest}; use serde::Serialize; -use std::collections::HashMap; +use url::Url; use crate::{ - api::{core::two_factor::duo::get_duo_keys_email, EmptyResult}, + CONFIG, + api::{EmptyResult, core::two_factor::duo::get_duo_keys_email}, crypto, db::{ - models::{DeviceId, EventType, TwoFactorDuoContext}, DbConn, DbPool, + models::{DeviceId, EventType, TwoFactorDuoContext}, }, error::Error, http_client::make_http_request, - CONFIG, }; -use url::Url; // The location on this service that Duo should redirect users to. For us, this is a bridge // built in to the Bitwarden clients. @@ -124,7 +125,7 @@ impl DuoClient { ClientAssertion { iss: self.client_id.clone(), sub: self.client_id.clone(), - aud: url.to_string(), + aud: url.to_owned(), exp: now + JWT_VALIDITY_SECS, jti: jwt_id, iat: now, @@ -302,7 +303,7 @@ impl DuoClient { if !(matching_nonces && matching_usernames) { err!("Error validating Duo authorization, nonce or username mismatch.") - }; + } Ok(()) } @@ -347,7 +348,7 @@ pub async fn purge_duo_contexts(pool: DbPool) { if let Ok(conn) = pool.get().await { TwoFactorDuoContext::purge_expired_duo_contexts(&conn).await; } else { - error!("Failed to get DB connection while purging expired Duo authentications") + error!("Failed to get DB connection while purging expired Duo authentications"); } } @@ -394,7 +395,7 @@ pub async fn get_duo_auth_url( match client.health_check().await { Ok(()) => {} Err(e) => return Err(e), - }; + } // Generate random OAuth2 state and OIDC Nonce let state: String = crypto::get_random_string_alphanum(STATE_LENGTH); @@ -438,16 +439,13 @@ pub async fn validate_duo_login( // Get the context by the state reported by the client. If we don't have one, // it means the context is either missing or expired. - let ctx = match extract_context(state, conn).await { - Some(c) => c, - None => { - err!( - "Error validating duo authentication", - ErrorEvent { - event: EventType::UserFailedLogIn2fa - } - ) - } + let Some(ctx) = extract_context(state, conn).await else { + err!( + "Error validating duo authentication", + ErrorEvent { + event: EventType::UserFailedLogIn2fa + } + ) }; // Context validation steps @@ -476,13 +474,13 @@ pub async fn validate_duo_login( match client.health_check().await { Ok(()) => {} Err(e) => return Err(e), - }; + } let d: Digest = digest(&SHA512_256, format!("{}{device_identifier}", ctx.nonce).as_bytes()); let hash: String = HEXLOWER.encode(d.as_ref()); match client.exchange_authz_code_for_result(code, email, hash.as_str()).await { - Ok(_) => Ok(()), + Ok(()) => Ok(()), Err(_) => { err!( "Error validating duo authentication", diff --git a/src/api/core/two_factor/email.rs b/src/api/core/two_factor/email.rs index 7fa350de..44ba2e7f 100644 --- a/src/api/core/two_factor/email.rs +++ b/src/api/core/two_factor/email.rs @@ -1,20 +1,20 @@ use chrono::{DateTime, TimeDelta, Utc}; -use rocket::serde::json::Json; -use rocket::Route; +use rocket::{Route, serde::json::Json}; use crate::{ + CONFIG, api::{ - core::{log_user_event, two_factor::_generate_recover_code}, EmptyResult, JsonResult, PasswordOrOtpData, + core::{log_user_event, two_factor::generate_recover_code}, }, auth::{ClientHeaders, Headers}, crypto, db::{ - models::{AuthRequest, AuthRequestId, DeviceId, EventType, TwoFactor, TwoFactorType, User, UserId}, DbConn, + models::{AuthRequest, AuthRequestId, DeviceId, EventType, TwoFactor, TwoFactorType, User, UserId}, }, error::{Error, MapResult}, - mail, CONFIG, + mail, }; pub fn routes() -> Vec { @@ -232,7 +232,7 @@ async fn email(data: Json, headers: Headers, conn: DbConn) -> JsonRes twofactor.data = email_data.to_json(); twofactor.save(&conn).await?; - _generate_recover_code(&mut user, &conn).await; + generate_recover_code(&mut user, &conn).await; log_user_event(EventType::UserUpdated2fa as i32, &user.uuid, headers.device.atype, &headers.ip.ip, &conn).await; @@ -284,9 +284,9 @@ pub async fn validate_email_code_str( twofactor.data = email_data.to_json(); twofactor.save(conn).await?; - let date = DateTime::from_timestamp(email_data.token_sent, 0).expect("Email token timestamp invalid.").naive_utc(); - let max_time = CONFIG.email_expiration_time() as i64; - if date + TimeDelta::try_seconds(max_time).unwrap() < Utc::now().naive_utc() { + let dt = DateTime::from_timestamp(email_data.token_sent, 0).expect("Email token timestamp invalid.").naive_utc(); + let max_time = CONFIG.email_expiration_time().cast_signed(); + if dt + TimeDelta::try_seconds(max_time).unwrap() < Utc::now().naive_utc() { err!( "Token has expired", ErrorEvent { @@ -342,9 +342,10 @@ impl EmailTokenData { pub fn from_json(string: &str) -> Result { let res: Result = serde_json::from_str(string); - match res { - Ok(x) => Ok(x), - Err(_) => err!("Could not decode EmailTokenData from string"), + if let Ok(x) = res { + Ok(x) + } else { + err!("Could not decode EmailTokenData from string") } } } @@ -362,18 +363,17 @@ pub async fn activate_email_2fa(user: &User, conn: &DbConn) -> EmptyResult { pub fn obscure_email(email: &str) -> String { let split: Vec<&str> = email.rsplitn(2, '@').collect(); - let mut name = split[1].to_string(); + let mut name = split[1].to_owned(); let domain = &split[0]; let name_size = name.chars().count(); - let new_name = match name_size { - 1..=3 => "*".repeat(name_size), - _ => { - let stars = "*".repeat(name_size - 2); - name.truncate(2); - format!("{name}{stars}") - } + let new_name = if let 1..=3 = name_size { + "*".repeat(name_size) + } else { + let stars = "*".repeat(name_size - 2); + name.truncate(2); + format!("{name}{stars}") }; format!("{new_name}@{domain}") diff --git a/src/api/core/two_factor/mod.rs b/src/api/core/two_factor/mod.rs index 3a503a23..8869d23d 100644 --- a/src/api/core/two_factor/mod.rs +++ b/src/api/core/two_factor/mod.rs @@ -1,28 +1,27 @@ use chrono::{TimeDelta, Utc}; use data_encoding::BASE32; use num_traits::FromPrimitive; -use rocket::serde::json::Json; -use rocket::Route; +use rocket::{Route, serde::json::Json}; use serde::Deserialize; use serde_json::Value; use crate::{ + CONFIG, api::{ - core::{log_event, log_user_event}, EmptyResult, JsonResult, PasswordOrOtpData, + core::{log_event, log_user_event}, }, auth::Headers, crypto, db::{ + DbConn, DbPool, models::{ DeviceType, EventType, Membership, MembershipType, OrgPolicyType, Organization, OrganizationId, TwoFactor, TwoFactorIncomplete, TwoFactorType, User, UserId, }, - DbConn, DbPool, }, mail, util::NumberOrString, - CONFIG, }; pub mod authenticator; @@ -37,7 +36,7 @@ fn has_global_duo_credentials() -> bool { CONFIG._enable_duo() && CONFIG.duo_host().is_some() && CONFIG.duo_ikey().is_some() && CONFIG.duo_skey().is_some() } -pub fn is_twofactor_provider_usable(provider_type: TwoFactorType, provider_data: Option<&str>) -> bool { +pub fn is_twofactor_provider_usable(provider_type: &TwoFactorType, provider_data: Option<&str>) -> bool { #[derive(Deserialize)] struct DuoProviderData { host: String, @@ -46,7 +45,7 @@ pub fn is_twofactor_provider_usable(provider_type: TwoFactorType, provider_data: } match provider_type { - TwoFactorType::Authenticator => true, + TwoFactorType::Authenticator | TwoFactorType::RecoveryCode => true, TwoFactorType::Email => CONFIG._enable_email_2fa(), TwoFactorType::Duo | TwoFactorType::OrganizationDuo => { provider_data @@ -59,7 +58,6 @@ pub fn is_twofactor_provider_usable(provider_type: TwoFactorType, provider_data: } TwoFactorType::Webauthn => CONFIG.is_webauthn_2fa_supported(), TwoFactorType::Remember => !CONFIG.disable_2fa_remember(), - TwoFactorType::RecoveryCode => true, TwoFactorType::U2f | TwoFactorType::U2fRegisterChallenge | TwoFactorType::U2fLoginChallenge @@ -96,7 +94,7 @@ async fn get_twofactor(headers: Headers, conn: DbConn) -> Json { .iter() .filter_map(|tf| { let provider_type = TwoFactorType::from_i32(tf.atype)?; - is_twofactor_provider_usable(provider_type, Some(&tf.data)).then(|| TwoFactor::to_json_provider(tf)) + is_twofactor_provider_usable(&provider_type, Some(&tf.data)).then(|| TwoFactor::to_json_provider(tf)) }) .collect(); @@ -120,7 +118,7 @@ async fn get_recover(data: Json, headers: Headers, conn: DbCo }))) } -async fn _generate_recover_code(user: &mut User, conn: &DbConn) { +async fn generate_recover_code(user: &mut User, conn: &DbConn) { if user.totp_recover.is_none() { let totp_recover = crypto::encode_random_bytes::<20>(&BASE32); user.totp_recover = Some(totp_recover); @@ -180,9 +178,7 @@ pub async fn enforce_2fa_policy( ip: &std::net::IpAddr, conn: &DbConn, ) -> EmptyResult { - for member in - Membership::find_by_user_and_policy(&user.uuid, OrgPolicyType::TwoFactorAuthentication, conn).await.into_iter() - { + for member in Membership::find_by_user_and_policy(&user.uuid, OrgPolicyType::TwoFactorAuthentication, conn).await { // Policy only applies to non-Owner/non-Admin members who have accepted joining the org if member.atype < MembershipType::Admin { if CONFIG.mail_enabled() { @@ -217,7 +213,7 @@ pub async fn enforce_2fa_policy_for_org( conn: &DbConn, ) -> EmptyResult { let org = Organization::find_by_uuid(org_id, conn).await.unwrap(); - for member in Membership::find_confirmed_by_org(org_id, conn).await.into_iter() { + for member in Membership::find_confirmed_by_org(org_id, conn).await { // Don't enforce the policy for Admins and Owners. if member.atype < MembershipType::Admin && TwoFactor::find_by_user(&member.user_uuid, conn).await.is_empty() { if CONFIG.mail_enabled() { @@ -251,12 +247,9 @@ pub async fn send_incomplete_2fa_notifications(pool: DbPool) { return; } - let conn = match pool.get().await { - Ok(conn) => conn, - _ => { - error!("Failed to get DB connection in send_incomplete_2fa_notifications()"); - return; - } + let Ok(conn) = pool.get().await else { + error!("Failed to get DB connection in send_incomplete_2fa_notifications()"); + return; }; let now = Utc::now().naive_utc(); @@ -278,7 +271,7 @@ pub async fn send_incomplete_2fa_notifications(pool: DbPool) { ) .await { - Ok(_) => { + Ok(()) => { if let Err(e) = login.delete(&conn).await { error!("Error deleting incomplete 2FA record: {e:#?}"); } diff --git a/src/api/core/two_factor/protected_actions.rs b/src/api/core/two_factor/protected_actions.rs index 800a6cf4..c0c1b5e8 100644 --- a/src/api/core/two_factor/protected_actions.rs +++ b/src/api/core/two_factor/protected_actions.rs @@ -1,16 +1,17 @@ -use chrono::{naive::serde::ts_seconds, NaiveDateTime, TimeDelta, Utc}; -use rocket::{serde::json::Json, Route}; +use chrono::{NaiveDateTime, TimeDelta, Utc, naive::serde::ts_seconds}; +use rocket::{Route, serde::json::Json}; use crate::{ + CONFIG, api::EmptyResult, auth::Headers, crypto, db::{ - models::{TwoFactor, TwoFactorType, UserId}, DbConn, + models::{TwoFactor, TwoFactorType, UserId}, }, error::{Error, MapResult}, - mail, CONFIG, + mail, }; pub fn routes() -> Vec { @@ -44,9 +45,10 @@ impl ProtectedActionData { pub fn from_json(string: &str) -> Result { let res: Result = serde_json::from_str(string); - match res { - Ok(x) => Ok(x), - Err(_) => err!("Could not decode ProtectedActionData from string"), + if let Ok(x) = res { + Ok(x) + } else { + err!("Could not decode ProtectedActionData from string") } } @@ -62,7 +64,9 @@ impl ProtectedActionData { #[post("/accounts/request-otp")] async fn request_otp(headers: Headers, conn: DbConn) -> EmptyResult { if !CONFIG.mail_enabled() { - err!("Email is disabled for this server. Either enable email or login using your master password instead of login via device."); + err!( + "Email is disabled for this server. Either enable email or login using your master password instead of login via device." + ); } let user = headers.user; @@ -102,7 +106,9 @@ struct ProtectedActionVerify { #[post("/accounts/verify-otp", data = "")] async fn verify_otp(data: Json, headers: Headers, conn: DbConn) -> EmptyResult { if !CONFIG.mail_enabled() { - err!("Email is disabled for this server. Either enable email or login using your master password instead of login via device."); + err!( + "Email is disabled for this server. Either enable email or login using your master password instead of login via device." + ); } let user = headers.user; @@ -133,7 +139,7 @@ pub async fn validate_protected_action_otp( } // Check if the token has expired (Using the email 2fa expiration time) - let max_time = CONFIG.email_expiration_time() as i64; + let max_time = CONFIG.email_expiration_time().cast_signed(); if pa_data.time_since_sent().num_seconds() > max_time { pa.delete(conn).await?; err!("Token has expired") diff --git a/src/api/core/two_factor/webauthn.rs b/src/api/core/two_factor/webauthn.rs index ad17ce36..07b964e5 100644 --- a/src/api/core/two_factor/webauthn.rs +++ b/src/api/core/two_factor/webauthn.rs @@ -1,34 +1,35 @@ -use crate::{ - api::{ - core::{log_user_event, two_factor::_generate_recover_code}, - EmptyResult, JsonResult, PasswordOrOtpData, - }, - auth::Headers, - crypto::ct_eq, - db::{ - models::{EventType, TwoFactor, TwoFactorType, UserId}, - DbConn, - }, - error::Error, - util::NumberOrString, - CONFIG, -}; -use rocket::serde::json::Json; -use rocket::Route; +use std::{str::FromStr, sync::LazyLock, time::Duration}; + +use rocket::{Route, serde::json::Json}; use serde_json::Value; -use std::str::FromStr; -use std::sync::LazyLock; -use std::time::Duration; use url::Url; use uuid::Uuid; -use webauthn_rs::prelude::{Base64UrlSafeData, Credential, Passkey, PasskeyAuthentication, PasskeyRegistration}; -use webauthn_rs::{Webauthn, WebauthnBuilder}; +use webauthn_rs::{ + Webauthn, WebauthnBuilder, + prelude::{Base64UrlSafeData, Credential, Passkey, PasskeyAuthentication, PasskeyRegistration}, +}; use webauthn_rs_proto::{ AuthenticationExtensionsClientOutputs, AuthenticatorAssertionResponseRaw, AuthenticatorAttestationResponseRaw, PublicKeyCredential, RegisterPublicKeyCredential, RegistrationExtensionsClientOutputs, RequestAuthenticationExtensions, UserVerificationPolicy, }; +use crate::{ + CONFIG, + api::{ + EmptyResult, JsonResult, PasswordOrOtpData, + core::{log_user_event, two_factor::generate_recover_code}, + }, + auth::Headers, + crypto::ct_eq, + db::{ + DbConn, + models::{EventType, TwoFactor, TwoFactorType, UserId}, + }, + error::Error, + util::NumberOrString, +}; + static WEBAUTHN: LazyLock = LazyLock::new(|| { let domain = CONFIG.domain(); let domain_origin = CONFIG.domain_origin(); @@ -149,7 +150,7 @@ async fn generate_webauthn_challenge(data: Json, headers: Hea )?; let mut state = serde_json::to_value(&state)?; - state["rs"]["policy"] = Value::String("discouraged".to_string()); + state["rs"]["policy"] = Value::String("discouraged".to_owned()); state["rs"]["extensions"].as_object_mut().unwrap().clear(); let type_ = TwoFactorType::WebauthnRegisterChallenge; @@ -265,13 +266,12 @@ async fn activate_webauthn(data: Json, headers: Headers, con // Retrieve and delete the saved challenge state let type_ = TwoFactorType::WebauthnRegisterChallenge as i32; - let state = match TwoFactor::find_by_user_and_type(&user.uuid, type_, &conn).await { - Some(tf) => { - let state: PasskeyRegistration = serde_json::from_str(&tf.data)?; - tf.delete(&conn).await?; - state - } - None => err!("Can't recover challenge"), + let state = if let Some(tf) = TwoFactor::find_by_user_and_type(&user.uuid, type_, &conn).await { + let state: PasskeyRegistration = serde_json::from_str(&tf.data)?; + tf.delete(&conn).await?; + state + } else { + err!("Can't recover challenge") }; // Verify the credentials with the saved state @@ -291,7 +291,7 @@ async fn activate_webauthn(data: Json, headers: Headers, con TwoFactor::new(user.uuid.clone(), TwoFactorType::Webauthn, serde_json::to_string(®istrations)?) .save(&conn) .await?; - _generate_recover_code(&mut user, &conn).await; + generate_recover_code(&mut user, &conn).await; log_user_event(EventType::UserUpdated2fa as i32, &user.uuid, headers.device.atype, &headers.ip.ip, &conn).await; @@ -342,9 +342,10 @@ async fn delete_webauthn(data: Json, headers: Headers, conn: DbCo // If entry is migrated from u2f, delete the u2f entry as well if let Some(mut u2f) = TwoFactor::find_by_user_and_type(&headers.user.uuid, TwoFactorType::U2f as i32, &conn).await { - let mut data: Vec = match serde_json::from_str(&u2f.data) { - Ok(d) => d, - Err(_) => err!("Error parsing U2F data"), + let mut data: Vec = if let Ok(d) = serde_json::from_str(&u2f.data) { + d + } else { + err!("Error parsing U2F data") }; data.retain(|r| r.reg.key_handle != removed_item.credential.cred_id().as_slice()); @@ -388,10 +389,10 @@ pub async fn generate_webauthn_login(user_id: &UserId, conn: &DbConn) -> JsonRes // Modify to discourage user verification let mut state = serde_json::to_value(&state)?; - state["ast"]["policy"] = Value::String("discouraged".to_string()); + state["ast"]["policy"] = Value::String("discouraged".to_owned()); // Add appid, this is only needed for U2F compatibility, so maybe it can be removed as well - let app_id = format!("{}/app-id.json", &CONFIG.domain()); + let app_id = format!("{}/app-id.json", CONFIG.domain()); state["ast"]["appid"] = Value::String(app_id.clone()); response.public_key.user_verification = UserVerificationPolicy::Discouraged_DO_NOT_USE; @@ -416,18 +417,17 @@ pub async fn generate_webauthn_login(user_id: &UserId, conn: &DbConn) -> JsonRes pub async fn validate_webauthn_login(user_id: &UserId, response: &str, conn: &DbConn) -> EmptyResult { let type_ = TwoFactorType::WebauthnLoginChallenge as i32; - let mut state = match TwoFactor::find_by_user_and_type(user_id, type_, conn).await { - Some(tf) => { - let state: PasskeyAuthentication = serde_json::from_str(&tf.data)?; - tf.delete(conn).await?; - state - } - None => err!( + let mut state = if let Some(tf) = TwoFactor::find_by_user_and_type(user_id, type_, conn).await { + let state: PasskeyAuthentication = serde_json::from_str(&tf.data)?; + tf.delete(conn).await?; + state + } else { + err!( "Can't recover login challenge", ErrorEvent { event: EventType::UserFailedLogIn2fa } - ), + ) }; let rsp: PublicKeyCredentialCopy = serde_json::from_str(response)?; diff --git a/src/api/core/two_factor/yubikey.rs b/src/api/core/two_factor/yubikey.rs index 1cf11255..08e8d269 100644 --- a/src/api/core/two_factor/yubikey.rs +++ b/src/api/core/two_factor/yubikey.rs @@ -1,20 +1,19 @@ -use rocket::serde::json::Json; -use rocket::Route; +use rocket::{Route, serde::json::Json}; use serde_json::Value; use yubico::{config::Config, verify_async}; use crate::{ + CONFIG, api::{ - core::{log_user_event, two_factor::_generate_recover_code}, EmptyResult, JsonResult, PasswordOrOtpData, + core::{log_user_event, two_factor::generate_recover_code}, }, auth::Headers, db::{ - models::{EventType, TwoFactor, TwoFactorType}, DbConn, + models::{EventType, TwoFactor, TwoFactorType}, }, error::{Error, MapResult}, - CONFIG, }; pub fn routes() -> Vec { @@ -46,7 +45,7 @@ pub struct YubikeyMetadata { fn parse_yubikeys(data: &EnableYubikeyData) -> Vec { let data_keys = [&data.key1, &data.key2, &data.key3, &data.key4, &data.key5]; - data_keys.iter().filter_map(|e| e.as_ref().cloned()).collect() + data_keys.into_iter().flatten().cloned().collect() } fn jsonify_yubikeys(yubikeys: Vec) -> Value { @@ -64,9 +63,10 @@ fn get_yubico_credentials() -> Result<(String, String), Error> { err!("Yubico support is disabled"); } - match (CONFIG.yubico_client_id(), CONFIG.yubico_secret_key()) { - (Some(id), Some(secret)) => Ok((id, secret)), - _ => err!("`YUBICO_CLIENT_ID` or `YUBICO_SECRET_KEY` environment variable is not set. Yubikey OTP Disabled"), + if let (Some(id), Some(secret)) = (CONFIG.yubico_client_id(), CONFIG.yubico_secret_key()) { + Ok((id, secret)) + } else { + err!("`YUBICO_CLIENT_ID` or `YUBICO_SECRET_KEY` environment variable is not set. Yubikey OTP Disabled") } } @@ -162,7 +162,7 @@ async fn activate_yubikey(data: Json, headers: Headers, conn: yubikey_data.data = serde_json::to_string(&yubikey_metadata).unwrap(); yubikey_data.save(&conn).await?; - _generate_recover_code(&mut user, &conn).await; + generate_recover_code(&mut user, &conn).await; log_user_event(EventType::UserUpdated2fa as i32, &user.uuid, headers.device.atype, &headers.ip.ip, &conn).await; diff --git a/src/api/icons.rs b/src/api/icons.rs index 5c9ed113..02a14844 100644 --- a/src/api/icons.rs +++ b/src/api/icons.rs @@ -6,28 +6,29 @@ use std::{ }; use bytes::{Bytes, BytesMut}; -use futures::{stream::StreamExt, TryFutureExt}; +use futures::{TryFutureExt, stream::StreamExt}; use html5gum::{Emitter, HtmlString, Readable, StringReader, Tokenizer}; use regex::Regex; use reqwest::{ - header::{self, HeaderMap, HeaderValue}, Client, Response, + header::{self, HeaderMap, HeaderValue}, }; -use rocket::{http::ContentType, response::Redirect, Route}; -use svg_hush::{data_url_filter, Filter}; +use rocket::{Route, http::ContentType, response::Redirect}; +use svg_hush::{Filter, data_url_filter}; use crate::{ + CONFIG, config::PathType, error::Error, - http_client::{get_reqwest_client_builder, get_valid_host, should_block_host, CustomHttpClientError}, + http_client::{CustomHttpClientError, get_reqwest_client_builder, get_valid_host, should_block_host}, util::Cached, - CONFIG, }; pub fn routes() -> Vec { - match CONFIG.icon_service().as_str() { - "internal" => routes![icon_internal], - _ => routes![icon_external], + if CONFIG.icon_service().as_str() == "internal" { + routes![icon_internal] + } else { + routes![icon_external] } } @@ -147,7 +148,7 @@ async fn get_icon(domain: &str) -> Option<(Vec, String)> { if let Some(icon) = get_cached_icon(&path).await { let icon_type = get_icon_type(&icon).unwrap_or("x-icon"); - return Some((icon, icon_type.to_string())); + return Some((icon, icon_type.to_owned())); } if CONFIG.disable_icon_download() { @@ -158,7 +159,7 @@ async fn get_icon(domain: &str) -> Option<(Vec, String)> { match download_icon(domain).await { Ok((icon, icon_type)) => { save_icon(&path, icon.to_vec()).await; - Some((icon.to_vec(), icon_type.unwrap_or("x-icon").to_string())) + Some((icon.to_vec(), icon_type.unwrap_or("x-icon").to_owned())) } Err(e) => { // If this error comes from the custom resolver, this means this is a blocked domain @@ -183,10 +184,10 @@ async fn get_cached_icon(path: &str) -> Option> { } // Try to read the cached icon, and return it if it exists - if let Ok(operator) = CONFIG.opendal_operator_for_path_type(&PathType::IconCache) { - if let Ok(buf) = operator.read(path).await { - return Some(buf.to_vec()); - } + if let Ok(operator) = CONFIG.opendal_operator_for_path_type(&PathType::IconCache) + && let Ok(buf) = operator.read(path).await + { + return Some(buf.to_vec()); } None @@ -280,17 +281,17 @@ fn get_favicons_node(dom: Tokenizer, FaviconEmitter>, icons: &m } for icon_tag in icon_tags { - if let Some(icon_href) = icon_tag.attributes.get(ATTR_HREF) { - if let Ok(full_href) = base_url.join(std::str::from_utf8(icon_href).unwrap_or_default()) { - let sizes = if let Some(v) = icon_tag.attributes.get(ATTR_SIZES) { - std::str::from_utf8(v).unwrap_or_default() - } else { - "" - }; - let priority = get_icon_priority(full_href.as_str(), sizes); - icons.push(Icon::new(priority, full_href.to_string())); - } - }; + if let Some(icon_href) = icon_tag.attributes.get(ATTR_HREF) + && let Ok(full_href) = base_url.join(std::str::from_utf8(icon_href).unwrap_or_default()) + { + let sizes = if let Some(v) = icon_tag.attributes.get(ATTR_SIZES) { + std::str::from_utf8(v).unwrap_or_default() + } else { + "" + }; + let priority = get_icon_priority(full_href.as_str(), sizes); + icons.push(Icon::new(priority, full_href.to_string())); + } } } @@ -406,7 +407,7 @@ async fn get_page(url: &str) -> Result { async fn get_page_with_referer(url: &str, referer: &str) -> Result { let mut client = CLIENT.get(url); if !referer.is_empty() { - client = client.header("Referer", referer) + client = client.header("Referer", referer); } Ok(client.send().await?.error_for_status()?) @@ -494,12 +495,10 @@ async fn download_icon(domain: &str) -> Result<(Bytes, Option<&str>), Error> { let mut buffer = Bytes::new(); let mut icon_type: Option<&str> = None; - use data_url::DataUrl; - let mut icons = icon_result.iconlist.iter().take(5).peekable(); while let Some(icon) = icons.next() { if icon.href.starts_with("data:image") { - let Ok(datauri) = DataUrl::process(&icon.href) else { + let Ok(datauri) = data_url::DataUrl::process(&icon.href) else { continue; }; // Check if we are able to decode the data uri @@ -523,7 +522,7 @@ async fn download_icon(domain: &str) -> Result<(Bytes, Option<&str>), Error> { } } _ => debug!("Extracted icon from data:image uri is invalid"), - }; + } } else { debug!("Trying {}", icon.href); // Make sure all icons are checked before returning error @@ -587,10 +586,10 @@ async fn save_icon(path: &str, icon: Vec) { fn get_icon_type(bytes: &[u8]) -> Option<&'static str> { fn check_svg_after_xml_declaration(bytes: &[u8]) -> Option<&'static str> { // Look for SVG tag within the first 1KB - if let Ok(content) = std::str::from_utf8(&bytes[..bytes.len().min(1024)]) { - if content.contains(" (), @@ -806,13 +805,13 @@ impl Emitter for FaviconEmitter { fn push_attribute_name(&mut self, s: &[u8]) { if let Some(attr) = &mut self.current_attribute { - attr.0.extend(s) + attr.0.extend(s); } } fn push_attribute_value(&mut self, s: &[u8]) { if let Some(attr) = &mut self.current_attribute { - attr.1.extend(s) + attr.1.extend(s); } } diff --git a/src/api/identity.rs b/src/api/identity.rs index 34078529..3962827d 100644 --- a/src/api/identity.rs +++ b/src/api/identity.rs @@ -1,18 +1,20 @@ use chrono::Utc; use num_traits::FromPrimitive; use rocket::{ + Route, form::{Form, FromForm}, http::{Cookie, CookieJar, SameSite}, response::Redirect, serde::json::Json, - Route, }; use serde_json::Value; use crate::{ + CONFIG, api::{ + ApiResult, EmptyResult, JsonResult, core::{ - accounts::{_prelogin, _register, kdf_upgrade, PreloginData, RegisterData}, + accounts::{PreloginData, RegisterData, kdf_upgrade, prelogin, register}, log_user_event, two_factor::{ authenticator, duo, duo_oidc, email, enforce_2fa_policy, is_twofactor_provider_usable, webauthn, @@ -21,29 +23,28 @@ use crate::{ }, master_password_policy, push::register_push_device, - ApiResult, EmptyResult, JsonResult, }, auth, - auth::{generate_organization_api_key_login_claims, AuthMethod, ClientHeaders, ClientIp, ClientVersion, Secure}, + auth::{AuthMethod, ClientHeaders, ClientIp, ClientVersion, Secure, generate_organization_api_key_login_claims}, crypto, db::{ + DbConn, models::{ AuthRequest, AuthRequestId, Device, DeviceId, EventType, Invitation, OIDCCodeResponseError, OrganizationApiKey, OrganizationId, SsoAuth, SsoUser, TwoFactor, TwoFactorIncomplete, TwoFactorType, User, UserId, }, - DbConn, }, error::MapResult, mail, sso, sso::{OIDCCode, OIDCCodeChallenge, OIDCCodeVerifier, OIDCState}, - util, CONFIG, + util, }; pub fn routes() -> Vec { routes![ login, - prelogin, + post_prelogin, prelogin_password, identity_register, register_verification_email, @@ -68,43 +69,43 @@ async fn login( let login_result = match data.grant_type.as_ref() { "refresh_token" => { - _check_is_some(data.refresh_token.as_ref(), "refresh_token cannot be blank")?; - _refresh_login(data, &conn, &client_header.ip).await + check_is_some(data.refresh_token.as_ref(), "refresh_token cannot be blank")?; + refresh_login(data, &conn, &client_header.ip).await } "password" if CONFIG.sso_enabled() && CONFIG.sso_only() => err!("SSO sign-in is required"), "password" => { - _check_is_some(data.client_id.as_ref(), "client_id cannot be blank")?; - _check_is_some(data.password.as_ref(), "password cannot be blank")?; - _check_is_some(data.scope.as_ref(), "scope cannot be blank")?; - _check_is_some(data.username.as_ref(), "username cannot be blank")?; + check_is_some(data.client_id.as_ref(), "client_id cannot be blank")?; + check_is_some(data.password.as_ref(), "password cannot be blank")?; + check_is_some(data.scope.as_ref(), "scope cannot be blank")?; + check_is_some(data.username.as_ref(), "username cannot be blank")?; - _check_is_some(data.device_identifier.as_ref(), "device_identifier cannot be blank")?; - _check_is_some(data.device_name.as_ref(), "device_name cannot be blank")?; - _check_is_some(data.device_type.as_ref(), "device_type cannot be blank")?; + check_is_some(data.device_identifier.as_ref(), "device_identifier cannot be blank")?; + check_is_some(data.device_name.as_ref(), "device_name cannot be blank")?; + check_is_some(data.device_type.as_ref(), "device_type cannot be blank")?; - _password_login(data, &mut user_id, &conn, &client_header.ip, client_version.as_ref()).await + password_login(data, &mut user_id, &conn, &client_header.ip, client_version.as_ref()).await } "client_credentials" => { - _check_is_some(data.client_id.as_ref(), "client_id cannot be blank")?; - _check_is_some(data.client_secret.as_ref(), "client_secret cannot be blank")?; - _check_is_some(data.scope.as_ref(), "scope cannot be blank")?; + check_is_some(data.client_id.as_ref(), "client_id cannot be blank")?; + check_is_some(data.client_secret.as_ref(), "client_secret cannot be blank")?; + check_is_some(data.scope.as_ref(), "scope cannot be blank")?; - _check_is_some(data.device_identifier.as_ref(), "device_identifier cannot be blank")?; - _check_is_some(data.device_name.as_ref(), "device_name cannot be blank")?; - _check_is_some(data.device_type.as_ref(), "device_type cannot be blank")?; + check_is_some(data.device_identifier.as_ref(), "device_identifier cannot be blank")?; + check_is_some(data.device_name.as_ref(), "device_name cannot be blank")?; + check_is_some(data.device_type.as_ref(), "device_type cannot be blank")?; - _api_key_login(data, &mut user_id, &conn, &client_header.ip).await + api_key_login(data, &mut user_id, &conn, &client_header.ip).await } "authorization_code" if CONFIG.sso_enabled() => { - _check_is_some(data.client_id.as_ref(), "client_id cannot be blank")?; - _check_is_some(data.code.as_ref(), "code cannot be blank")?; - _check_is_some(data.code_verifier.as_ref(), "code verifier cannot be blank")?; + check_is_some(data.client_id.as_ref(), "client_id cannot be blank")?; + check_is_some(data.code.as_ref(), "code cannot be blank")?; + check_is_some(data.code_verifier.as_ref(), "code verifier cannot be blank")?; - _check_is_some(data.device_identifier.as_ref(), "device_identifier cannot be blank")?; - _check_is_some(data.device_name.as_ref(), "device_name cannot be blank")?; - _check_is_some(data.device_type.as_ref(), "device_type cannot be blank")?; + check_is_some(data.device_identifier.as_ref(), "device_identifier cannot be blank")?; + check_is_some(data.device_name.as_ref(), "device_name cannot be blank")?; + check_is_some(data.device_type.as_ref(), "device_type cannot be blank")?; - _sso_login(data, &mut user_id, &conn, &client_header.ip, client_version.as_ref()).await + sso_login(data, &mut user_id, &conn, &client_header.ip, client_version.as_ref()).await } "authorization_code" => err!("SSO sign-in is not available"), t => err!("Invalid type", t), @@ -125,7 +126,7 @@ async fn login( Err(e) => { if let Some(ev) = e.get_event() { log_user_event(ev.event as i32, &user_id, client_header.device_type, &client_header.ip.ip, &conn) - .await + .await; } } } @@ -134,7 +135,7 @@ async fn login( login_result } -async fn _refresh_login(data: ConnectData, conn: &DbConn, ip: &ClientIp) -> JsonResult { +async fn refresh_login(data: ConnectData, conn: &DbConn, ip: &ClientIp) -> JsonResult { // When a refresh token is invalid or missing we need to respond with an HTTP BadRequest (400) // It also needs to return a json which holds at least a key `error` with the value `invalid_grant` // See the link below for details @@ -175,7 +176,7 @@ async fn _refresh_login(data: ConnectData, conn: &DbConn, ip: &ClientIp) -> Json } // After exchanging the code we need to check first if 2FA is needed before continuing -async fn _sso_login( +async fn sso_login( data: ConnectData, user_id: &mut Option, conn: &DbConn, @@ -344,7 +345,7 @@ async fn _sso_login( authenticated_response(&user, &mut device, auth_tokens, twofactor_token, conn, ip).await } -async fn _password_login( +async fn password_login( data: ConnectData, user_id: &mut Option, conn: &DbConn, @@ -428,9 +429,9 @@ async fn _password_login( if user.verified_at.is_none() && CONFIG.mail_enabled() && CONFIG.signups_verify() { if user.last_verifying_at.is_none() || now.signed_duration_since(user.last_verifying_at.unwrap()).num_seconds() - > CONFIG.signups_verify_resend_time() as i64 + > CONFIG.signups_verify_resend_time().cast_signed() { - let resend_limit = CONFIG.signups_verify_resend_limit() as i32; + let resend_limit = CONFIG.signups_verify_resend_limit().cast_signed(); if resend_limit == 0 || user.login_verify_count < resend_limit { // We want to send another email verification if we require signups to verify // their email address, and we haven't sent them a reminder in a while... @@ -566,19 +567,19 @@ async fn authenticated_response( Ok(Json(result)) } -async fn _api_key_login(data: ConnectData, user_id: &mut Option, conn: &DbConn, ip: &ClientIp) -> JsonResult { +async fn api_key_login(data: ConnectData, user_id: &mut Option, conn: &DbConn, ip: &ClientIp) -> JsonResult { // Ratelimit the login crate::ratelimit::check_limit_login(&ip.ip)?; // Validate scope match data.scope.as_ref() { - Some(scope) if scope == &AuthMethod::UserApiKey.scope() => _user_api_key_login(data, user_id, conn, ip).await, - Some(scope) if scope == &AuthMethod::OrgApiKey.scope() => _organization_api_key_login(data, conn, ip).await, + Some(scope) if scope == &AuthMethod::UserApiKey.scope() => user_api_key_login(data, user_id, conn, ip).await, + Some(scope) if scope == &AuthMethod::OrgApiKey.scope() => organization_api_key_login(data, conn, ip).await, _ => err!("Scope not supported"), } } -async fn _user_api_key_login( +async fn user_api_key_login( data: ConnectData, user_id: &mut Option, conn: &DbConn, @@ -710,13 +711,13 @@ async fn _user_api_key_login( Ok(Json(result)) } -async fn _organization_api_key_login(data: ConnectData, conn: &DbConn, ip: &ClientIp) -> JsonResult { +async fn organization_api_key_login(data: ConnectData, conn: &DbConn, ip: &ClientIp) -> JsonResult { // Get the org via the client_id let client_id = data.client_id.as_ref().unwrap(); let Some(org_id) = client_id.strip_prefix("organization.") else { err!("Malformed client_id", format!("IP: {}.", ip.ip)) }; - let org_id: OrganizationId = org_id.to_string().into(); + let org_id: OrganizationId = org_id.to_owned().into(); let Some(org_api_key) = OrganizationApiKey::find_by_org_uuid(&org_id, conn).await else { err!("Invalid client_id", format!("IP: {}.", ip.ip)) }; @@ -747,14 +748,13 @@ async fn get_device(data: &ConnectData, conn: &DbConn, user: &User) -> ApiResult let device_name = data.device_name.clone().expect("No device name provided"); // Find device or create new - match Device::find_by_uuid_and_user(&device_id, &user.uuid, conn).await { - Some(device) => Ok(device), - None => { - let mut device = Device::new(device_id, user.uuid.clone(), device_name, device_type); - // save device without updating `device.updated_at` - device.save(false, conn).await?; - Ok(device) - } + if let Some(device) = Device::find_by_uuid_and_user(&device_id, &user.uuid, conn).await { + Ok(device) + } else { + let mut device = Device::new(device_id, user.uuid.clone(), device_name, device_type); + // save device without updating `device.updated_at` + device.save(false, conn).await?; + Ok(device) } } @@ -780,7 +780,7 @@ async fn twofactor_auth( .iter() .filter_map(|tf| { let provider_type = TwoFactorType::from_i32(tf.atype)?; - (tf.enabled && is_twofactor_provider_usable(provider_type, Some(&tf.data))).then_some(tf.atype) + (tf.enabled && is_twofactor_provider_usable(&provider_type, Some(&tf.data))).then_some(tf.atype) }) .collect(); if twofactor_ids.is_empty() { @@ -788,59 +788,51 @@ async fn twofactor_auth( } let selected_id = data.two_factor_provider.unwrap_or(twofactor_ids[0]); // If we aren't given a two factor provider, assume the first one - // Ignore Remember and RecoveryCode Types during this check, these are special + // Ignore Remember and RecoveryCode Types during this check, these are special if ![TwoFactorType::Remember as i32, TwoFactorType::RecoveryCode as i32].contains(&selected_id) && !twofactor_ids.contains(&selected_id) { err_json!( - _json_err_twofactor(&twofactor_ids, &user.uuid, data, client_version, conn).await?, + json_err_twofactor(&twofactor_ids, &user.uuid, data, client_version, conn).await?, "Invalid two factor provider" ) } - let twofactor_code = match data.two_factor_token { - Some(ref code) => code, - None => { - err_json!( - _json_err_twofactor(&twofactor_ids, &user.uuid, data, client_version, conn).await?, - "2FA token not provided" - ) - } + let Some(ref twofactor_code) = data.two_factor_token else { + err_json!( + json_err_twofactor(&twofactor_ids, &user.uuid, data, client_version, conn).await?, + "2FA token not provided" + ) }; let selected_twofactor = twofactors.into_iter().find(|tf| tf.atype == selected_id && tf.enabled); - use crate::crypto::ct_eq; - - let selected_data = _selected_data(selected_twofactor); + let selected_data = selected_data(selected_twofactor); match TwoFactorType::from_i32(selected_id) { Some(TwoFactorType::Authenticator) => { - authenticator::validate_totp_code_str(&user.uuid, twofactor_code, &selected_data?, ip, conn).await? + authenticator::validate_totp_code_str(&user.uuid, twofactor_code, &selected_data?, ip, conn).await?; } Some(TwoFactorType::Webauthn) => webauthn::validate_webauthn_login(&user.uuid, twofactor_code, conn).await?, Some(TwoFactorType::YubiKey) => yubikey::validate_yubikey_login(twofactor_code, &selected_data?).await?, Some(TwoFactorType::Duo) => { - match CONFIG.duo_use_iframe() { - true => { - // Legacy iframe prompt flow - duo::validate_duo_login(&user.email, twofactor_code, conn).await? - } - false => { - // OIDC based flow - duo_oidc::validate_duo_login( - &user.email, - twofactor_code, - data.client_id.as_ref().unwrap(), - data.device_identifier.as_ref().unwrap(), - conn, - ) - .await? - } + if CONFIG.duo_use_iframe() { + // Legacy iframe prompt flow + duo::validate_duo_login(&user.email, twofactor_code, conn).await?; + } else { + // OIDC based flow + duo_oidc::validate_duo_login( + &user.email, + twofactor_code, + data.client_id.as_ref().unwrap(), + data.device_identifier.as_ref().unwrap(), + conn, + ) + .await?; } } Some(TwoFactorType::Email) => { - email::validate_email_code_str(&user.uuid, twofactor_code, &selected_data?, &ip.ip, conn).await? + email::validate_email_code_str(&user.uuid, twofactor_code, &selected_data?, &ip.ip, conn).await?; } Some(TwoFactorType::Remember) => { match device.twofactor_remember { @@ -848,7 +840,7 @@ async fn twofactor_auth( // If it is invalid we need to trigger the 2FA Login prompt Some(ref token) if !CONFIG.disable_2fa_remember() - && (ct_eq(token, twofactor_code) + && (crypto::ct_eq(token, twofactor_code) && auth::decode_2fa_remember(twofactor_code) .is_ok_and(|t| t.sub == device.uuid && t.user_uuid == user.uuid)) => {} _ => { @@ -859,7 +851,7 @@ async fn twofactor_auth( device.save(true, conn).await?; } err_json!( - _json_err_twofactor(&twofactor_ids, &user.uuid, data, client_version, conn).await?, + json_err_twofactor(&twofactor_ids, &user.uuid, data, client_version, conn).await?, "2FA Remember token not provided or expired" ) } @@ -900,11 +892,11 @@ async fn twofactor_auth( Ok(two_factor) } -fn _selected_data(tf: Option) -> ApiResult { +fn selected_data(tf: Option) -> ApiResult { tf.map(|t| t.data).map_res("Two factor doesn't exist") } -async fn _json_err_twofactor( +async fn json_err_twofactor( providers: &[i32], user_id: &UserId, data: &ConnectData, @@ -925,42 +917,38 @@ async fn _json_err_twofactor( result["TwoFactorProviders2"][provider.to_string()] = Value::Null; match TwoFactorType::from_i32(*provider) { - Some(TwoFactorType::Authenticator) => { /* Nothing to do for TOTP */ } - Some(TwoFactorType::Webauthn) if CONFIG.is_webauthn_2fa_supported() => { let request = webauthn::generate_webauthn_login(user_id, conn).await?; result["TwoFactorProviders2"][provider.to_string()] = request.0; } Some(TwoFactorType::Duo) => { - let email = match User::find_by_uuid(user_id, conn).await { - Some(u) => u.email, - None => err!("User does not exist"), + let email = if let Some(u) = User::find_by_uuid(user_id, conn).await { + u.email + } else { + err!("User does not exist") }; - match CONFIG.duo_use_iframe() { - true => { - // Legacy iframe prompt flow - let (signature, host) = duo::generate_duo_signature(&email, conn).await?; - result["TwoFactorProviders2"][provider.to_string()] = json!({ - "Host": host, - "Signature": signature, - }) - } - false => { - // OIDC based flow - let auth_url = duo_oidc::get_duo_auth_url( - &email, - data.client_id.as_ref().unwrap(), - data.device_identifier.as_ref().unwrap(), - conn, - ) - .await?; + if CONFIG.duo_use_iframe() { + // Legacy iframe prompt flow + let (signature, host) = duo::generate_duo_signature(&email, conn).await?; + result["TwoFactorProviders2"][provider.to_string()] = json!({ + "Host": host, + "Signature": signature, + }); + } else { + // OIDC based flow + let auth_url = duo_oidc::get_duo_auth_url( + &email, + data.client_id.as_ref().unwrap(), + data.device_identifier.as_ref().unwrap(), + conn, + ) + .await?; - result["TwoFactorProviders2"][provider.to_string()] = json!({ - "AuthUrl": auth_url, - }) - } + result["TwoFactorProviders2"][provider.to_string()] = json!({ + "AuthUrl": auth_url, + }); } } @@ -973,7 +961,7 @@ async fn _json_err_twofactor( result["TwoFactorProviders2"][provider.to_string()] = json!({ "Nfc": yubikey_metadata.nfc, - }) + }); } Some(tf_type @ TwoFactorType::Email) => { @@ -991,16 +979,30 @@ async fn _json_err_twofactor( // Send email immediately if email is the only 2FA option. if providers.len() == 1 && !disabled_send { - email::send_token(user_id, conn).await? + email::send_token(user_id, conn).await?; } let email_data = email::EmailTokenData::from_json(&twofactor.data)?; result["TwoFactorProviders2"][provider.to_string()] = json!({ "Email": email::obscure_email(&email_data.email), - }) + }); } - _ => {} + None + | Some( + TwoFactorType::Authenticator + | TwoFactorType::EmailVerificationChallenge + | TwoFactorType::OrganizationDuo + | TwoFactorType::ProtectedActions + | TwoFactorType::RecoveryCode + | TwoFactorType::Remember + | TwoFactorType::U2f + | TwoFactorType::U2fLoginChallenge + | TwoFactorType::U2fRegisterChallenge + | TwoFactorType::Webauthn + | TwoFactorType::WebauthnLoginChallenge + | TwoFactorType::WebauthnRegisterChallenge, + ) => { /* Nothing special to do for these providers */ } } } @@ -1008,18 +1010,18 @@ async fn _json_err_twofactor( } #[post("/accounts/prelogin", data = "")] -async fn prelogin(data: Json, conn: DbConn) -> Json { - _prelogin(data, conn).await +async fn post_prelogin(data: Json, conn: DbConn) -> Json { + prelogin(data, conn).await } #[post("/accounts/prelogin/password", data = "")] async fn prelogin_password(data: Json, conn: DbConn) -> Json { - _prelogin(data, conn).await + prelogin(data, conn).await } #[post("/accounts/register", data = "")] async fn identity_register(data: Json, conn: DbConn) -> JsonResult { - _register(data, false, conn).await + register(data, false, conn).await } #[derive(Debug, Deserialize)] @@ -1058,13 +1060,13 @@ async fn register_verification_email( if should_send_mail { let user = User::find_by_mail(&data.email, &conn).await; - if user.filter(|u| u.private_key.is_some()).is_some() { + if user.as_ref().is_some_and(|u| u.private_key.is_some()) { // There is still a timing side channel here in that the code // paths that send mail take noticeably longer than ones that don't. // Add a randomized sleep to mitigate this somewhat. - use rand::{rngs::SmallRng, RngExt}; + use rand::{RngExt, rngs::SmallRng}; let mut rng: SmallRng = rand::make_rng(); - let sleep_ms = rng.random_range(900..=1100) as u64; + let sleep_ms: u64 = rng.random_range(900..=1100); tokio::time::sleep(tokio::time::Duration::from_millis(sleep_ms)).await; } else { mail::send_register_verify_email(&data.email, &token).await?; @@ -1080,7 +1082,7 @@ async fn register_verification_email( #[post("/accounts/register/finish", data = "")] async fn register_finish(data: Json, conn: DbConn) -> JsonResult { - _register(data, true, conn).await + register(data, true, conn).await } // https://github.com/bitwarden/jslib/blob/master/common/src/models/request/tokenRequest.ts @@ -1143,7 +1145,7 @@ struct ConnectData { #[field(name = uncased("code_verifier"))] code_verifier: Option, } -fn _check_is_some(value: Option<&T>, msg: &str) -> EmptyResult { +fn check_is_some(value: Option<&T>, msg: &str) -> EmptyResult { if value.is_none() { err!(msg) } @@ -1166,7 +1168,7 @@ const SSO_BINDING_COOKIE: &str = "VW_SSO_BINDING"; #[get("/connect/oidc-signin?&", rank = 1)] async fn oidcsignin(code: OIDCCode, state: String, cookies: &CookieJar<'_>, mut conn: DbConn) -> ApiResult { - _oidcsignin_redirect(state, code, None, cookies, &mut conn).await + oidcsignin_redirect(state, code, None, cookies, &mut conn).await } // Bitwarden client appear to only care for code and state @@ -1180,7 +1182,7 @@ async fn oidcsignin_error( cookies: &CookieJar<'_>, mut conn: DbConn, ) -> ApiResult { - _oidcsignin_redirect( + oidcsignin_redirect( state.clone(), state.into(), Some(OIDCCodeResponseError { @@ -1195,7 +1197,8 @@ async fn oidcsignin_error( // The state was encoded using Base64 to ensure no issue with providers. // iss and scope parameters are needed for redirection to work on IOS. -async fn _oidcsignin_redirect( +// We pass the state as the code to get it back later on. +async fn oidcsignin_redirect( base64_state: String, code: OIDCCode, error: Option, @@ -1204,14 +1207,13 @@ async fn _oidcsignin_redirect( ) -> ApiResult { let state = sso::decode_state(&base64_state)?; - let mut sso_auth = match SsoAuth::find(&state, conn).await { - None => err!(format!("Cannot retrieve sso_auth for {state}")), - Some(sso_auth) => sso_auth, + let Some(mut sso_auth) = SsoAuth::find(&state, conn).await else { + err!(format!("Cannot retrieve sso_auth for {state}")) }; // Browser-binding check // The cookie was set on /connect/authorize and must come from the same browser that initiated the flow. - let cookie_value = cookies.get(SSO_BINDING_COOKIE).map(|c| c.value().to_string()); + let cookie_value = cookies.get(SSO_BINDING_COOKIE).map(|c| c.value().to_owned()); let provided_hash = cookie_value.as_deref().map(|v| crypto::sha256_hex(v.as_bytes())); match (sso_auth.binding_hash.as_deref(), provided_hash.as_deref()) { (Some(expected), Some(actual)) if crypto::ct_eq(expected, actual) => {} diff --git a/src/api/mod.rs b/src/api/mod.rs index ecdf9408..05c4215d 100644 --- a/src/api/mod.rs +++ b/src/api/mod.rs @@ -32,11 +32,13 @@ pub use crate::api::{ web::routes as web_routes, web::static_files, }; -use crate::db::{ - models::{OrgPolicy, OrgPolicyType, User}, - DbConn, +use crate::{ + CONFIG, + db::{ + DbConn, + models::{OrgPolicy, OrgPolicyType, User}, + }, }; -use crate::CONFIG; // Type aliases for API methods results pub type ApiResult = Result; @@ -74,6 +76,7 @@ impl PasswordOrOtpData { } } +#[expect(clippy::struct_excessive_bools, reason = "Bitwarden clients expect the data in this specific format")] #[derive(Debug, Default, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct MasterPasswordPolicy { diff --git a/src/api/notifications.rs b/src/api/notifications.rs index b1d64472..80067433 100644 --- a/src/api/notifications.rs +++ b/src/api/notifications.rs @@ -6,17 +6,22 @@ use std::{ use chrono::{NaiveDateTime, Utc}; use rmpv::Value; -use rocket::{futures::StreamExt, Route}; +use rocket::{Route, futures::StreamExt}; use rocket_ws::{Message, WebSocket}; use tokio::sync::mpsc::Sender; use crate::{ + CONFIG, Error, auth::{ClientIp, WsAccessTokenHeader}, db::{ - models::{AuthRequestId, Cipher, CollectionId, Device, DeviceId, Folder, PushId, Send as DbSend, User, UserId}, DbConn, + models::{AuthRequestId, Cipher, CollectionId, Device, DeviceId, Folder, PushId, Send as DbSend, User, UserId}, }, - Error, CONFIG, +}; + +use super::{ + push::push_auth_request, push::push_auth_response, push_cipher_update, push_folder_update, push_logout, + push_send_update, push_user_update, }; pub static WS_USERS: LazyLock> = LazyLock::new(|| { @@ -31,11 +36,6 @@ pub static WS_ANONYMOUS_SUBSCRIPTIONS: LazyLock = LazyLock::new(|| !CONFIG.enable_websocket() && !CONFIG.push_enabled()); pub fn routes() -> Vec { @@ -102,7 +102,7 @@ impl Drop for WSAnonymousEntryMapGuard { } } -#[allow(tail_expr_drop_order)] +#[expect(tail_expr_drop_order)] #[get("/hub?")] fn websockets_hub<'r>( ws: WebSocket, @@ -186,7 +186,7 @@ fn websockets_hub<'r>( }) } -#[allow(tail_expr_drop_order)] +#[expect(tail_expr_drop_order)] #[get("/anonymous-hub?")] fn anonymous_websockets_hub<'r>(ws: WebSocket, token: String, ip: ClientIp) -> Result { info!("Accepting Anonymous Rocket WS connection from {}", ip.ip); @@ -268,14 +268,15 @@ fn serialize(val: &Value) -> Vec { let mut len_buf: Vec = Vec::new(); loop { - let mut size_part = size & 0x7f; + #[expect(clippy::cast_possible_truncation, reason = "masked to 7 bits, fits u8")] + let mut size_part = (size & 0x7f) as u8; size >>= 7; if size > 0 { size_part |= 0x80; } - len_buf.push(size_part as u8); + len_buf.push(size_part); if size == 0 { break; @@ -329,7 +330,7 @@ pub struct WebSocketUsers { impl WebSocketUsers { async fn send_update(&self, user_id: &UserId, data: &[u8]) { if let Some(user) = self.map.get(user_id.as_ref()).map(|v| v.clone()) { - for (_, sender) in user.iter() { + for (_, sender) in &user { if let Err(e) = sender.send(Message::binary(data)).await { error!("Error sending WS update {e}"); } @@ -538,10 +539,10 @@ pub struct AnonymousWebSocketSubscriptions { impl AnonymousWebSocketSubscriptions { async fn send_update(&self, token: &str, data: &[u8]) { - if let Some(sender) = self.map.get(token).map(|v| v.clone()) { - if let Err(e) = sender.send(Message::binary(data)).await { - error!("Error sending WS update {e}"); - } + if let Some(sender) = self.map.get(token).map(|v| v.clone()) + && let Err(e) = sender.send(Message::binary(data)).await + { + error!("Error sending WS update {e}"); } } @@ -582,7 +583,7 @@ fn create_update(payload: Vec<(Value, Value)>, ut: UpdateType, acting_device_id: V::Nil, "ReceiveMessage".into(), V::Array(vec![V::Map(vec![ - ("ContextId".into(), acting_device_id.map(|v| v.to_string().into()).unwrap_or_else(|| V::Nil)), + ("ContextId".into(), acting_device_id.map_or(V::Nil, |v| v.to_string().into())), ("Type".into(), (ut as i32).into()), ("Payload".into(), payload.into()), ])]), diff --git a/src/api/push.rs b/src/api/push.rs index e3ff1383..e87a0985 100644 --- a/src/api/push.rs +++ b/src/api/push.rs @@ -4,21 +4,21 @@ use std::{ }; use reqwest::{ - header::{ACCEPT, AUTHORIZATION, CONTENT_TYPE}, Method, + header::{ACCEPT, AUTHORIZATION, CONTENT_TYPE}, }; use serde_json::Value; use tokio::sync::RwLock; use crate::{ + CONFIG, api::{ApiResult, EmptyResult, UpdateType}, db::{ - models::{AuthRequestId, Cipher, Device, Folder, PushId, Send, User, UserId}, DbConn, + models::{AuthRequestId, Cipher, Device, Folder, PushId, Send, User, UserId}, }, http_client::make_http_request, util::{format_date, get_uuid}, - CONFIG, }; #[derive(Deserialize)] @@ -74,9 +74,9 @@ async fn get_auth_api_token() -> ApiResult { }; let mut api_token = API_TOKEN.write().await; - api_token.valid_until = Instant::now() - .checked_add(Duration::new((json_pushtoken.expires_in / 2) as u64, 0)) // Token valid for half the specified time - .unwrap(); + // Token valid for half the specified time + let half_expires_in = u64::from((json_pushtoken.expires_in / 2).max(0).cast_unsigned()); + api_token.valid_until = Instant::now().checked_add(Duration::from_secs(half_expires_in)).unwrap(); api_token.access_token = json_pushtoken.access_token; @@ -161,7 +161,7 @@ pub async fn push_cipher_update(ut: UpdateType, cipher: &Cipher, device: &Device // We shouldn't send a push notification on cipher update if the cipher belongs to an organization, this isn't implemented in the upstream server too. if cipher.organization_uuid.is_some() { return; - }; + } let Some(user_id) = &cipher.user_uuid else { debug!("Cipher has no uuid"); return; @@ -244,23 +244,23 @@ pub async fn push_folder_update(ut: UpdateType, folder: &Folder, device: &Device } pub async fn push_send_update(ut: UpdateType, send: &Send, device: &Device, conn: &DbConn) { - if let Some(s) = &send.user_uuid { - if Device::check_user_has_push_device(s, conn).await { - tokio::task::spawn(send_to_push_relay(json!({ + if let Some(s) = &send.user_uuid + && Device::check_user_has_push_device(s, conn).await + { + tokio::task::spawn(send_to_push_relay(json!({ + "userId": send.user_uuid, + "organizationId": null, + "deviceId": device.push_uuid, // Should be the records unique uuid of the acting device (unique uuid per user/device) + "identifier": device.uuid, // Should be the acting device id (aka uuid per device/app) + "type": ut as i32, + "payload": { + "id": send.uuid, "userId": send.user_uuid, - "organizationId": null, - "deviceId": device.push_uuid, // Should be the records unique uuid of the acting device (unique uuid per user/device) - "identifier": device.uuid, // Should be the acting device id (aka uuid per device/app) - "type": ut as i32, - "payload": { - "id": send.uuid, - "userId": send.user_uuid, - "revisionDate": format_date(&send.revision_date) - }, - "clientType": null, - "installationId": null - }))); - } + "revisionDate": format_date(&send.revision_date) + }, + "clientType": null, + "installationId": null + }))); } } @@ -296,7 +296,7 @@ async fn send_to_push_relay(notification_data: Value) { .await { error!("An error occurred while sending a send update to the push relay: {e}"); - }; + } } pub async fn push_auth_request(user_id: &UserId, auth_request_id: &str, device: &Device, conn: &DbConn) { diff --git a/src/api/web.rs b/src/api/web.rs index 0ae9c7db..8c628f96 100644 --- a/src/api/web.rs +++ b/src/api/web.rs @@ -1,21 +1,24 @@ use std::path::{Path, PathBuf}; use rocket::{ + Catcher, Route, fs::NamedFile, http::ContentType, - response::{content::RawCss as Css, content::RawHtml as Html, Redirect}, + response::{Redirect, content::RawCss as Css, content::RawHtml as Html}, serde::json::Json, - Catcher, Route, }; use serde_json::Value; use crate::{ - api::{core::now, ApiResult, EmptyResult}, + CONFIG, + api::{ApiResult, EmptyResult, core::now}, auth::decode_file_download, - db::models::{AttachmentId, CipherId}, + db::{ + DbConn, + models::{AttachmentId, CipherId}, + }, error::Error, util::Cached, - CONFIG, }; pub fn routes() -> Vec { @@ -28,7 +31,7 @@ pub fn routes() -> Vec { #[cfg(debug_assertions)] if CONFIG.reload_templates() { - routes.append(&mut routes![_static_files_dev]); + routes.append(&mut routes![static_files_dev]); } routes @@ -178,7 +181,6 @@ async fn attachments(cipher_id: CipherId, file_id: AttachmentId, token: String) } // We use DbConn here to let the alive healthcheck also verify the database connection. -use crate::db::DbConn; #[get("/alive")] fn alive(_conn: DbConn) -> Json { now() @@ -197,7 +199,7 @@ fn alive_head(_conn: DbConn) -> EmptyResult { // NOTE: Do not forget to add any new files added to the `static_files` function below! #[cfg(debug_assertions)] #[get("/vw_static/", rank = 1)] -pub async fn _static_files_dev(filename: PathBuf) -> Option { +pub async fn static_files_dev(filename: PathBuf) -> Option { warn!("LOADING STATIC FILES FROM DISK"); let file = filename.to_str().unwrap_or_default(); let ext = filename.extension().unwrap_or_default(); @@ -210,7 +212,7 @@ pub async fn _static_files_dev(filename: PathBuf) -> Option { if let Ok(path) = path { return NamedFile::open(path).await.ok(); - }; + } None } diff --git a/src/auth.rs b/src/auth.rs index 06bd9c22..2ad95036 100644 --- a/src/auth.rs +++ b/src/auth.rs @@ -5,21 +5,30 @@ use std::{ }; use chrono::{DateTime, TimeDelta, Utc}; -use jsonwebtoken::{errors::ErrorKind, Algorithm, DecodingKey, EncodingKey, Header}; +use jsonwebtoken::{Algorithm, DecodingKey, EncodingKey, Header, errors::ErrorKind}; use num_traits::FromPrimitive; use openssl::rsa::Rsa; -use serde::de::DeserializeOwned; -use serde::ser::Serialize; +use serde::{de::DeserializeOwned, ser::Serialize}; + +use rocket::{ + outcome::try_outcome, + request::{FromRequest, Outcome, Request}, +}; use crate::{ + CONFIG, api::ApiResult, config::PathType, - db::models::{ - AttachmentId, CipherId, CollectionId, DeviceId, DeviceType, EmergencyAccessId, MembershipId, OrgApiKeyId, - OrganizationId, SendFileId, SendId, UserId, + db::{ + DbConn, + models::{ + AttachmentId, CipherId, Collection, CollectionId, Device, DeviceId, DeviceType, EmergencyAccessId, + Membership, MembershipId, MembershipStatus, MembershipType, OrgApiKeyId, OrganizationId, SendFileId, + SendId, User, UserId, UserStampException, + }, }, error::Error, - sso, CONFIG, + sso, }; const JWT_ALGORITHM: Algorithm = Algorithm::RS256; @@ -52,12 +61,12 @@ static PRIVATE_RSA_KEY: OnceLock = OnceLock::new(); static PUBLIC_RSA_KEY: OnceLock = OnceLock::new(); pub async fn initialize_keys() -> Result<(), Error> { - use std::io::Error; + use std::io::Error as IoError; let rsa_key_filename = crate::storage::file_name(&CONFIG.private_rsa_key()) - .ok_or_else(|| Error::other("Private RSA key path missing filename"))?; + .ok_or_else(|| IoError::other("Private RSA key path missing filename"))?; - let operator = CONFIG.opendal_operator_for_path_type(&PathType::RsaKey).map_err(Error::other)?; + let operator = CONFIG.opendal_operator_for_path_type(&PathType::RsaKey).map_err(IoError::other)?; let priv_key_buffer = match operator.read(&rsa_key_filename).await { Ok(buffer) => Some(buffer), @@ -226,7 +235,7 @@ impl LoginJwtClaims { // let orgmanager: Vec<_> = orgs.iter().filter(|o| o.atype == 3).map(|o| o.org_uuid.clone()).collect(); if exp <= (now + *BW_EXPIRATION).timestamp() { - warn!("Raise access_token lifetime to more than 5min.") + warn!("Raise access_token lifetime to more than 5min."); } // Create the JWT claims struct, to send to the client @@ -253,7 +262,7 @@ impl LoginJwtClaims { sstamp: user.security_stamp.clone(), device: device.uuid.clone(), devicetype: DeviceType::from_i32(device.atype).to_string(), - client_id: client_id.unwrap_or("undefined".to_string()), + client_id: client_id.unwrap_or("undefined".to_owned()), scope, amr: vec!["Application".into()], } @@ -506,7 +515,7 @@ pub fn generate_admin_claims() -> BasicJwtClaims { nbf: time_now.timestamp(), exp: (time_now + TimeDelta::try_minutes(CONFIG.admin_session_lifetime()).unwrap()).timestamp(), iss: JWT_ADMIN_ISSUER.to_string(), - sub: "admin_panel".to_string(), + sub: "admin_panel".to_owned(), } } @@ -523,16 +532,6 @@ pub fn generate_send_claims(send_id: &SendId, file_id: &SendFileId) -> BasicJwtC // // Bearer token authentication // -use rocket::{ - outcome::try_outcome, - request::{FromRequest, Outcome, Request}, -}; - -use crate::db::{ - models::{Collection, Device, Membership, MembershipStatus, MembershipType, User, UserStampException}, - DbConn, -}; - pub struct Host { pub host: String, } @@ -548,7 +547,7 @@ impl<'r> FromRequest<'r> for Host { let host = if CONFIG.domain_set() { CONFIG.domain() } else if let Some(referer) = headers.get_one("Referer") { - referer.to_string() + referer.to_owned() } else { // Try to guess from the headers let protocol = if let Some(proto) = headers.get_one("X-Forwarded-Proto") { @@ -584,13 +583,15 @@ impl<'r> FromRequest<'r> for ClientHeaders { type Error = &'static str; async fn from_request(request: &'r Request<'_>) -> Outcome { - let ip = match ClientIp::from_request(request).await { - Outcome::Success(ip) => ip, - _ => err_handler!("Error getting Client IP"), + let Outcome::Success(ip) = ClientIp::from_request(request).await else { + err_handler!("Error getting Client IP") }; - // When unknown or unable to parse, return 14, which is 'Unknown Browser' - let device_type: i32 = - request.headers().get_one("device-type").map(|d| d.parse().unwrap_or(14)).unwrap_or_else(|| 14); + // When unknown or unable to parse, return 'UnknownBrowser' + let device_type: i32 = request + .headers() + .get_one("device-type") + .and_then(|d| d.parse().ok()) + .unwrap_or(DeviceType::UnknownBrowser as i32); Outcome::Success(ClientHeaders { device_type, @@ -614,18 +615,19 @@ impl<'r> FromRequest<'r> for Headers { let headers = request.headers(); let host = try_outcome!(Host::from_request(request).await).host; - let ip = match ClientIp::from_request(request).await { - Outcome::Success(ip) => ip, - _ => err_handler!("Error getting Client IP"), + let Outcome::Success(ip) = ClientIp::from_request(request).await else { + err_handler!("Error getting Client IP") }; // Get access_token - let access_token: &str = match headers.get_one("Authorization") { - Some(a) => match a.rsplit("Bearer ").next() { - Some(split) => split, - None => err_handler!("No access token provided"), - }, - None => err_handler!("No access token provided"), + let access_token: &str = if let Some(a) = headers.get_one("Authorization") { + if let Some(split) = a.rsplit("Bearer ").next() { + split + } else { + err_handler!("No access token provided") + } + } else { + err_handler!("No access token provided") }; // Check JWT token is valid and get device and user from it @@ -636,9 +638,8 @@ impl<'r> FromRequest<'r> for Headers { let device_id = claims.device; let user_id = claims.sub; - let conn = match DbConn::from_request(request).await { - Outcome::Success(conn) => conn, - _ => err_handler!("Error getting DB"), + let Outcome::Success(conn) = DbConn::from_request(request).await else { + err_handler!("Error getting DB") }; let Some(device) = Device::find_by_uuid_and_user(&device_id, &user_id, &conn).await else { @@ -669,7 +670,7 @@ impl<'r> FromRequest<'r> for Headers { error!("Error updating user: {e:#?}"); } err_handler!("Stamp exception is expired") - } else if !stamp_exception.routes.contains(¤t_route.to_string()) { + } else if !stamp_exception.routes.contains(¤t_route.to_owned()) { err_handler!("Invalid security stamp: Current route and exception route do not match") } else if stamp_exception.security_stamp != claims.sstamp { err_handler!("Invalid security stamp for matched stamp exception") @@ -757,9 +758,8 @@ impl<'r> FromRequest<'r> for OrgHeaders { match url_org_id { Some(org_id) if uuid::Uuid::parse_str(&org_id).is_ok() => { - let conn = match DbConn::from_request(request).await { - Outcome::Success(conn) => conn, - _ => err_handler!("Error getting DB"), + let Outcome::Success(conn) = DbConn::from_request(request).await else { + err_handler!("Error getting DB") }; let user = headers.user; @@ -831,16 +831,16 @@ impl<'r> FromRequest<'r> for AdminHeaders { // but there could be cases where it is a query value. // First check the path, if this is not a valid uuid, try the query values. fn get_col_id(request: &Request<'_>) -> Option { - if let Some(Ok(col_id)) = request.param::(3) { - if uuid::Uuid::parse_str(&col_id).is_ok() { - return Some(col_id.into()); - } + if let Some(Ok(col_id)) = request.param::(3) + && uuid::Uuid::parse_str(&col_id).is_ok() + { + return Some(col_id.into()); } - if let Some(Ok(col_id)) = request.query_value::("collectionId") { - if uuid::Uuid::parse_str(&col_id).is_ok() { - return Some(col_id.into()); - } + if let Some(Ok(col_id)) = request.query_value::("collectionId") + && uuid::Uuid::parse_str(&col_id).is_ok() + { + return Some(col_id.into()); } None @@ -864,18 +864,16 @@ impl<'r> FromRequest<'r> for ManagerHeaders { async fn from_request(request: &'r Request<'_>) -> Outcome { let headers = try_outcome!(OrgHeaders::from_request(request).await); if headers.is_confirmed_and_manager() { - match get_col_id(request) { - Some(col_id) => { - let conn = match DbConn::from_request(request).await { - Outcome::Success(conn) => conn, - _ => err_handler!("Error getting DB"), - }; + if let Some(col_id) = get_col_id(request) { + let Outcome::Success(conn) = DbConn::from_request(request).await else { + err_handler!("Error getting DB") + }; - if !Collection::is_coll_manageable_by_user(&col_id, &headers.membership.user_uuid, &conn).await { - err_handler!("The current user isn't a manager for this collection") - } + if !Collection::is_coll_manageable_by_user(&col_id, &headers.membership.user_uuid, &conn).await { + err_handler!("The current user isn't a manager for this collection") } - _ => err_handler!("Error getting the collection id"), + } else { + err_handler!("Error getting the collection id") } Outcome::Success(Self { @@ -1036,7 +1034,7 @@ impl From for Headers { // // Client IP address detection // - +#[derive(Copy, Clone)] pub struct ClientIp { pub ip: IpAddr, } @@ -1068,6 +1066,7 @@ impl<'r> FromRequest<'r> for ClientIp { } } +#[derive(Copy, Clone)] pub struct Secure { pub https: bool, } @@ -1153,15 +1152,14 @@ pub enum AuthMethod { impl AuthMethod { pub fn scope(&self) -> String { match self { - AuthMethod::OrgApiKey => "api.organization".to_string(), - AuthMethod::Password => "api offline_access".to_string(), - AuthMethod::Sso => "api offline_access".to_string(), - AuthMethod::UserApiKey => "api".to_string(), + AuthMethod::OrgApiKey => "api.organization".to_owned(), + AuthMethod::UserApiKey => "api".to_owned(), + AuthMethod::Password | AuthMethod::Sso => "api offline_access".to_owned(), } } pub fn scope_vec(&self) -> Vec { - self.scope().split_whitespace().map(str::to_string).collect() + self.scope().split_whitespace().map(str::to_owned).collect() } pub fn check_scope(&self, scope: Option<&String>) -> ApiResult { @@ -1274,17 +1272,15 @@ pub async fn refresh_tokens( }; // Get device by refresh token - let mut device = match Device::find_by_refresh_token(&refresh_claims.device_token, conn).await { - None => err!("Invalid refresh token"), - Some(device) => device, + let Some(mut device) = Device::find_by_refresh_token(&refresh_claims.device_token, conn).await else { + err!("Invalid refresh token") }; // Save to update `updated_at`. device.save(true, conn).await?; - let user = match User::find_by_uuid(&device.user_uuid, conn).await { - None => err!("Impossible to find user"), - Some(user) => user, + let Some(user) = User::find_by_uuid(&device.user_uuid, conn).await else { + err!("Impossible to find user") }; let auth_tokens = match refresh_claims.sub { diff --git a/src/config.rs b/src/config.rs index 770fb553..5c826fe9 100644 --- a/src/config.rs +++ b/src/config.rs @@ -3,8 +3,8 @@ use std::{ fmt, process::exit, sync::{ - atomic::{AtomicBool, Ordering}, LazyLock, RwLock, + atomic::{AtomicBool, Ordering}, }, }; @@ -16,8 +16,8 @@ use crate::{ error::Error, storage, util::{ - get_active_web_release, get_env, get_env_bool, is_valid_email, parse_experimental_client_feature_flags, - FeatureFlagFilter, + FeatureFlagFilter, get_active_web_release, get_env, get_env_bool, is_valid_email, + parse_experimental_client_feature_flags, }, }; @@ -27,10 +27,10 @@ static CONFIG_FILE: LazyLock = LazyLock::new(|| { }); static CONFIG_FILE_PARENT_DIR: LazyLock = - LazyLock::new(|| storage::parent(&CONFIG_FILE).unwrap_or_else(|| "data".to_string())); + LazyLock::new(|| storage::parent(&CONFIG_FILE).unwrap_or_else(|| "data".to_owned())); static CONFIG_FILENAME: LazyLock = - LazyLock::new(|| storage::file_name(&CONFIG_FILE).unwrap_or_else(|| "config.json".to_string())); + LazyLock::new(|| storage::file_name(&CONFIG_FILE).unwrap_or_else(|| "config.json".to_owned())); pub static SKIP_CONFIG_VALIDATION: AtomicBool = AtomicBool::new(false); @@ -360,13 +360,7 @@ macro_rules! make_config { )+)+ pub fn prepare_json(&self) -> serde_json::Value { - let (def, cfg, overridden) = { - // Lock the inner as short as possible and clone what is needed to prevent deadlocks - let inner = &self.inner.read().unwrap(); - (inner._env.build(), inner.config.clone(), inner._overrides.clone()) - }; - - fn _get_form_type(rust_type: &'static str) -> &'static str { + fn get_form_type(rust_type: &'static str) -> &'static str { match rust_type { "Pass" => "password", "String" => "text", @@ -375,7 +369,7 @@ macro_rules! make_config { } } - fn _get_doc(doc_str: &'static str) -> ElementDoc { + fn get_doc(doc_str: &'static str) -> ElementDoc { let mut split = doc_str.split("|>").map(str::trim); ElementDoc { name: split.next().unwrap_or_default(), @@ -383,6 +377,12 @@ macro_rules! make_config { } } + let (def, cfg, overridden) = { + // Lock the inner as short as possible and clone what is needed to prevent deadlocks + let inner = &self.inner.read().unwrap(); + (inner._env.build(), inner.config.clone(), inner._overrides.clone()) + }; + let data: Vec = vec![ $( // This repetition is for each group GroupData { @@ -397,8 +397,8 @@ macro_rules! make_config { name: stringify!($name), value: serde_json::to_value(&cfg.$name).unwrap_or_default(), default: serde_json::to_value(&def.$name).unwrap_or_default(), - r#type: _get_form_type(stringify!($ty)), - doc: _get_doc(concat!($($doc),+)), + r#type: get_form_type(stringify!($ty)), + doc: get_doc(concat!($($doc),+)), overridden: overridden.contains(&pastey::paste!(stringify!([<$name:upper>]))), }, )+], // End of elements repetition @@ -408,9 +408,31 @@ macro_rules! make_config { } pub fn get_support_json(&self) -> serde_json::Value { + /// We map over the string and remove all alphanumeric, _ and - characters. + /// This is the fastest way (within micro-seconds) instead of using a regex (which takes mili-seconds) + fn privacy_mask(value: &str) -> String { + let mut n: u16 = 0; + let mut colon_match = false; + value + .chars() + .map(|c| { + n += 1; + match c { + ':' if n <= 11 => { + colon_match = true; + c + } + '/' if n <= 13 && colon_match => c, + ',' => c, + _ => '*', + } + }) + .collect::() + } + // Define which config keys need to be masked. // Pass types will always be masked and no need to put them in the list. - // Besides Pass, only String types will be masked via _privacy_mask. + // Besides Pass, only String types will be masked via privacy_mask. const PRIVACY_CONFIG: &[&str] = &[ "allowed_connect_src", "allowed_iframe_ancestors", @@ -437,28 +459,6 @@ macro_rules! make_config { inner.config.clone() }; - /// We map over the string and remove all alphanumeric, _ and - characters. - /// This is the fastest way (within micro-seconds) instead of using a regex (which takes mili-seconds) - fn _privacy_mask(value: &str) -> String { - let mut n: u16 = 0; - let mut colon_match = false; - value - .chars() - .map(|c| { - n += 1; - match c { - ':' if n <= 11 => { - colon_match = true; - c - } - '/' if n <= 13 && colon_match => c, - ',' => c, - _ => '*', - } - }) - .collect::() - } - serde_json::Value::Object({ let mut json = serde_json::Map::new(); $($( @@ -468,7 +468,7 @@ macro_rules! make_config { for mask_key in PRIVACY_CONFIG { if let Some(value) = json.get_mut(*mask_key) { if let Some(s) = value.as_str() { - *value = _privacy_mask(s).into(); + *value = privacy_mask(s).into(); } } } @@ -502,7 +502,7 @@ macro_rules! make_config { make_config! { folders { /// Data folder |> Main data folder - data_folder: String, false, def, "data".to_string(); + data_folder: String, false, def, "data".to_owned(); /// Database URL database_url: String, false, auto, |c| format!("sqlite://{}", storage::join_path(&c.data_folder, "db.sqlite3")); /// Icon cache folder @@ -518,7 +518,7 @@ make_config! { /// Session JWT key rsa_key_filename: String, false, auto, |c| storage::join_path(&c.data_folder, "rsa_key"); /// Web vault folder - web_vault_folder: String, false, def, "web-vault/".to_string(); + web_vault_folder: String, false, def, "web-vault/".to_owned(); }, ws { /// Enable websocket notifications @@ -528,9 +528,9 @@ make_config! { /// Enable push notifications push_enabled: bool, false, def, false; /// Push relay uri - push_relay_uri: String, false, def, "https://push.bitwarden.com".to_string(); + push_relay_uri: String, false, def, "https://push.bitwarden.com".to_owned(); /// Push identity uri - push_identity_uri: String, false, def, "https://identity.bitwarden.com".to_string(); + push_identity_uri: String, false, def, "https://identity.bitwarden.com".to_owned(); /// Installation id |> The installation id from https://bitwarden.com/host push_installation_id: Pass, false, def, String::new(); /// Installation key |> The installation key from https://bitwarden.com/host @@ -542,38 +542,38 @@ make_config! { job_poll_interval_ms: u64, false, def, 30_000; /// Send purge schedule |> Cron schedule of the job that checks for Sends past their deletion date. /// Defaults to hourly. Set blank to disable this job. - send_purge_schedule: String, false, def, "0 5 * * * *".to_string(); + send_purge_schedule: String, false, def, "0 5 * * * *".to_owned(); /// Trash purge schedule |> Cron schedule of the job that checks for trashed items to delete permanently. /// Defaults to daily. Set blank to disable this job. - trash_purge_schedule: String, false, def, "0 5 0 * * *".to_string(); + trash_purge_schedule: String, false, def, "0 5 0 * * *".to_owned(); /// Incomplete 2FA login schedule |> Cron schedule of the job that checks for incomplete 2FA logins. /// Defaults to once every minute. Set blank to disable this job. - incomplete_2fa_schedule: String, false, def, "30 * * * * *".to_string(); + incomplete_2fa_schedule: String, false, def, "30 * * * * *".to_owned(); /// Emergency notification reminder schedule |> Cron schedule of the job that sends expiration reminders to emergency access grantors. /// Defaults to hourly. (3 minutes after the hour) Set blank to disable this job. - emergency_notification_reminder_schedule: String, false, def, "0 3 * * * *".to_string(); + emergency_notification_reminder_schedule: String, false, def, "0 3 * * * *".to_owned(); /// Emergency request timeout schedule |> Cron schedule of the job that grants emergency access requests that have met the required wait time. /// Defaults to hourly. (7 minutes after the hour) Set blank to disable this job. - emergency_request_timeout_schedule: String, false, def, "0 7 * * * *".to_string(); + emergency_request_timeout_schedule: String, false, def, "0 7 * * * *".to_owned(); /// Event cleanup schedule |> Cron schedule of the job that cleans old events from the event table. /// Defaults to daily. Set blank to disable this job. - event_cleanup_schedule: String, false, def, "0 10 0 * * *".to_string(); + event_cleanup_schedule: String, false, def, "0 10 0 * * *".to_owned(); /// Auth Request cleanup schedule |> Cron schedule of the job that cleans old auth requests from the auth request. /// Defaults to every minute. Set blank to disable this job. - auth_request_purge_schedule: String, false, def, "30 * * * * *".to_string(); + auth_request_purge_schedule: String, false, def, "30 * * * * *".to_owned(); /// Duo Auth context cleanup schedule |> Cron schedule of the job that cleans expired Duo contexts from the database. Does nothing if Duo MFA is disabled or set to use the legacy iframe prompt. /// Defaults to once every minute. Set blank to disable this job. - duo_context_purge_schedule: String, false, def, "30 * * * * *".to_string(); + duo_context_purge_schedule: String, false, def, "30 * * * * *".to_owned(); /// Purge incomplete SSO auth. |> Cron schedule of the job that cleans leftover auth in db due to incomplete SSO login. /// Defaults to daily. Set blank to disable this job. - purge_incomplete_sso_auth: String, false, def, "0 20 0 * * *".to_string(); + purge_incomplete_sso_auth: String, false, def, "0 20 0 * * *".to_owned(); }, /// General settings settings { /// Domain URL |> This needs to be set to the URL used to access the server, including 'http[s]://' /// and port, if it's different than the default. Some server functions don't work correctly without this value - domain: String, true, def, "http://localhost".to_string(); + domain: String, true, def, "http://localhost".to_owned(); /// Domain Set |> Indicates if the domain is set by the admin. Otherwise the default will be used. domain_set: bool, false, def, false; /// Domain origin |> Domain URL origin (in https://example.com:8443/path, https://example.com:8443 is the origin) @@ -653,7 +653,7 @@ make_config! { admin_token: Pass, true, option; /// Invitation organization name |> Name shown in the invitation emails that don't come from a specific organization - invitation_org_name: String, true, def, "Vaultwarden".to_string(); + invitation_org_name: String, true, def, "Vaultwarden".to_owned(); /// Events days retain |> Number of days to retain events stored in the database. If unset, events are kept indefinitely. events_days_retain: i64, false, option; @@ -663,7 +663,7 @@ make_config! { advanced { /// Client IP header |> If not present, the remote IP is used. /// Set to the string "none" (without quotes), to disable any headers and just use the remote IP - ip_header: String, true, def, "X-Real-IP".to_string(); + ip_header: String, true, def, "X-Real-IP".to_owned(); /// Internal IP header property, used to avoid recomputing each time _ip_header_enabled: bool, false, generated, |c| &c.ip_header.trim().to_lowercase() != "none"; /// Icon service |> The predefined icon services are: internal, bitwarden, duckduckgo, google. @@ -672,7 +672,7 @@ make_config! { /// `internal` refers to Vaultwarden's built-in icon fetching implementation. If an external /// service is set, an icon request to Vaultwarden will return an HTTP redirect to the /// corresponding icon at the external service. - icon_service: String, false, def, "internal".to_string(); + icon_service: String, false, def, "internal".to_owned(); /// _icon_service_url _icon_service_url: String, false, generated, |c| generate_icon_service_url(&c.icon_service); /// _icon_service_csp @@ -723,14 +723,14 @@ make_config! { /// Enable extended logging extended_logging: bool, false, def, true; /// Log timestamp format - log_timestamp_format: String, true, def, "%Y-%m-%d %H:%M:%S.%3f".to_string(); + log_timestamp_format: String, true, def, "%Y-%m-%d %H:%M:%S.%3f".to_owned(); /// Enable the log to output to Syslog use_syslog: bool, false, def, false; /// Log file path log_file: String, false, option; /// Log level |> Valid values are "trace", "debug", "info", "warn", "error" and "off" /// For a specific module append it as a comma separated value "info,path::to::module=debug" - log_level: String, false, def, "info".to_string(); + log_level: String, false, def, "info".to_owned(); /// Enable DB WAL |> Turning this off might lead to worse performance, but might help if using vaultwarden on some exotic filesystems, /// that do not support WAL. Please make sure you read project wiki on the topic before changing this setting. @@ -812,7 +812,7 @@ make_config! { /// Authority Server |> Base url of the OIDC provider discovery endpoint (without `/.well-known/openid-configuration`) sso_authority: String, true, def, String::new(); /// Authorization request scopes |> List the of the needed scope (`openid` is implicit) - sso_scopes: String, true, def, "email profile".to_string(); + sso_scopes: String, true, def, "email profile".to_owned(); /// Authorization request extra parameters sso_authorize_extra_params: String, true, def, String::new(); /// Use PKCE during Authorization flow @@ -880,7 +880,7 @@ make_config! { /// From Address smtp_from: String, true, def, String::new(); /// From Name - smtp_from_name: String, true, def, "Vaultwarden".to_string(); + smtp_from_name: String, true, def, "Vaultwarden".to_owned(); /// Username smtp_username: String, true, option; /// Password @@ -930,13 +930,13 @@ fn validate_config(cfg: &ConfigItems, on_update: bool) -> Result<(), Error> { let file_path = url.strip_prefix("sqlite://").unwrap_or(url); if file_path.contains('/') { let path = std::path::Path::new(file_path); - if let Some(parent) = path.parent() { - if !parent.is_dir() { - err!(format!( - "SQLite database directory `{}` does not exist or is not a directory", - parent.display() - )); - } + if let Some(parent) = path.parent() + && !parent.is_dir() + { + err!(format!( + "SQLite database directory `{}` does not exist or is not a directory", + parent.display() + )); } } } @@ -959,10 +959,10 @@ fn validate_config(cfg: &ConfigItems, on_update: bool) -> Result<(), Error> { err!(format!("`DATABASE_MIN_CONNS` must be smaller than or equal to `DATABASE_MAX_CONNS`.",)); } - if let Some(log_file) = &cfg.log_file { - if std::fs::OpenOptions::new().append(true).create(true).open(log_file).is_err() { - err!("Unable to write to log file", log_file); - } + if let Some(log_file) = &cfg.log_file + && std::fs::OpenOptions::new().append(true).create(true).open(log_file).is_err() + { + err!("Unable to write to log file", log_file); } let dom = cfg.domain.to_lowercase(); @@ -975,7 +975,9 @@ fn validate_config(cfg: &ConfigItems, on_update: bool) -> Result<(), Error> { let connect_src = cfg.allowed_connect_src.to_lowercase(); for url in connect_src.split_whitespace() { if !url.starts_with("https://") || Url::parse(url).is_err() { - err!("ALLOWED_CONNECT_SRC variable contains one or more invalid URLs. Only FQDN's starting with https are allowed"); + err!( + "ALLOWED_CONNECT_SRC variable contains one or more invalid URLs. Only FQDN's starting with https are allowed" + ); } } @@ -991,11 +993,12 @@ fn validate_config(cfg: &ConfigItems, on_update: bool) -> Result<(), Error> { err!("`ORG_CREATION_USERS` contains invalid email addresses"); } - if let Some(ref token) = cfg.admin_token { - if token.trim().is_empty() && !cfg.disable_admin_token { - println!("[WARNING] `ADMIN_TOKEN` is enabled but has an empty value, so the admin page will be disabled."); - println!("[WARNING] To enable the admin page without a token, use `DISABLE_ADMIN_TOKEN`."); - } + if let Some(ref token) = cfg.admin_token + && token.trim().is_empty() + && !cfg.disable_admin_token + { + println!("[WARNING] `ADMIN_TOKEN` is enabled but has an empty value, so the admin page will be disabled."); + println!("[WARNING] To enable the admin page without a token, use `DISABLE_ADMIN_TOKEN`."); } if cfg.push_enabled && (cfg.push_installation_id == String::new() || cfg.push_installation_key == String::new()) { @@ -1029,37 +1032,41 @@ fn validate_config(cfg: &ConfigItems, on_update: bool) -> Result<(), Error> { } } - let invalid_flags = - parse_experimental_client_feature_flags(&cfg.experimental_client_feature_flags, FeatureFlagFilter::InvalidOnly); + let invalid_flags = parse_experimental_client_feature_flags( + &cfg.experimental_client_feature_flags, + &FeatureFlagFilter::InvalidOnly, + ); if !invalid_flags.is_empty() { - let feature_flags_error = format!("Unrecognized experimental client feature flags: {:?}.\n\ + let feature_flags_error = format!( + "Unrecognized experimental client feature flags: {invalid_flags:?}.\n\ Please ensure all feature flags are spelled correctly and that they are supported in this version.\n\ - Supported flags: {:?}\n", invalid_flags, SUPPORTED_FEATURE_FLAGS); + Supported flags: {SUPPORTED_FEATURE_FLAGS:?}\n" + ); if on_update { err!(feature_flags_error); - } else { - println!("[WARNING] {feature_flags_error}"); } + println!("[WARNING] {feature_flags_error}"); } + #[expect(clippy::items_after_statements, reason = "Keep this close to where it is used")] const MAX_FILESIZE_KB: i64 = i64::MAX >> 10; - if let Some(limit) = cfg.user_attachment_limit { - if !(0i64..=MAX_FILESIZE_KB).contains(&limit) { - err!("`USER_ATTACHMENT_LIMIT` is out of bounds"); - } + if let Some(limit) = cfg.user_attachment_limit + && !(0i64..=MAX_FILESIZE_KB).contains(&limit) + { + err!("`USER_ATTACHMENT_LIMIT` is out of bounds"); } - if let Some(limit) = cfg.org_attachment_limit { - if !(0i64..=MAX_FILESIZE_KB).contains(&limit) { - err!("`ORG_ATTACHMENT_LIMIT` is out of bounds"); - } + if let Some(limit) = cfg.org_attachment_limit + && !(0i64..=MAX_FILESIZE_KB).contains(&limit) + { + err!("`ORG_ATTACHMENT_LIMIT` is out of bounds"); } - if let Some(limit) = cfg.user_send_limit { - if !(0i64..=MAX_FILESIZE_KB).contains(&limit) { - err!("`USER_SEND_LIMIT` is out of bounds"); - } + if let Some(limit) = cfg.user_send_limit + && !(0i64..=MAX_FILESIZE_KB).contains(&limit) + { + err!("`USER_SEND_LIMIT` is out of bounds"); } if cfg._enable_duo @@ -1087,7 +1094,9 @@ fn validate_config(cfg: &ConfigItems, on_update: bool) -> Result<(), Error> { if let Some(yubico_server) = &cfg.yubico_server { let yubico_server = yubico_server.to_lowercase(); if !yubico_server.starts_with("https://") { - err!("`YUBICO_SERVER` must be a valid URL and start with 'https://'. Either unset this variable or provide a valid URL.") + err!( + "`YUBICO_SERVER` must be a valid URL and start with 'https://'. Either unset this variable or provide a valid URL." + ) } } } @@ -1139,7 +1148,9 @@ fn validate_config(cfg: &ConfigItems, on_update: bool) -> Result<(), Error> { } if cfg.smtp_username.is_some() != cfg.smtp_password.is_some() { - err!("Both `SMTP_USERNAME` and `SMTP_PASSWORD` need to be set to enable email authentication without `USE_SENDMAIL`") + err!( + "Both `SMTP_USERNAME` and `SMTP_PASSWORD` need to be set to enable email authentication without `USE_SENDMAIL`" + ) } } @@ -1300,7 +1311,7 @@ fn extract_url_origin(url: &str) -> String { /// All trailing '/' chars are trimmed, even if the path is a lone '/'. fn extract_url_path(url: &str) -> String { match Url::parse(url) { - Ok(u) => u.path().trim_end_matches('/').to_string(), + Ok(u) => u.path().trim_end_matches('/').to_owned(), Err(_) => { // We already print it in the method above, no need to do it again String::new() @@ -1310,7 +1321,7 @@ fn extract_url_path(url: &str) -> String { fn generate_smtp_img_src(embed_images: bool, domain: &str) -> String { if embed_images { - "cid:".to_string() + "cid:".to_owned() } else { // normalize base_url let base_url = domain.trim_end_matches('/'); @@ -1329,10 +1340,10 @@ fn generate_sso_callback_path(domain: &str) -> String { fn generate_icon_service_url(icon_service: &str) -> String { match icon_service { "internal" => String::new(), - "bitwarden" => "https://icons.bitwarden.net/{}/icon.png".to_string(), - "duckduckgo" => "https://icons.duckduckgo.com/ip3/{}.ico".to_string(), - "google" => "https://www.google.com/s2/favicons?domain={}&sz=32".to_string(), - _ => icon_service.to_string(), + "bitwarden" => "https://icons.bitwarden.net/{}/icon.png".to_owned(), + "duckduckgo" => "https://icons.duckduckgo.com/ip3/{}.ico".to_owned(), + "google" => "https://www.google.com/s2/favicons?domain={}&sz=32".to_owned(), + _ => icon_service.to_owned(), } } @@ -1341,7 +1352,7 @@ fn generate_icon_service_csp(icon_service: &str, icon_service_url: &str) -> Stri // We split on the first '{', since that is the variable delimiter for an icon service URL. // Everything up until the first '{' should be fixed and can be used as an CSP string. let csp_string = match icon_service_url.split_once('{') { - Some((c, _)) => c.to_string(), + Some((c, _)) => c.to_owned(), None => String::new(), }; @@ -1358,12 +1369,12 @@ fn smtp_convert_deprecated_ssl_options(smtp_ssl: Option, smtp_explicit_tls println!("[DEPRECATED]: `SMTP_SSL` or `SMTP_EXPLICIT_TLS` is set. Please use `SMTP_SECURITY` instead."); } if smtp_explicit_tls.is_some() && smtp_explicit_tls.unwrap() { - return "force_tls".to_string(); + return "force_tls".to_owned(); } else if smtp_ssl.is_some() && !smtp_ssl.unwrap() { - return "off".to_string(); + return "off".to_owned(); } // Return the default `starttls` in all other cases - "starttls".to_string() + "starttls".to_owned() } pub enum PathType { @@ -1406,12 +1417,12 @@ pub const SUPPORTED_FEATURE_FLAGS: &[&str] = &[ impl Config { pub async fn load() -> Result { // Loading from env and file - let _env = ConfigBuilder::from_env(); - let _usr = ConfigBuilder::from_file().await.unwrap_or_default(); + let env = ConfigBuilder::from_env(); + let usr = ConfigBuilder::from_file().await.unwrap_or_default(); // Create merged config, config file overwrites env - let mut _overrides = Vec::new(); - let builder = _env.merge(&_usr, true, &mut _overrides); + let mut overrides = Vec::new(); + let builder = env.merge(&usr, true, &mut overrides); // Fill any missing with defaults let config = builder.build(); @@ -1424,9 +1435,9 @@ impl Config { rocket_shutdown_handle: None, templates: load_templates(&config.templates_folder), config, - _env, - _usr, - _overrides, + _env: env, + _usr: usr, + _overrides: overrides, }), }) } @@ -1472,8 +1483,8 @@ impl Config { async fn update_config_partial(&self, other: ConfigBuilder) -> Result<(), Error> { let builder = { let usr = &self.inner.read().unwrap()._usr; - let mut _overrides = Vec::new(); - usr.merge(&other, false, &mut _overrides) + let mut overrides = Vec::new(); + usr.merge(&other, false, &mut overrides) }; self.update_config(builder, false).await } @@ -1496,11 +1507,11 @@ impl Config { /// Tests whether signup is allowed for an email address, taking into /// account the signups_allowed and signups_domains_whitelist settings. pub fn is_signup_allowed(&self, email: &str) -> bool { - if !self.signups_domains_whitelist().is_empty() { + if self.signups_domains_whitelist().is_empty() { + self.signups_allowed() + } else { // The whitelist setting overrides the signups_allowed setting. self.is_email_domain_allowed(email) - } else { - self.signups_allowed() } } @@ -1621,10 +1632,10 @@ impl Config { } pub fn shutdown(&self) { - if let Ok(mut c) = self.inner.write() { - if let Some(handle) = c.rocket_shutdown_handle.take() { - handle.notify(); - } + if let Ok(mut c) = self.inner.write() + && let Some(handle) = c.rocket_shutdown_handle.take() + { + handle.notify(); } } @@ -1641,7 +1652,7 @@ impl Config { } pub fn sso_scopes_vec(&self) -> Vec { - self.sso_scopes().split_whitespace().map(str::to_string).collect() + self.sso_scopes().split_whitespace().map(str::to_owned).collect() } pub fn sso_authorize_extra_params_vec(&self) -> Vec<(String, String)> { @@ -1751,7 +1762,7 @@ fn case_helper<'reg, 'rc>( let value = param.value().clone(); if h.params().iter().skip(1).any(|x| x.value() == &value) { - h.template().map(|t| t.render(r, ctx, rc, out)).unwrap_or_else(|| Ok(())) + h.template().map_or(Ok(()), |t| t.render(r, ctx, rc, out)) } else { Ok(()) } diff --git a/src/db/mod.rs b/src/db/mod.rs index 4aafe995..2eae3f3c 100644 --- a/src/db/mod.rs +++ b/src/db/mod.rs @@ -6,25 +6,23 @@ use std::{ }; use diesel::{ + Connection, RunQueryDsl, connection::SimpleConnection, r2d2::{CustomizeConnection, Pool, PooledConnection}, - Connection, RunQueryDsl, }; - use rocket::{ + Request, http::Status, request::{FromRequest, Outcome}, - Request, }; - use tokio::{ sync::{Mutex, OwnedSemaphorePermit, Semaphore}, time::timeout, }; use crate::{ - error::{Error, MapResult}, CONFIG, + error::{Error, MapResult}, }; // These changes are based on Rocket 0.5-rc wrapper of Diesel: https://github.com/SergioBenitez/Rocket/blob/v0.5-rc/contrib/sync_db_pools @@ -62,7 +60,7 @@ pub struct DbConnManager { impl DbConnManager { pub fn new(database_url: &str) -> Self { Self { - database_url: database_url.to_string(), + database_url: database_url.to_owned(), } } @@ -224,7 +222,7 @@ impl DbPool { // Set a global to determine the database more easily throughout the rest of the code if ACTIVE_DB_TYPE.set(conn_type).is_err() { - error!("Tried to set the active database connection type more than once.") + error!("Tried to set the active database connection type more than once."); } Ok(DbPool { @@ -279,34 +277,33 @@ impl DbConnType { #[cfg(not(sqlite))] err!("`DATABASE_URL` is a SQLite URL, but the 'sqlite' feature is not enabled") + } // No recognized scheme — assume legacy bare-path SQLite, but the database file must already exist. // This prevents misconfigured URLs (typos, quoted strings) from silently creating a new empty SQLite database. - } else { - #[cfg(sqlite)] - { - if std::path::Path::new(url).exists() { - return Ok(DbConnType::Sqlite); - } - err!(format!( - "`DATABASE_URL` does not match any known database scheme (mysql://, postgresql://, sqlite://) \ - and no existing SQLite database was found at '{url}'. \ - If you intend to use SQLite, use an explicit `sqlite://` scheme in your `DATABASE_URL`. \ - Otherwise, check your DATABASE_URL for typos or quoting issues." - )) + #[cfg(sqlite)] + { + if std::path::Path::new(url).exists() { + return Ok(DbConnType::Sqlite); } - - #[cfg(not(sqlite))] - err!("`DATABASE_URL` does not match any known database scheme (mysql://, postgresql://, sqlite://)") + err!(format!( + "`DATABASE_URL` does not match any known database scheme (mysql://, postgresql://, sqlite://) \ + and no existing SQLite database was found at '{url}'. \ + If you intend to use SQLite, use an explicit `sqlite://` scheme in your `DATABASE_URL`. \ + Otherwise, check your DATABASE_URL for typos or quoting issues." + )) } + + #[cfg(not(sqlite))] + err!("`DATABASE_URL` does not match any known database scheme (mysql://, postgresql://, sqlite://)") } pub fn get_init_stmts(&self) -> String { let init_stmts = CONFIG.database_conn_init(); - if !init_stmts.is_empty() { - init_stmts - } else { + if init_stmts.is_empty() { self.default_init_stmts() + } else { + init_stmts } } @@ -317,7 +314,7 @@ impl DbConnType { #[cfg(postgresql)] Self::Postgresql => String::new(), #[cfg(sqlite)] - Self::Sqlite => "PRAGMA busy_timeout = 5000; PRAGMA synchronous = NORMAL;".to_string(), + Self::Sqlite => "PRAGMA busy_timeout = 5000; PRAGMA synchronous = NORMAL;".to_owned(), } } } @@ -408,7 +405,7 @@ pub fn backup_sqlite() -> Result { use diesel::Connection; let db_url = CONFIG.database_url(); - if DbConnType::from_url(&CONFIG.database_url()).map(|t| t == DbConnType::Sqlite).unwrap_or(false) { + if DbConnType::from_url(&CONFIG.database_url()).is_ok_and(|t| t == DbConnType::Sqlite) { // Strip the sqlite:// prefix if present to get the raw file path let file_path = db_url.strip_prefix("sqlite://").unwrap_or(&db_url); // Open a read-only connection for the backup @@ -443,12 +440,12 @@ pub async fn get_sql_server_version(conn: &DbConn) -> String { postgresql,mysql { diesel::select(diesel::dsl::sql::("version();")) .get_result::(conn) - .unwrap_or_else(|_| "Unknown".to_string()) + .unwrap_or_else(|_| "Unknown".to_owned()) } sqlite { diesel::select(diesel::dsl::sql::("sqlite_version();")) .get_result::(conn) - .unwrap_or_else(|_| "Unknown".to_string()) + .unwrap_or_else(|_| "Unknown".to_owned()) } } } diff --git a/src/db/models/archive.rs b/src/db/models/archive.rs index f576e7ed..83d547f2 100644 --- a/src/db/models/archive.rs +++ b/src/db/models/archive.rs @@ -1,11 +1,13 @@ use chrono::NaiveDateTime; use diesel::prelude::*; +use crate::{ + api::EmptyResult, + db::{DbConn, schema::archives}, + error::MapResult, +}; + use super::{CipherId, User, UserId}; -use crate::api::EmptyResult; -use crate::db::schema::archives; -use crate::db::DbConn; -use crate::error::MapResult; #[derive(Identifiable, Queryable, Insertable)] #[diesel(table_name = archives)] @@ -19,13 +21,15 @@ pub struct Archive { impl Archive { // Returns the date the specified cipher was archived pub async fn get_archived_at(cipher_uuid: &CipherId, user_uuid: &UserId, conn: &DbConn) -> Option { - db_run! { conn: { + conn.run(move |conn| { archives::table .filter(archives::cipher_uuid.eq(cipher_uuid)) .filter(archives::user_uuid.eq(user_uuid)) .select(archives::archived_at) - .first::(conn).ok() - }} + .first::(conn) + .ok() + }) + .await } // Saves (inserts or updates) an archive record with the provided timestamp @@ -66,26 +70,26 @@ impl Archive { // Deletes an archive record for a specific cipher pub async fn delete_by_cipher(user_uuid: &UserId, cipher_uuid: &CipherId, conn: &DbConn) -> EmptyResult { User::update_uuid_revision(user_uuid, conn).await; - db_run! { conn: { + conn.run(move |conn| { diesel::delete( - archives::table - .filter(archives::user_uuid.eq(user_uuid)) - .filter(archives::cipher_uuid.eq(cipher_uuid)) + archives::table.filter(archives::user_uuid.eq(user_uuid)).filter(archives::cipher_uuid.eq(cipher_uuid)), ) .execute(conn) .map_res("Error deleting archive") - }} + }) + .await } /// Return a vec with (cipher_uuid, archived_at) /// This is used during a full sync so we only need one query for all archive matches pub async fn find_by_user(user_uuid: &UserId, conn: &DbConn) -> Vec<(CipherId, NaiveDateTime)> { - db_run! { conn: { + conn.run(move |conn| { archives::table .filter(archives::user_uuid.eq(user_uuid)) .select((archives::cipher_uuid, archives::archived_at)) .load::<(CipherId, NaiveDateTime)>(conn) .unwrap_or_default() - }} + }) + .await } } diff --git a/src/db/models/attachment.rs b/src/db/models/attachment.rs index dad081bd..244f8c27 100644 --- a/src/db/models/attachment.rs +++ b/src/db/models/attachment.rs @@ -1,13 +1,24 @@ +use std::time::Duration; + use bigdecimal::{BigDecimal, ToPrimitive}; use derive_more::{AsRef, Deref, Display}; use diesel::prelude::*; use serde_json::Value; -use std::time::Duration; + +use crate::{ + CONFIG, + api::EmptyResult, + auth::{encode_jwt, generate_file_download_claims}, + config::PathType, + db::{ + DbConn, + schema::{attachments, ciphers}, + }, + error::MapResult, +}; +use macros::IdFromParam; use super::{CipherId, OrganizationId, UserId}; -use crate::db::schema::{attachments, ciphers}; -use crate::{config::PathType, CONFIG}; -use macros::IdFromParam; #[derive(Identifiable, Queryable, Insertable, AsChangeset)] #[diesel(table_name = attachments)] @@ -67,12 +78,6 @@ impl Attachment { } } -use crate::auth::{encode_jwt, generate_file_download_claims}; -use crate::db::DbConn; - -use crate::api::EmptyResult; -use crate::error::MapResult; - /// Database methods impl Attachment { pub async fn save(&self, conn: &DbConn) -> EmptyResult { @@ -107,15 +112,15 @@ impl Attachment { } pub async fn delete(&self, conn: &DbConn) -> EmptyResult { - db_run! { conn: { - crate::util::retry(|| - diesel::delete(attachments::table.filter(attachments::id.eq(&self.id))) - .execute(conn), + conn.run(move |conn| { + crate::util::retry( + || diesel::delete(attachments::table.filter(attachments::id.eq(&self.id))).execute(conn), 10, ) .map(|_| ()) .map_res("Error deleting attachment") - }}?; + }) + .await?; let operator = CONFIG.opendal_operator_for_path_type(&PathType::Attachments)?; let file_path = self.get_file_path(); @@ -139,25 +144,22 @@ impl Attachment { } pub async fn find_by_id(id: &AttachmentId, conn: &DbConn) -> Option { - db_run! { conn: { - attachments::table - .filter(attachments::id.eq(id.to_lowercase())) - .first::(conn) - .ok() - }} + conn.run(move |conn| attachments::table.filter(attachments::id.eq(id.to_lowercase())).first::(conn).ok()) + .await } pub async fn find_by_cipher(cipher_uuid: &CipherId, conn: &DbConn) -> Vec { - db_run! { conn: { + conn.run(move |conn| { attachments::table .filter(attachments::cipher_uuid.eq(cipher_uuid)) .load::(conn) .expect("Error loading attachments") - }} + }) + .await } pub async fn size_by_user(user_uuid: &UserId, conn: &DbConn) -> i64 { - db_run! { conn: { + conn.run(move |conn| { let result: Option = attachments::table .left_join(ciphers::table.on(ciphers::uuid.eq(attachments::cipher_uuid))) .filter(ciphers::user_uuid.eq(user_uuid)) @@ -168,24 +170,26 @@ impl Attachment { match result.map(|r| r.to_i64()) { Some(Some(r)) => r, Some(None) => i64::MAX, - None => 0 + None => 0, } - }} + }) + .await } pub async fn count_by_user(user_uuid: &UserId, conn: &DbConn) -> i64 { - db_run! { conn: { + conn.run(move |conn| { attachments::table .left_join(ciphers::table.on(ciphers::uuid.eq(attachments::cipher_uuid))) .filter(ciphers::user_uuid.eq(user_uuid)) .count() .first(conn) .unwrap_or(0) - }} + }) + .await } pub async fn size_by_org(org_uuid: &OrganizationId, conn: &DbConn) -> i64 { - db_run! { conn: { + conn.run(move |conn| { let result: Option = attachments::table .left_join(ciphers::table.on(ciphers::uuid.eq(attachments::cipher_uuid))) .filter(ciphers::organization_uuid.eq(org_uuid)) @@ -196,20 +200,22 @@ impl Attachment { match result.map(|r| r.to_i64()) { Some(Some(r)) => r, Some(None) => i64::MAX, - None => 0 + None => 0, } - }} + }) + .await } pub async fn count_by_org(org_uuid: &OrganizationId, conn: &DbConn) -> i64 { - db_run! { conn: { + conn.run(move |conn| { attachments::table .left_join(ciphers::table.on(ciphers::uuid.eq(attachments::cipher_uuid))) .filter(ciphers::organization_uuid.eq(org_uuid)) .count() .first(conn) .unwrap_or(0) - }} + }) + .await } // This will return all attachments linked to the user or org @@ -220,7 +226,7 @@ impl Attachment { org_uuids: &Vec, conn: &DbConn, ) -> Vec { - db_run! { conn: { + conn.run(move |conn| { attachments::table .left_join(ciphers::table.on(ciphers::uuid.eq(attachments::cipher_uuid))) .filter(ciphers::user_uuid.eq(user_uuid)) @@ -228,7 +234,8 @@ impl Attachment { .select(attachments::all_columns) .load::(conn) .expect("Error loading attachments") - }} + }) + .await } } diff --git a/src/db/models/auth_request.rs b/src/db/models/auth_request.rs index 93c6e445..a3876661 100644 --- a/src/db/models/auth_request.rs +++ b/src/db/models/auth_request.rs @@ -1,12 +1,19 @@ -use super::{DeviceId, OrganizationId, UserId}; -use crate::db::schema::auth_requests; -use crate::{crypto::ct_eq, util::format_date}; use chrono::{NaiveDateTime, Utc}; use derive_more::{AsRef, Deref, Display, From}; use diesel::prelude::*; -use macros::UuidFromParam; use serde_json::Value; +use crate::{ + api::EmptyResult, + crypto::ct_eq, + db::{DbConn, schema::auth_requests}, + error::MapResult, + util::format_date, +}; +use macros::UuidFromParam; + +use super::{DeviceId, OrganizationId, UserId}; + #[derive(Identifiable, Queryable, Insertable, AsChangeset, Deserialize, Serialize)] #[diesel(table_name = auth_requests)] #[diesel(treat_none_as_null = true)] @@ -74,11 +81,6 @@ impl AuthRequest { } } -use crate::db::DbConn; - -use crate::api::EmptyResult; -use crate::error::MapResult; - impl AuthRequest { pub async fn save(&mut self, conn: &DbConn) -> EmptyResult { db_run! { conn: @@ -112,31 +114,28 @@ impl AuthRequest { } pub async fn find_by_uuid(uuid: &AuthRequestId, conn: &DbConn) -> Option { - db_run! { conn: { - auth_requests::table - .filter(auth_requests::uuid.eq(uuid)) - .first::(conn) - .ok() - }} + conn.run(move |conn| auth_requests::table.filter(auth_requests::uuid.eq(uuid)).first::(conn).ok()).await } pub async fn find_by_uuid_and_user(uuid: &AuthRequestId, user_uuid: &UserId, conn: &DbConn) -> Option { - db_run! { conn: { + conn.run(move |conn| { auth_requests::table .filter(auth_requests::uuid.eq(uuid)) .filter(auth_requests::user_uuid.eq(user_uuid)) .first::(conn) .ok() - }} + }) + .await } pub async fn find_by_user(user_uuid: &UserId, conn: &DbConn) -> Vec { - db_run! { conn: { + conn.run(move |conn| { auth_requests::table .filter(auth_requests::user_uuid.eq(user_uuid)) .load::(conn) .expect("Error loading auth_requests") - }} + }) + .await } pub async fn find_by_user_and_requested_device( @@ -144,7 +143,7 @@ impl AuthRequest { device_uuid: &DeviceId, conn: &DbConn, ) -> Option { - db_run! { conn: { + conn.run(move |conn| { auth_requests::table .filter(auth_requests::user_uuid.eq(user_uuid)) .filter(auth_requests::request_device_identifier.eq(device_uuid)) @@ -152,24 +151,27 @@ impl AuthRequest { .order_by(auth_requests::creation_date.desc()) .first::(conn) .ok() - }} + }) + .await } pub async fn find_created_before(dt: &NaiveDateTime, conn: &DbConn) -> Vec { - db_run! { conn: { + conn.run(move |conn| { auth_requests::table .filter(auth_requests::creation_date.lt(dt)) .load::(conn) .expect("Error loading auth_requests") - }} + }) + .await } pub async fn delete(&self, conn: &DbConn) -> EmptyResult { - db_run! { conn: { + conn.run(move |conn| { diesel::delete(auth_requests::table.filter(auth_requests::uuid.eq(&self.uuid))) .execute(conn) .map_res("Error deleting auth request") - }} + }) + .await } pub fn check_access_code(&self, access_code: &str) -> bool { diff --git a/src/db/models/cipher.rs b/src/db/models/cipher.rs index db906179..3852ceff 100644 --- a/src/db/models/cipher.rs +++ b/src/db/models/cipher.rs @@ -1,22 +1,32 @@ -use crate::db::schema::{ - ciphers, ciphers_collections, collections, collections_groups, folders, folders_ciphers, groups, groups_users, - users_collections, users_organizations, -}; -use crate::util::LowerCase; -use crate::CONFIG; +use std::borrow::Cow; + use chrono::{NaiveDateTime, TimeDelta, Utc}; use derive_more::{AsRef, Deref, Display, From}; use diesel::prelude::*; use serde_json::Value; +use crate::{ + CONFIG, + api::{ + EmptyResult, + core::{CipherData, CipherSyncData, CipherSyncType}, + }, + db::{ + DbConn, + schema::{ + ciphers, ciphers_collections, collections, collections_groups, folders, folders_ciphers, groups, + groups_users, users_collections, users_organizations, + }, + }, + error::MapResult, + util::LowerCase, +}; +use macros::UuidFromParam; + use super::{ Archive, Attachment, CollectionCipher, CollectionId, Favorite, FolderCipher, FolderId, Group, Membership, MembershipStatus, MembershipType, OrganizationId, User, UserId, }; -use crate::api::core::{CipherData, CipherSyncData, CipherSyncType}; -use macros::UuidFromParam; - -use std::borrow::Cow; #[derive(Identifiable, Queryable, Insertable, AsChangeset)] #[diesel(table_name = ciphers)] @@ -91,27 +101,27 @@ impl Cipher { format!("The field Notes exceeds the maximum encrypted value length of {max_note_size} characters."); for (index, cipher) in cipher_data.iter().enumerate() { // Validate the note size and if it is exceeded return a warning - if let Some(note) = &cipher.notes { - if note.len() > max_note_size { - validation_errors - .insert(format!("Ciphers[{index}].Notes"), serde_json::to_value([&max_note_size_msg]).unwrap()); - } + if let Some(note) = &cipher.notes + && note.len() > max_note_size + { + validation_errors + .insert(format!("Ciphers[{index}].Notes"), serde_json::to_value([&max_note_size_msg]).unwrap()); } // Validate the password history if it contains `null` values and if so, return a warning if let Some(Value::Array(password_history)) = &cipher.password_history { for pwh in password_history { - if let Value::Object(pwo) = pwh { - if pwo.get("password").is_some_and(|p| !p.is_string()) { - validation_errors.insert( - format!("Ciphers[{index}].Notes"), - serde_json::to_value([ - "The password history contains a `null` value. Only strings are allowed.", - ]) - .unwrap(), - ); - break; - } + if let Value::Object(pwo) = pwh + && pwo.get("password").is_some_and(|p| !p.is_string()) + { + validation_errors.insert( + format!("Ciphers[{index}].Notes"), + serde_json::to_value([ + "The password history contains a `null` value. Only strings are allowed.", + ]) + .unwrap(), + ); + break; } } } @@ -124,17 +134,12 @@ impl Cipher { "object": "error" }); err_json!(err_json, "Import validation errors") - } else { - Ok(()) } + + Ok(()) } } -use crate::db::DbConn; - -use crate::api::EmptyResult; -use crate::error::MapResult; - /// Database methods impl Cipher { pub async fn to_json( @@ -149,14 +154,14 @@ impl Cipher { let mut attachments_json: Value = Value::Null; if let Some(cipher_sync_data) = cipher_sync_data { - if let Some(attachments) = cipher_sync_data.cipher_attachments.get(&self.uuid) { - if !attachments.is_empty() { - let mut attachments_json_vec = vec![]; - for attachment in attachments { - attachments_json_vec.push(attachment.to_json(host).await?); - } - attachments_json = Value::Array(attachments_json_vec); + if let Some(attachments) = cipher_sync_data.cipher_attachments.get(&self.uuid) + && !attachments.is_empty() + { + let mut attachments_json_vec = vec![]; + for attachment in attachments { + attachments_json_vec.push(attachment.to_json(host).await?); } + attachments_json = Value::Array(attachments_json_vec); } } else { let attachments = Attachment::find_by_cipher(&self.uuid, conn).await; @@ -172,12 +177,11 @@ impl Cipher { // We don't need these values at all for Organizational syncs // Skip any other database calls if this is the case and just return false. let (read_only, hide_passwords, _) = if sync_type == CipherSyncType::User { - match self.get_access_restrictions(user_uuid, cipher_sync_data, conn).await { - Some((ro, hp, mn)) => (ro, hp, mn), - None => { - error!("Cipher ownership assertion failure"); - (true, true, false) - } + if let Some((ro, hp, mn)) = self.get_access_restrictions(user_uuid, cipher_sync_data, conn).await { + (ro, hp, mn) + } else { + error!("Cipher ownership assertion failure"); + (true, true, false) } } else { (false, false, false) @@ -231,15 +235,14 @@ impl Cipher { Some(p) if p.is_string() => Some(d.data), _ => None, }) - .map(|mut d| match d.get("lastUsedDate").and_then(|l| l.as_str()) { - Some(l) => { - d["lastUsedDate"] = json!(validate_and_format_date(l)); - d - } - _ => { - d["lastUsedDate"] = json!("1970-01-01T00:00:00.000000Z"); - d - } + .map(|mut d| { + let lud = if let Some(l) = d.get("lastUsedDate").and_then(|l| l.as_str()) { + validate_and_format_date(l) + } else { + "1970-01-01T00:00:00.000000Z".to_owned() + }; + d["lastUsedDate"] = json!(lud); + d }) .collect() }) @@ -247,32 +250,30 @@ impl Cipher { // Get the type_data or a default to an empty json object '{}'. // If not passing an empty object, mobile clients will crash. - let mut type_data_json = - serde_json::from_str::>(&self.data).map(|d| d.data).unwrap_or_else(|_| { - warn!("Error parsing data field for {}", self.uuid); - Value::Object(serde_json::Map::new()) - }); + let mut type_data_json = serde_json::from_str::>(&self.data) + .inspect_err(|_| warn!("Error parsing data field for {}", self.uuid)) + .map_or_else(|_| Value::Object(serde_json::Map::new()), |d| d.data); // NOTE: This was marked as *Backwards Compatibility Code*, but as of January 2021 this is still being used by upstream // Set the first element of the Uris array as Uri, this is needed several (mobile) clients. if self.atype == 1 { // Upstream always has an `uri` key/value type_data_json["uri"] = Value::Null; - if let Some(uris) = type_data_json["uris"].as_array_mut() { - if !uris.is_empty() { - // Fix uri match values first, they are only allowed to be a number or null - // If it is a string, convert it to an int or null if that fails - for uri in &mut *uris { - if uri["match"].is_string() { - let match_value = match uri["match"].as_str().unwrap_or_default().parse::() { - Ok(n) => json!(n), - _ => Value::Null, - }; - uri["match"] = match_value; - } + if let Some(uris) = type_data_json["uris"].as_array_mut() + && !uris.is_empty() + { + // Fix uri match values first, they are only allowed to be a number or null + // If it is a string, convert it to an int or null if that fails + for uri in &mut *uris { + if uri["match"].is_string() { + let match_value = match uri["match"].as_str().unwrap_or_default().parse::() { + Ok(n) => json!(n), + _ => Value::Null, + }; + uri["match"] = match_value; } - type_data_json["uri"] = uris[0]["uri"].clone(); } + type_data_json["uri"] = uris[0]["uri"].clone(); } // Check if `passwordRevisionDate` is a valid date, else convert it @@ -285,7 +286,7 @@ impl Cipher { // This breaks at least the native mobile clients if self.atype == 2 { match type_data_json { - Value::Object(ref t) if t.get("type").is_some_and(|t| t.is_number()) => {} + Value::Object(ref t) if t.get("type").is_some_and(Value::is_number) => {} _ => { type_data_json = json!({"type": 0}); } @@ -297,9 +298,9 @@ impl Cipher { // The only way to fix this is by setting type_data_json to `null` // Opening this ssh-key in the mobile client will probably crash the client, but you can edit, save and afterwards delete it if self.atype == 5 - && (type_data_json["keyFingerprint"].as_str().is_none_or(|v| v.is_empty()) - || type_data_json["privateKey"].as_str().is_none_or(|v| v.is_empty()) - || type_data_json["publicKey"].as_str().is_none_or(|v| v.is_empty())) + && (type_data_json["keyFingerprint"].as_str().is_none_or(str::is_empty) + || type_data_json["privateKey"].as_str().is_none_or(str::is_empty) + || type_data_json["publicKey"].as_str().is_none_or(str::is_empty)) { warn!("Error parsing ssh-key, mandatory fields are invalid for {}", self.uuid); type_data_json = Value::Null; @@ -415,7 +416,7 @@ impl Cipher { match self.user_uuid { Some(ref user_uuid) => { User::update_uuid_revision(user_uuid, conn).await; - user_uuids.push(user_uuid.clone()) + user_uuids.push(user_uuid.clone()); } None => { // Belongs to Organization, need to update affected users @@ -430,11 +431,11 @@ impl Cipher { } for member in collection_users { User::update_uuid_revision(&member.user_uuid, conn).await; - user_uuids.push(member.user_uuid.clone()) + user_uuids.push(member.user_uuid.clone()); } } } - }; + } user_uuids } @@ -480,11 +481,12 @@ impl Cipher { Attachment::delete_all_by_cipher(&self.uuid, conn).await?; Favorite::delete_all_by_cipher(&self.uuid, conn).await?; - db_run! { conn: { + conn.run(move |conn| { diesel::delete(ciphers::table.filter(ciphers::uuid.eq(&self.uuid))) .execute(conn) .map_res("Error deleting cipher") - }} + }) + .await } pub async fn delete_all_by_organization(org_uuid: &OrganizationId, conn: &DbConn) -> EmptyResult { @@ -531,9 +533,10 @@ impl Cipher { // Remove from folder (Some(old_folder), None) => { - match FolderCipher::find_by_folder_and_cipher(&old_folder, &self.uuid, conn).await { - Some(old_folder) => old_folder.delete(conn).await, - None => err!("Couldn't move from previous folder"), + if let Some(old_folder) = FolderCipher::find_by_folder_and_cipher(&old_folder, &self.uuid, conn).await { + old_folder.delete(conn).await + } else { + err!("Couldn't move from previous folder") } } @@ -584,9 +587,8 @@ impl Cipher { if let Some(ref org_uuid) = self.organization_uuid { if let Some(cipher_sync_data) = cipher_sync_data { return cipher_sync_data.user_group_full_access_for_organizations.contains(org_uuid); - } else { - return Group::is_in_full_access_group(user_uuid, org_uuid, conn).await; } + return Group::is_in_full_access_group(user_uuid, org_uuid, conn).await; } false } @@ -628,10 +630,10 @@ impl Cipher { rows } else { let user_permissions = self.get_user_collections_access_flags(user_uuid, conn).await; - if !user_permissions.is_empty() { - user_permissions - } else { + if user_permissions.is_empty() { self.get_group_collections_access_flags(user_uuid, conn).await + } else { + user_permissions } }; @@ -657,7 +659,7 @@ impl Cipher { let mut read_only = true; let mut hide_passwords = true; let mut manage = false; - for (ro, hp, mn) in rows.iter() { + for (ro, hp, mn) in &rows { read_only &= ro; hide_passwords &= hp; manage |= mn; @@ -667,51 +669,51 @@ impl Cipher { } async fn get_user_collections_access_flags(&self, user_uuid: &UserId, conn: &DbConn) -> Vec<(bool, bool, bool)> { - db_run! { conn: { + conn.run(move |conn| { // Check whether this cipher is in any collections accessible to the // user. If so, retrieve the access flags for each collection. ciphers::table .filter(ciphers::uuid.eq(&self.uuid)) - .inner_join(ciphers_collections::table.on( - ciphers::uuid.eq(ciphers_collections::cipher_uuid) - )) - .inner_join(users_collections::table.on( - ciphers_collections::collection_uuid.eq(users_collections::collection_uuid) - .and(users_collections::user_uuid.eq(user_uuid)) - )) + .inner_join(ciphers_collections::table.on(ciphers::uuid.eq(ciphers_collections::cipher_uuid))) + .inner_join( + users_collections::table.on(ciphers_collections::collection_uuid + .eq(users_collections::collection_uuid) + .and(users_collections::user_uuid.eq(user_uuid))), + ) .select((users_collections::read_only, users_collections::hide_passwords, users_collections::manage)) .load::<(bool, bool, bool)>(conn) .expect("Error getting user access restrictions") - }} + }) + .await } async fn get_group_collections_access_flags(&self, user_uuid: &UserId, conn: &DbConn) -> Vec<(bool, bool, bool)> { if !CONFIG.org_groups_enabled() { return Vec::new(); } - db_run! { conn: { + conn.run(move |conn| { ciphers::table .filter(ciphers::uuid.eq(&self.uuid)) - .inner_join(ciphers_collections::table.on( - ciphers::uuid.eq(ciphers_collections::cipher_uuid) - )) - .inner_join(collections_groups::table.on( - collections_groups::collections_uuid.eq(ciphers_collections::collection_uuid) - )) - .inner_join(groups_users::table.on( - groups_users::groups_uuid.eq(collections_groups::groups_uuid) - )) - .inner_join(users_organizations::table.on( - users_organizations::uuid.eq(groups_users::users_organizations_uuid) - )) - .inner_join(groups::table.on(groups::uuid.eq(collections_groups::groups_uuid) - .and(groups::organizations_uuid.eq(users_organizations::org_uuid)) - )) + .inner_join(ciphers_collections::table.on(ciphers::uuid.eq(ciphers_collections::cipher_uuid))) + .inner_join( + collections_groups::table + .on(collections_groups::collections_uuid.eq(ciphers_collections::collection_uuid)), + ) + .inner_join(groups_users::table.on(groups_users::groups_uuid.eq(collections_groups::groups_uuid))) + .inner_join( + users_organizations::table.on(users_organizations::uuid.eq(groups_users::users_organizations_uuid)), + ) + .inner_join( + groups::table.on(groups::uuid + .eq(collections_groups::groups_uuid) + .and(groups::organizations_uuid.eq(users_organizations::org_uuid))), + ) .filter(users_organizations::user_uuid.eq(user_uuid)) .select((collections_groups::read_only, collections_groups::hide_passwords, collections_groups::manage)) .load::<(bool, bool, bool)>(conn) .expect("Error getting group access restrictions") - }} + }) + .await } pub async fn is_write_accessible_to_user(&self, user_uuid: &UserId, conn: &DbConn) -> bool { @@ -760,7 +762,7 @@ impl Cipher { } pub async fn get_folder_uuid(&self, user_uuid: &UserId, conn: &DbConn) -> Option { - db_run! { conn: { + conn.run(move |conn| { folders_ciphers::table .inner_join(folders::table) .filter(folders::user_uuid.eq(&user_uuid)) @@ -768,16 +770,12 @@ impl Cipher { .select(folders_ciphers::folder_uuid) .first::(conn) .ok() - }} + }) + .await } pub async fn find_by_uuid(uuid: &CipherId, conn: &DbConn) -> Option { - db_run! { conn: { - ciphers::table - .filter(ciphers::uuid.eq(uuid)) - .first::(conn) - .ok() - }} + conn.run(move |conn| ciphers::table.filter(ciphers::uuid.eq(uuid)).first::(conn).ok()).await } pub async fn find_by_uuid_and_org( @@ -785,13 +783,14 @@ impl Cipher { org_uuid: &OrganizationId, conn: &DbConn, ) -> Option { - db_run! { conn: { + conn.run(move |conn| { ciphers::table .filter(ciphers::uuid.eq(cipher_uuid)) .filter(ciphers::organization_uuid.eq(org_uuid)) .first::(conn) .ok() - }} + }) + .await } // Find all ciphers accessible or visible to the specified user. @@ -813,32 +812,35 @@ impl Cipher { conn: &DbConn, ) -> Vec { if CONFIG.org_groups_enabled() { - db_run! { conn: { + conn.run(move |conn| { let mut query = ciphers::table - .left_join(ciphers_collections::table.on( - ciphers::uuid.eq(ciphers_collections::cipher_uuid) - )) - .left_join(users_organizations::table.on( - ciphers::organization_uuid.eq(users_organizations::org_uuid.nullable()) + .left_join(ciphers_collections::table.on(ciphers::uuid.eq(ciphers_collections::cipher_uuid))) + .left_join( + users_organizations::table.on(ciphers::organization_uuid + .eq(users_organizations::org_uuid.nullable()) .and(users_organizations::user_uuid.eq(user_uuid)) - .and(users_organizations::status.eq(MembershipStatus::Confirmed as i32)) - )) - .left_join(users_collections::table.on( - ciphers_collections::collection_uuid.eq(users_collections::collection_uuid) + .and(users_organizations::status.eq(MembershipStatus::Confirmed as i32))), + ) + .left_join( + users_collections::table.on(ciphers_collections::collection_uuid + .eq(users_collections::collection_uuid) // Ensure that users_collections::user_uuid is NULL for unconfirmed users. - .and(users_organizations::user_uuid.eq(users_collections::user_uuid)) - )) - .left_join(groups_users::table.on( - groups_users::users_organizations_uuid.eq(users_organizations::uuid) - )) - .left_join(groups::table.on(groups::uuid.eq(groups_users::groups_uuid) - // Ensure that group and membership belong to the same org - .and(groups::organizations_uuid.eq(users_organizations::org_uuid)) - )) - .left_join(collections_groups::table.on( - collections_groups::collections_uuid.eq(ciphers_collections::collection_uuid) - .and(collections_groups::groups_uuid.eq(groups::uuid)) - )) + .and(users_organizations::user_uuid.eq(users_collections::user_uuid))), + ) + .left_join( + groups_users::table.on(groups_users::users_organizations_uuid.eq(users_organizations::uuid)), + ) + .left_join( + groups::table.on(groups::uuid + .eq(groups_users::groups_uuid) + // Ensure that group and membership belong to the same org + .and(groups::organizations_uuid.eq(users_organizations::org_uuid))), + ) + .left_join( + collections_groups::table.on(collections_groups::collections_uuid + .eq(ciphers_collections::collection_uuid) + .and(collections_groups::groups_uuid.eq(groups::uuid))), + ) .filter(ciphers::user_uuid.eq(user_uuid)) // Cipher owner .or_filter(users_organizations::access_all.eq(true)) // access_all in org .or_filter(users_collections::user_uuid.eq(user_uuid)) // Access to collection @@ -848,39 +850,34 @@ impl Cipher { if !visible_only { query = query.or_filter( - users_organizations::atype.le(MembershipType::Admin as i32) // Org admin/owner + users_organizations::atype.le(MembershipType::Admin as i32), // Org admin/owner ); } // Only filter for one specific cipher if !cipher_uuids.is_empty() { - query = query.filter( - ciphers::uuid.eq_any(cipher_uuids) - ); + query = query.filter(ciphers::uuid.eq_any(cipher_uuids)); } - query - .select(ciphers::all_columns) - .distinct() - .load::(conn) - .expect("Error loading ciphers") - }} + query.select(ciphers::all_columns).distinct().load::(conn).expect("Error loading ciphers") + }) + .await } else { - db_run! { conn: { + conn.run(move |conn| { let mut query = ciphers::table - .left_join(ciphers_collections::table.on( - ciphers::uuid.eq(ciphers_collections::cipher_uuid) - )) - .left_join(users_organizations::table.on( - ciphers::organization_uuid.eq(users_organizations::org_uuid.nullable()) + .left_join(ciphers_collections::table.on(ciphers::uuid.eq(ciphers_collections::cipher_uuid))) + .left_join( + users_organizations::table.on(ciphers::organization_uuid + .eq(users_organizations::org_uuid.nullable()) .and(users_organizations::user_uuid.eq(user_uuid)) - .and(users_organizations::status.eq(MembershipStatus::Confirmed as i32)) - )) - .left_join(users_collections::table.on( - ciphers_collections::collection_uuid.eq(users_collections::collection_uuid) + .and(users_organizations::status.eq(MembershipStatus::Confirmed as i32))), + ) + .left_join( + users_collections::table.on(ciphers_collections::collection_uuid + .eq(users_collections::collection_uuid) // Ensure that users_collections::user_uuid is NULL for unconfirmed users. - .and(users_organizations::user_uuid.eq(users_collections::user_uuid)) - )) + .and(users_organizations::user_uuid.eq(users_collections::user_uuid))), + ) .filter(ciphers::user_uuid.eq(user_uuid)) // Cipher owner .or_filter(users_organizations::access_all.eq(true)) // access_all in org .or_filter(users_collections::user_uuid.eq(user_uuid)) // Access to collection @@ -888,23 +885,18 @@ impl Cipher { if !visible_only { query = query.or_filter( - users_organizations::atype.le(MembershipType::Admin as i32) // Org admin/owner + users_organizations::atype.le(MembershipType::Admin as i32), // Org admin/owner ); } // Only filter for one specific cipher if !cipher_uuids.is_empty() { - query = query.filter( - ciphers::uuid.eq_any(cipher_uuids) - ); + query = query.filter(ciphers::uuid.eq_any(cipher_uuids)); } - query - .select(ciphers::all_columns) - .distinct() - .load::(conn) - .expect("Error loading ciphers") - }} + query.select(ciphers::all_columns).distinct().load::(conn).expect("Error loading ciphers") + }) + .await } } @@ -927,193 +919,208 @@ impl Cipher { // Find all ciphers directly owned by the specified user. pub async fn find_owned_by_user(user_uuid: &UserId, conn: &DbConn) -> Vec { - db_run! { conn: { + conn.run(move |conn| { ciphers::table - .filter( - ciphers::user_uuid.eq(user_uuid) - .and(ciphers::organization_uuid.is_null()) - ) + .filter(ciphers::user_uuid.eq(user_uuid).and(ciphers::organization_uuid.is_null())) .load::(conn) .expect("Error loading ciphers") - }} + }) + .await } pub async fn count_owned_by_user(user_uuid: &UserId, conn: &DbConn) -> i64 { - db_run! { conn: { - ciphers::table - .filter(ciphers::user_uuid.eq(user_uuid)) - .count() - .first::(conn) - .ok() - .unwrap_or(0) - }} + conn.run(move |conn| { + ciphers::table.filter(ciphers::user_uuid.eq(user_uuid)).count().first::(conn).ok().unwrap_or(0) + }) + .await } pub async fn find_by_org(org_uuid: &OrganizationId, conn: &DbConn) -> Vec { - db_run! { conn: { + conn.run(move |conn| { ciphers::table .filter(ciphers::organization_uuid.eq(org_uuid)) .load::(conn) .expect("Error loading ciphers") - }} + }) + .await } pub async fn count_by_org(org_uuid: &OrganizationId, conn: &DbConn) -> i64 { - db_run! { conn: { - ciphers::table - .filter(ciphers::organization_uuid.eq(org_uuid)) - .count() - .first::(conn) - .ok() - .unwrap_or(0) - }} + conn.run(move |conn| { + ciphers::table.filter(ciphers::organization_uuid.eq(org_uuid)).count().first::(conn).ok().unwrap_or(0) + }) + .await } pub async fn find_by_folder(folder_uuid: &FolderId, conn: &DbConn) -> Vec { - db_run! { conn: { - folders_ciphers::table.inner_join(ciphers::table) + conn.run(move |conn| { + folders_ciphers::table + .inner_join(ciphers::table) .filter(folders_ciphers::folder_uuid.eq(folder_uuid)) .select(ciphers::all_columns) .load::(conn) .expect("Error loading ciphers") - }} + }) + .await } /// Find all ciphers that were deleted before the specified datetime. pub async fn find_deleted_before(dt: &NaiveDateTime, conn: &DbConn) -> Vec { - db_run! { conn: { - ciphers::table - .filter(ciphers::deleted_at.lt(dt)) - .load::(conn) - .expect("Error loading ciphers") - }} + conn.run(move |conn| { + ciphers::table.filter(ciphers::deleted_at.lt(dt)).load::(conn).expect("Error loading ciphers") + }) + .await } pub async fn get_collections(&self, user_uuid: UserId, conn: &DbConn) -> Vec { if CONFIG.org_groups_enabled() { - db_run! { conn: { + conn.run(move |conn| { ciphers_collections::table .filter(ciphers_collections::cipher_uuid.eq(&self.uuid)) - .inner_join(collections::table.on( - collections::uuid.eq(ciphers_collections::collection_uuid) - )) - .left_join(users_organizations::table.on( - users_organizations::org_uuid.eq(collections::org_uuid) - .and(users_organizations::user_uuid.eq(user_uuid.clone())) - )) - .left_join(users_collections::table.on( - users_collections::collection_uuid.eq(ciphers_collections::collection_uuid) - .and(users_collections::user_uuid.eq(user_uuid.clone())) - )) - .left_join(groups_users::table.on( - groups_users::users_organizations_uuid.eq(users_organizations::uuid) - )) - .left_join(groups::table.on(groups::uuid.eq(groups_users::groups_uuid) - .and(groups::organizations_uuid.eq(users_organizations::org_uuid)) - )) - .left_join(collections_groups::table.on( - collections_groups::collections_uuid.eq(ciphers_collections::collection_uuid) - .and(collections_groups::groups_uuid.eq(groups::uuid)) - )) - .filter(users_organizations::access_all.eq(true) // User has access all - .or(users_collections::user_uuid.eq(user_uuid) // User has access to collection - .and(users_collections::read_only.eq(false))) - .or(groups::access_all.eq(true)) // Access via groups - .or(collections_groups::collections_uuid.is_not_null() // Access via groups - .and(collections_groups::read_only.eq(false))) + .inner_join(collections::table.on(collections::uuid.eq(ciphers_collections::collection_uuid))) + .left_join( + users_organizations::table.on(users_organizations::org_uuid + .eq(collections::org_uuid) + .and(users_organizations::user_uuid.eq(user_uuid.clone()))), + ) + .left_join( + users_collections::table.on(users_collections::collection_uuid + .eq(ciphers_collections::collection_uuid) + .and(users_collections::user_uuid.eq(user_uuid.clone()))), + ) + .left_join( + groups_users::table.on(groups_users::users_organizations_uuid.eq(users_organizations::uuid)), + ) + .left_join( + groups::table.on(groups::uuid + .eq(groups_users::groups_uuid) + .and(groups::organizations_uuid.eq(users_organizations::org_uuid))), + ) + .left_join( + collections_groups::table.on(collections_groups::collections_uuid + .eq(ciphers_collections::collection_uuid) + .and(collections_groups::groups_uuid.eq(groups::uuid))), + ) + .filter( + users_organizations::access_all + .eq(true) // User has access all + .or(users_collections::user_uuid + .eq(user_uuid) // User has access to collection + .and(users_collections::read_only.eq(false))) + .or(groups::access_all.eq(true)) // Access via groups + .or(collections_groups::collections_uuid + .is_not_null() // Access via groups + .and(collections_groups::read_only.eq(false))), ) .select(ciphers_collections::collection_uuid) .load::(conn) .unwrap_or_default() - }} + }) + .await } else { - db_run! { conn: { + conn.run(move |conn| { ciphers_collections::table .filter(ciphers_collections::cipher_uuid.eq(&self.uuid)) - .inner_join(collections::table.on( - collections::uuid.eq(ciphers_collections::collection_uuid) - )) - .inner_join(users_organizations::table.on( - users_organizations::org_uuid.eq(collections::org_uuid) - .and(users_organizations::user_uuid.eq(user_uuid.clone())) - )) - .left_join(users_collections::table.on( - users_collections::collection_uuid.eq(ciphers_collections::collection_uuid) - .and(users_collections::user_uuid.eq(user_uuid.clone())) - )) - .filter(users_organizations::access_all.eq(true) // User has access all - .or(users_collections::user_uuid.eq(user_uuid) // User has access to collection - .and(users_collections::read_only.eq(false))) + .inner_join(collections::table.on(collections::uuid.eq(ciphers_collections::collection_uuid))) + .inner_join( + users_organizations::table.on(users_organizations::org_uuid + .eq(collections::org_uuid) + .and(users_organizations::user_uuid.eq(user_uuid.clone()))), + ) + .left_join( + users_collections::table.on(users_collections::collection_uuid + .eq(ciphers_collections::collection_uuid) + .and(users_collections::user_uuid.eq(user_uuid.clone()))), + ) + .filter( + users_organizations::access_all + .eq(true) // User has access all + .or(users_collections::user_uuid + .eq(user_uuid) // User has access to collection + .and(users_collections::read_only.eq(false))), ) .select(ciphers_collections::collection_uuid) .load::(conn) .unwrap_or_default() - }} + }) + .await } } pub async fn get_admin_collections(&self, user_uuid: UserId, conn: &DbConn) -> Vec { if CONFIG.org_groups_enabled() { - db_run! { conn: { + conn.run(move |conn| { ciphers_collections::table .filter(ciphers_collections::cipher_uuid.eq(&self.uuid)) - .inner_join(collections::table.on( - collections::uuid.eq(ciphers_collections::collection_uuid) - )) - .left_join(users_organizations::table.on( - users_organizations::org_uuid.eq(collections::org_uuid) - .and(users_organizations::user_uuid.eq(user_uuid.clone())) - )) - .left_join(users_collections::table.on( - users_collections::collection_uuid.eq(ciphers_collections::collection_uuid) - .and(users_collections::user_uuid.eq(user_uuid.clone())) - )) - .left_join(groups_users::table.on( - groups_users::users_organizations_uuid.eq(users_organizations::uuid) - )) - .left_join(groups::table.on(groups::uuid.eq(groups_users::groups_uuid) - .and(groups::organizations_uuid.eq(users_organizations::org_uuid)) - )) - .left_join(collections_groups::table.on( - collections_groups::collections_uuid.eq(ciphers_collections::collection_uuid) - .and(collections_groups::groups_uuid.eq(groups::uuid)) - )) - .filter(users_organizations::access_all.eq(true) // User has access all - .or(users_collections::user_uuid.eq(user_uuid) // User has access to collection - .and(users_collections::read_only.eq(false))) - .or(groups::access_all.eq(true)) // Access via groups - .or(collections_groups::collections_uuid.is_not_null() // Access via groups - .and(collections_groups::read_only.eq(false))) - .or(users_organizations::atype.le(MembershipType::Admin as i32)) // User is admin or owner + .inner_join(collections::table.on(collections::uuid.eq(ciphers_collections::collection_uuid))) + .left_join( + users_organizations::table.on(users_organizations::org_uuid + .eq(collections::org_uuid) + .and(users_organizations::user_uuid.eq(user_uuid.clone()))), + ) + .left_join( + users_collections::table.on(users_collections::collection_uuid + .eq(ciphers_collections::collection_uuid) + .and(users_collections::user_uuid.eq(user_uuid.clone()))), + ) + .left_join( + groups_users::table.on(groups_users::users_organizations_uuid.eq(users_organizations::uuid)), + ) + .left_join( + groups::table.on(groups::uuid + .eq(groups_users::groups_uuid) + .and(groups::organizations_uuid.eq(users_organizations::org_uuid))), + ) + .left_join( + collections_groups::table.on(collections_groups::collections_uuid + .eq(ciphers_collections::collection_uuid) + .and(collections_groups::groups_uuid.eq(groups::uuid))), + ) + .filter( + users_organizations::access_all + .eq(true) // User has access all + .or(users_collections::user_uuid + .eq(user_uuid) // User has access to collection + .and(users_collections::read_only.eq(false))) + .or(groups::access_all.eq(true)) // Access via groups + .or(collections_groups::collections_uuid + .is_not_null() // Access via groups + .and(collections_groups::read_only.eq(false))) + .or(users_organizations::atype.le(MembershipType::Admin as i32)), // User is admin or owner ) .select(ciphers_collections::collection_uuid) .load::(conn) .unwrap_or_default() - }} + }) + .await } else { - db_run! { conn: { + conn.run(move |conn| { ciphers_collections::table .filter(ciphers_collections::cipher_uuid.eq(&self.uuid)) - .inner_join(collections::table.on( - collections::uuid.eq(ciphers_collections::collection_uuid) - )) - .inner_join(users_organizations::table.on( - users_organizations::org_uuid.eq(collections::org_uuid) - .and(users_organizations::user_uuid.eq(user_uuid.clone())) - )) - .left_join(users_collections::table.on( - users_collections::collection_uuid.eq(ciphers_collections::collection_uuid) - .and(users_collections::user_uuid.eq(user_uuid.clone())) - )) - .filter(users_organizations::access_all.eq(true) // User has access all - .or(users_collections::user_uuid.eq(user_uuid) // User has access to collection - .and(users_collections::read_only.eq(false))) - .or(users_organizations::atype.le(MembershipType::Admin as i32)) // User is admin or owner + .inner_join(collections::table.on(collections::uuid.eq(ciphers_collections::collection_uuid))) + .inner_join( + users_organizations::table.on(users_organizations::org_uuid + .eq(collections::org_uuid) + .and(users_organizations::user_uuid.eq(user_uuid.clone()))), + ) + .left_join( + users_collections::table.on(users_collections::collection_uuid + .eq(ciphers_collections::collection_uuid) + .and(users_collections::user_uuid.eq(user_uuid.clone()))), + ) + .filter( + users_organizations::access_all + .eq(true) // User has access all + .or(users_collections::user_uuid + .eq(user_uuid) // User has access to collection + .and(users_collections::read_only.eq(false))) + .or(users_organizations::atype.le(MembershipType::Admin as i32)), // User is admin or owner ) .select(ciphers_collections::collection_uuid) .load::(conn) .unwrap_or_default() - }} + }) + .await } } @@ -1123,42 +1130,41 @@ impl Cipher { user_uuid: UserId, conn: &DbConn, ) -> Vec<(CipherId, CollectionId)> { - db_run! { conn: { + conn.run(move |conn| { ciphers_collections::table - .inner_join(collections::table.on( - collections::uuid.eq(ciphers_collections::collection_uuid) - )) - .inner_join(users_organizations::table.on( - users_organizations::org_uuid.eq(collections::org_uuid).and( - users_organizations::user_uuid.eq(user_uuid.clone()) + .inner_join(collections::table.on(collections::uuid.eq(ciphers_collections::collection_uuid))) + .inner_join( + users_organizations::table.on(users_organizations::org_uuid + .eq(collections::org_uuid) + .and(users_organizations::user_uuid.eq(user_uuid.clone()))), ) - )) - .left_join(users_collections::table.on( - users_collections::collection_uuid.eq(ciphers_collections::collection_uuid).and( - users_collections::user_uuid.eq(user_uuid.clone()) + .left_join( + users_collections::table.on(users_collections::collection_uuid + .eq(ciphers_collections::collection_uuid) + .and(users_collections::user_uuid.eq(user_uuid.clone()))), ) - )) - .left_join(groups_users::table.on( - groups_users::users_organizations_uuid.eq(users_organizations::uuid) - )) - .left_join(groups::table.on(groups::uuid.eq(groups_users::groups_uuid) - .and(groups::organizations_uuid.eq(users_organizations::org_uuid)) - )) - .left_join(collections_groups::table.on( - collections_groups::collections_uuid.eq(ciphers_collections::collection_uuid).and( - collections_groups::groups_uuid.eq(groups::uuid) + .left_join(groups_users::table.on(groups_users::users_organizations_uuid.eq(users_organizations::uuid))) + .left_join( + groups::table.on(groups::uuid + .eq(groups_users::groups_uuid) + .and(groups::organizations_uuid.eq(users_organizations::org_uuid))), ) - )) - .or_filter(users_collections::user_uuid.eq(user_uuid)) // User has access to collection - .or_filter(users_organizations::access_all.eq(true)) // User has access all - .or_filter(users_organizations::atype.le(MembershipType::Admin as i32)) // User is admin or owner - .or_filter(groups::access_all.eq(true)) //Access via group - .or_filter(collections_groups::collections_uuid.is_not_null()) //Access via group - .select(ciphers_collections::all_columns) - .distinct() - .load::<(CipherId, CollectionId)>(conn) - .unwrap_or_default() - }} + .left_join( + collections_groups::table.on(collections_groups::collections_uuid + .eq(ciphers_collections::collection_uuid) + .and(collections_groups::groups_uuid.eq(groups::uuid))), + ) + .or_filter(users_collections::user_uuid.eq(user_uuid)) // User has access to collection + .or_filter(users_organizations::access_all.eq(true)) // User has access all + .or_filter(users_organizations::atype.le(MembershipType::Admin as i32)) // User is admin or owner + .or_filter(groups::access_all.eq(true)) //Access via group + .or_filter(collections_groups::collections_uuid.is_not_null()) //Access via group + .select(ciphers_collections::all_columns) + .distinct() + .load::<(CipherId, CollectionId)>(conn) + .unwrap_or_default() + }) + .await } } diff --git a/src/db/models/collection.rs b/src/db/models/collection.rs index b1f82335..f29843f7 100644 --- a/src/db/models/collection.rs +++ b/src/db/models/collection.rs @@ -1,16 +1,25 @@ use derive_more::{AsRef, Deref, Display, From}; +use diesel::prelude::*; use serde_json::Value; +use crate::{ + CONFIG, + api::EmptyResult, + db::{ + DbConn, + schema::{ + ciphers_collections, collections, collections_groups, groups, groups_users, users_collections, + users_organizations, + }, + }, + error::MapResult, +}; +use macros::UuidFromParam; + use super::{ CipherId, CollectionGroup, GroupUser, Membership, MembershipId, MembershipStatus, MembershipType, OrganizationId, User, UserId, }; -use crate::db::schema::{ - ciphers_collections, collections, collections_groups, groups, groups_users, users_collections, users_organizations, -}; -use crate::CONFIG; -use diesel::prelude::*; -use macros::UuidFromParam; #[derive(Identifiable, Queryable, Insertable, AsChangeset)] #[diesel(table_name = collections)] @@ -74,7 +83,7 @@ impl Collection { if external_id.is_empty() { self.external_id = None; } else { - self.external_id = Some(external_id) + self.external_id = Some(external_id); } } None => self.external_id = None, @@ -147,11 +156,6 @@ impl Collection { } } -use crate::db::DbConn; - -use crate::api::EmptyResult; -use crate::error::MapResult; - /// Database methods impl Collection { pub async fn save(&self, conn: &DbConn) -> EmptyResult { @@ -193,11 +197,12 @@ impl Collection { CollectionUser::delete_all_by_collection(&self.uuid, conn).await?; CollectionGroup::delete_all_by_collection(&self.uuid, &self.org_uuid, conn).await?; - db_run! { conn: { + conn.run(move |conn| { diesel::delete(collections::table.filter(collections::uuid.eq(self.uuid))) .execute(conn) .map_res("Error deleting collection") - }} + }) + .await } pub async fn delete_all_by_organization(org_uuid: &OrganizationId, conn: &DbConn) -> EmptyResult { @@ -208,90 +213,90 @@ impl Collection { } pub async fn update_users_revision(&self, conn: &DbConn) { - for member in Membership::find_by_collection_and_org(&self.uuid, &self.org_uuid, conn).await.iter() { + for member in &Membership::find_by_collection_and_org(&self.uuid, &self.org_uuid, conn).await { User::update_uuid_revision(&member.user_uuid, conn).await; } } pub async fn find_by_uuid(uuid: &CollectionId, conn: &DbConn) -> Option { - db_run! { conn: { - collections::table - .filter(collections::uuid.eq(uuid)) - .first::(conn) - .ok() - }} + conn.run(move |conn| collections::table.filter(collections::uuid.eq(uuid)).first::(conn).ok()).await } pub async fn find_by_user_uuid(user_uuid: UserId, conn: &DbConn) -> Vec { if CONFIG.org_groups_enabled() { - db_run! { conn: { + conn.run(move |conn| { collections::table - .left_join(users_collections::table.on( - users_collections::collection_uuid.eq(collections::uuid).and( - users_collections::user_uuid.eq(user_uuid.clone()) + .left_join( + users_collections::table.on(users_collections::collection_uuid + .eq(collections::uuid) + .and(users_collections::user_uuid.eq(user_uuid.clone()))), ) - )) - .left_join(users_organizations::table.on( - collections::org_uuid.eq(users_organizations::org_uuid).and( - users_organizations::user_uuid.eq(user_uuid.clone()) + .left_join( + users_organizations::table.on(collections::org_uuid + .eq(users_organizations::org_uuid) + .and(users_organizations::user_uuid.eq(user_uuid.clone()))), ) - )) - .left_join(groups_users::table.on( - groups_users::users_organizations_uuid.eq(users_organizations::uuid) - )) - .left_join(groups::table.on(groups::uuid.eq(groups_users::groups_uuid) - .and(groups::organizations_uuid.eq(users_organizations::org_uuid)) - )) - .left_join(collections_groups::table.on( - collections_groups::groups_uuid.eq(groups_users::groups_uuid).and( - collections_groups::collections_uuid.eq(collections::uuid) + .left_join( + groups_users::table.on(groups_users::users_organizations_uuid.eq(users_organizations::uuid)), ) - )) - .filter( - users_organizations::status.eq(MembershipStatus::Confirmed as i32) - ) - .filter( - users_collections::user_uuid.eq(user_uuid).or( // Directly accessed collection - users_organizations::access_all.eq(true) // access_all in Organization - ).or( - groups::access_all.eq(true) // access_all in groups - ).or( // access via groups - groups_users::users_organizations_uuid.eq(users_organizations::uuid).and( - collections_groups::collections_uuid.is_not_null() - ) + .left_join( + groups::table.on(groups::uuid + .eq(groups_users::groups_uuid) + .and(groups::organizations_uuid.eq(users_organizations::org_uuid))), ) - ) - .select(collections::all_columns) - .distinct() - .load::(conn) - .expect("Error loading collections") - }} + .left_join( + collections_groups::table.on(collections_groups::groups_uuid + .eq(groups_users::groups_uuid) + .and(collections_groups::collections_uuid.eq(collections::uuid))), + ) + .filter(users_organizations::status.eq(MembershipStatus::Confirmed as i32)) + .filter( + users_collections::user_uuid + .eq(user_uuid) + .or( + // Directly accessed collection + users_organizations::access_all.eq(true), // access_all in Organization + ) + .or( + groups::access_all.eq(true), // access_all in groups + ) + .or( + // access via groups + groups_users::users_organizations_uuid + .eq(users_organizations::uuid) + .and(collections_groups::collections_uuid.is_not_null()), + ), + ) + .select(collections::all_columns) + .distinct() + .load::(conn) + .expect("Error loading collections") + }) + .await } else { - db_run! { conn: { + conn.run(move |conn| { collections::table - .left_join(users_collections::table.on( - users_collections::collection_uuid.eq(collections::uuid).and( - users_collections::user_uuid.eq(user_uuid.clone()) + .left_join( + users_collections::table.on(users_collections::collection_uuid + .eq(collections::uuid) + .and(users_collections::user_uuid.eq(user_uuid.clone()))), ) - )) - .left_join(users_organizations::table.on( - collections::org_uuid.eq(users_organizations::org_uuid).and( - users_organizations::user_uuid.eq(user_uuid.clone()) + .left_join( + users_organizations::table.on(collections::org_uuid + .eq(users_organizations::org_uuid) + .and(users_organizations::user_uuid.eq(user_uuid.clone()))), ) - )) - .filter( - users_organizations::status.eq(MembershipStatus::Confirmed as i32) - ) - .filter( - users_collections::user_uuid.eq(user_uuid).or( // Directly accessed collection - users_organizations::access_all.eq(true) // access_all in Organization - ) - ) - .select(collections::all_columns) - .distinct() - .load::(conn) - .expect("Error loading collections") - }} + .filter(users_organizations::status.eq(MembershipStatus::Confirmed as i32)) + .filter(users_collections::user_uuid.eq(user_uuid).or( + // Directly accessed collection + users_organizations::access_all.eq(true), // access_all in Organization + )) + .select(collections::all_columns) + .distinct() + .load::(conn) + .expect("Error loading collections") + }) + .await } } @@ -308,256 +313,311 @@ impl Collection { } pub async fn find_by_organization(org_uuid: &OrganizationId, conn: &DbConn) -> Vec { - db_run! { conn: { + conn.run(move |conn| { collections::table .filter(collections::org_uuid.eq(org_uuid)) .load::(conn) .expect("Error loading collections") - }} + }) + .await } pub async fn count_by_org(org_uuid: &OrganizationId, conn: &DbConn) -> i64 { - db_run! { conn: { - collections::table - .filter(collections::org_uuid.eq(org_uuid)) - .count() - .first::(conn) - .ok() - .unwrap_or(0) - }} + conn.run(move |conn| { + collections::table.filter(collections::org_uuid.eq(org_uuid)).count().first::(conn).ok().unwrap_or(0) + }) + .await } pub async fn find_by_uuid_and_org(uuid: &CollectionId, org_uuid: &OrganizationId, conn: &DbConn) -> Option { - db_run! { conn: { + conn.run(move |conn| { collections::table .filter(collections::uuid.eq(uuid)) .filter(collections::org_uuid.eq(org_uuid)) .select(collections::all_columns) .first::(conn) .ok() - }} + }) + .await } pub async fn find_by_uuid_and_user(uuid: &CollectionId, user_uuid: UserId, conn: &DbConn) -> Option { if CONFIG.org_groups_enabled() { - db_run! { conn: { + conn.run(move |conn| { collections::table - .left_join(users_collections::table.on( - users_collections::collection_uuid.eq(collections::uuid).and( - users_collections::user_uuid.eq(user_uuid.clone()) + .left_join( + users_collections::table.on(users_collections::collection_uuid + .eq(collections::uuid) + .and(users_collections::user_uuid.eq(user_uuid.clone()))), ) - )) - .left_join(users_organizations::table.on( - collections::org_uuid.eq(users_organizations::org_uuid).and( - users_organizations::user_uuid.eq(user_uuid) + .left_join( + users_organizations::table.on(collections::org_uuid + .eq(users_organizations::org_uuid) + .and(users_organizations::user_uuid.eq(user_uuid))), ) - )) - .left_join(groups_users::table.on( - groups_users::users_organizations_uuid.eq(users_organizations::uuid) - )) - .left_join(groups::table.on(groups::uuid.eq(groups_users::groups_uuid) - .and(groups::organizations_uuid.eq(users_organizations::org_uuid)) - )) - .left_join(collections_groups::table.on( - collections_groups::groups_uuid.eq(groups_users::groups_uuid).and( - collections_groups::collections_uuid.eq(collections::uuid) + .left_join( + groups_users::table.on(groups_users::users_organizations_uuid.eq(users_organizations::uuid)), ) - )) - .filter(collections::uuid.eq(uuid)) - .filter( - users_collections::collection_uuid.eq(uuid).or( // Directly accessed collection - users_organizations::access_all.eq(true).or( // access_all in Organization - users_organizations::atype.le(MembershipType::Admin as i32) // Org admin or owner - )).or( - groups::access_all.eq(true) // access_all in groups - ).or( // access via groups - groups_users::users_organizations_uuid.eq(users_organizations::uuid).and( - collections_groups::collections_uuid.is_not_null() - ) + .left_join( + groups::table.on(groups::uuid + .eq(groups_users::groups_uuid) + .and(groups::organizations_uuid.eq(users_organizations::org_uuid))), ) - ).select(collections::all_columns) - .first::(conn) - .ok() - }} + .left_join( + collections_groups::table.on(collections_groups::groups_uuid + .eq(groups_users::groups_uuid) + .and(collections_groups::collections_uuid.eq(collections::uuid))), + ) + .filter(collections::uuid.eq(uuid)) + .filter( + users_collections::collection_uuid + .eq(uuid) + .or( + // Directly accessed collection + users_organizations::access_all.eq(true).or( + // access_all in Organization + users_organizations::atype.le(MembershipType::Admin as i32), // Org admin or owner + ), + ) + .or( + groups::access_all.eq(true), // access_all in groups + ) + .or( + // access via groups + groups_users::users_organizations_uuid + .eq(users_organizations::uuid) + .and(collections_groups::collections_uuid.is_not_null()), + ), + ) + .select(collections::all_columns) + .first::(conn) + .ok() + }) + .await } else { - db_run! { conn: { + conn.run(move |conn| { collections::table - .left_join(users_collections::table.on( - users_collections::collection_uuid.eq(collections::uuid).and( - users_collections::user_uuid.eq(user_uuid.clone()) + .left_join( + users_collections::table.on(users_collections::collection_uuid + .eq(collections::uuid) + .and(users_collections::user_uuid.eq(user_uuid.clone()))), ) - )) - .left_join(users_organizations::table.on( - collections::org_uuid.eq(users_organizations::org_uuid).and( - users_organizations::user_uuid.eq(user_uuid) + .left_join( + users_organizations::table.on(collections::org_uuid + .eq(users_organizations::org_uuid) + .and(users_organizations::user_uuid.eq(user_uuid))), ) - )) - .filter(collections::uuid.eq(uuid)) - .filter( - users_collections::collection_uuid.eq(uuid).or( // Directly accessed collection - users_organizations::access_all.eq(true).or( // access_all in Organization - users_organizations::atype.le(MembershipType::Admin as i32) // Org admin or owner + .filter(collections::uuid.eq(uuid)) + .filter(users_collections::collection_uuid.eq(uuid).or( + // Directly accessed collection + users_organizations::access_all.eq(true).or( + // access_all in Organization + users_organizations::atype.le(MembershipType::Admin as i32), // Org admin or owner + ), )) - ).select(collections::all_columns) - .first::(conn) - .ok() - }} + .select(collections::all_columns) + .first::(conn) + .ok() + }) + .await } } pub async fn is_writable_by_user(&self, user_uuid: &UserId, conn: &DbConn) -> bool { let user_uuid = user_uuid.to_string(); if CONFIG.org_groups_enabled() { - db_run! { conn: { + conn.run(move |conn| { collections::table .filter(collections::uuid.eq(&self.uuid)) - .inner_join(users_organizations::table.on( - collections::org_uuid.eq(users_organizations::org_uuid) - .and(users_organizations::user_uuid.eq(user_uuid.clone())) - )) - .left_join(users_collections::table.on( - users_collections::collection_uuid.eq(collections::uuid) - .and(users_collections::user_uuid.eq(user_uuid)) - )) - .left_join(groups_users::table.on( - groups_users::users_organizations_uuid.eq(users_organizations::uuid) - )) - .left_join(groups::table.on(groups::uuid.eq(groups_users::groups_uuid) - .and(groups::organizations_uuid.eq(users_organizations::org_uuid)) - )) - .left_join(collections_groups::table.on( - collections_groups::groups_uuid.eq(groups_users::groups_uuid) - .and(collections_groups::collections_uuid.eq(collections::uuid)) - )) - .filter(users_organizations::atype.le(MembershipType::Admin as i32) // Org admin or owner - .or(users_organizations::access_all.eq(true)) // access_all via membership - .or(users_collections::collection_uuid.eq(&self.uuid) // write access given to collection - .and(users_collections::read_only.eq(false))) - .or(groups::access_all.eq(true)) // access_all via group - .or(collections_groups::collections_uuid.is_not_null() // write access given via group - .and(collections_groups::read_only.eq(false))) + .inner_join( + users_organizations::table.on(collections::org_uuid + .eq(users_organizations::org_uuid) + .and(users_organizations::user_uuid.eq(user_uuid.clone()))), + ) + .left_join( + users_collections::table.on(users_collections::collection_uuid + .eq(collections::uuid) + .and(users_collections::user_uuid.eq(user_uuid))), + ) + .left_join( + groups_users::table.on(groups_users::users_organizations_uuid.eq(users_organizations::uuid)), + ) + .left_join( + groups::table.on(groups::uuid + .eq(groups_users::groups_uuid) + .and(groups::organizations_uuid.eq(users_organizations::org_uuid))), + ) + .left_join( + collections_groups::table.on(collections_groups::groups_uuid + .eq(groups_users::groups_uuid) + .and(collections_groups::collections_uuid.eq(collections::uuid))), + ) + .filter( + users_organizations::atype + .le(MembershipType::Admin as i32) // Org admin or owner + .or(users_organizations::access_all.eq(true)) // access_all via membership + .or(users_collections::collection_uuid + .eq(&self.uuid) // write access given to collection + .and(users_collections::read_only.eq(false))) + .or(groups::access_all.eq(true)) // access_all via group + .or(collections_groups::collections_uuid + .is_not_null() // write access given via group + .and(collections_groups::read_only.eq(false))), ) .count() .first::(conn) .ok() - .unwrap_or(0) != 0 - }} + .unwrap_or(0) + != 0 + }) + .await } else { - db_run! { conn: { + conn.run(move |conn| { collections::table .filter(collections::uuid.eq(&self.uuid)) - .inner_join(users_organizations::table.on( - collections::org_uuid.eq(users_organizations::org_uuid) - .and(users_organizations::user_uuid.eq(user_uuid.clone())) - )) - .left_join(users_collections::table.on( - users_collections::collection_uuid.eq(collections::uuid) - .and(users_collections::user_uuid.eq(user_uuid)) - )) - .filter(users_organizations::atype.le(MembershipType::Admin as i32) // Org admin or owner - .or(users_organizations::access_all.eq(true)) // access_all via membership - .or(users_collections::collection_uuid.eq(&self.uuid) // write access given to collection - .and(users_collections::read_only.eq(false))) + .inner_join( + users_organizations::table.on(collections::org_uuid + .eq(users_organizations::org_uuid) + .and(users_organizations::user_uuid.eq(user_uuid.clone()))), + ) + .left_join( + users_collections::table.on(users_collections::collection_uuid + .eq(collections::uuid) + .and(users_collections::user_uuid.eq(user_uuid))), + ) + .filter( + users_organizations::atype + .le(MembershipType::Admin as i32) // Org admin or owner + .or(users_organizations::access_all.eq(true)) // access_all via membership + .or(users_collections::collection_uuid + .eq(&self.uuid) // write access given to collection + .and(users_collections::read_only.eq(false))), ) .count() .first::(conn) .ok() - .unwrap_or(0) != 0 - }} + .unwrap_or(0) + != 0 + }) + .await } } pub async fn hide_passwords_for_user(&self, user_uuid: &UserId, conn: &DbConn) -> bool { let user_uuid = user_uuid.to_string(); - db_run! { conn: { + conn.run(move |conn| { collections::table - .left_join(users_collections::table.on( - users_collections::collection_uuid.eq(collections::uuid).and( - users_collections::user_uuid.eq(user_uuid.clone()) + .left_join( + users_collections::table.on(users_collections::collection_uuid + .eq(collections::uuid) + .and(users_collections::user_uuid.eq(user_uuid.clone()))), ) - )) - .left_join(users_organizations::table.on( - collections::org_uuid.eq(users_organizations::org_uuid).and( - users_organizations::user_uuid.eq(user_uuid) + .left_join( + users_organizations::table.on(collections::org_uuid + .eq(users_organizations::org_uuid) + .and(users_organizations::user_uuid.eq(user_uuid))), ) - )) - .left_join(groups_users::table.on( - groups_users::users_organizations_uuid.eq(users_organizations::uuid) - )) - .left_join(groups::table.on(groups::uuid.eq(groups_users::groups_uuid) - .and(groups::organizations_uuid.eq(users_organizations::org_uuid)) - )) - .left_join(collections_groups::table.on( - collections_groups::groups_uuid.eq(groups_users::groups_uuid).and( - collections_groups::collections_uuid.eq(collections::uuid) + .left_join(groups_users::table.on(groups_users::users_organizations_uuid.eq(users_organizations::uuid))) + .left_join( + groups::table.on(groups::uuid + .eq(groups_users::groups_uuid) + .and(groups::organizations_uuid.eq(users_organizations::org_uuid))), ) - )) - .filter(collections::uuid.eq(&self.uuid)) - .filter( - users_collections::collection_uuid.eq(&self.uuid).and(users_collections::hide_passwords.eq(true)).or(// Directly accessed collection - users_organizations::access_all.eq(true).or( // access_all in Organization - users_organizations::atype.le(MembershipType::Admin as i32) // Org admin or owner - )).or( - groups::access_all.eq(true) // access_all in groups - ).or( // access via groups - groups_users::users_organizations_uuid.eq(users_organizations::uuid).and( - collections_groups::collections_uuid.is_not_null().and( - collections_groups::hide_passwords.eq(true)) - ) + .left_join( + collections_groups::table.on(collections_groups::groups_uuid + .eq(groups_users::groups_uuid) + .and(collections_groups::collections_uuid.eq(collections::uuid))), ) - ) - .count() - .first::(conn) - .ok() - .unwrap_or(0) != 0 - }} + .filter(collections::uuid.eq(&self.uuid)) + .filter( + users_collections::collection_uuid + .eq(&self.uuid) + .and(users_collections::hide_passwords.eq(true)) + .or( + // Directly accessed collection + users_organizations::access_all.eq(true).or( + // access_all in Organization + users_organizations::atype.le(MembershipType::Admin as i32), // Org admin or owner + ), + ) + .or( + groups::access_all.eq(true), // access_all in groups + ) + .or( + // access via groups + groups_users::users_organizations_uuid.eq(users_organizations::uuid).and( + collections_groups::collections_uuid + .is_not_null() + .and(collections_groups::hide_passwords.eq(true)), + ), + ), + ) + .count() + .first::(conn) + .ok() + .unwrap_or(0) + != 0 + }) + .await } pub async fn is_coll_manageable_by_user(uuid: &CollectionId, user_uuid: &UserId, conn: &DbConn) -> bool { let uuid = uuid.to_string(); let user_uuid = user_uuid.to_string(); - db_run! { conn: { + conn.run(move |conn| { collections::table - .left_join(users_collections::table.on( - users_collections::collection_uuid.eq(collections::uuid).and( - users_collections::user_uuid.eq(user_uuid.clone()) + .left_join( + users_collections::table.on(users_collections::collection_uuid + .eq(collections::uuid) + .and(users_collections::user_uuid.eq(user_uuid.clone()))), ) - )) - .left_join(users_organizations::table.on( - collections::org_uuid.eq(users_organizations::org_uuid).and( - users_organizations::user_uuid.eq(user_uuid) + .left_join( + users_organizations::table.on(collections::org_uuid + .eq(users_organizations::org_uuid) + .and(users_organizations::user_uuid.eq(user_uuid))), ) - )) - .left_join(groups_users::table.on( - groups_users::users_organizations_uuid.eq(users_organizations::uuid) - )) - .left_join(groups::table.on(groups::uuid.eq(groups_users::groups_uuid) - .and(groups::organizations_uuid.eq(users_organizations::org_uuid)) - )) - .left_join(collections_groups::table.on( - collections_groups::groups_uuid.eq(groups_users::groups_uuid).and( - collections_groups::collections_uuid.eq(collections::uuid) + .left_join(groups_users::table.on(groups_users::users_organizations_uuid.eq(users_organizations::uuid))) + .left_join( + groups::table.on(groups::uuid + .eq(groups_users::groups_uuid) + .and(groups::organizations_uuid.eq(users_organizations::org_uuid))), ) - )) - .filter(collections::uuid.eq(&uuid)) - .filter( - users_collections::collection_uuid.eq(&uuid).and(users_collections::manage.eq(true)).or(// Directly accessed collection - users_organizations::access_all.eq(true).or( // access_all in Organization - users_organizations::atype.le(MembershipType::Admin as i32) // Org admin or owner - )).or( - groups::access_all.eq(true) // access_all in groups - ).or( // access via groups - groups_users::users_organizations_uuid.eq(users_organizations::uuid).and( - collections_groups::collections_uuid.is_not_null().and( - collections_groups::manage.eq(true)) - ) + .left_join( + collections_groups::table.on(collections_groups::groups_uuid + .eq(groups_users::groups_uuid) + .and(collections_groups::collections_uuid.eq(collections::uuid))), ) - ) - .count() - .first::(conn) - .ok() - .unwrap_or(0) != 0 - }} + .filter(collections::uuid.eq(&uuid)) + .filter( + users_collections::collection_uuid + .eq(&uuid) + .and(users_collections::manage.eq(true)) + .or( + // Directly accessed collection + users_organizations::access_all.eq(true).or( + // access_all in Organization + users_organizations::atype.le(MembershipType::Admin as i32), // Org admin or owner + ), + ) + .or( + groups::access_all.eq(true), // access_all in groups + ) + .or( + // access via groups + groups_users::users_organizations_uuid.eq(users_organizations::uuid).and( + collections_groups::collections_uuid + .is_not_null() + .and(collections_groups::manage.eq(true)), + ), + ), + ) + .count() + .first::(conn) + .ok() + .unwrap_or(0) + != 0 + }) + .await } pub async fn is_manageable_by_user(&self, user_uuid: &UserId, conn: &DbConn) -> bool { @@ -572,7 +632,7 @@ impl CollectionUser { user_uuid: &UserId, conn: &DbConn, ) -> Vec { - db_run! { conn: { + conn.run(move |conn| { users_collections::table .filter(users_collections::user_uuid.eq(user_uuid)) .inner_join(collections::table.on(collections::uuid.eq(users_collections::collection_uuid))) @@ -580,24 +640,35 @@ impl CollectionUser { .select(users_collections::all_columns) .load::(conn) .expect("Error loading users_collections") - }} + }) + .await } pub async fn find_by_organization_swap_user_uuid_with_member_uuid( org_uuid: &OrganizationId, conn: &DbConn, ) -> Vec { - let col_users = db_run! { conn: { - users_collections::table - .inner_join(collections::table.on(collections::uuid.eq(users_collections::collection_uuid))) - .filter(collections::org_uuid.eq(org_uuid)) - .inner_join(users_organizations::table.on(users_organizations::user_uuid.eq(users_collections::user_uuid))) - .filter(users_organizations::org_uuid.eq(org_uuid)) - .select((users_organizations::uuid, users_collections::collection_uuid, users_collections::read_only, users_collections::hide_passwords, users_collections::manage)) - .load::(conn) - .expect("Error loading users_collections") - }}; - col_users.into_iter().map(|c| c.into()).collect() + let col_users = conn + .run(move |conn| { + users_collections::table + .inner_join(collections::table.on(collections::uuid.eq(users_collections::collection_uuid))) + .filter(collections::org_uuid.eq(org_uuid)) + .inner_join( + users_organizations::table.on(users_organizations::user_uuid.eq(users_collections::user_uuid)), + ) + .filter(users_organizations::org_uuid.eq(org_uuid)) + .select(( + users_organizations::uuid, + users_collections::collection_uuid, + users_collections::read_only, + users_collections::hide_passwords, + users_collections::manage, + )) + .load::(conn) + .expect("Error loading users_collections") + }) + .await; + col_users.into_iter().map(Into::into).collect() } pub async fn save( @@ -666,7 +737,7 @@ impl CollectionUser { pub async fn delete(self, conn: &DbConn) -> EmptyResult { User::update_uuid_revision(&self.user_uuid, conn).await; - db_run! { conn: { + conn.run(move |conn| { diesel::delete( users_collections::table .filter(users_collections::user_uuid.eq(&self.user_uuid)) @@ -674,17 +745,19 @@ impl CollectionUser { ) .execute(conn) .map_res("Error removing user from collection") - }} + }) + .await } pub async fn find_by_collection(collection_uuid: &CollectionId, conn: &DbConn) -> Vec { - db_run! { conn: { + conn.run(move |conn| { users_collections::table .filter(users_collections::collection_uuid.eq(collection_uuid)) .select(users_collections::all_columns) .load::(conn) .expect("Error loading users_collections") - }} + }) + .await } pub async fn find_by_org_and_coll_swap_user_uuid_with_member_uuid( @@ -692,16 +765,26 @@ impl CollectionUser { collection_uuid: &CollectionId, conn: &DbConn, ) -> Vec { - let col_users = db_run! { conn: { - users_collections::table - .filter(users_collections::collection_uuid.eq(collection_uuid)) - .filter(users_organizations::org_uuid.eq(org_uuid)) - .inner_join(users_organizations::table.on(users_organizations::user_uuid.eq(users_collections::user_uuid))) - .select((users_organizations::uuid, users_collections::collection_uuid, users_collections::read_only, users_collections::hide_passwords, users_collections::manage)) - .load::(conn) - .expect("Error loading users_collections") - }}; - col_users.into_iter().map(|c| c.into()).collect() + let col_users = conn + .run(move |conn| { + users_collections::table + .filter(users_collections::collection_uuid.eq(collection_uuid)) + .filter(users_organizations::org_uuid.eq(org_uuid)) + .inner_join( + users_organizations::table.on(users_organizations::user_uuid.eq(users_collections::user_uuid)), + ) + .select(( + users_organizations::uuid, + users_collections::collection_uuid, + users_collections::read_only, + users_collections::hide_passwords, + users_collections::manage, + )) + .load::(conn) + .expect("Error loading users_collections") + }) + .await; + col_users.into_iter().map(Into::into).collect() } pub async fn find_by_collection_and_user( @@ -709,36 +792,39 @@ impl CollectionUser { user_uuid: &UserId, conn: &DbConn, ) -> Option { - db_run! { conn: { + conn.run(move |conn| { users_collections::table .filter(users_collections::collection_uuid.eq(collection_uuid)) .filter(users_collections::user_uuid.eq(user_uuid)) .select(users_collections::all_columns) .first::(conn) .ok() - }} + }) + .await } pub async fn find_by_user(user_uuid: &UserId, conn: &DbConn) -> Vec { - db_run! { conn: { + conn.run(move |conn| { users_collections::table .filter(users_collections::user_uuid.eq(user_uuid)) .select(users_collections::all_columns) .load::(conn) .expect("Error loading users_collections") - }} + }) + .await } pub async fn delete_all_by_collection(collection_uuid: &CollectionId, conn: &DbConn) -> EmptyResult { - for collection in CollectionUser::find_by_collection(collection_uuid, conn).await.iter() { + for collection in &CollectionUser::find_by_collection(collection_uuid, conn).await { User::update_uuid_revision(&collection.user_uuid, conn).await; } - db_run! { conn: { + conn.run(move |conn| { diesel::delete(users_collections::table.filter(users_collections::collection_uuid.eq(collection_uuid))) .execute(conn) .map_res("Error deleting users from collection") - }} + }) + .await } pub async fn delete_all_by_user_and_org( @@ -748,17 +834,21 @@ impl CollectionUser { ) -> EmptyResult { let collectionusers = Self::find_by_organization_and_user_uuid(org_uuid, user_uuid, conn).await; - db_run! { conn: { + conn.run(move |conn| { for user in collectionusers { - let _: () = diesel::delete(users_collections::table.filter( - users_collections::user_uuid.eq(user_uuid) - .and(users_collections::collection_uuid.eq(user.collection_uuid)) - )) - .execute(conn) - .map_res("Error removing user from collections")?; + let _: () = diesel::delete( + users_collections::table.filter( + users_collections::user_uuid + .eq(user_uuid) + .and(users_collections::collection_uuid.eq(user.collection_uuid)), + ), + ) + .execute(conn) + .map_res("Error removing user from collections")?; } Ok(()) - }} + }) + .await } pub async fn has_access_to_collection_by_user(col_id: &CollectionId, user_uuid: &UserId, conn: &DbConn) -> bool { @@ -801,7 +891,7 @@ impl CollectionCipher { pub async fn delete(cipher_uuid: &CipherId, collection_uuid: &CollectionId, conn: &DbConn) -> EmptyResult { Self::update_users_revision(collection_uuid, conn).await; - db_run! { conn: { + conn.run(move |conn| { diesel::delete( ciphers_collections::table .filter(ciphers_collections::cipher_uuid.eq(cipher_uuid)) @@ -809,23 +899,26 @@ impl CollectionCipher { ) .execute(conn) .map_res("Error deleting cipher from collection") - }} + }) + .await } pub async fn delete_all_by_cipher(cipher_uuid: &CipherId, conn: &DbConn) -> EmptyResult { - db_run! { conn: { + conn.run(move |conn| { diesel::delete(ciphers_collections::table.filter(ciphers_collections::cipher_uuid.eq(cipher_uuid))) .execute(conn) .map_res("Error removing cipher from collections") - }} + }) + .await } pub async fn delete_all_by_collection(collection_uuid: &CollectionId, conn: &DbConn) -> EmptyResult { - db_run! { conn: { + conn.run(move |conn| { diesel::delete(ciphers_collections::table.filter(ciphers_collections::collection_uuid.eq(collection_uuid))) .execute(conn) .map_res("Error removing ciphers from collection") - }} + }) + .await } pub async fn update_users_revision(collection_uuid: &CollectionId, conn: &DbConn) { diff --git a/src/db/models/device.rs b/src/db/models/device.rs index 7364a2ec..6c1b686a 100644 --- a/src/db/models/device.rs +++ b/src/db/models/device.rs @@ -1,18 +1,20 @@ use chrono::{NaiveDateTime, Utc}; - use data_encoding::BASE64URL; use derive_more::{Display, From}; +use diesel::prelude::*; use serde_json::Value; -use super::{AuthRequest, UserId}; -use crate::db::schema::devices; use crate::{ + api::EmptyResult, crypto, + db::{DbConn, schema::devices}, + error::MapResult, util::{format_date, get_uuid}, }; -use diesel::prelude::*; use macros::{IdFromParam, UuidFromParam}; +use super::{AuthRequest, UserId}; + #[derive(Identifiable, Queryable, Insertable, AsChangeset)] #[diesel(table_name = devices)] #[diesel(treat_none_as_null = true)] @@ -135,10 +137,6 @@ impl DeviceWithAuthRequest { } } } -use crate::db::DbConn; - -use crate::api::EmptyResult; -use crate::error::MapResult; /// Database methods impl Device { @@ -171,21 +169,23 @@ impl Device { } pub async fn delete_all_by_user(user_uuid: &UserId, conn: &DbConn) -> EmptyResult { - db_run! { conn: { + conn.run(move |conn| { diesel::delete(devices::table.filter(devices::user_uuid.eq(user_uuid))) .execute(conn) .map_res("Error removing devices for user") - }} + }) + .await } pub async fn find_by_uuid_and_user(uuid: &DeviceId, user_uuid: &UserId, conn: &DbConn) -> Option { - db_run! { conn: { + conn.run(move |conn| { devices::table .filter(devices::uuid.eq(uuid)) .filter(devices::user_uuid.eq(user_uuid)) .first::(conn) .ok() - }} + }) + .await } pub async fn find_with_auth_request_by_user(user_uuid: &UserId, conn: &DbConn) -> Vec { @@ -199,71 +199,65 @@ impl Device { } pub async fn find_by_user(user_uuid: &UserId, conn: &DbConn) -> Vec { - db_run! { conn: { - devices::table - .filter(devices::user_uuid.eq(user_uuid)) - .load::(conn) - .expect("Error loading devices") - }} + conn.run(move |conn| { + devices::table.filter(devices::user_uuid.eq(user_uuid)).load::(conn).expect("Error loading devices") + }) + .await } pub async fn find_by_uuid(uuid: &DeviceId, conn: &DbConn) -> Option { - db_run! { conn: { - devices::table - .filter(devices::uuid.eq(uuid)) - .first::(conn) - .ok() - }} + conn.run(move |conn| devices::table.filter(devices::uuid.eq(uuid)).first::(conn).ok()).await } pub async fn clear_push_token_by_uuid(uuid: &DeviceId, conn: &DbConn) -> EmptyResult { - db_run! { conn: { + conn.run(move |conn| { diesel::update(devices::table) .filter(devices::uuid.eq(uuid)) .set(devices::push_token.eq::>(None)) .execute(conn) .map_res("Error removing push token") - }} + }) + .await } pub async fn find_by_refresh_token(refresh_token: &str, conn: &DbConn) -> Option { - db_run! { conn: { - devices::table - .filter(devices::refresh_token.eq(refresh_token)) - .first::(conn) - .ok() - }} + conn.run(move |conn| devices::table.filter(devices::refresh_token.eq(refresh_token)).first::(conn).ok()) + .await } pub async fn find_latest_active_by_user(user_uuid: &UserId, conn: &DbConn) -> Option { - db_run! { conn: { + conn.run(move |conn| { devices::table .filter(devices::user_uuid.eq(user_uuid)) .order(devices::updated_at.desc()) .first::(conn) .ok() - }} + }) + .await } pub async fn find_push_devices_by_user(user_uuid: &UserId, conn: &DbConn) -> Vec { - db_run! { conn: { + conn.run(move |conn| { devices::table .filter(devices::user_uuid.eq(user_uuid)) .filter(devices::push_token.is_not_null()) .load::(conn) .expect("Error loading push devices") - }} + }) + .await } pub async fn check_user_has_push_device(user_uuid: &UserId, conn: &DbConn) -> bool { - db_run! { conn: { + conn.run(move |conn| { devices::table - .filter(devices::user_uuid.eq(user_uuid)) - .filter(devices::push_token.is_not_null()) - .count() - .first::(conn) - .ok() - .unwrap_or(0) != 0 - }} + .filter(devices::user_uuid.eq(user_uuid)) + .filter(devices::push_token.is_not_null()) + .count() + .first::(conn) + .ok() + .unwrap_or(0) + != 0 + }) + .await } pub async fn rotate_refresh_tokens_by_user(user_uuid: &UserId, conn: &DbConn) -> EmptyResult { @@ -337,6 +331,7 @@ pub enum DeviceType { } impl DeviceType { + #[expect(clippy::match_same_arms, reason = "Specifically define 14 and have a fallback for new types")] pub fn from_i32(value: i32) -> DeviceType { match value { 0 => DeviceType::Android, diff --git a/src/db/models/emergency_access.rs b/src/db/models/emergency_access.rs index 5ea334a4..45fad91f 100644 --- a/src/db/models/emergency_access.rs +++ b/src/db/models/emergency_access.rs @@ -1,13 +1,17 @@ use chrono::{NaiveDateTime, Utc}; use derive_more::{AsRef, Deref, Display, From}; +use diesel::prelude::*; use serde_json::Value; -use super::{User, UserId}; -use crate::db::schema::emergency_access; -use crate::{api::EmptyResult, db::DbConn, error::MapResult}; -use diesel::prelude::*; +use crate::{ + api::EmptyResult, + db::{DbConn, schema::emergency_access}, + error::MapResult, +}; use macros::UuidFromParam; +use super::{User, UserId}; + #[derive(Identifiable, Queryable, Insertable, AsChangeset)] #[diesel(table_name = emergency_access)] #[diesel(treat_none_as_null = true)] @@ -87,13 +91,12 @@ impl EmergencyAccess { User::find_by_uuid(grantee_uuid, conn).await.expect("Grantee user not found.") } else { let email = self.email.as_deref()?; - match User::find_by_mail(email, conn).await { - Some(user) => user, - None => { - // remove outstanding invitations which should not exist - Self::delete_all_by_grantee_email(email, conn).await.ok(); - return None; - } + if let Some(user) = User::find_by_mail(email, conn).await { + user + } else { + // remove outstanding invitations which should not exist + Self::delete_all_by_grantee_email(email, conn).await.ok(); + return None; } }; @@ -183,28 +186,36 @@ impl EmergencyAccess { self.status = status; date.clone_into(&mut self.updated_at); - db_run! { conn: { - crate::util::retry(|| { - diesel::update(emergency_access::table.filter(emergency_access::uuid.eq(&self.uuid))) - .set((emergency_access::status.eq(status), emergency_access::updated_at.eq(date))) - .execute(conn) - }, 10) + conn.run(move |conn| { + crate::util::retry( + || { + diesel::update(emergency_access::table.filter(emergency_access::uuid.eq(&self.uuid))) + .set((emergency_access::status.eq(status), emergency_access::updated_at.eq(date))) + .execute(conn) + }, + 10, + ) .map_res("Error updating emergency access status") - }} + }) + .await } pub async fn update_last_notification_date_and_save(&mut self, date: &NaiveDateTime, conn: &DbConn) -> EmptyResult { self.last_notification_at = Some(date.to_owned()); date.clone_into(&mut self.updated_at); - db_run! { conn: { - crate::util::retry(|| { - diesel::update(emergency_access::table.filter(emergency_access::uuid.eq(&self.uuid))) - .set((emergency_access::last_notification_at.eq(date), emergency_access::updated_at.eq(date))) - .execute(conn) - }, 10) + conn.run(move |conn| { + crate::util::retry( + || { + diesel::update(emergency_access::table.filter(emergency_access::uuid.eq(&self.uuid))) + .set((emergency_access::last_notification_at.eq(date), emergency_access::updated_at.eq(date))) + .execute(conn) + }, + 10, + ) .map_res("Error updating emergency access status") - }} + }) + .await } pub async fn delete_all_by_user(user_uuid: &UserId, conn: &DbConn) -> EmptyResult { @@ -227,11 +238,12 @@ impl EmergencyAccess { pub async fn delete(self, conn: &DbConn) -> EmptyResult { User::update_uuid_revision(&self.grantor_uuid, conn).await; - db_run! { conn: { + conn.run(move |conn| { diesel::delete(emergency_access::table.filter(emergency_access::uuid.eq(self.uuid))) .execute(conn) .map_res("Error removing user from emergency access") - }} + }) + .await } pub async fn find_by_grantor_uuid_and_grantee_uuid_or_email( @@ -240,23 +252,25 @@ impl EmergencyAccess { email: &str, conn: &DbConn, ) -> Option { - db_run! { conn: { + conn.run(move |conn| { emergency_access::table .filter(emergency_access::grantor_uuid.eq(grantor_uuid)) .filter(emergency_access::grantee_uuid.eq(grantee_uuid).or(emergency_access::email.eq(email))) .first::(conn) .ok() - }} + }) + .await } pub async fn find_all_recoveries_initiated(conn: &DbConn) -> Vec { - db_run! { conn: { + conn.run(move |conn| { emergency_access::table .filter(emergency_access::status.eq(EmergencyAccessStatus::RecoveryInitiated as i32)) .filter(emergency_access::recovery_initiated_at.is_not_null()) .load::(conn) .expect("Error loading emergency_access") - }} + }) + .await } pub async fn find_by_uuid_and_grantor_uuid( @@ -264,13 +278,14 @@ impl EmergencyAccess { grantor_uuid: &UserId, conn: &DbConn, ) -> Option { - db_run! { conn: { + conn.run(move |conn| { emergency_access::table .filter(emergency_access::uuid.eq(uuid)) .filter(emergency_access::grantor_uuid.eq(grantor_uuid)) .first::(conn) .ok() - }} + }) + .await } pub async fn find_by_uuid_and_grantee_uuid( @@ -278,13 +293,14 @@ impl EmergencyAccess { grantee_uuid: &UserId, conn: &DbConn, ) -> Option { - db_run! { conn: { + conn.run(move |conn| { emergency_access::table .filter(emergency_access::uuid.eq(uuid)) .filter(emergency_access::grantee_uuid.eq(grantee_uuid)) .first::(conn) .ok() - }} + }) + .await } pub async fn find_by_uuid_and_grantee_email( @@ -292,61 +308,67 @@ impl EmergencyAccess { grantee_email: &str, conn: &DbConn, ) -> Option { - db_run! { conn: { + conn.run(move |conn| { emergency_access::table .filter(emergency_access::uuid.eq(uuid)) .filter(emergency_access::email.eq(grantee_email)) .first::(conn) .ok() - }} + }) + .await } pub async fn find_all_by_grantee_uuid(grantee_uuid: &UserId, conn: &DbConn) -> Vec { - db_run! { conn: { + conn.run(move |conn| { emergency_access::table .filter(emergency_access::grantee_uuid.eq(grantee_uuid)) .load::(conn) .expect("Error loading emergency_access") - }} + }) + .await } pub async fn find_invited_by_grantee_email(grantee_email: &str, conn: &DbConn) -> Option { - db_run! { conn: { + conn.run(move |conn| { emergency_access::table .filter(emergency_access::email.eq(grantee_email)) .filter(emergency_access::status.eq(EmergencyAccessStatus::Invited as i32)) .first::(conn) .ok() - }} + }) + .await } pub async fn find_all_invited_by_grantee_email(grantee_email: &str, conn: &DbConn) -> Vec { - db_run! { conn: { + conn.run(move |conn| { emergency_access::table .filter(emergency_access::email.eq(grantee_email)) .filter(emergency_access::status.eq(EmergencyAccessStatus::Invited as i32)) .load::(conn) .expect("Error loading emergency_access") - }} + }) + .await } pub async fn find_all_by_grantor_uuid(grantor_uuid: &UserId, conn: &DbConn) -> Vec { - db_run! { conn: { + conn.run(move |conn| { emergency_access::table .filter(emergency_access::grantor_uuid.eq(grantor_uuid)) .load::(conn) .expect("Error loading emergency_access") - }} + }) + .await } pub async fn find_all_confirmed_by_grantor_uuid(grantor_uuid: &UserId, conn: &DbConn) -> Vec { - db_run! { conn: { + conn.run(move |conn| { emergency_access::table .filter(emergency_access::grantor_uuid.eq(grantor_uuid)) .filter(emergency_access::status.ge(EmergencyAccessStatus::Confirmed as i32)) .load::(conn) .expect("Error loading emergency_access") - }} + }) + .await } pub async fn accept_invite(&mut self, grantee_uuid: &UserId, grantee_email: &str, conn: &DbConn) -> EmptyResult { diff --git a/src/db/models/event.rs b/src/db/models/event.rs index bd4b2310..3a6b610c 100644 --- a/src/db/models/event.rs +++ b/src/db/models/event.rs @@ -1,11 +1,18 @@ use chrono::{NaiveDateTime, TimeDelta, Utc}; -//use derive_more::{AsRef, Deref, Display, From}; +use diesel::prelude::*; use serde_json::Value; +use crate::{ + CONFIG, + api::EmptyResult, + db::{ + DbConn, + schema::{event, users_organizations}, + }, + error::MapResult, +}; + use super::{CipherId, CollectionId, GroupId, MembershipId, OrgPolicyId, OrganizationId, UserId}; -use crate::db::schema::{event, users_organizations}; -use crate::{api::EmptyResult, db::DbConn, error::MapResult, CONFIG}; -use diesel::prelude::*; // https://bitwarden.com/help/event-logs/ @@ -249,11 +256,10 @@ impl Event { } pub async fn delete(self, conn: &DbConn) -> EmptyResult { - db_run! { conn: { - diesel::delete(event::table.filter(event::uuid.eq(self.uuid))) - .execute(conn) - .map_res("Error deleting event") - }} + conn.run(move |conn| { + diesel::delete(event::table.filter(event::uuid.eq(self.uuid))).execute(conn).map_res("Error deleting event") + }) + .await } /// ############## @@ -264,7 +270,7 @@ impl Event { end: &NaiveDateTime, conn: &DbConn, ) -> Vec { - db_run! { conn: { + conn.run(move |conn| { event::table .filter(event::org_uuid.eq(org_uuid)) .filter(event::event_date.between(start, end)) @@ -272,18 +278,15 @@ impl Event { .limit(Self::PAGE_SIZE) .load::(conn) .expect("Error filtering events") - }} + }) + .await } pub async fn count_by_org(org_uuid: &OrganizationId, conn: &DbConn) -> i64 { - db_run! { conn: { - event::table - .filter(event::org_uuid.eq(org_uuid)) - .count() - .first::(conn) - .ok() - .unwrap_or(0) - }} + conn.run(move |conn| { + event::table.filter(event::org_uuid.eq(org_uuid)).count().first::(conn).ok().unwrap_or(0) + }) + .await } pub async fn find_by_org_and_member( @@ -293,18 +296,23 @@ impl Event { end: &NaiveDateTime, conn: &DbConn, ) -> Vec { - db_run! { conn: { + conn.run(move |conn| { event::table .inner_join(users_organizations::table.on(users_organizations::uuid.eq(member_uuid))) .filter(event::org_uuid.eq(org_uuid)) .filter(event::event_date.between(start, end)) - .filter(event::user_uuid.eq(users_organizations::user_uuid.nullable()).or(event::act_user_uuid.eq(users_organizations::user_uuid.nullable()))) + .filter( + event::user_uuid + .eq(users_organizations::user_uuid.nullable()) + .or(event::act_user_uuid.eq(users_organizations::user_uuid.nullable())), + ) .select(event::all_columns) .order_by(event::event_date.desc()) .limit(Self::PAGE_SIZE) .load::(conn) .expect("Error filtering events") - }} + }) + .await } pub async fn find_by_cipher_uuid( @@ -313,7 +321,7 @@ impl Event { end: &NaiveDateTime, conn: &DbConn, ) -> Vec { - db_run! { conn: { + conn.run(move |conn| { event::table .filter(event::cipher_uuid.eq(cipher_uuid)) .filter(event::event_date.between(start, end)) @@ -321,17 +329,19 @@ impl Event { .limit(Self::PAGE_SIZE) .load::(conn) .expect("Error filtering events") - }} + }) + .await } pub async fn clean_events(conn: &DbConn) -> EmptyResult { if let Some(days_to_retain) = CONFIG.events_days_retain() { let dt = Utc::now().naive_utc() - TimeDelta::try_days(days_to_retain).unwrap(); - db_run! { conn: { + conn.run(move |conn| { diesel::delete(event::table.filter(event::event_date.lt(dt))) - .execute(conn) - .map_res("Error cleaning old events") - }} + .execute(conn) + .map_res("Error cleaning old events") + }) + .await } else { Ok(()) } diff --git a/src/db/models/favorite.rs b/src/db/models/favorite.rs index d7aa74bb..ee79857a 100644 --- a/src/db/models/favorite.rs +++ b/src/db/models/favorite.rs @@ -1,7 +1,13 @@ -use super::{CipherId, User, UserId}; -use crate::db::schema::favorites; use diesel::prelude::*; +use crate::{ + api::EmptyResult, + db::{DbConn, schema::favorites}, + error::MapResult, +}; + +use super::{CipherId, User, UserId}; + #[derive(Identifiable, Queryable, Insertable)] #[diesel(table_name = favorites)] #[diesel(primary_key(user_uuid, cipher_uuid))] @@ -10,24 +16,18 @@ pub struct Favorite { pub cipher_uuid: CipherId, } -use crate::db::DbConn; - -use crate::api::EmptyResult; -use crate::error::MapResult; - impl Favorite { // Returns whether the specified cipher is a favorite of the specified user. pub async fn is_favorite(cipher_uuid: &CipherId, user_uuid: &UserId, conn: &DbConn) -> bool { - db_run! { conn: { + conn.run(move |conn| { let query = favorites::table .filter(favorites::cipher_uuid.eq(cipher_uuid)) .filter(favorites::user_uuid.eq(user_uuid)) .count(); - query.first::(conn) - .ok() - .unwrap_or(0) != 0 - }} + query.first::(conn).ok().unwrap_or(0) != 0 + }) + .await } // Sets whether the specified cipher is a favorite of the specified user. @@ -41,27 +41,26 @@ impl Favorite { match (old, new) { (false, true) => { User::update_uuid_revision(user_uuid, conn).await; - db_run! { conn: { - diesel::insert_into(favorites::table) - .values(( - favorites::user_uuid.eq(user_uuid), - favorites::cipher_uuid.eq(cipher_uuid), - )) - .execute(conn) - .map_res("Error adding favorite") - }} + conn.run(move |conn| { + diesel::insert_into(favorites::table) + .values((favorites::user_uuid.eq(user_uuid), favorites::cipher_uuid.eq(cipher_uuid))) + .execute(conn) + .map_res("Error adding favorite") + }) + .await } (true, false) => { User::update_uuid_revision(user_uuid, conn).await; - db_run! { conn: { + conn.run(move |conn| { diesel::delete( favorites::table .filter(favorites::user_uuid.eq(user_uuid)) - .filter(favorites::cipher_uuid.eq(cipher_uuid)) + .filter(favorites::cipher_uuid.eq(cipher_uuid)), ) .execute(conn) .map_res("Error removing favorite") - }} + }) + .await } // Otherwise, the favorite status is already what it should be. _ => Ok(()), @@ -70,31 +69,34 @@ impl Favorite { // Delete all favorite entries associated with the specified cipher. pub async fn delete_all_by_cipher(cipher_uuid: &CipherId, conn: &DbConn) -> EmptyResult { - db_run! { conn: { + conn.run(move |conn| { diesel::delete(favorites::table.filter(favorites::cipher_uuid.eq(cipher_uuid))) .execute(conn) .map_res("Error removing favorites by cipher") - }} + }) + .await } // Delete all favorite entries associated with the specified user. pub async fn delete_all_by_user(user_uuid: &UserId, conn: &DbConn) -> EmptyResult { - db_run! { conn: { + conn.run(move |conn| { diesel::delete(favorites::table.filter(favorites::user_uuid.eq(user_uuid))) .execute(conn) .map_res("Error removing favorites by user") - }} + }) + .await } /// Return a vec with (cipher_uuid) this will only contain favorite flagged ciphers /// This is used during a full sync so we only need one query for all favorite cipher matches. pub async fn get_all_cipher_uuid_by_user(user_uuid: &UserId, conn: &DbConn) -> Vec { - db_run! { conn: { + conn.run(move |conn| { favorites::table .filter(favorites::user_uuid.eq(user_uuid)) .select(favorites::cipher_uuid) .load::(conn) .unwrap_or_default() - }} + }) + .await } } diff --git a/src/db/models/folder.rs b/src/db/models/folder.rs index b4cbc7ff..745608e3 100644 --- a/src/db/models/folder.rs +++ b/src/db/models/folder.rs @@ -1,12 +1,20 @@ use chrono::{NaiveDateTime, Utc}; use derive_more::{AsRef, Deref, Display, From}; +use diesel::prelude::*; use serde_json::Value; -use super::{CipherId, User, UserId}; -use crate::db::schema::{folders, folders_ciphers}; -use diesel::prelude::*; +use crate::{ + api::EmptyResult, + db::{ + DbConn, + schema::{folders, folders_ciphers}, + }, + error::MapResult, +}; use macros::UuidFromParam; +use super::{CipherId, User, UserId}; + #[derive(Identifiable, Queryable, Insertable, AsChangeset)] #[diesel(table_name = folders)] #[diesel(primary_key(uuid))] @@ -56,17 +64,12 @@ impl Folder { impl FolderCipher { pub fn new(folder_uuid: FolderId, cipher_uuid: CipherId) -> Self { Self { - folder_uuid, cipher_uuid, + folder_uuid, } } } -use crate::db::DbConn; - -use crate::api::EmptyResult; -use crate::error::MapResult; - /// Database methods impl Folder { pub async fn save(&mut self, conn: &DbConn) -> EmptyResult { @@ -107,11 +110,12 @@ impl Folder { User::update_uuid_revision(&self.user_uuid, conn).await; FolderCipher::delete_all_by_folder(&self.uuid, conn).await?; - db_run! { conn: { + conn.run(move |conn| { diesel::delete(folders::table.filter(folders::uuid.eq(&self.uuid))) .execute(conn) .map_res("Error deleting folder") - }} + }) + .await } pub async fn delete_all_by_user(user_uuid: &UserId, conn: &DbConn) -> EmptyResult { @@ -122,22 +126,21 @@ impl Folder { } pub async fn find_by_uuid_and_user(uuid: &FolderId, user_uuid: &UserId, conn: &DbConn) -> Option { - db_run! { conn: { + conn.run(move |conn| { folders::table .filter(folders::uuid.eq(uuid)) .filter(folders::user_uuid.eq(user_uuid)) .first::(conn) .ok() - }} + }) + .await } pub async fn find_by_user(user_uuid: &UserId, conn: &DbConn) -> Vec { - db_run! { conn: { - folders::table - .filter(folders::user_uuid.eq(user_uuid)) - .load::(conn) - .expect("Error loading folders") - }} + conn.run(move |conn| { + folders::table.filter(folders::user_uuid.eq(user_uuid)).load::(conn).expect("Error loading folders") + }) + .await } } @@ -165,7 +168,7 @@ impl FolderCipher { } pub async fn delete(self, conn: &DbConn) -> EmptyResult { - db_run! { conn: { + conn.run(move |conn| { diesel::delete( folders_ciphers::table .filter(folders_ciphers::cipher_uuid.eq(self.cipher_uuid)) @@ -173,23 +176,26 @@ impl FolderCipher { ) .execute(conn) .map_res("Error removing cipher from folder") - }} + }) + .await } pub async fn delete_all_by_cipher(cipher_uuid: &CipherId, conn: &DbConn) -> EmptyResult { - db_run! { conn: { + conn.run(move |conn| { diesel::delete(folders_ciphers::table.filter(folders_ciphers::cipher_uuid.eq(cipher_uuid))) .execute(conn) .map_res("Error removing cipher from folders") - }} + }) + .await } pub async fn delete_all_by_folder(folder_uuid: &FolderId, conn: &DbConn) -> EmptyResult { - db_run! { conn: { + conn.run(move |conn| { diesel::delete(folders_ciphers::table.filter(folders_ciphers::folder_uuid.eq(folder_uuid))) .execute(conn) .map_res("Error removing ciphers from folder") - }} + }) + .await } pub async fn find_by_folder_and_cipher( @@ -197,35 +203,38 @@ impl FolderCipher { cipher_uuid: &CipherId, conn: &DbConn, ) -> Option { - db_run! { conn: { + conn.run(move |conn| { folders_ciphers::table .filter(folders_ciphers::folder_uuid.eq(folder_uuid)) .filter(folders_ciphers::cipher_uuid.eq(cipher_uuid)) .first::(conn) .ok() - }} + }) + .await } pub async fn find_by_folder(folder_uuid: &FolderId, conn: &DbConn) -> Vec { - db_run! { conn: { + conn.run(move |conn| { folders_ciphers::table .filter(folders_ciphers::folder_uuid.eq(folder_uuid)) .load::(conn) .expect("Error loading folders") - }} + }) + .await } /// Return a vec with (cipher_uuid, folder_uuid) /// This is used during a full sync so we only need one query for all folder matches. pub async fn find_by_user(user_uuid: &UserId, conn: &DbConn) -> Vec<(CipherId, FolderId)> { - db_run! { conn: { + conn.run(move |conn| { folders_ciphers::table .inner_join(folders::table) .filter(folders::user_uuid.eq(user_uuid)) .select(folders_ciphers::all_columns) .load::<(CipherId, FolderId)>(conn) .unwrap_or_default() - }} + }) + .await } } diff --git a/src/db/models/group.rs b/src/db/models/group.rs index f41ad9ca..820d3700 100644 --- a/src/db/models/group.rs +++ b/src/db/models/group.rs @@ -1,14 +1,20 @@ -use super::{CollectionId, Membership, MembershipId, OrganizationId, User, UserId}; -use crate::api::EmptyResult; -use crate::db::schema::{collections, collections_groups, groups, groups_users, users_organizations}; -use crate::db::DbConn; -use crate::error::MapResult; use chrono::{NaiveDateTime, Utc}; use derive_more::{AsRef, Deref, Display, From}; use diesel::prelude::*; -use macros::UuidFromParam; use serde_json::Value; +use crate::{ + api::EmptyResult, + db::{ + DbConn, + schema::{collections, collections_groups, groups, groups_users, users_organizations}, + }, + error::MapResult, +}; +use macros::UuidFromParam; + +use super::{CollectionId, Membership, MembershipId, OrganizationId, User, UserId}; + #[derive(Identifiable, Queryable, Insertable, AsChangeset)] #[diesel(table_name = groups)] #[diesel(treat_none_as_null = true)] @@ -197,33 +203,31 @@ impl Group { } pub async fn find_by_organization(org_uuid: &OrganizationId, conn: &DbConn) -> Vec { - db_run! { conn: { + conn.run(move |conn| { groups::table .filter(groups::organizations_uuid.eq(org_uuid)) .load::(conn) .expect("Error loading groups") - }} + }) + .await } pub async fn count_by_org(org_uuid: &OrganizationId, conn: &DbConn) -> i64 { - db_run! { conn: { - groups::table - .filter(groups::organizations_uuid.eq(org_uuid)) - .count() - .first::(conn) - .ok() - .unwrap_or(0) - }} + conn.run(move |conn| { + groups::table.filter(groups::organizations_uuid.eq(org_uuid)).count().first::(conn).ok().unwrap_or(0) + }) + .await } pub async fn find_by_uuid_and_org(uuid: &GroupId, org_uuid: &OrganizationId, conn: &DbConn) -> Option { - db_run! { conn: { + conn.run(move |conn| { groups::table .filter(groups::uuid.eq(uuid)) .filter(groups::organizations_uuid.eq(org_uuid)) .first::(conn) .ok() - }} + }) + .await } pub async fn find_by_external_id_and_org( @@ -231,77 +235,85 @@ impl Group { org_uuid: &OrganizationId, conn: &DbConn, ) -> Option { - db_run! { conn: { + conn.run(move |conn| { groups::table .filter(groups::external_id.eq(external_id)) .filter(groups::organizations_uuid.eq(org_uuid)) .first::(conn) .ok() - }} + }) + .await } //Returns all organizations the user has full access to pub async fn get_orgs_by_user_with_full_access(user_uuid: &UserId, conn: &DbConn) -> Vec { - db_run! { conn: { + conn.run(move |conn| { groups_users::table - .inner_join(users_organizations::table.on( - users_organizations::uuid.eq(groups_users::users_organizations_uuid) - )) - .inner_join(groups::table.on(groups::uuid.eq(groups_users::groups_uuid) - .and(groups::organizations_uuid.eq(users_organizations::org_uuid)) - )) + .inner_join( + users_organizations::table.on(users_organizations::uuid.eq(groups_users::users_organizations_uuid)), + ) + .inner_join( + groups::table.on(groups::uuid + .eq(groups_users::groups_uuid) + .and(groups::organizations_uuid.eq(users_organizations::org_uuid))), + ) .filter(users_organizations::user_uuid.eq(user_uuid)) .filter(groups::access_all.eq(true)) .select(groups::organizations_uuid) .distinct() .load::(conn) .expect("Error loading organization group full access information for user") - }} + }) + .await } pub async fn is_in_full_access_group(user_uuid: &UserId, org_uuid: &OrganizationId, conn: &DbConn) -> bool { - db_run! { conn: { + conn.run(move |conn| { groups::table - .inner_join(groups_users::table.on( - groups_users::groups_uuid.eq(groups::uuid) - )) - .inner_join(users_organizations::table.on( - users_organizations::uuid.eq(groups_users::users_organizations_uuid) - )) + .inner_join(groups_users::table.on(groups_users::groups_uuid.eq(groups::uuid))) + .inner_join( + users_organizations::table.on(users_organizations::uuid.eq(groups_users::users_organizations_uuid)), + ) .filter(users_organizations::user_uuid.eq(user_uuid)) .filter(groups::organizations_uuid.eq(org_uuid)) .filter(groups::access_all.eq(true)) .select(groups::access_all) .first::(conn) .unwrap_or_default() - }} + }) + .await } pub async fn delete(&self, org_uuid: &OrganizationId, conn: &DbConn) -> EmptyResult { CollectionGroup::delete_all_by_group(&self.uuid, org_uuid, conn).await?; GroupUser::delete_all_by_group(&self.uuid, org_uuid, conn).await?; - db_run! { conn: { + conn.run(move |conn| { diesel::delete(groups::table.filter(groups::uuid.eq(&self.uuid))) .execute(conn) .map_res("Error deleting group") - }} + }) + .await } pub async fn update_revision(uuid: &GroupId, conn: &DbConn) { - if let Err(e) = Self::_update_revision(uuid, &Utc::now().naive_utc(), conn).await { + if let Err(e) = Self::update_revision_impl(uuid, &Utc::now().naive_utc(), conn).await { warn!("Failed to update revision for {uuid}: {e:#?}"); } } - async fn _update_revision(uuid: &GroupId, date: &NaiveDateTime, conn: &DbConn) -> EmptyResult { - db_run! { conn: { - crate::util::retry(|| { - diesel::update(groups::table.filter(groups::uuid.eq(uuid))) - .set(groups::revision_date.eq(date)) - .execute(conn) - }, 10) + async fn update_revision_impl(uuid: &GroupId, date: &NaiveDateTime, conn: &DbConn) -> EmptyResult { + conn.run(move |conn| { + crate::util::retry( + || { + diesel::update(groups::table.filter(groups::uuid.eq(uuid))) + .set(groups::revision_date.eq(date)) + .execute(conn) + }, + 10, + ) .map_res("Error updating group revision") - }} + }) + .await } } @@ -366,60 +378,63 @@ impl CollectionGroup { } pub async fn find_by_group(group_uuid: &GroupId, org_uuid: &OrganizationId, conn: &DbConn) -> Vec { - db_run! { conn: { + conn.run(move |conn| { collections_groups::table - .inner_join(groups::table.on( - groups::uuid.eq(collections_groups::groups_uuid) - )) - .inner_join(collections::table.on( - collections::uuid.eq(collections_groups::collections_uuid) - .and(collections::org_uuid.eq(groups::organizations_uuid)) - )) + .inner_join(groups::table.on(groups::uuid.eq(collections_groups::groups_uuid))) + .inner_join( + collections::table.on(collections::uuid + .eq(collections_groups::collections_uuid) + .and(collections::org_uuid.eq(groups::organizations_uuid))), + ) .filter(collections_groups::groups_uuid.eq(group_uuid)) .filter(collections::org_uuid.eq(org_uuid)) .select(collections_groups::all_columns) .load::(conn) .expect("Error loading collection groups") - }} + }) + .await } pub async fn find_by_user(user_uuid: &UserId, conn: &DbConn) -> Vec { - db_run! { conn: { + conn.run(move |conn| { collections_groups::table - .inner_join(groups_users::table.on( - groups_users::groups_uuid.eq(collections_groups::groups_uuid) - )) - .inner_join(users_organizations::table.on( - users_organizations::uuid.eq(groups_users::users_organizations_uuid) - )) - .inner_join(groups::table.on(groups::uuid.eq(collections_groups::groups_uuid) - .and(groups::organizations_uuid.eq(users_organizations::org_uuid)) - )) - .inner_join(collections::table.on( - collections::uuid.eq(collections_groups::collections_uuid) - .and(collections::org_uuid.eq(groups::organizations_uuid)) - )) + .inner_join(groups_users::table.on(groups_users::groups_uuid.eq(collections_groups::groups_uuid))) + .inner_join( + users_organizations::table.on(users_organizations::uuid.eq(groups_users::users_organizations_uuid)), + ) + .inner_join( + groups::table.on(groups::uuid + .eq(collections_groups::groups_uuid) + .and(groups::organizations_uuid.eq(users_organizations::org_uuid))), + ) + .inner_join( + collections::table.on(collections::uuid + .eq(collections_groups::collections_uuid) + .and(collections::org_uuid.eq(groups::organizations_uuid))), + ) .filter(users_organizations::user_uuid.eq(user_uuid)) .select(collections_groups::all_columns) .load::(conn) .expect("Error loading user collection groups") - }} + }) + .await } pub async fn find_by_collection(collection_uuid: &CollectionId, conn: &DbConn) -> Vec { - db_run! { conn: { + conn.run(move |conn| { collections_groups::table .filter(collections_groups::collections_uuid.eq(collection_uuid)) - .inner_join(collections::table.on( - collections::uuid.eq(collections_groups::collections_uuid) - )) - .inner_join(groups::table.on(groups::uuid.eq(collections_groups::groups_uuid) - .and(groups::organizations_uuid.eq(collections::org_uuid)) - )) + .inner_join(collections::table.on(collections::uuid.eq(collections_groups::collections_uuid))) + .inner_join( + groups::table.on(groups::uuid + .eq(collections_groups::groups_uuid) + .and(groups::organizations_uuid.eq(collections::org_uuid))), + ) .select(collections_groups::all_columns) .load::(conn) .expect("Error loading collection groups") - }} + }) + .await } pub async fn delete(&self, org_uuid: &OrganizationId, conn: &DbConn) -> EmptyResult { @@ -428,13 +443,14 @@ impl CollectionGroup { group_user.update_user_revision(conn).await; } - db_run! { conn: { + conn.run(move |conn| { diesel::delete(collections_groups::table) .filter(collections_groups::collections_uuid.eq(&self.collections_uuid)) .filter(collections_groups::groups_uuid.eq(&self.groups_uuid)) .execute(conn) .map_res("Error deleting collection group") - }} + }) + .await } pub async fn delete_all_by_group(group_uuid: &GroupId, org_uuid: &OrganizationId, conn: &DbConn) -> EmptyResult { @@ -443,12 +459,13 @@ impl CollectionGroup { group_user.update_user_revision(conn).await; } - db_run! { conn: { + conn.run(move |conn| { diesel::delete(collections_groups::table) .filter(collections_groups::groups_uuid.eq(group_uuid)) .execute(conn) .map_res("Error deleting collection group") - }} + }) + .await } pub async fn delete_all_by_collection( @@ -464,12 +481,13 @@ impl CollectionGroup { } } - db_run! { conn: { + conn.run(move |conn| { diesel::delete(collections_groups::table) .filter(collections_groups::collections_uuid.eq(collection_uuid)) .execute(conn) .map_res("Error deleting collection group") - }} + }) + .await } } @@ -521,30 +539,31 @@ impl GroupUser { } pub async fn find_by_group(group_uuid: &GroupId, org_uuid: &OrganizationId, conn: &DbConn) -> Vec { - db_run! { conn: { + conn.run(move |conn| { groups_users::table - .inner_join(groups::table.on( - groups::uuid.eq(groups_users::groups_uuid) - )) - .inner_join(users_organizations::table.on( - users_organizations::uuid.eq(groups_users::users_organizations_uuid) - .and(users_organizations::org_uuid.eq(groups::organizations_uuid)) - )) + .inner_join(groups::table.on(groups::uuid.eq(groups_users::groups_uuid))) + .inner_join( + users_organizations::table.on(users_organizations::uuid + .eq(groups_users::users_organizations_uuid) + .and(users_organizations::org_uuid.eq(groups::organizations_uuid))), + ) .filter(groups_users::groups_uuid.eq(group_uuid)) .filter(groups::organizations_uuid.eq(org_uuid)) .select(groups_users::all_columns) .load::(conn) .expect("Error loading group users") - }} + }) + .await } pub async fn find_by_member(member_uuid: &MembershipId, conn: &DbConn) -> Vec { - db_run! { conn: { + conn.run(move |conn| { groups_users::table .filter(groups_users::users_organizations_uuid.eq(member_uuid)) .load::(conn) .expect("Error loading groups for user") - }} + }) + .await } pub async fn has_access_to_collection_by_member( @@ -552,24 +571,23 @@ impl GroupUser { member_uuid: &MembershipId, conn: &DbConn, ) -> bool { - db_run! { conn: { + conn.run(move |conn| { groups_users::table - .inner_join(collections_groups::table.on( - collections_groups::groups_uuid.eq(groups_users::groups_uuid) - )) - .inner_join(groups::table.on( - groups::uuid.eq(groups_users::groups_uuid) - )) - .inner_join(collections::table.on( - collections::uuid.eq(collections_groups::collections_uuid) - .and(collections::org_uuid.eq(groups::organizations_uuid)) - )) + .inner_join(collections_groups::table.on(collections_groups::groups_uuid.eq(groups_users::groups_uuid))) + .inner_join(groups::table.on(groups::uuid.eq(groups_users::groups_uuid))) + .inner_join( + collections::table.on(collections::uuid + .eq(collections_groups::collections_uuid) + .and(collections::org_uuid.eq(groups::organizations_uuid))), + ) .filter(collections_groups::collections_uuid.eq(collection_uuid)) .filter(groups_users::users_organizations_uuid.eq(member_uuid)) .count() .first::(conn) - .unwrap_or(0) != 0 - }} + .unwrap_or(0) + != 0 + }) + .await } pub async fn has_full_access_by_member( @@ -577,18 +595,18 @@ impl GroupUser { member_uuid: &MembershipId, conn: &DbConn, ) -> bool { - db_run! { conn: { + conn.run(move |conn| { groups_users::table - .inner_join(groups::table.on( - groups::uuid.eq(groups_users::groups_uuid) - )) + .inner_join(groups::table.on(groups::uuid.eq(groups_users::groups_uuid))) .filter(groups::organizations_uuid.eq(org_uuid)) .filter(groups::access_all.eq(true)) .filter(groups_users::users_organizations_uuid.eq(member_uuid)) .count() .first::(conn) - .unwrap_or(0) != 0 - }} + .unwrap_or(0) + != 0 + }) + .await } pub async fn update_user_revision(&self, conn: &DbConn) { @@ -606,15 +624,16 @@ impl GroupUser { match Membership::find_by_uuid(member_uuid, conn).await { Some(member) => User::update_uuid_revision(&member.user_uuid, conn).await, None => warn!("Member could not be found!"), - }; + } - db_run! { conn: { + conn.run(move |conn| { diesel::delete(groups_users::table) .filter(groups_users::groups_uuid.eq(group_uuid)) .filter(groups_users::users_organizations_uuid.eq(member_uuid)) .execute(conn) .map_res("Error deleting group users") - }} + }) + .await } pub async fn delete_all_by_group(group_uuid: &GroupId, org_uuid: &OrganizationId, conn: &DbConn) -> EmptyResult { @@ -623,12 +642,13 @@ impl GroupUser { group_user.update_user_revision(conn).await; } - db_run! { conn: { + conn.run(move |conn| { diesel::delete(groups_users::table) .filter(groups_users::groups_uuid.eq(group_uuid)) .execute(conn) .map_res("Error deleting group users") - }} + }) + .await } pub async fn delete_all_by_member(member_uuid: &MembershipId, conn: &DbConn) -> EmptyResult { @@ -637,12 +657,13 @@ impl GroupUser { None => warn!("Member could not be found!"), } - db_run! { conn: { + conn.run(move |conn| { diesel::delete(groups_users::table) .filter(groups_users::users_organizations_uuid.eq(member_uuid)) .execute(conn) .map_res("Error deleting user groups") - }} + }) + .await } } diff --git a/src/db/models/mod.rs b/src/db/models/mod.rs index 7cc81852..1cacbcac 100644 --- a/src/db/models/mod.rs +++ b/src/db/models/mod.rs @@ -23,7 +23,7 @@ pub use self::attachment::{Attachment, AttachmentId}; pub use self::auth_request::{AuthRequest, AuthRequestId}; pub use self::cipher::{Cipher, CipherId, RepromptType}; pub use self::collection::{Collection, CollectionCipher, CollectionId, CollectionUser}; -pub use self::device::{Device, DeviceId, DeviceType, PushId}; +pub use self::device::{Device, DeviceId, DeviceType, DeviceWithAuthRequest, PushId}; pub use self::emergency_access::{EmergencyAccess, EmergencyAccessId, EmergencyAccessStatus, EmergencyAccessType}; pub use self::event::{Event, EventType}; pub use self::favorite::Favorite; @@ -35,8 +35,8 @@ pub use self::organization::{ OrganizationId, }; pub use self::send::{ - id::{SendFileId, SendId}, Send, SendType, + id::{SendFileId, SendId}, }; pub use self::sso_auth::{OIDCAuthenticatedUser, OIDCCodeResponseError, SsoAuth}; pub use self::two_factor::{TwoFactor, TwoFactorType}; diff --git a/src/db/models/org_policy.rs b/src/db/models/org_policy.rs index 7e922f35..88b7872c 100644 --- a/src/db/models/org_policy.rs +++ b/src/db/models/org_policy.rs @@ -1,14 +1,17 @@ use derive_more::{AsRef, From}; +use diesel::prelude::*; use serde::Deserialize; use serde_json::Value; -use crate::api::core::two_factor; -use crate::api::EmptyResult; -use crate::db::schema::{org_policies, users_organizations}; -use crate::db::DbConn; -use crate::error::MapResult; -use crate::CONFIG; -use diesel::prelude::*; +use crate::{ + CONFIG, + api::{EmptyResult, core::two_factor}, + db::{ + DbConn, + schema::{org_policies, users_organizations}, + }, + error::MapResult, +}; use super::{Membership, MembershipId, MembershipStatus, MembershipType, OrganizationId, TwoFactor, UserId}; @@ -148,37 +151,38 @@ impl OrgPolicy { } pub async fn delete(self, conn: &DbConn) -> EmptyResult { - db_run! { conn: { + conn.run(move |conn| { diesel::delete(org_policies::table.filter(org_policies::uuid.eq(self.uuid))) .execute(conn) .map_res("Error deleting org_policy") - }} + }) + .await } pub async fn find_by_org(org_uuid: &OrganizationId, conn: &DbConn) -> Vec { - db_run! { conn: { + conn.run(move |conn| { org_policies::table .filter(org_policies::org_uuid.eq(org_uuid)) .load::(conn) .expect("Error loading org_policy") - }} + }) + .await } pub async fn find_confirmed_by_user(user_uuid: &UserId, conn: &DbConn) -> Vec { - db_run! { conn: { + conn.run(move |conn| { org_policies::table .inner_join( - users_organizations::table.on( - users_organizations::org_uuid.eq(org_policies::org_uuid) - .and(users_organizations::user_uuid.eq(user_uuid))) - ) - .filter( - users_organizations::status.eq(MembershipStatus::Confirmed as i32) + users_organizations::table.on(users_organizations::org_uuid + .eq(org_policies::org_uuid) + .and(users_organizations::user_uuid.eq(user_uuid))), ) + .filter(users_organizations::status.eq(MembershipStatus::Confirmed as i32)) .select(org_policies::all_columns) .load::(conn) .expect("Error loading org_policy") - }} + }) + .await } pub async fn find_by_org_and_type( @@ -186,21 +190,23 @@ impl OrgPolicy { policy_type: OrgPolicyType, conn: &DbConn, ) -> Option { - db_run! { conn: { + conn.run(move |conn| { org_policies::table .filter(org_policies::org_uuid.eq(org_uuid)) .filter(org_policies::atype.eq(policy_type as i32)) .first::(conn) .ok() - }} + }) + .await } pub async fn delete_all_by_organization(org_uuid: &OrganizationId, conn: &DbConn) -> EmptyResult { - db_run! { conn: { + conn.run(move |conn| { diesel::delete(org_policies::table.filter(org_policies::org_uuid.eq(org_uuid))) .execute(conn) .map_res("Error deleting org_policy") - }} + }) + .await } pub async fn find_accepted_and_confirmed_by_user_and_active_policy( @@ -208,25 +214,22 @@ impl OrgPolicy { policy_type: OrgPolicyType, conn: &DbConn, ) -> Vec { - db_run! { conn: { + conn.run(move |conn| { org_policies::table .inner_join( - users_organizations::table.on( - users_organizations::org_uuid.eq(org_policies::org_uuid) - .and(users_organizations::user_uuid.eq(user_uuid))) - ) - .filter( - users_organizations::status.eq(MembershipStatus::Accepted as i32) - ) - .or_filter( - users_organizations::status.eq(MembershipStatus::Confirmed as i32) + users_organizations::table.on(users_organizations::org_uuid + .eq(org_policies::org_uuid) + .and(users_organizations::user_uuid.eq(user_uuid))), ) + .filter(users_organizations::status.eq(MembershipStatus::Accepted as i32)) + .or_filter(users_organizations::status.eq(MembershipStatus::Confirmed as i32)) .filter(org_policies::atype.eq(policy_type as i32)) .filter(org_policies::enabled.eq(true)) .select(org_policies::all_columns) .load::(conn) .expect("Error loading org_policy") - }} + }) + .await } pub async fn find_confirmed_by_user_and_active_policy( @@ -234,22 +237,21 @@ impl OrgPolicy { policy_type: OrgPolicyType, conn: &DbConn, ) -> Vec { - db_run! { conn: { + conn.run(move |conn| { org_policies::table .inner_join( - users_organizations::table.on( - users_organizations::org_uuid.eq(org_policies::org_uuid) - .and(users_organizations::user_uuid.eq(user_uuid))) - ) - .filter( - users_organizations::status.eq(MembershipStatus::Confirmed as i32) + users_organizations::table.on(users_organizations::org_uuid + .eq(org_policies::org_uuid) + .and(users_organizations::user_uuid.eq(user_uuid))), ) + .filter(users_organizations::status.eq(MembershipStatus::Confirmed as i32)) .filter(org_policies::atype.eq(policy_type as i32)) .filter(org_policies::enabled.eq(true)) .select(org_policies::all_columns) .load::(conn) .expect("Error loading org_policy") - }} + }) + .await } /// Returns true if the user belongs to an org that has enabled the specified policy type, @@ -269,10 +271,10 @@ impl OrgPolicy { continue; } - if let Some(user) = Membership::find_confirmed_by_user_and_org(user_uuid, &policy.org_uuid, conn).await { - if user.atype < MembershipType::Admin { - return true; - } + if let Some(user) = Membership::find_confirmed_by_user_and_org(user_uuid, &policy.org_uuid, conn).await + && user.atype < MembershipType::Admin + { + return true; } } false @@ -282,13 +284,13 @@ impl OrgPolicy { if m.atype < MembershipType::Admin && m.status > (MembershipStatus::Invited as i32) { // Enforce TwoFactor/TwoStep login if let Some(p) = Self::find_by_org_and_type(&m.org_uuid, OrgPolicyType::TwoFactorAuthentication, conn).await + && p.enabled + && TwoFactor::find_by_user(&m.user_uuid, conn).await.is_empty() { - if p.enabled && TwoFactor::find_by_user(&m.user_uuid, conn).await.is_empty() { - if CONFIG.email_2fa_auto_fallback() { - two_factor::email::find_and_activate_email_2fa(&m.user_uuid, conn).await?; - } else { - err!(format!("Cannot {} because 2FA is required (membership {})", action, m.uuid)); - } + if CONFIG.email_2fa_auto_fallback() { + two_factor::email::find_and_activate_email_2fa(&m.user_uuid, conn).await?; + } else { + err!(format!("Cannot {} because 2FA is required (membership {})", action, m.uuid)); } } @@ -300,12 +302,14 @@ impl OrgPolicy { )); } - if let Some(p) = Self::find_by_org_and_type(&m.org_uuid, OrgPolicyType::SingleOrg, conn).await { - if p.enabled - && Membership::count_accepted_and_confirmed_by_user(&m.user_uuid, &m.org_uuid, conn).await > 0 - { - err!(format!("Cannot {} because the organization policy forbids being part of other organization (membership {})", action, m.uuid)); - } + if let Some(p) = Self::find_by_org_and_type(&m.org_uuid, OrgPolicyType::SingleOrg, conn).await + && p.enabled + && Membership::count_accepted_and_confirmed_by_user(&m.user_uuid, &m.org_uuid, conn).await > 0 + { + err!(format!( + "Cannot {} because the organization policy forbids being part of other organization (membership {})", + action, m.uuid + )); } } @@ -332,16 +336,16 @@ impl OrgPolicy { for policy in OrgPolicy::find_confirmed_by_user_and_active_policy(user_uuid, OrgPolicyType::SendOptions, conn).await { - if let Some(user) = Membership::find_confirmed_by_user_and_org(user_uuid, &policy.org_uuid, conn).await { - if user.atype < MembershipType::Admin { - match serde_json::from_str::(&policy.data) { - Ok(opts) => { - if opts.disable_hide_email { - return true; - } + if let Some(user) = Membership::find_confirmed_by_user_and_org(user_uuid, &policy.org_uuid, conn).await + && user.atype < MembershipType::Admin + { + match serde_json::from_str::(&policy.data) { + Ok(opts) => { + if opts.disable_hide_email { + return true; } - _ => error!("Failed to deserialize SendOptionsPolicyData: {}", policy.data), } + _ => error!("Failed to deserialize SendOptionsPolicyData: {}", policy.data), } } } @@ -349,10 +353,10 @@ impl OrgPolicy { } pub async fn is_enabled_for_member(member_uuid: &MembershipId, policy_type: OrgPolicyType, conn: &DbConn) -> bool { - if let Some(member) = Membership::find_by_uuid(member_uuid, conn).await { - if let Some(policy) = OrgPolicy::find_by_org_and_type(&member.org_uuid, policy_type, conn).await { - return policy.enabled; - } + if let Some(member) = Membership::find_by_uuid(member_uuid, conn).await + && let Some(policy) = OrgPolicy::find_by_org_and_type(&member.org_uuid, policy_type, conn).await + { + return policy.enabled; } false } diff --git a/src/db/models/organization.rs b/src/db/models/organization.rs index ae19b30c..d604add4 100644 --- a/src/db/models/organization.rs +++ b/src/db/models/organization.rs @@ -1,23 +1,32 @@ -use chrono::{NaiveDateTime, Utc}; -use derive_more::{AsRef, Deref, Display, From}; -use diesel::prelude::*; -use num_traits::FromPrimitive; -use serde_json::Value; use std::{ cmp::Ordering, collections::{HashMap, HashSet}, }; +use chrono::{NaiveDateTime, Utc}; +use derive_more::{AsRef, Deref, Display, From}; +use diesel::prelude::*; +use num_traits::FromPrimitive; +use serde_json::Value; + +use crate::{ + CONFIG, + api::EmptyResult, + db::{ + DbConn, + schema::{ + ciphers, ciphers_collections, collections_groups, groups, groups_users, org_policies, organization_api_key, + organizations, users, users_collections, users_organizations, + }, + }, + error::MapResult, +}; +use macros::UuidFromParam; + use super::{ - CipherId, Collection, CollectionGroup, CollectionId, CollectionUser, Group, GroupId, GroupUser, OrgPolicy, + Cipher, CipherId, Collection, CollectionGroup, CollectionId, CollectionUser, Group, GroupId, GroupUser, OrgPolicy, OrgPolicyType, TwoFactor, User, UserId, }; -use crate::db::schema::{ - ciphers, ciphers_collections, collections_groups, groups, groups_users, org_policies, organization_api_key, - organizations, users, users_collections, users_organizations, -}; -use crate::CONFIG; -use macros::UuidFromParam; #[derive(Identifiable, Queryable, Insertable, AsChangeset)] #[diesel(table_name = organizations)] @@ -93,6 +102,10 @@ pub enum MembershipType { impl MembershipType { pub fn from_str(s: &str) -> Option { + #[expect( + clippy::match_same_arms, + reason = "Specifically define `4|Custom` since this is a hack, not a default" + )] match s { "0" | "Owner" => Some(MembershipType::Owner), "1" | "Admin" => Some(MembershipType::Admin), @@ -321,11 +334,6 @@ impl OrganizationApiKey { } } -use crate::db::DbConn; - -use crate::api::EmptyResult; -use crate::error::MapResult; - /// Database methods impl Organization { pub async fn save(&self, conn: &DbConn) -> EmptyResult { @@ -333,7 +341,7 @@ impl Organization { err!(format!("BillingEmail {} is not a valid email address", self.billing_email)) } - for member in Membership::find_by_org(&self.uuid, conn).await.iter() { + for member in &Membership::find_by_org(&self.uuid, conn).await { User::update_uuid_revision(&member.user_uuid, conn).await; } @@ -369,8 +377,6 @@ impl Organization { } pub async fn delete(self, conn: &DbConn) -> EmptyResult { - use super::{Cipher, Collection}; - Cipher::delete_all_by_organization(&self.uuid, conn).await?; Collection::delete_all_by_organization(&self.uuid, conn).await?; Membership::delete_all_by_organization(&self.uuid, conn).await?; @@ -378,43 +384,30 @@ impl Organization { Group::delete_all_by_organization(&self.uuid, conn).await?; OrganizationApiKey::delete_all_by_organization(&self.uuid, conn).await?; - db_run! { conn: { + conn.run(move |conn| { diesel::delete(organizations::table.filter(organizations::uuid.eq(self.uuid))) .execute(conn) .map_res("Error saving organization") - }} + }) + .await } pub async fn find_by_uuid(uuid: &OrganizationId, conn: &DbConn) -> Option { - db_run! { conn: { - organizations::table - .filter(organizations::uuid.eq(uuid)) - .first::(conn) - .ok() - }} + conn.run(move |conn| organizations::table.filter(organizations::uuid.eq(uuid)).first::(conn).ok()).await } pub async fn find_by_name(name: &str, conn: &DbConn) -> Option { - db_run! { conn: { - organizations::table - .filter(organizations::name.eq(name)) - .first::(conn) - .ok() - }} + conn.run(move |conn| organizations::table.filter(organizations::name.eq(name)).first::(conn).ok()).await } pub async fn get_all(conn: &DbConn) -> Vec { - db_run! { conn: { - organizations::table - .load::(conn) - .expect("Error loading organizations") - }} + conn.run(move |conn| organizations::table.load::(conn).expect("Error loading organizations")).await } pub async fn find_main_org_user_email(user_email: &str, conn: &DbConn) -> Option { let lower_mail = user_email.to_lowercase(); - db_run! { conn: { + conn.run(move |conn| { organizations::table .inner_join(users_organizations::table.on(users_organizations::org_uuid.eq(organizations::uuid))) .inner_join(users::table.on(users::uuid.eq(users_organizations::user_uuid))) @@ -424,13 +417,14 @@ impl Organization { .select(organizations::all_columns) .first::(conn) .ok() - }} + }) + .await } pub async fn find_org_user_email(user_email: &str, conn: &DbConn) -> Vec { let lower_mail = user_email.to_lowercase(); - db_run! { conn: { + conn.run(move |conn| { organizations::table .inner_join(users_organizations::table.on(users_organizations::org_uuid.eq(organizations::uuid))) .inner_join(users::table.on(users::uuid.eq(users_organizations::user_uuid))) @@ -440,7 +434,8 @@ impl Organization { .select(organizations::all_columns) .load::(conn) .expect("Error loading user orgs") - }} + }) + .await } } @@ -780,11 +775,12 @@ impl Membership { CollectionUser::delete_all_by_user_and_org(&self.user_uuid, &self.org_uuid, conn).await?; GroupUser::delete_all_by_member(&self.uuid, conn).await?; - db_run! { conn: { + conn.run(move |conn| { diesel::delete(users_organizations::table.filter(users_organizations::uuid.eq(self.uuid))) .execute(conn) .map_res("Error removing user from organization") - }} + }) + .await } pub async fn delete_all_by_organization(org_uuid: &OrganizationId, conn: &DbConn) -> EmptyResult { @@ -802,10 +798,10 @@ impl Membership { } pub async fn find_by_email_and_org(email: &str, org_uuid: &OrganizationId, conn: &DbConn) -> Option { - if let Some(user) = User::find_by_mail(email, conn).await { - if let Some(member) = Membership::find_by_user_and_org(&user.uuid, org_uuid, conn).await { - return Some(member); - } + if let Some(user) = User::find_by_mail(email, conn).await + && let Some(member) = Membership::find_by_user_and_org(&user.uuid, org_uuid, conn).await + { + return Some(member); } None @@ -824,64 +820,67 @@ impl Membership { } pub async fn find_by_uuid(uuid: &MembershipId, conn: &DbConn) -> Option { - db_run! { conn: { - users_organizations::table - .filter(users_organizations::uuid.eq(uuid)) - .first::(conn) - .ok() - }} + conn.run(move |conn| { + users_organizations::table.filter(users_organizations::uuid.eq(uuid)).first::(conn).ok() + }) + .await } pub async fn find_by_uuid_and_org(uuid: &MembershipId, org_uuid: &OrganizationId, conn: &DbConn) -> Option { - db_run! { conn: { + conn.run(move |conn| { users_organizations::table .filter(users_organizations::uuid.eq(uuid)) .filter(users_organizations::org_uuid.eq(org_uuid)) .first::(conn) .ok() - }} + }) + .await } pub async fn find_confirmed_by_user(user_uuid: &UserId, conn: &DbConn) -> Vec { - db_run! { conn: { + conn.run(move |conn| { users_organizations::table .filter(users_organizations::user_uuid.eq(user_uuid)) .filter(users_organizations::status.eq(MembershipStatus::Confirmed as i32)) .load::(conn) .unwrap_or_default() - }} + }) + .await } pub async fn find_invited_by_user(user_uuid: &UserId, conn: &DbConn) -> Vec { - db_run! { conn: { + conn.run(move |conn| { users_organizations::table .filter(users_organizations::user_uuid.eq(user_uuid)) .filter(users_organizations::status.eq(MembershipStatus::Invited as i32)) .load::(conn) .unwrap_or_default() - }} + }) + .await } // Should be used only when email are disabled. // In Organizations::send_invite status is set to Accepted only if the user has a password. pub async fn accept_user_invitations(user_uuid: &UserId, conn: &DbConn) -> EmptyResult { - db_run! { conn: { + conn.run(move |conn| { diesel::update(users_organizations::table) .filter(users_organizations::user_uuid.eq(user_uuid)) .filter(users_organizations::status.eq(MembershipStatus::Invited as i32)) .set(users_organizations::status.eq(MembershipStatus::Accepted as i32)) .execute(conn) .map_res("Error confirming invitations") - }} + }) + .await } pub async fn find_any_state_by_user(user_uuid: &UserId, conn: &DbConn) -> Vec { - db_run! { conn: { + conn.run(move |conn| { users_organizations::table .filter(users_organizations::user_uuid.eq(user_uuid)) .load::(conn) .unwrap_or_default() - }} + }) + .await } pub async fn count_accepted_and_confirmed_by_user( @@ -889,70 +888,83 @@ impl Membership { excluded_org: &OrganizationId, conn: &DbConn, ) -> i64 { - db_run! { conn: { + conn.run(move |conn| { users_organizations::table .filter(users_organizations::user_uuid.eq(user_uuid)) .filter(users_organizations::org_uuid.ne(excluded_org)) - .filter(users_organizations::status.eq(MembershipStatus::Accepted as i32).or(users_organizations::status.eq(MembershipStatus::Confirmed as i32))) + .filter( + users_organizations::status + .eq(MembershipStatus::Accepted as i32) + .or(users_organizations::status.eq(MembershipStatus::Confirmed as i32)), + ) .count() .first::(conn) .unwrap_or(0) - }} + }) + .await } pub async fn find_by_org(org_uuid: &OrganizationId, conn: &DbConn) -> Vec { - db_run! { conn: { + conn.run(move |conn| { users_organizations::table .filter(users_organizations::org_uuid.eq(org_uuid)) .load::(conn) .expect("Error loading user organizations") - }} + }) + .await } pub async fn find_confirmed_by_org(org_uuid: &OrganizationId, conn: &DbConn) -> Vec { - db_run! { conn: { + conn.run(move |conn| { users_organizations::table .filter(users_organizations::org_uuid.eq(org_uuid)) .filter(users_organizations::status.eq(MembershipStatus::Confirmed as i32)) .load::(conn) .unwrap_or_default() - }} + }) + .await } // Get all users which are either owner or admin, or a manager which can manage/access all pub async fn find_confirmed_and_manage_all_by_org(org_uuid: &OrganizationId, conn: &DbConn) -> Vec { - db_run! { conn: { + conn.run(move |conn| { users_organizations::table .filter(users_organizations::org_uuid.eq(org_uuid)) .filter(users_organizations::status.eq(MembershipStatus::Confirmed as i32)) .filter( - users_organizations::atype.eq_any(vec![MembershipType::Owner as i32, MembershipType::Admin as i32]) - .or(users_organizations::atype.eq(MembershipType::Manager as i32).and(users_organizations::access_all.eq(true))) + users_organizations::atype + .eq_any(vec![MembershipType::Owner as i32, MembershipType::Admin as i32]) + .or(users_organizations::atype + .eq(MembershipType::Manager as i32) + .and(users_organizations::access_all.eq(true))), ) .load::(conn) .unwrap_or_default() - }} + }) + .await } pub async fn count_by_org(org_uuid: &OrganizationId, conn: &DbConn) -> i64 { - db_run! { conn: { + conn.run(move |conn| { users_organizations::table .filter(users_organizations::org_uuid.eq(org_uuid)) .count() .first::(conn) .ok() .unwrap_or(0) - }} + }) + .await } pub async fn find_by_org_and_type(org_uuid: &OrganizationId, atype: MembershipType, conn: &DbConn) -> Vec { - db_run! { conn: { + conn.run(move |conn| { users_organizations::table .filter(users_organizations::org_uuid.eq(org_uuid)) .filter(users_organizations::atype.eq(atype as i32)) .load::(conn) .expect("Error loading user organizations") - }} + }) + .await } pub async fn count_confirmed_by_org_and_type( @@ -960,7 +972,7 @@ impl Membership { atype: MembershipType, conn: &DbConn, ) -> i64 { - db_run! { conn: { + conn.run(move |conn| { users_organizations::table .filter(users_organizations::org_uuid.eq(org_uuid)) .filter(users_organizations::atype.eq(atype as i32)) @@ -968,17 +980,19 @@ impl Membership { .count() .first::(conn) .unwrap_or(0) - }} + }) + .await } pub async fn find_by_user_and_org(user_uuid: &UserId, org_uuid: &OrganizationId, conn: &DbConn) -> Option { - db_run! { conn: { + conn.run(move |conn| { users_organizations::table .filter(users_organizations::user_uuid.eq(user_uuid)) .filter(users_organizations::org_uuid.eq(org_uuid)) .first::(conn) .ok() - }} + }) + .await } pub async fn find_confirmed_by_user_and_org( @@ -986,78 +1000,76 @@ impl Membership { org_uuid: &OrganizationId, conn: &DbConn, ) -> Option { - db_run! { conn: { + conn.run(move |conn| { users_organizations::table .filter(users_organizations::user_uuid.eq(user_uuid)) .filter(users_organizations::org_uuid.eq(org_uuid)) - .filter( - users_organizations::status.eq(MembershipStatus::Confirmed as i32) - ) + .filter(users_organizations::status.eq(MembershipStatus::Confirmed as i32)) .first::(conn) .ok() - }} + }) + .await } pub async fn find_by_user(user_uuid: &UserId, conn: &DbConn) -> Vec { - db_run! { conn: { + conn.run(move |conn| { users_organizations::table .filter(users_organizations::user_uuid.eq(user_uuid)) .load::(conn) .expect("Error loading user organizations") - }} + }) + .await } pub async fn get_orgs_by_user(user_uuid: &UserId, conn: &DbConn) -> Vec { - db_run! { conn: { + conn.run(move |conn| { users_organizations::table .filter(users_organizations::user_uuid.eq(user_uuid)) .select(users_organizations::org_uuid) .load::(conn) .unwrap_or_default() - }} + }) + .await } pub async fn find_by_user_and_policy(user_uuid: &UserId, policy_type: OrgPolicyType, conn: &DbConn) -> Vec { - db_run! { conn: { + conn.run(move |conn| { users_organizations::table .inner_join( - org_policies::table.on( - org_policies::org_uuid.eq(users_organizations::org_uuid) - .and(users_organizations::user_uuid.eq(user_uuid)) - .and(org_policies::atype.eq(policy_type as i32)) - .and(org_policies::enabled.eq(true))) - ) - .filter( - users_organizations::status.eq(MembershipStatus::Confirmed as i32) + org_policies::table.on(org_policies::org_uuid + .eq(users_organizations::org_uuid) + .and(users_organizations::user_uuid.eq(user_uuid)) + .and(org_policies::atype.eq(policy_type as i32)) + .and(org_policies::enabled.eq(true))), ) + .filter(users_organizations::status.eq(MembershipStatus::Confirmed as i32)) .select(users_organizations::all_columns) .load::(conn) .unwrap_or_default() - }} + }) + .await } pub async fn find_by_cipher_and_org(cipher_uuid: &CipherId, org_uuid: &OrganizationId, conn: &DbConn) -> Vec { - db_run! { conn: { + conn.run(move |conn| { users_organizations::table - .filter(users_organizations::org_uuid.eq(org_uuid)) - .left_join(users_collections::table.on( - users_collections::user_uuid.eq(users_organizations::user_uuid) - )) - .left_join(ciphers_collections::table.on( - ciphers_collections::collection_uuid.eq(users_collections::collection_uuid).and( - ciphers_collections::cipher_uuid.eq(&cipher_uuid) + .filter(users_organizations::org_uuid.eq(org_uuid)) + .left_join(users_collections::table.on(users_collections::user_uuid.eq(users_organizations::user_uuid))) + .left_join( + ciphers_collections::table.on(ciphers_collections::collection_uuid + .eq(users_collections::collection_uuid) + .and(ciphers_collections::cipher_uuid.eq(&cipher_uuid))), ) - )) - .filter( - users_organizations::access_all.eq(true).or( // AccessAll.. - ciphers_collections::cipher_uuid.eq(&cipher_uuid) // ..or access to collection with cipher - ) - ) - .select(users_organizations::all_columns) - .distinct() - .load::(conn) - .expect("Error loading user organizations") - }} + .filter(users_organizations::access_all.eq(true).or( + // AccessAll.. + ciphers_collections::cipher_uuid.eq(&cipher_uuid), // ..or access to collection with cipher + )) + .select(users_organizations::all_columns) + .distinct() + .load::(conn) + .expect("Error loading user organizations") + }) + .await } pub async fn find_by_cipher_and_org_with_group( @@ -1065,45 +1077,54 @@ impl Membership { org_uuid: &OrganizationId, conn: &DbConn, ) -> Vec { - db_run! { conn: { + conn.run(move |conn| { users_organizations::table - .filter(users_organizations::org_uuid.eq(org_uuid)) - .inner_join(groups_users::table.on( - groups_users::users_organizations_uuid.eq(users_organizations::uuid) - )) - .left_join(collections_groups::table.on( - collections_groups::groups_uuid.eq(groups_users::groups_uuid) - )) - .left_join(groups::table.on(groups::uuid.eq(groups_users::groups_uuid) - .and(groups::organizations_uuid.eq(users_organizations::org_uuid)) - )) - .left_join(ciphers_collections::table.on( - ciphers_collections::collection_uuid.eq(collections_groups::collections_uuid).and(ciphers_collections::cipher_uuid.eq(&cipher_uuid)) - - )) - .filter( - groups::access_all.eq(true).or( // AccessAll via groups - ciphers_collections::cipher_uuid.eq(&cipher_uuid) // ..or access to collection via group - ) + .filter(users_organizations::org_uuid.eq(org_uuid)) + .inner_join( + groups_users::table.on(groups_users::users_organizations_uuid.eq(users_organizations::uuid)), ) + .left_join(collections_groups::table.on(collections_groups::groups_uuid.eq(groups_users::groups_uuid))) + .left_join( + groups::table.on(groups::uuid + .eq(groups_users::groups_uuid) + .and(groups::organizations_uuid.eq(users_organizations::org_uuid))), + ) + .left_join( + ciphers_collections::table.on(ciphers_collections::collection_uuid + .eq(collections_groups::collections_uuid) + .and(ciphers_collections::cipher_uuid.eq(&cipher_uuid))), + ) + .filter(groups::access_all.eq(true).or( + // AccessAll via groups + ciphers_collections::cipher_uuid.eq(&cipher_uuid), // ..or access to collection via group + )) .select(users_organizations::all_columns) .distinct() - .load::(conn) - .expect("Error loading user organizations with groups") - }} + .load::(conn) + .expect("Error loading user organizations with groups") + }) + .await } pub async fn user_has_ge_admin_access_to_cipher(user_uuid: &UserId, cipher_uuid: &CipherId, conn: &DbConn) -> bool { - db_run! { conn: { + conn.run(move |conn| { users_organizations::table - .inner_join(ciphers::table.on(ciphers::uuid.eq(cipher_uuid).and(ciphers::organization_uuid.eq(users_organizations::org_uuid.nullable())))) - .filter(users_organizations::user_uuid.eq(user_uuid)) - .filter(users_organizations::atype.eq_any(vec![MembershipType::Owner as i32, MembershipType::Admin as i32])) - .count() - .first::(conn) - .ok() - .unwrap_or(0) != 0 - }} + .inner_join( + ciphers::table.on(ciphers::uuid + .eq(cipher_uuid) + .and(ciphers::organization_uuid.eq(users_organizations::org_uuid.nullable()))), + ) + .filter(users_organizations::user_uuid.eq(user_uuid)) + .filter( + users_organizations::atype.eq_any(vec![MembershipType::Owner as i32, MembershipType::Admin as i32]), + ) + .count() + .first::(conn) + .ok() + .unwrap_or(0) + != 0 + }) + .await } pub async fn find_by_collection_and_org( @@ -1111,44 +1132,41 @@ impl Membership { org_uuid: &OrganizationId, conn: &DbConn, ) -> Vec { - db_run! { conn: { + conn.run(move |conn| { users_organizations::table - .filter(users_organizations::org_uuid.eq(org_uuid)) - .left_join(users_collections::table.on( - users_collections::user_uuid.eq(users_organizations::user_uuid) - )) - .filter( - users_organizations::access_all.eq(true).or( // AccessAll.. - users_collections::collection_uuid.eq(&collection_uuid) // ..or access to collection with cipher - ) - ) - .select(users_organizations::all_columns) - .load::(conn) - .expect("Error loading user organizations") - }} + .filter(users_organizations::org_uuid.eq(org_uuid)) + .left_join(users_collections::table.on(users_collections::user_uuid.eq(users_organizations::user_uuid))) + .filter(users_organizations::access_all.eq(true).or( + // AccessAll.. + users_collections::collection_uuid.eq(&collection_uuid), // ..or access to collection with cipher + )) + .select(users_organizations::all_columns) + .load::(conn) + .expect("Error loading user organizations") + }) + .await } pub async fn find_by_external_id_and_org(ext_id: &str, org_uuid: &OrganizationId, conn: &DbConn) -> Option { - db_run! { conn: { + conn.run(move |conn| { users_organizations::table - .filter( - users_organizations::external_id.eq(ext_id) - .and(users_organizations::org_uuid.eq(org_uuid)) - ) - .first::(conn) - .ok() - }} + .filter(users_organizations::external_id.eq(ext_id).and(users_organizations::org_uuid.eq(org_uuid))) + .first::(conn) + .ok() + }) + .await } pub async fn find_main_user_org(user_uuid: &str, conn: &DbConn) -> Option { - db_run! { conn: { + conn.run(move |conn| { users_organizations::table .filter(users_organizations::user_uuid.eq(user_uuid)) .filter(users_organizations::status.ne(MembershipStatus::Revoked as i32)) .order(users_organizations::atype.asc()) .first::(conn) .ok() - }} + }) + .await } } @@ -1186,20 +1204,19 @@ impl OrganizationApiKey { } pub async fn find_by_org_uuid(org_uuid: &OrganizationId, conn: &DbConn) -> Option { - db_run! { conn: { - organization_api_key::table - .filter(organization_api_key::org_uuid.eq(org_uuid)) - .first::(conn) - .ok() - }} + conn.run(move |conn| { + organization_api_key::table.filter(organization_api_key::org_uuid.eq(org_uuid)).first::(conn).ok() + }) + .await } pub async fn delete_all_by_organization(org_uuid: &OrganizationId, conn: &DbConn) -> EmptyResult { - db_run! { conn: { + conn.run(move |conn| { diesel::delete(organization_api_key::table.filter(organization_api_key::org_uuid.eq(org_uuid))) .execute(conn) .map_res("Error removing organization api key from organization") - }} + }) + .await } } diff --git a/src/db/models/send.rs b/src/db/models/send.rs index 5b6611fa..0a2f1a2a 100644 --- a/src/db/models/send.rs +++ b/src/db/models/send.rs @@ -1,11 +1,19 @@ use chrono::{NaiveDateTime, Utc}; +use data_encoding::BASE64URL_NOPAD; +use diesel::prelude::*; use serde_json::Value; +use uuid::Uuid; -use crate::{config::PathType, util::LowerCase, CONFIG}; +use crate::{ + CONFIG, + api::EmptyResult, + config::PathType, + db::{DbConn, schema::sends}, + error::MapResult, + util::{LowerCase, NumberOrString, format_date}, +}; use super::{OrganizationId, User, UserId}; -use crate::db::schema::sends; -use diesel::prelude::*; use id::SendId; #[derive(Identifiable, Queryable, Insertable, AsChangeset)] @@ -107,37 +115,33 @@ impl Send { pub fn check_password(&self, password: &str) -> bool { match (&self.password_hash, &self.password_salt, self.password_iter) { (Some(hash), Some(salt), Some(iter)) => { - crate::crypto::verify_password_hash(password.as_bytes(), salt, hash, iter as u32) + crate::crypto::verify_password_hash(password.as_bytes(), salt, hash, iter.cast_unsigned()) } _ => false, } } pub async fn creator_identifier(&self, conn: &DbConn) -> Option { - if let Some(hide_email) = self.hide_email { - if hide_email { - return None; - } + if let Some(hide_email) = self.hide_email + && hide_email + { + return None; } - if let Some(user_uuid) = &self.user_uuid { - if let Some(user) = User::find_by_uuid(user_uuid, conn).await { - return Some(user.email); - } + if let Some(user_uuid) = &self.user_uuid + && let Some(user) = User::find_by_uuid(user_uuid, conn).await + { + return Some(user.email); } None } pub fn to_json(&self) -> Value { - use crate::util::format_date; - use data_encoding::BASE64URL_NOPAD; - use uuid::Uuid; - let mut data = serde_json::from_str::>(&self.data).map(|d| d.data).unwrap_or_default(); // Mobile clients expect size to be a string instead of a number - if let Some(size) = data.get("size").and_then(|v| v.as_i64()) { + if let Some(size) = data.get("size").and_then(Value::as_i64) { data["size"] = Value::String(size.to_string()); } @@ -167,12 +171,10 @@ impl Send { } pub async fn to_json_access(&self, conn: &DbConn) -> Value { - use crate::util::format_date; - let mut data = serde_json::from_str::>(&self.data).map(|d| d.data).unwrap_or_default(); // Mobile clients expect size to be a string instead of a number - if let Some(size) = data.get("size").and_then(|v| v.as_i64()) { + if let Some(size) = data.get("size").and_then(Value::as_i64) { data["size"] = Value::String(size.to_string()); } @@ -191,12 +193,6 @@ impl Send { } } -use crate::db::DbConn; - -use crate::api::EmptyResult; -use crate::error::MapResult; -use crate::util::NumberOrString; - impl Send { pub async fn save(&mut self, conn: &DbConn) -> EmptyResult { self.update_users_revision(conn).await; @@ -240,11 +236,10 @@ impl Send { operator.delete_with(&self.uuid).recursive(true).await.ok(); } - db_run! { conn: { - diesel::delete(sends::table.filter(sends::uuid.eq(&self.uuid))) - .execute(conn) - .map_res("Error deleting send") - }} + conn.run(move |conn| { + diesel::delete(sends::table.filter(sends::uuid.eq(&self.uuid))).execute(conn).map_res("Error deleting send") + }) + .await } /// Purge all sends that are past their deletion date. @@ -256,15 +251,12 @@ impl Send { pub async fn update_users_revision(&self, conn: &DbConn) -> Vec { let mut user_uuids = Vec::new(); - match &self.user_uuid { - Some(user_uuid) => { - User::update_uuid_revision(user_uuid, conn).await; - user_uuids.push(user_uuid.clone()) - } - None => { - // Belongs to Organization, not implemented - } - }; + if let Some(user_uuid) = &self.user_uuid { + User::update_uuid_revision(user_uuid, conn).await; + user_uuids.push(user_uuid.clone()); + } else { + // Belongs to Organization, not implemented + } user_uuids } @@ -276,9 +268,6 @@ impl Send { } pub async fn find_by_access_id(access_id: &str, conn: &DbConn) -> Option { - use data_encoding::BASE64URL_NOPAD; - use uuid::Uuid; - let Ok(uuid_vec) = BASE64URL_NOPAD.decode(access_id.as_bytes()) else { return None; }; @@ -292,50 +281,38 @@ impl Send { } pub async fn find_by_uuid(uuid: &SendId, conn: &DbConn) -> Option { - db_run! { conn: { - sends::table - .filter(sends::uuid.eq(uuid)) - .first::(conn) - .ok() - }} + conn.run(move |conn| sends::table.filter(sends::uuid.eq(uuid)).first::(conn).ok()).await } pub async fn find_by_uuid_and_user(uuid: &SendId, user_uuid: &UserId, conn: &DbConn) -> Option { - db_run! { conn: { - sends::table - .filter(sends::uuid.eq(uuid)) - .filter(sends::user_uuid.eq(user_uuid)) - .first::(conn) - .ok() - }} + conn.run(move |conn| { + sends::table.filter(sends::uuid.eq(uuid)).filter(sends::user_uuid.eq(user_uuid)).first::(conn).ok() + }) + .await } pub async fn find_by_user(user_uuid: &UserId, conn: &DbConn) -> Vec { - db_run! { conn: { - sends::table - .filter(sends::user_uuid.eq(user_uuid)) - .load::(conn) - .expect("Error loading sends") - }} + conn.run(move |conn| { + sends::table.filter(sends::user_uuid.eq(user_uuid)).load::(conn).expect("Error loading sends") + }) + .await } pub async fn size_by_user(user_uuid: &UserId, conn: &DbConn) -> Option { - let sends = Self::find_by_user(user_uuid, conn).await; - #[derive(serde::Deserialize)] struct FileData { #[serde(rename = "size", alias = "Size")] size: NumberOrString, } + let sends = Self::find_by_user(user_uuid, conn).await; let mut total: i64 = 0; for send in sends { - if send.atype == SendType::File as i32 { - if let Ok(size) = + if send.atype == SendType::File as i32 + && let Ok(size) = serde_json::from_str::(&send.data).map_err(Into::into).and_then(|d| d.size.into_i64()) - { - total = total.checked_add(size)?; - }; + { + total = total.checked_add(size)?; } } @@ -343,22 +320,18 @@ impl Send { } pub async fn find_by_org(org_uuid: &OrganizationId, conn: &DbConn) -> Vec { - db_run! { conn: { - sends::table - .filter(sends::organization_uuid.eq(org_uuid)) - .load::(conn) - .expect("Error loading sends") - }} + conn.run(move |conn| { + sends::table.filter(sends::organization_uuid.eq(org_uuid)).load::(conn).expect("Error loading sends") + }) + .await } pub async fn find_by_past_deletion_date(conn: &DbConn) -> Vec { let now = Utc::now().naive_utc(); - db_run! { conn: { - sends::table - .filter(sends::deletion_date.lt(now)) - .load::(conn) - .expect("Error loading sends") - }} + conn.run(move |conn| { + sends::table.filter(sends::deletion_date.lt(now)).load::(conn).expect("Error loading sends") + }) + .await } } diff --git a/src/db/models/sso_auth.rs b/src/db/models/sso_auth.rs index 3ecaf9f7..311e9bf9 100644 --- a/src/db/models/sso_auth.rs +++ b/src/db/models/sso_auth.rs @@ -1,17 +1,20 @@ -use chrono::{NaiveDateTime, Utc}; use std::time::Duration; -use crate::api::EmptyResult; -use crate::db::schema::sso_auth; -use crate::db::{DbConn, DbPool}; -use crate::error::MapResult; -use crate::sso::{OIDCCode, OIDCCodeChallenge, OIDCIdentifier, OIDCState, SSO_AUTH_EXPIRATION}; +use chrono::{NaiveDateTime, Utc}; +use diesel::{ + deserialize::FromSql, + expression::AsExpression, + prelude::*, + serialize::{Output, ToSql}, + sql_types::Text, +}; -use diesel::deserialize::FromSql; -use diesel::expression::AsExpression; -use diesel::prelude::*; -use diesel::serialize::{Output, ToSql}; -use diesel::sql_types::Text; +use crate::{ + api::EmptyResult, + db::{DbConn, DbPool, schema::sso_auth}, + error::MapResult, + sso::{OIDCCode, OIDCCodeChallenge, OIDCIdentifier, OIDCState, SSO_AUTH_EXPIRATION}, +}; #[derive(AsExpression, Clone, Debug, Serialize, Deserialize, FromSqlRow)] #[diesel(sql_type = Text)] @@ -106,13 +109,14 @@ impl SsoAuth { pub async fn find(state: &OIDCState, conn: &DbConn) -> Option { let oldest = Utc::now().naive_utc() - *SSO_AUTH_EXPIRATION; - db_run! { conn: { + conn.run(move |conn| { sso_auth::table .filter(sso_auth::state.eq(state)) .filter(sso_auth::created_at.ge(oldest)) .first::(conn) .ok() - }} + }) + .await } pub async fn find_by_code(code: &OIDCCode, conn: &DbConn) -> Option { @@ -127,22 +131,24 @@ impl SsoAuth { } pub async fn delete(self, conn: &DbConn) -> EmptyResult { - db_run! {conn: { + conn.run(move |conn| { diesel::delete(sso_auth::table.filter(sso_auth::state.eq(self.state))) .execute(conn) .map_res("Error deleting sso_auth") - }} + }) + .await } pub async fn delete_expired(pool: DbPool) -> EmptyResult { debug!("Purging expired sso_auth"); if let Ok(conn) = pool.get().await { let oldest = Utc::now().naive_utc() - *SSO_AUTH_EXPIRATION; - db_run! { conn: { + conn.run(move |conn| { diesel::delete(sso_auth::table.filter(sso_auth::created_at.lt(oldest))) .execute(conn) .map_res("Error deleting expired SSO nonce") - }} + }) + .await } else { err!("Failed to get DB connection while purging expired sso_auth") } diff --git a/src/db/models/two_factor.rs b/src/db/models/two_factor.rs index 0dc08e3e..5f57635e 100644 --- a/src/db/models/two_factor.rs +++ b/src/db/models/two_factor.rs @@ -1,13 +1,17 @@ -use super::UserId; -use crate::api::core::two_factor::webauthn::WebauthnRegistration; -use crate::db::schema::twofactor; -use crate::{api::EmptyResult, db::DbConn, error::MapResult}; use diesel::prelude::*; use serde_json::Value; use webauthn_rs::prelude::{Credential, ParsedAttestation}; use webauthn_rs_core::proto::CredentialV3; use webauthn_rs_proto::{AttestationFormat, RegisteredExtensions}; +use crate::{ + api::{EmptyResult, core::two_factor::webauthn::WebauthnRegistration}, + db::{DbConn, schema::twofactor}, + error::MapResult, +}; + +use super::UserId; + #[derive(Identifiable, Queryable, Insertable, AsChangeset)] #[diesel(table_name = twofactor)] #[diesel(primary_key(uuid))] @@ -114,54 +118,59 @@ impl TwoFactor { } pub async fn delete(self, conn: &DbConn) -> EmptyResult { - db_run! { conn: { + conn.run(move |conn| { diesel::delete(twofactor::table.filter(twofactor::uuid.eq(self.uuid))) .execute(conn) .map_res("Error deleting twofactor") - }} + }) + .await } pub async fn find_by_user(user_uuid: &UserId, conn: &DbConn) -> Vec { - db_run! { conn: { + conn.run(move |conn| { twofactor::table .filter(twofactor::user_uuid.eq(user_uuid)) .filter(twofactor::atype.lt(1000)) // Filter implementation types .load::(conn) .expect("Error loading twofactor") - }} + }) + .await } pub async fn find_by_user_and_type(user_uuid: &UserId, atype: i32, conn: &DbConn) -> Option { - db_run! { conn: { + conn.run(move |conn| { twofactor::table .filter(twofactor::user_uuid.eq(user_uuid)) .filter(twofactor::atype.eq(atype)) .first::(conn) .ok() - }} + }) + .await } pub async fn delete_all_by_user(user_uuid: &UserId, conn: &DbConn) -> EmptyResult { - db_run! { conn: { + conn.run(move |conn| { diesel::delete(twofactor::table.filter(twofactor::user_uuid.eq(user_uuid))) .execute(conn) .map_res("Error deleting twofactors") - }} + }) + .await } pub async fn migrate_u2f_to_webauthn(conn: &DbConn) -> EmptyResult { - let u2f_factors = db_run! { conn: { - twofactor::table - .filter(twofactor::atype.eq(TwoFactorType::U2f as i32)) - .load::(conn) - .expect("Error loading twofactor") - }}; - - use crate::api::core::two_factor::webauthn::U2FRegistration; - use crate::api::core::two_factor::webauthn::{get_webauthn_registrations, WebauthnRegistration}; + use crate::api::core::two_factor::webauthn::{U2FRegistration, get_webauthn_registrations}; use webauthn_rs::prelude::{COSEEC2Key, COSEKey, COSEKeyType, ECDSACurve}; use webauthn_rs_proto::{COSEAlgorithm, UserVerificationPolicy}; + let u2f_factors = conn + .run(move |conn| { + twofactor::table + .filter(twofactor::atype.eq(TwoFactorType::U2f as i32)) + .load::(conn) + .expect("Error loading twofactor") + }) + .await; + for mut u2f in u2f_factors { let mut regs: Vec = serde_json::from_str(&u2f.data)?; // If there are no registrations or they are migrated (we do the migration in batch so we can consider them all migrated when the first one is) @@ -227,12 +236,14 @@ impl TwoFactor { } pub async fn migrate_credential_to_passkey(conn: &DbConn) -> EmptyResult { - let webauthn_factors = db_run! { conn: { - twofactor::table - .filter(twofactor::atype.eq(TwoFactorType::Webauthn as i32)) - .load::(conn) - .expect("Error loading twofactor") - }}; + let webauthn_factors = conn + .run(move |conn| { + twofactor::table + .filter(twofactor::atype.eq(TwoFactorType::Webauthn as i32)) + .load::(conn) + .expect("Error loading twofactor") + }) + .await; for webauthn_factor in webauthn_factors { // assume that a failure to parse into the old struct, means that it was already converted @@ -241,7 +252,7 @@ impl TwoFactor { continue; }; - let regs = regs.into_iter().map(|r| r.into()).collect::>(); + let regs = regs.into_iter().map(Into::into).collect::>(); TwoFactor::new(webauthn_factor.user_uuid.clone(), TwoFactorType::Webauthn, serde_json::to_string(®s)?) .save(conn) diff --git a/src/db/models/two_factor_duo_context.rs b/src/db/models/two_factor_duo_context.rs index 205a57d8..1a4ae266 100644 --- a/src/db/models/two_factor_duo_context.rs +++ b/src/db/models/two_factor_duo_context.rs @@ -1,9 +1,12 @@ use chrono::Utc; - -use crate::db::schema::twofactor_duo_ctx; -use crate::{api::EmptyResult, db::DbConn, error::MapResult}; use diesel::prelude::*; +use crate::{ + api::EmptyResult, + db::{DbConn, schema::twofactor_duo_ctx}, + error::MapResult, +}; + #[derive(Identifiable, Queryable, Insertable, AsChangeset)] #[diesel(table_name = twofactor_duo_ctx)] #[diesel(primary_key(state))] @@ -16,12 +19,10 @@ pub struct TwoFactorDuoContext { impl TwoFactorDuoContext { pub async fn find_by_state(state: &str, conn: &DbConn) -> Option { - db_run! { conn: { - twofactor_duo_ctx::table - .filter(twofactor_duo_ctx::state.eq(state)) - .first::(conn) - .ok() - }} + conn.run(move |conn| { + twofactor_duo_ctx::table.filter(twofactor_duo_ctx::state.eq(state)).first::(conn).ok() + }) + .await } pub async fn save(state: &str, user_email: &str, nonce: &str, ttl: i64, conn: &DbConn) -> EmptyResult { @@ -29,41 +30,42 @@ impl TwoFactorDuoContext { let exists = Self::find_by_state(state, conn).await; if exists.is_some() { return Ok(()); - }; + } let exp = Utc::now().timestamp() + ttl; - db_run! { conn: { + conn.run(move |conn| { diesel::insert_into(twofactor_duo_ctx::table) .values(( twofactor_duo_ctx::state.eq(state), twofactor_duo_ctx::user_email.eq(user_email), twofactor_duo_ctx::nonce.eq(nonce), - twofactor_duo_ctx::exp.eq(exp) - )) - .execute(conn) - .map_res("Error saving context to twofactor_duo_ctx") - }} + twofactor_duo_ctx::exp.eq(exp), + )) + .execute(conn) + .map_res("Error saving context to twofactor_duo_ctx") + }) + .await } pub async fn find_expired(conn: &DbConn) -> Vec { let now = Utc::now().timestamp(); - db_run! { conn: { + conn.run(move |conn| { twofactor_duo_ctx::table .filter(twofactor_duo_ctx::exp.lt(now)) .load::(conn) .expect("Error finding expired contexts in twofactor_duo_ctx") - }} + }) + .await } pub async fn delete(&self, conn: &DbConn) -> EmptyResult { - db_run! { conn: { - diesel::delete( - twofactor_duo_ctx::table - .filter(twofactor_duo_ctx::state.eq(&self.state))) + conn.run(move |conn| { + diesel::delete(twofactor_duo_ctx::table.filter(twofactor_duo_ctx::state.eq(&self.state))) .execute(conn) .map_res("Error deleting from twofactor_duo_ctx") - }} + }) + .await } pub async fn purge_expired_duo_contexts(conn: &DbConn) { diff --git a/src/db/models/two_factor_incomplete.rs b/src/db/models/two_factor_incomplete.rs index 2f7e4779..4b8cadcb 100644 --- a/src/db/models/two_factor_incomplete.rs +++ b/src/db/models/two_factor_incomplete.rs @@ -1,17 +1,17 @@ use chrono::{NaiveDateTime, Utc}; +use diesel::prelude::*; -use crate::db::schema::twofactor_incomplete; use crate::{ + CONFIG, api::EmptyResult, auth::ClientIp, db::{ - models::{DeviceId, UserId}, DbConn, + models::{DeviceId, UserId}, + schema::twofactor_incomplete, }, error::MapResult, - CONFIG, }; -use diesel::prelude::*; #[derive(Identifiable, Queryable, Insertable, AsChangeset)] #[diesel(table_name = twofactor_incomplete)] @@ -49,7 +49,7 @@ impl TwoFactorIncomplete { return Ok(()); } - db_run! { conn: { + conn.run(move |conn| { diesel::insert_into(twofactor_incomplete::table) .values(( twofactor_incomplete::user_uuid.eq(user_uuid), @@ -61,7 +61,8 @@ impl TwoFactorIncomplete { )) .execute(conn) .map_res("Error adding twofactor_incomplete record") - }} + }) + .await } pub async fn mark_complete(user_uuid: &UserId, device_uuid: &DeviceId, conn: &DbConn) -> EmptyResult { @@ -73,22 +74,24 @@ impl TwoFactorIncomplete { } pub async fn find_by_user_and_device(user_uuid: &UserId, device_uuid: &DeviceId, conn: &DbConn) -> Option { - db_run! { conn: { + conn.run(move |conn| { twofactor_incomplete::table .filter(twofactor_incomplete::user_uuid.eq(user_uuid)) .filter(twofactor_incomplete::device_uuid.eq(device_uuid)) .first::(conn) .ok() - }} + }) + .await } pub async fn find_logins_before(dt: &NaiveDateTime, conn: &DbConn) -> Vec { - db_run! { conn: { + conn.run(move |conn| { twofactor_incomplete::table .filter(twofactor_incomplete::login_time.lt(dt)) .load::(conn) .expect("Error loading twofactor_incomplete") - }} + }) + .await } pub async fn delete(self, conn: &DbConn) -> EmptyResult { @@ -96,20 +99,24 @@ impl TwoFactorIncomplete { } pub async fn delete_by_user_and_device(user_uuid: &UserId, device_uuid: &DeviceId, conn: &DbConn) -> EmptyResult { - db_run! { conn: { - diesel::delete(twofactor_incomplete::table - .filter(twofactor_incomplete::user_uuid.eq(user_uuid)) - .filter(twofactor_incomplete::device_uuid.eq(device_uuid))) - .execute(conn) - .map_res("Error in twofactor_incomplete::delete_by_user_and_device()") - }} + conn.run(move |conn| { + diesel::delete( + twofactor_incomplete::table + .filter(twofactor_incomplete::user_uuid.eq(user_uuid)) + .filter(twofactor_incomplete::device_uuid.eq(device_uuid)), + ) + .execute(conn) + .map_res("Error in twofactor_incomplete::delete_by_user_and_device()") + }) + .await } pub async fn delete_all_by_user(user_uuid: &UserId, conn: &DbConn) -> EmptyResult { - db_run! { conn: { + conn.run(move |conn| { diesel::delete(twofactor_incomplete::table.filter(twofactor_incomplete::user_uuid.eq(user_uuid))) .execute(conn) .map_res("Error in twofactor_incomplete::delete_all_by_user()") - }} + }) + .await } } diff --git a/src/db/models/user.rs b/src/db/models/user.rs index ebc72101..24bee751 100644 --- a/src/db/models/user.rs +++ b/src/db/models/user.rs @@ -1,23 +1,27 @@ -use crate::db::schema::{invitations, sso_users, twofactor_incomplete, users}; use chrono::{NaiveDateTime, TimeDelta, Utc}; use derive_more::{AsRef, Deref, Display, From}; use diesel::prelude::*; use serde_json::Value; -use super::{ - Cipher, Device, EmergencyAccess, Favorite, Folder, Membership, MembershipType, TwoFactor, TwoFactorIncomplete, -}; use crate::{ + CONFIG, api::EmptyResult, crypto, - db::{models::DeviceId, DbConn}, + db::{ + DbConn, + models::DeviceId, + schema::{invitations, sso_users, twofactor_incomplete, users}, + }, error::MapResult, sso::OIDCIdentifier, util::{format_date, get_uuid, retry}, - CONFIG, }; use macros::UuidFromParam; +use super::{ + Cipher, Device, EmergencyAccess, Favorite, Folder, Membership, MembershipType, TwoFactor, TwoFactorIncomplete, +}; + #[derive(Identifiable, Queryable, Insertable, AsChangeset, Selectable)] #[diesel(table_name = users)] #[diesel(treat_none_as_null = true)] @@ -137,8 +141,8 @@ impl User { _totp_secret: None, totp_recover: None, - equivalent_domains: "[]".to_string(), - excluded_globals: "[]".to_string(), + equivalent_domains: "[]".to_owned(), + excluded_globals: "[]".to_owned(), client_kdf_type: Self::CLIENT_KDF_TYPE_DEFAULT, client_kdf_iter: Self::CLIENT_KDF_ITER_DEFAULT, @@ -158,7 +162,7 @@ impl User { password.as_bytes(), &self.salt, &self.password_hash, - self.password_iterations as u32, + self.password_iterations.cast_unsigned(), ) } @@ -193,7 +197,8 @@ impl User { allow_next_route: Option>, conn: &DbConn, ) -> EmptyResult { - self.password_hash = crypto::hash_password(password.as_bytes(), &self.salt, self.password_iterations as u32); + self.password_hash = + crypto::hash_password(password.as_bytes(), &self.salt, self.password_iterations.cast_unsigned()); if let Some(route) = allow_next_route { self.set_stamp_exception(route); @@ -238,10 +243,10 @@ impl User { pub fn display_name(&self) -> &str { // default to email if name is empty - if !&self.name.is_empty() { - &self.name - } else { + if self.name.is_empty() { &self.email + } else { + &self.name } } } @@ -337,15 +342,14 @@ impl User { TwoFactorIncomplete::delete_all_by_user(&self.uuid, conn).await?; Invitation::take(&self.email, conn).await; // Delete invitation if any - db_run! { conn: { - diesel::delete(users::table.filter(users::uuid.eq(self.uuid))) - .execute(conn) - .map_res("Error deleting user") - }} + conn.run(move |conn| { + diesel::delete(users::table.filter(users::uuid.eq(self.uuid))).execute(conn).map_res("Error deleting user") + }) + .await } pub async fn update_uuid_revision(uuid: &UserId, conn: &DbConn) { - if let Err(e) = Self::_update_revision(uuid, &Utc::now().naive_utc(), conn).await { + if let Err(e) = Self::update_revision_impl(uuid, &Utc::now().naive_utc(), conn).await { warn!("Failed to update revision for {uuid}: {e:#?}"); } } @@ -353,68 +357,62 @@ impl User { pub async fn update_all_revisions(conn: &DbConn) -> EmptyResult { let updated_at = Utc::now().naive_utc(); - db_run! { conn: { - retry(|| { - diesel::update(users::table) - .set(users::updated_at.eq(updated_at)) - .execute(conn) - }, 10) - .map_res("Error updating revision date for all users") - }} + conn.run(move |conn| { + retry(|| diesel::update(users::table).set(users::updated_at.eq(updated_at)).execute(conn), 10) + .map_res("Error updating revision date for all users") + }) + .await } pub async fn update_revision(&mut self, conn: &DbConn) -> EmptyResult { self.updated_at = Utc::now().naive_utc(); - Self::_update_revision(&self.uuid, &self.updated_at, conn).await + Self::update_revision_impl(&self.uuid, &self.updated_at, conn).await } - async fn _update_revision(uuid: &UserId, date: &NaiveDateTime, conn: &DbConn) -> EmptyResult { - db_run! { conn: { - retry(|| { - diesel::update(users::table.filter(users::uuid.eq(uuid))) - .set(users::updated_at.eq(date)) - .execute(conn) - }, 10) + async fn update_revision_impl(uuid: &UserId, date: &NaiveDateTime, conn: &DbConn) -> EmptyResult { + conn.run(move |conn| { + retry( + || { + diesel::update(users::table.filter(users::uuid.eq(uuid))) + .set(users::updated_at.eq(date)) + .execute(conn) + }, + 10, + ) .map_res("Error updating user revision") - }} + }) + .await } pub async fn find_by_mail(mail: &str, conn: &DbConn) -> Option { let lower_mail = mail.to_lowercase(); - db_run! { conn: { - users::table - .filter(users::email.eq(lower_mail)) - .first::(conn) - .ok() - }} + conn.run(move |conn| users::table.filter(users::email.eq(lower_mail)).first::(conn).ok()).await } pub async fn find_by_uuid(uuid: &UserId, conn: &DbConn) -> Option { - db_run! { conn: { - users::table - .filter(users::uuid.eq(uuid)) - .first::(conn) - .ok() - }} + conn.run(move |conn| users::table.filter(users::uuid.eq(uuid)).first::(conn).ok()).await } pub async fn find_by_device_for_email2fa(device_uuid: &DeviceId, conn: &DbConn) -> Option { - if let Some(user_uuid) = db_run! ( conn: { - twofactor_incomplete::table - .filter(twofactor_incomplete::device_uuid.eq(device_uuid)) - .order_by(twofactor_incomplete::login_time.desc()) - .select(twofactor_incomplete::user_uuid) - .first::(conn) - .ok() - }) { + if let Some(user_uuid) = conn + .run(move |conn| { + twofactor_incomplete::table + .filter(twofactor_incomplete::device_uuid.eq(device_uuid)) + .order_by(twofactor_incomplete::login_time.desc()) + .select(twofactor_incomplete::user_uuid) + .first::(conn) + .ok() + }) + .await + { return Self::find_by_uuid(&user_uuid, conn).await; } None } pub async fn get_all(conn: &DbConn) -> Vec<(Self, Option)> { - db_run! { conn: { + conn.run(move |conn| { users::table .left_join(sso_users::table) .select(<(Self, Option)>::as_select()) @@ -422,7 +420,8 @@ impl User { .expect("Error loading groups for user") .into_iter() .collect() - }} + }) + .await } pub async fn last_active(&self, conn: &DbConn) -> Option { @@ -467,21 +466,18 @@ impl Invitation { } pub async fn delete(self, conn: &DbConn) -> EmptyResult { - db_run! { conn: { + conn.run(move |conn| { diesel::delete(invitations::table.filter(invitations::email.eq(self.email))) .execute(conn) .map_res("Error deleting invitation") - }} + }) + .await } pub async fn find_by_mail(mail: &str, conn: &DbConn) -> Option { let lower_mail = mail.to_lowercase(); - db_run! { conn: { - invitations::table - .filter(invitations::email.eq(lower_mail)) - .first::(conn) - .ok() - }} + conn.run(move |conn| invitations::table.filter(invitations::email.eq(lower_mail)).first::(conn).ok()) + .await } pub async fn take(mail: &str, conn: &DbConn) -> bool { @@ -531,34 +527,37 @@ impl SsoUser { } pub async fn find_by_identifier(identifier: &str, conn: &DbConn) -> Option<(User, Self)> { - db_run! { conn: { + conn.run(move |conn| { users::table .inner_join(sso_users::table) .select(<(User, Self)>::as_select()) .filter(sso_users::identifier.eq(identifier)) .first::<(User, Self)>(conn) .ok() - }} + }) + .await } pub async fn find_by_mail(mail: &str, conn: &DbConn) -> Option<(User, Option)> { let lower_mail = mail.to_lowercase(); - db_run! { conn: { + conn.run(move |conn| { users::table .left_join(sso_users::table) .select(<(User, Option)>::as_select()) .filter(users::email.eq(lower_mail)) .first::<(User, Option)>(conn) .ok() - }} + }) + .await } pub async fn delete(user_uuid: &UserId, conn: &DbConn) -> EmptyResult { - db_run! { conn: { + conn.run(move |conn| { diesel::delete(sso_users::table.filter(sso_users::user_uuid.eq(user_uuid))) .execute(conn) .map_res("Error deleting sso user") - }} + }) + .await } } diff --git a/src/db/query_logger.rs b/src/db/query_logger.rs index 0a207918..89a1f3b5 100644 --- a/src/db/query_logger.rs +++ b/src/db/query_logger.rs @@ -1,6 +1,7 @@ -use diesel::connection::{Instrumentation, InstrumentationEvent}; use std::{cell::RefCell, collections::HashMap, time::Instant}; +use diesel::connection::{Instrumentation, InstrumentationEvent}; + thread_local! { static QUERY_PERF_TRACKER: RefCell> = RefCell::new(HashMap::new()); } @@ -11,7 +12,7 @@ pub fn simple_logger() -> Option> { url, .. } => { - debug!("Establishing connection: {url}") + debug!("Establishing connection: {url}"); } InstrumentationEvent::FinishEstablishConnection { url, @@ -19,9 +20,9 @@ pub fn simple_logger() -> Option> { .. } => { if let Some(e) = error { - error!("Error during establishing a connection with {url}: {e:?}") + error!("Error during establishing a connection with {url}: {e:?}"); } else { - debug!("Connection established: {url}") + debug!("Connection established: {url}"); } } InstrumentationEvent::StartQuery { @@ -47,7 +48,7 @@ pub fn simple_logger() -> Option> { } else if duration.as_secs() >= 1 { info!("SLOW QUERY [{:.2}s]: {}", duration.as_secs_f32(), query_string); } else { - debug!("QUERY [{:?}]: {}", duration, query_string); + debug!("QUERY [{duration:?}]: {query_string}"); } } }); diff --git a/src/error.rs b/src/error.rs index 1a258fd1..d075dfe5 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,10 +1,11 @@ // // Error generator macro // +use std::error::Error as StdError; + use crate::db::models::EventType; use crate::http_client::CustomHttpClientError; use serde::ser::{Serialize, SerializeStruct, Serializer}; -use std::error::Error as StdError; macro_rules! make_error { ( $( $name:ident ( $ty:ty ): $src_fn:expr, $usr_msg_fun:expr ),+ $(,)? ) => { @@ -14,24 +15,24 @@ macro_rules! make_error { #[derive(Debug)] pub struct ErrorEvent { pub event: EventType } - pub struct Error { message: String, error: ErrorKind, error_code: u16, event: Option } + pub struct Error { message: String, kind: ErrorKind, code: u16, event: Option } $(impl From<$ty> for Error { fn from(err: $ty) -> Self { Error::from((stringify!($name), err)) } })+ $(impl> From<(S, $ty)> for Error { fn from(val: (S, $ty)) -> Self { - Error { message: val.0.into(), error: ErrorKind::$name(val.1), error_code: BAD_REQUEST, event: None } + Error { message: val.0.into(), kind: ErrorKind::$name(val.1), code: BAD_REQUEST, event: None } } })+ impl StdError for Error { fn source(&self) -> Option<&(dyn StdError + 'static)> { - match &self.error {$( ErrorKind::$name(e) => $src_fn(e), )+} + match &self.kind {$( ErrorKind::$name(e) => $src_fn(e), )+} } } impl std::fmt::Display for Error { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match &self.error {$( + match &self.kind {$( ErrorKind::$name(e) => f.write_str(&$usr_msg_fun(e, &self.message)), )+} } @@ -39,10 +40,10 @@ macro_rules! make_error { }; } +use diesel::ConnectionError as DieselConErr; use diesel::r2d2::Error as R2d2Err; use diesel::r2d2::PoolError as R2d2PoolErr; use diesel::result::Error as DieselErr; -use diesel::ConnectionError as DieselConErr; use handlebars::RenderError as HbErr; use jsonwebtoken::errors::Error as JwtErr; use lettre::address::AddressError as AddrErr; @@ -71,46 +72,46 @@ pub struct Compact {} // The second one contains the function used to obtain the response sent to the client make_error! { // Just an empty error - Empty(Empty): _no_source, _serialize, + Empty(Empty): no_source, serialize, // Used to represent err! calls - Simple(String): _no_source, _api_error, - Compact(Compact): _no_source, _compact_api_error, + Simple(String): no_source, api_error, + Compact(Compact): no_source, compact_api_error, // Used in our custom http client to handle non-global IPs and blocked domains - CustomHttpClient(CustomHttpClientError): _has_source, _api_error, + CustomHttpClient(CustomHttpClientError): has_source, api_error, // Used for special return values, like 2FA errors - Json(Value): _no_source, _serialize, - Db(DieselErr): _has_source, _api_error, - R2d2(R2d2Err): _has_source, _api_error, - R2d2Pool(R2d2PoolErr): _has_source, _api_error, - Serde(SerdeErr): _has_source, _api_error, - JWt(JwtErr): _has_source, _api_error, - Handlebars(HbErr): _has_source, _api_error, + Json(Value): no_source, serialize, + Db(DieselErr): has_source, api_error, + R2d2(R2d2Err): has_source, api_error, + R2d2Pool(R2d2PoolErr): has_source, api_error, + Serde(SerdeErr): has_source, api_error, + JWt(JwtErr): has_source, api_error, + Handlebars(HbErr): has_source, api_error, - Io(IoErr): _has_source, _api_error, - Time(TimeErr): _has_source, _api_error, - Req(ReqErr): _has_source, _api_error, - Regex(RegexErr): _has_source, _api_error, - Yubico(YubiErr): _has_source, _api_error, + Io(IoErr): has_source, api_error, + Time(TimeErr): has_source, api_error, + Req(ReqErr): has_source, api_error, + Regex(RegexErr): has_source, api_error, + Yubico(YubiErr): has_source, api_error, - Lettre(LettreErr): _has_source, _api_error, - Address(AddrErr): _has_source, _api_error, - Smtp(SmtpErr): _has_source, _api_error, - OpenSSL(SSLErr): _has_source, _api_error, - Rocket(RocketErr): _has_source, _api_error, + Lettre(LettreErr): has_source, api_error, + Address(AddrErr): has_source, api_error, + Smtp(SmtpErr): has_source, api_error, + OpenSSL(SSLErr): has_source, api_error, + Rocket(RocketErr): has_source, api_error, - DieselCon(DieselConErr): _has_source, _api_error, - Webauthn(WebauthnErr): _has_source, _api_error, + DieselCon(DieselConErr): has_source, api_error, + Webauthn(WebauthnErr): has_source, api_error, - OpenDAL(OpenDALErr): _has_source, _api_error, + OpenDAL(OpenDALErr): has_source, api_error, } impl std::fmt::Debug for Error { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self.source() { Some(e) => write!(f, "{}.\n[CAUSE] {:#?}", self.message, e), - None => match self.error { + None => match self.kind { ErrorKind::Empty(_) => Ok(()), ErrorKind::Simple(ref s) => { if &self.message == s { @@ -135,6 +136,7 @@ impl Error { (usr_msg.clone(), usr_msg.into()).into() } + #[must_use] pub fn empty() -> Self { Empty {}.into() } @@ -147,13 +149,13 @@ impl Error { #[must_use] pub fn with_kind(mut self, kind: ErrorKind) -> Self { - self.error = kind; + self.kind = kind; self } #[must_use] pub const fn with_code(mut self, code: u16) -> Self { - self.error_code = code; + self.code = code; self } @@ -194,14 +196,14 @@ impl MapResult for Option { } } -const fn _has_source(e: T) -> Option { +const fn has_source(e: T) -> Option { Some(e) } -fn _no_source(_: T) -> Option { +fn no_source(_: T) -> Option { None } -fn _serialize(e: &impl Serialize, _msg: &str) -> String { +fn serialize(e: &impl Serialize, _msg: &str) -> String { serde_json::to_string(e).unwrap() } @@ -280,14 +282,14 @@ struct ApiErrorResponse<'a>(ApiErrorMsg<'a>); /// The custom serialization adds all other needed fields struct CompactApiErrorResponse<'a>(ApiErrorMsg<'a>); -fn _api_error(_: &impl std::any::Any, msg: &str) -> String { +fn api_error(_: &impl std::any::Any, msg: &str) -> String { let response = ApiErrorMsg { message: msg, }; serde_json::to_string(&ApiErrorResponse(response)).unwrap() } -fn _compact_api_error(_: &impl std::any::Any, msg: &str) -> String { +fn compact_api_error(_: &impl std::any::Any, msg: &str) -> String { let response = ApiErrorMsg { message: msg, }; @@ -299,18 +301,20 @@ fn _compact_api_error(_: &impl std::any::Any, msg: &str) -> String { // use std::io::Cursor; -use rocket::http::{ContentType, Status}; -use rocket::request::Request; -use rocket::response::{self, Responder, Response}; +use rocket::{ + http::{ContentType, Status}, + request::Request, + response::{self, Responder, Response}, +}; impl Responder<'_, 'static> for Error { fn respond_to(self, _: &Request<'_>) -> response::Result<'static> { - match self.error { + match self.kind { ErrorKind::Empty(_) | ErrorKind::Simple(_) | ErrorKind::Compact(_) => {} // Don't print the error in this situation _ => error!(target: "error", "{self:#?}"), - }; + } - let code = Status::from_code(self.error_code).unwrap_or(Status::BadRequest); + let code = Status::from_code(self.code).unwrap_or(Status::BadRequest); let body = self.to_string(); Response::build().status(code).header(ContentType::JSON).sized_body(Some(body.len()), Cursor::new(body)).ok() } diff --git a/src/http_client.rs b/src/http_client.rs index d39b884d..232ba7da 100644 --- a/src/http_client.rs +++ b/src/http_client.rs @@ -5,17 +5,21 @@ use std::{ time::Duration, }; -use hickory_resolver::{net::runtime::TokioRuntimeProvider, TokioResolver}; +use hickory_resolver::{TokioResolver, net::runtime::TokioRuntimeProvider}; use regex::Regex; use reqwest::{ + Client, ClientBuilder, dns::{Name, Resolve, Resolving}, - header, Client, ClientBuilder, + header, }; use url::Host; -use crate::{util::is_global, CONFIG}; +use crate::{CONFIG, util::is_global}; pub fn make_http_request(method: reqwest::Method, url: &str) -> Result { + static INSTANCE: LazyLock = + LazyLock::new(|| get_reqwest_client_builder().build().expect("Failed to build client")); + let Ok(url) = url::Url::parse(url) else { err!("Invalid URL"); }; @@ -25,9 +29,6 @@ pub fn make_http_request(method: reqwest::Method, url: &str) -> Result = - LazyLock::new(|| get_reqwest_client_builder().build().expect("Failed to build client")); - Ok(INSTANCE.request(method, url)) } @@ -67,18 +68,19 @@ fn should_block_ip(ip: IpAddr) -> bool { } fn should_block_address_regex(domain_or_ip: &str) -> bool { + static COMPILED_REGEX: Mutex> = Mutex::new(None); + let Some(block_regex) = CONFIG.http_request_block_regex() else { return false; }; - static COMPILED_REGEX: Mutex> = Mutex::new(None); let mut guard = COMPILED_REGEX.lock().unwrap(); // If the stored regex is up to date, use it - if let Some((value, regex)) = &*guard { - if value == &block_regex { - return regex.is_match(domain_or_ip); - } + if let Some((value, regex)) = &*guard + && value == &block_regex + { + return regex.is_match(domain_or_ip); } // If we don't have a regex stored, or it's not up to date, recreate it @@ -92,7 +94,7 @@ fn should_block_address_regex(domain_or_ip: &str) -> bool { pub fn get_valid_host(host: &str) -> Result { let Ok(host) = Host::parse(host) else { return Err(CustomHttpClientError::Invalid { - domain: host.to_string(), + domain: host.to_owned(), }); }; @@ -136,16 +138,16 @@ pub fn should_block_host>(host: &Host) -> Result<(), CustomHttp let (ip, host_str): (Option, String) = match host { Host::Ipv4(ip) => (Some(IpAddr::V4(*ip)), ip.to_string()), Host::Ipv6(ip) => (Some(IpAddr::V6(*ip)), ip.to_string()), - Host::Domain(d) => (None, d.as_ref().to_string()), + Host::Domain(d) => (None, d.as_ref().to_owned()), }; - if let Some(ip) = ip { - if should_block_ip(ip) { - return Err(CustomHttpClientError::NonGlobalIp { - domain: None, - ip, - }); - } + if let Some(ip) = ip + && should_block_ip(ip) + { + return Err(CustomHttpClientError::NonGlobalIp { + domain: None, + ip, + }); } if should_block_address_regex(&host_str) { @@ -233,8 +235,7 @@ impl CustomDnsResolver { builder.build() }) .inspect_err(|e| warn!("Error creating Hickory resolver, falling back to default: {e:?}")) - .map(|resolver| Arc::new(Self::Hickory(Arc::new(resolver)))) - .unwrap_or_else(|_| Arc::new(Self::Default())) + .map_or_else(|_| Arc::new(Self::Default()), |resolver| Arc::new(Self::Hickory(Arc::new(resolver)))) } // Note that we get an iterator of addresses, but we only grab the first one for convenience @@ -257,13 +258,13 @@ impl CustomDnsResolver { fn pre_resolve(name: &str) -> Result<(), CustomHttpClientError> { let Ok(host) = get_valid_host(name) else { return Err(CustomHttpClientError::Invalid { - domain: name.to_string(), + domain: name.to_owned(), }); }; if should_block_host(&host).is_err() { return Err(CustomHttpClientError::Blocked { - domain: name.to_string(), + domain: name.to_owned(), }); } @@ -273,7 +274,7 @@ fn pre_resolve(name: &str) -> Result<(), CustomHttpClientError> { fn post_resolve(name: &str, ip: IpAddr) -> Result<(), CustomHttpClientError> { if should_block_ip(ip) { Err(CustomHttpClientError::NonGlobalIp { - domain: Some(name.to_string()), + domain: Some(name.to_owned()), ip, }) } else { @@ -318,7 +319,7 @@ pub(crate) mod aws { let future = async move { let method = reqwest::Method::from_bytes(request.method().as_bytes()) .map_err(|e| ConnectorError::user(Box::new(e)))?; - let mut req_builder = client.request(method, request.uri().to_string()); + let mut req_builder = client.request(method, request.uri().to_owned()); for (name, value) in request.headers() { req_builder = req_builder.header(name, value); diff --git a/src/mail.rs b/src/mail.rs index cdbd269a..f31234d7 100644 --- a/src/mail.rs +++ b/src/mail.rs @@ -1,16 +1,17 @@ -use chrono::NaiveDateTime; -use percent_encoding::{percent_encode, NON_ALPHANUMERIC}; use std::{env::consts::EXE_SUFFIX, str::FromStr}; +use chrono::NaiveDateTime; use lettre::{ + Address, AsyncSendmailTransport, AsyncSmtpTransport, AsyncTransport, Tokio1Executor, message::{Attachment, Body, Mailbox, Message, MultiPart, SinglePart}, transport::smtp::authentication::{Credentials, Mechanism as SmtpAuthMechanism}, transport::smtp::client::{Tls, TlsParameters}, transport::smtp::extension::ClientId, - Address, AsyncSendmailTransport, AsyncSmtpTransport, AsyncTransport, Tokio1Executor, }; +use percent_encoding::{NON_ALPHANUMERIC, percent_encode}; use crate::{ + CONFIG, api::EmptyResult, auth::{ encode_jwt, generate_delete_claims, generate_emergency_access_invite_claims, generate_invite_claims, @@ -18,7 +19,7 @@ use crate::{ }, db::models::{Device, DeviceType, EmergencyAccessId, MembershipId, OrganizationId, User, UserId}, error::Error, - CONFIG, + util::upcase_first, }; fn sendmail_transport() -> AsyncSendmailTransport { @@ -38,7 +39,9 @@ fn smtp_transport() -> AsyncSmtpTransport { .timeout(Some(Duration::from_secs(CONFIG.smtp_timeout()))); // Determine security - let smtp_client = if CONFIG.smtp_security() != *"off" { + let smtp_client = if CONFIG.smtp_security() == *"off" { + smtp_client + } else { let mut tls_parameters = TlsParameters::builder(host); if CONFIG.smtp_accept_invalid_hostnames() { tls_parameters = tls_parameters.dangerous_accept_invalid_hostnames(true); @@ -53,8 +56,6 @@ fn smtp_transport() -> AsyncSmtpTransport { } else { smtp_client.tls(Tls::Required(tls_parameters)) } - } else { - smtp_client }; let smtp_client = match (CONFIG.smtp_username(), CONFIG.smtp_password()) { @@ -81,12 +82,12 @@ fn smtp_transport() -> AsyncSmtpTransport { } } - if !selected_mechanisms.is_empty() { - smtp_client.authentication(selected_mechanisms) - } else { + if selected_mechanisms.is_empty() { // Only show a warning, and return without setting an actual authentication mechanism warn!("No valid SMTP Auth mechanism found for '{mechanism}', using default values"); smtp_client + } else { + smtp_client.authentication(selected_mechanisms) } } _ => smtp_client, @@ -129,14 +130,16 @@ fn get_template(template_name: &str, data: &serde_json::Value) -> Result<(String let text = CONFIG.render_template(template_name, data)?; let mut text_split = text.split(""); - let subject = match text_split.next() { - Some(s) => s.trim().to_string(), - None => err!("Template doesn't contain subject"), + let subject = if let Some(s) = text_split.next() { + s.trim().to_owned() + } else { + err!("Template doesn't contain subject") }; - let body = match text_split.next() { - Some(s) => s.trim().to_string(), - None => err!("Template doesn't contain body"), + let body = if let Some(s) = text_split.next() { + s.trim().to_owned() + } else { + err!("Template doesn't contain body") }; if text_split.next().is_some() { @@ -204,9 +207,8 @@ pub async fn send_verify_email(address: &str, user_id: &UserId) -> EmptyResult { pub async fn send_register_verify_email(email: &str, token: &str) -> EmptyResult { let mut query = url::Url::parse("https://query.builder").unwrap(); query.query_pairs_mut().append_pair("email", email).append_pair("token", token); - let query_string = match query.query() { - None => err!("Failed to build verify URL query parameters"), - Some(query) => query, + let Some(query_string) = query.query() else { + err!("Failed to build verify URL query parameters") }; let (subject, body_html, body_text) = get_text( @@ -504,8 +506,6 @@ pub async fn send_invite_confirmed(address: &str, org_name: &str) -> EmptyResult } pub async fn send_new_device_logged_in(address: &str, ip: &str, dt: &NaiveDateTime, device: &Device) -> EmptyResult { - use crate::util::upcase_first; - let fmt = "%A, %B %_d, %Y at %r %Z"; let (subject, body_html, body_text) = get_text( "email/new_device_logged_in", @@ -529,8 +529,6 @@ pub async fn send_incomplete_2fa_login( device_name: &str, device_type: &str, ) -> EmptyResult { - use crate::util::upcase_first; - let fmt = "%A, %B %_d, %Y at %r %Z"; let (subject, body_html, body_text) = get_text( "email/incomplete_2fa_login", @@ -655,7 +653,7 @@ pub async fn send_protected_action_token(address: &str, token: &str) -> EmptyRes async fn send_with_selected_transport(email: Message) -> EmptyResult { if CONFIG.use_sendmail() { match sendmail_transport().send(email).await { - Ok(_) => Ok(()), + Ok(()) => Ok(()), // Match some common errors and make them more user friendly Err(e) => { if e.is_client() { @@ -664,10 +662,9 @@ async fn send_with_selected_transport(email: Message) -> EmptyResult { } else if e.is_response() { debug!("Sendmail response error: {e:?}"); err!(format!("Sendmail response error: {e}")); - } else { - debug!("Sendmail error: {e:?}"); - err!(format!("Sendmail error: {e}")); } + debug!("Sendmail error: {e:?}"); + err!(format!("Sendmail error: {e}")); } } } else { @@ -695,10 +692,9 @@ async fn send_with_selected_transport(email: Message) -> EmptyResult { } else if e.is_tls() { debug!("SMTP encryption error: {e:#?}"); err!(format!("SMTP encryption error: {e}")); - } else { - debug!("SMTP error: {e:#?}"); - err!(format!("SMTP error: {e}")); } + debug!("SMTP error: {e:#?}"); + err!(format!("SMTP error: {e}")); } } } diff --git a/src/main.rs b/src/main.rs index 4ffeacc1..15467ea4 100644 --- a/src/main.rs +++ b/src/main.rs @@ -33,6 +33,7 @@ use std::{ path::Path, process::exit, str::FromStr, + sync::{Arc, atomic::Ordering}, thread, }; @@ -44,6 +45,8 @@ use tokio::{ #[cfg(unix)] use tokio::signal::unix::SignalKind; +use rocket::data::{Limits, ToByteUnit}; + #[macro_use] mod error; mod api; @@ -60,13 +63,11 @@ mod sso_client; mod storage; mod util; -use crate::api::core::two_factor::duo_oidc::purge_duo_contexts; -use crate::api::purge_auth_requests; -use crate::api::{WS_ANONYMOUS_SUBSCRIPTIONS, WS_USERS}; -pub use config::{PathType, CONFIG}; +use crate::api::{ + WS_ANONYMOUS_SUBSCRIPTIONS, WS_USERS, core::two_factor::duo_oidc::purge_duo_contexts, purge_auth_requests, +}; +pub use config::{CONFIG, PathType}; pub use error::{Error, MapResult}; -use rocket::data::{Limits, ToByteUnit}; -use std::sync::{atomic::Ordering, Arc}; pub use util::is_running_in_container; #[rocket::main] @@ -137,26 +138,23 @@ fn parse_args() { if let Some(command) = pargs.subcommand().unwrap_or_default() { if command == "hash" { use argon2::{ - password_hash::SaltString, Algorithm::Argon2id, Argon2, ParamsBuilder, PasswordHasher, Version::V0x13, + Algorithm::Argon2id, Argon2, ParamsBuilder, PasswordHasher, Version::V0x13, password_hash::SaltString, }; let mut argon2_params = ParamsBuilder::new(); let preset: Option = pargs.opt_value_from_str(["-p", "--preset"]).unwrap_or_default(); let selected_preset; - match preset.as_deref() { - Some("owasp") => { - selected_preset = "owasp"; - argon2_params.m_cost(19456); - argon2_params.t_cost(2); - argon2_params.p_cost(1); - } - _ => { - // Bitwarden preset is the default - selected_preset = "bitwarden"; - argon2_params.m_cost(65540); - argon2_params.t_cost(3); - argon2_params.p_cost(4); - } + if preset.as_deref() == Some("owasp") { + selected_preset = "owasp"; + argon2_params.m_cost(19456); + argon2_params.t_cost(2); + argon2_params.p_cost(1); + } else { + // Bitwarden preset is the default + selected_preset = "bitwarden"; + argon2_params.m_cost(65540); + argon2_params.t_cost(3); + argon2_params.p_cost(4); } println!("Generate an Argon2id PHC string using the '{selected_preset}' preset:\n"); @@ -247,7 +245,7 @@ fn init_logging() -> Result { let level = caps .get(1) .and_then(|m| log::LevelFilter::from_str(m.as_str()).ok()) - .ok_or(Error::new("Failed to parse global log level".to_string(), ""))?; + .ok_or(Error::new("Failed to parse global log level".to_owned(), ""))?; let levels_override: Vec<(&str, log::LevelFilter)> = caps .get(2) @@ -256,13 +254,13 @@ fn init_logging() -> Result { .split(',') .collect::>() .into_iter() - .flat_map(|s| match s.split_once('=') { + .filter_map(|s| match s.split_once('=') { Some((log, lvl_str)) => log::LevelFilter::from_str(lvl_str).ok().map(|lvl| (log, lvl)), _ => None, }) .collect() }) - .ok_or(Error::new("Failed to parse overrides".to_string(), ""))?; + .ok_or(Error::new("Failed to parse overrides".to_owned(), ""))?; (level, levels_override) } else { @@ -338,7 +336,7 @@ fn init_logging() -> Result { ("vaultwarden::db::query_logger", log::LevelFilter::Off), ]); - for (path, level) in levels_override.into_iter() { + for (path, level) in levels_override { let _ = default_levels.insert(path, level); } @@ -352,7 +350,7 @@ fn init_logging() -> Result { let mut logger = fern::Dispatch::new().level(level).chain(std::io::stdout()); for (path, level) in default_levels { - logger = logger.level_for(path.to_string(), level); + logger = logger.level_for(path.to_owned(), level); } if CONFIG.extended_logging() { @@ -363,7 +361,7 @@ fn init_logging() -> Result { record.target(), record.level(), message - )) + )); }); } else { logger = logger.format(|out, message, _| out.finish(format_args!("{message}"))); @@ -609,9 +607,7 @@ async fn launch_rocket(pool: db::DbPool, extra_debug: bool) -> Result<(), Error> #[cfg(all(unix, sqlite))] { - if db::ACTIVE_DB_TYPE.get() != Some(&db::DbConnType::Sqlite) { - debug!("PostgreSQL and MySQL/MariaDB do not support this backup feature, skip adding USR1 signal."); - } else { + if db::ACTIVE_DB_TYPE.get() == Some(&db::DbConnType::Sqlite) { tokio::spawn(async move { let mut signal_user1 = tokio::signal::unix::signal(SignalKind::user_defined1()).unwrap(); loop { @@ -624,6 +620,8 @@ async fn launch_rocket(pool: db::DbPool, extra_debug: bool) -> Result<(), Error> } } }); + } else { + debug!("PostgreSQL and MySQL/MariaDB do not support this backup feature, skip adding USR1 signal."); } } @@ -671,7 +669,7 @@ fn schedule_jobs(pool: db::DbPool) { let runtime = tokio::runtime::Runtime::new().unwrap(); thread::Builder::new() - .name("job-scheduler".to_string()) + .name("job-scheduler".to_owned()) .spawn(move || { use job_scheduler_ng::{Job, JobScheduler}; let _runtime_guard = runtime.enter(); diff --git a/src/ratelimit.rs b/src/ratelimit.rs index 854bcc53..2b422924 100644 --- a/src/ratelimit.rs +++ b/src/ratelimit.rs @@ -1,8 +1,8 @@ use std::{net::IpAddr, num::NonZeroU32, sync::LazyLock, time::Duration}; -use governor::{clock::DefaultClock, state::keyed::DashMapStateStore, Quota, RateLimiter}; +use governor::{Quota, RateLimiter, clock::DefaultClock, state::keyed::DashMapStateStore}; -use crate::{Error, CONFIG}; +use crate::{CONFIG, Error}; type Limiter = RateLimiter, DefaultClock>; @@ -20,7 +20,7 @@ static LIMITER_ADMIN: LazyLock = LazyLock::new(|| { pub fn check_limit_login(ip: &IpAddr) -> Result<(), Error> { match LIMITER_LOGIN.check_key(ip) { - Ok(_) => Ok(()), + Ok(()) => Ok(()), Err(_e) => { err_code!("Too many login requests", 429); } @@ -29,7 +29,7 @@ pub fn check_limit_login(ip: &IpAddr) -> Result<(), Error> { pub fn check_limit_admin(ip: &IpAddr) -> Result<(), Error> { match LIMITER_ADMIN.check_key(ip) { - Ok(_) => Ok(()), + Ok(()) => Ok(()), Err(_e) => { err_code!("Too many admin requests", 429); } diff --git a/src/sso.rs b/src/sso.rs index 56e9a534..01fbd906 100644 --- a/src/sso.rs +++ b/src/sso.rs @@ -6,15 +6,15 @@ use regex::Regex; use url::Url; use crate::{ + CONFIG, api::ApiResult, auth, - auth::{AuthMethod, AuthTokens, TokenWrapper, BW_EXPIRATION, DEFAULT_REFRESH_VALIDITY}, + auth::{AuthMethod, AuthTokens, BW_EXPIRATION, DEFAULT_REFRESH_VALIDITY, TokenWrapper}, db::{ - models::{Device, OIDCAuthenticatedUser, SsoAuth, SsoUser, User}, DbConn, + models::{Device, OIDCAuthenticatedUser, SsoAuth, SsoUser, User}, }, sso_client::Client, - CONFIG, }; pub static FAKE_SSO_IDENTIFIER: &str = "00000000-01DC-01DC-01DC-000000000000"; @@ -123,7 +123,7 @@ pub fn encode_ssotoken_claims() -> String { nbf: time_now.timestamp(), exp: (time_now + chrono::TimeDelta::try_minutes(2).unwrap()).timestamp(), iss: SSO_JWT_ISSUER.to_string(), - sub: "vaultwarden".to_string(), + sub: "vaultwarden".to_owned(), }; auth::encode_jwt(&claims) @@ -171,12 +171,14 @@ fn decode_token_claims(token_name: &str, token: &str) -> ApiResult ApiResult { - let state = match data_encoding::BASE64.decode(base64_state.as_bytes()) { - Ok(vec) => match String::from_utf8(vec) { - Ok(valid) => OIDCState(valid), - Err(_) => err!(format!("Invalid utf8 chars in {base64_state} after base64 decoding")), - }, - Err(_) => err!(format!("Failed to decode {base64_state} using base64")), + let state = if let Ok(vec) = data_encoding::BASE64.decode(base64_state.as_bytes()) { + if let Ok(valid) = String::from_utf8(vec) { + OIDCState(valid) + } else { + err!(format!("Invalid utf8 chars in {base64_state} after base64 decoding")) + } + } else { + err!(format!("Failed to decode {base64_state} using base64")) }; Ok(state) @@ -193,12 +195,15 @@ pub async fn authorize_url( ) -> ApiResult { let redirect_uri = match client_id { "web" | "browser" => format!("{}/sso-connector.html", CONFIG.domain()), - "desktop" | "mobile" => "bitwarden://sso-callback".to_string(), + "desktop" | "mobile" => "bitwarden://sso-callback".to_owned(), "cli" => { let port_regex = Regex::new(r"^http://localhost:([0-9]{4})$").unwrap(); - match port_regex.captures(raw_redirect_uri).and_then(|captures| captures.get(1).map(|c| c.as_str())) { - Some(port) => format!("http://localhost:{port}"), - None => err!("Failed to extract port number"), + if let Some(port) = + port_regex.captures(raw_redirect_uri).and_then(|captures| captures.get(1).map(|c| c.as_str())) + { + format!("http://localhost:{port}") + } else { + err!("Failed to extract port number") } } _ => err!(format!("Unsupported client {client_id}")), @@ -246,9 +251,8 @@ pub async fn exchange_code( ) -> ApiResult<(SsoAuth, OIDCAuthenticatedUser)> { use openidconnect::OAuth2TokenResponse; - let mut sso_auth = match SsoAuth::find_by_code(code, conn).await { - None => err!(format!("Invalid code cannot retrieve sso auth")), - Some(sso_auth) => sso_auth, + let Some(mut sso_auth) = SsoAuth::find_by_code(code, conn).await else { + err!("Invalid code cannot retrieve sso auth") }; if let Some(authenticated_user) = sso_auth.auth_response.clone() { @@ -286,8 +290,8 @@ pub async fn exchange_code( let user_name = id_claims.preferred_username().or(user_info.preferred_username()).map(|un| un.to_string()); - let refresh_token = token_response.refresh_token().map(|t| t.secret()); - if refresh_token.is_none() && CONFIG.sso_scopes_vec().contains(&"offline_access".to_string()) { + let refresh_token = token_response.refresh_token().map(openidconnect::RefreshToken::secret); + if refresh_token.is_none() && CONFIG.sso_scopes_vec().contains(&"offline_access".to_owned()) { error!("Scope offline_access is present but response contain no refresh_token"); } @@ -331,7 +335,9 @@ pub async fn redeem( user_sso.save(conn).await?; } - if !CONFIG.sso_auth_only_not_session() { + if CONFIG.sso_auth_only_not_session() { + Ok(AuthTokens::new(device, user, AuthMethod::Sso, client_id)) + } else { let now = Utc::now(); let (ap_nbf, ap_exp) = @@ -344,9 +350,7 @@ pub async fn redeem( let access_claims = auth::LoginJwtClaims::new(device, user, ap_nbf, ap_exp, AuthMethod::Sso.scope_vec(), client_id, now); - _create_auth_tokens(device, auth_user.refresh_token, access_claims, auth_user.access_token) - } else { - Ok(AuthTokens::new(device, user, AuthMethod::Sso, client_id)) + create_auth_tokens_impl(device, auth_user.refresh_token, access_claims, auth_user.access_token) } } @@ -360,7 +364,9 @@ pub fn create_auth_tokens( access_token: String, expires_in: Option, ) -> ApiResult { - if !CONFIG.sso_auth_only_not_session() { + if CONFIG.sso_auth_only_not_session() { + Ok(AuthTokens::new(device, user, AuthMethod::Sso, client_id)) + } else { let now = Utc::now(); let (ap_nbf, ap_exp) = match (decode_token_claims("access_token", &access_token), expires_in) { @@ -372,13 +378,11 @@ pub fn create_auth_tokens( let access_claims = auth::LoginJwtClaims::new(device, user, ap_nbf, ap_exp, AuthMethod::Sso.scope_vec(), client_id, now); - _create_auth_tokens(device, refresh_token, access_claims, access_token) - } else { - Ok(AuthTokens::new(device, user, AuthMethod::Sso, client_id)) + create_auth_tokens_impl(device, refresh_token, access_claims, access_token) } } -fn _create_auth_tokens( +fn create_auth_tokens_impl( device: &Device, refresh_token: Option, access_claims: auth::LoginJwtClaims, @@ -462,7 +466,7 @@ pub async fn exchange_refresh_token( now, ); - _create_auth_tokens(device, None, access_claims, access_token) + create_auth_tokens_impl(device, None, access_claims, access_token) } None => err!("No token present while in SSO"), } diff --git a/src/sso_client.rs b/src/sso_client.rs index 68e171c6..5aa77750 100644 --- a/src/sso_client.rs +++ b/src/sso_client.rs @@ -1,18 +1,31 @@ use std::{borrow::Cow, future::Future, pin::Pin, sync::LazyLock, time::Duration}; -use openidconnect::{core::*, *}; +use openidconnect::{ + AccessToken, AsyncHttpClient, AuthDisplay, AuthPrompt, AuthenticationFlow, AuthorizationCode, AuthorizationRequest, + ClientId, ClientSecret, CsrfToken, EmptyAdditionalClaims, EmptyExtraTokenFields, EndpointNotSet, EndpointSet, + HttpClientError, HttpRequest, HttpResponse, IdTokenClaims, IdTokenFields, Nonce, OAuth2TokenResponse, + PkceCodeChallenge, PkceCodeVerifier, RefreshToken, ResponseType, Scope, StandardErrorResponse, + StandardTokenResponse, + core::{ + CoreAuthDisplay, CoreAuthPrompt, CoreClient, CoreErrorResponseType, CoreGenderClaim, CoreIdTokenVerifier, + CoreJsonWebKey, CoreJweContentEncryptionAlgorithm, CoreJwsSigningAlgorithm, CoreProviderMetadata, + CoreResponseType, CoreRevocableToken, CoreRevocationErrorResponse, CoreTokenIntrospectionResponse, + CoreTokenResponse, CoreTokenType, CoreUserInfoClaims, + }, + http, url, +}; use regex::Regex; use url::Url; use crate::{ + CONFIG, api::{ApiResult, EmptyResult}, db::models::SsoAuth, http_client::get_reqwest_client_builder, sso::{OIDCCode, OIDCCodeChallenge, OIDCCodeVerifier, OIDCState}, - CONFIG, }; -static CLIENT_CACHE_KEY: LazyLock = LazyLock::new(|| "sso-client".to_string()); +static CLIENT_CACHE_KEY: LazyLock = LazyLock::new(|| "sso-client".to_owned()); static CLIENT_CACHE: LazyLock> = LazyLock::new(|| { moka::sync::Cache::builder() .max_capacity(1) @@ -85,7 +98,7 @@ impl<'c> AsyncHttpClient<'c> for OidcHttpClient { impl Client { // Call the OpenId discovery endpoint to retrieve configuration - async fn _get_client() -> ApiResult { + async fn get_client() -> ApiResult { let client_id = ClientId::new(CONFIG.sso_client_id()); let client_secret = ClientSecret::new(CONFIG.sso_client_secret()); @@ -103,14 +116,16 @@ impl Client { let base_client = CoreClient::from_provider_metadata(provider_metadata, client_id, Some(client_secret)); - let token_uri = match base_client.token_uri() { - Some(uri) => uri.clone(), - None => err!("Failed to discover token_url, cannot proceed"), + let token_uri = if let Some(uri) = base_client.token_uri() { + uri.clone() + } else { + err!("Failed to discover token_url, cannot proceed") }; - let user_info_url = match base_client.user_info_url() { - Some(url) => url.clone(), - None => err!("Failed to discover user_info url, cannot proceed"), + let user_info_url = if let Some(url) = base_client.user_info_url() { + url.clone() + } else { + err!("Failed to discover user_info url, cannot proceed") }; let core_client = base_client @@ -129,13 +144,13 @@ impl Client { if CONFIG.sso_client_cache_expiration() > 0 { match CLIENT_CACHE.get(&*CLIENT_CACHE_KEY) { Some(client) => Ok(client), - None => Self::_get_client().await.inspect(|client| { + None => Self::get_client().await.inspect(|client| { debug!("Inserting new client in cache"); CLIENT_CACHE.insert(CLIENT_CACHE_KEY.clone(), client.clone()); }), } } else { - Self::_get_client().await + Self::get_client().await } } @@ -214,15 +229,14 @@ impl Client { Ok(token_response) => { let oidc_nonce = Nonce::new(sso_auth.nonce.clone()); - let id_token = match token_response.extra_fields().id_token() { - None => err!("Token response did not contain an id_token"), - Some(token) => token, + let Some(id_token) = token_response.extra_fields().id_token() else { + err!("Token response did not contain an id_token") }; if CONFIG.sso_debug_tokens() { debug!("Id token: {}", id_token.to_string()); debug!("Access token: {}", token_response.access_token().secret()); - debug!("Refresh token: {:?}", token_response.refresh_token().map(|t| t.secret())); + debug!("Refresh token: {:?}", token_response.refresh_token().map(RefreshToken::secret)); debug!("Expiration time: {:?}", token_response.expires_in()); } @@ -275,12 +289,12 @@ impl Client { let client = Client::cached().await?; REFRESH_CACHE - .get_with(refresh_token.clone(), async move { client._exchange_refresh_token(refresh_token).await }) + .get_with(refresh_token.clone(), async move { client.exchange_refresh_token_impl(refresh_token).await }) .await .map_err(Into::into) } - async fn _exchange_refresh_token(&self, refresh_token: String) -> Result { + async fn exchange_refresh_token_impl(&self, refresh_token: String) -> Result { let rt = RefreshToken::new(refresh_token); match self.core_client.exchange_refresh_token(&rt).request_async(&self.http_client).await { diff --git a/src/storage.rs b/src/storage.rs index ada2a951..ac88d026 100644 --- a/src/storage.rs +++ b/src/storage.rs @@ -9,9 +9,9 @@ pub(crate) fn join_path(base: &str, child: &str) -> String { let base = base.trim_end_matches('/'); let child = child.trim_start_matches('/'); if base.is_empty() { - child.to_string() + child.to_owned() } else if child.is_empty() { - base.to_string() + base.to_owned() } else { format!("{base}/{child}") } @@ -34,7 +34,7 @@ pub(crate) fn parent(path: &str) -> Option { return s3::parent(path); } - std::path::Path::new(path).parent()?.to_str().map(ToString::to_string) + std::path::Path::new(path).parent()?.to_str().map(str::to_owned) } pub(crate) fn file_name(path: &str) -> Option { @@ -43,7 +43,7 @@ pub(crate) fn file_name(path: &str) -> Option { return s3::file_name(path); } - std::path::Path::new(path).file_name()?.to_str().map(ToString::to_string) + std::path::Path::new(path).file_name()?.to_str().map(str::to_owned) } pub(crate) fn is_fs_operator(operator: &opendal::Operator) -> bool { @@ -70,7 +70,7 @@ pub(crate) fn operator_for_path(path: &str) -> Result String { if let Ok(mut url) = Url::parse(base) { let mut segments = path_segments(&url); - segments.extend(child.split('/').filter(|segment| !segment.is_empty()).map(ToString::to_string)); + segments.extend(child.split('/').filter(|segment| !segment.is_empty()).map(str::to_owned)); set_path_segments(&mut url, &segments); return url.to_string(); } @@ -96,9 +96,9 @@ mod s3 { let base = base.trim_end_matches('/'); let child = child.trim_start_matches('/'); if base.is_empty() { - child.to_string() + child.to_owned() } else if child.is_empty() { - base.to_string() + base.to_owned() } else { format!("{base}/{child}") } @@ -126,7 +126,7 @@ mod s3 { return Some(url.to_string()); } - std::path::Path::new(path).parent()?.to_str().map(ToString::to_string) + std::path::Path::new(path).parent()?.to_str().map(str::to_owned) } pub(super) fn file_name(path: &str) -> Option { @@ -134,12 +134,12 @@ mod s3 { return path_segments(&url).pop(); } - std::path::Path::new(path).file_name()?.to_str().map(ToString::to_string) + std::path::Path::new(path).file_name()?.to_str().map(str::to_owned) } fn path_segments(url: &Url) -> Vec { url.path_segments() - .map(|segments| segments.filter(|segment| !segment.is_empty()).map(ToString::to_string).collect()) + .map(|segments| segments.filter(|segment| !segment.is_empty()).map(str::to_owned).collect()) .unwrap_or_default() } @@ -206,9 +206,9 @@ mod s3 { }; Ok(Some(Credential { - access_key_id: creds.access_key_id().to_string(), - secret_access_key: creds.secret_access_key().to_string(), - session_token: creds.session_token().map(|s| s.to_string()), + access_key_id: creds.access_key_id().to_owned(), + secret_access_key: creds.secret_access_key().to_owned(), + session_token: creds.session_token().map(ToOwned::to_owned), expires_in, })) } @@ -218,7 +218,7 @@ mod s3 { let mut config = opendal::services::S3Config::from_uri(&uri)?; if !uri_has_option(&uri, &["default_storage_class"]) { - config.default_storage_class = Some("INTELLIGENT_TIERING".to_string()); + config.default_storage_class = Some("INTELLIGENT_TIERING".to_owned()); } if !uri_has_option( diff --git a/src/util.rs b/src/util.rs index 5cd78eed..91f075d1 100644 --- a/src/util.rs +++ b/src/util.rs @@ -1,24 +1,28 @@ // // Web Headers and caching // -use std::{collections::HashMap, io::Cursor, path::Path}; +use std::{collections::HashMap, env, fmt, io::Cursor, path::Path, str::FromStr}; +use chrono::{DateTime, Local, NaiveDateTime, TimeZone}; use num_traits::ToPrimitive; +use tokio::{ + runtime::Handle, + time::{Duration, sleep}, +}; + +use serde::de::{self, DeserializeOwned, Deserializer, MapAccess, SeqAccess, Visitor}; +use serde_json::Value; + use rocket::{ + Data, Orbit, Request, Response, Rocket, fairing::{Fairing, Info, Kind}, http::{ContentType, Header, HeaderMap, Method, Status}, response::{self, Responder}, - Data, Orbit, Request, Response, Rocket, -}; - -use tokio::{ - runtime::Handle, - time::{sleep, Duration}, }; use crate::{ - config::{PathType, SUPPORTED_FEATURE_FLAGS}, CONFIG, + config::{PathType, SUPPORTED_FEATURE_FLAGS}, }; pub struct AppHeaders(); @@ -75,11 +79,16 @@ impl Fairing for AppHeaders { // Do not send the Content-Security-Policy (CSP) Header and X-Frame-Options for the *-connector.html files. // This can cause issues when some MFA requests needs to open a popup or page within the clients like WebAuthn, or Duo. // This is the same behavior as upstream Bitwarden. - if !req_uri_path.ends_with("connector.html") { + if req_uri_path.ends_with("connector.html") { + // It looks like this header get's set somewhere else also, make sure this is not sent for these files, it will cause MFA issues. + res.remove_header("X-Frame-Options"); + } else { let csp = if is_image { // Prevent scripts, frames, objects, etc., from loading with images, mainly for SVG images, since these could contain JavaScript and other unsafe items. // Even though we sanitize SVG images before storing and viewing them, it's better to prevent allowing these elements. - String::from("default-src 'none'; img-src 'self' data:; style-src 'unsafe-inline'; script-src 'none'; frame-src 'none'; object-src 'none") + String::from( + "default-src 'none'; img-src 'self' data:; style-src 'unsafe-inline'; script-src 'none'; frame-src 'none'; object-src 'none", + ) } else { // # Frame Ancestors: // Chrome Web Store: https://chrome.google.com/webstore/detail/bitwarden-free-password-m/nngceckbapebfimnlniiiahkandclblb @@ -129,9 +138,6 @@ impl Fairing for AppHeaders { res.set_raw_header("Content-Security-Policy", csp); res.set_raw_header("X-Frame-Options", "SAMEORIGIN"); - } else { - // It looks like this header get's set somewhere else also, make sure this is not sent for these files, it will cause MFA issues. - res.remove_header("X-Frame-Options"); } // Disable cache unless otherwise specified @@ -146,7 +152,7 @@ pub struct Cors(); impl Cors { fn get_header(headers: &HeaderMap<'_>, name: &str) -> String { match headers.get_one(name) { - Some(h) => h.to_string(), + Some(h) => h.to_owned(), _ => String::new(), } } @@ -212,7 +218,7 @@ impl Cached { Self { response, is_immutable, - ttl: 604800, // 7 days + ttl: 604_800, // 7 days } } @@ -286,7 +292,7 @@ impl Fairing for BetterLogging { } else { "http" }; - let addr = format!("{scheme}://{}:{}", &config.address, &config.port); + let addr = format!("{scheme}://{}:{}", config.address, config.port); info!(target: "start", "Rocket has launched from {addr}"); } @@ -303,7 +309,7 @@ impl Fairing for BetterLogging { match uri.query() { Some(q) => info!(target: "request", "{method} {uri_path_str}?{}", &q[..q.len().min(30)]), None => info!(target: "request", "{method} {uri_path_str}"), - }; + } } } @@ -316,10 +322,10 @@ impl Fairing for BetterLogging { let uri_subpath = uri_path_str.strip_prefix(&CONFIG.domain_path()).unwrap_or(&uri_path_str); if self.0 || LOGGED_ROUTES.iter().any(|r| uri_subpath.starts_with(r)) { let status = response.status(); - if let Some(ref route) = request.route() { - info!(target: "response", "{route} => {status}") + if let Some(route) = request.route() { + info!(target: "response", "{route} => {status}"); } else { - info!(target: "response", "{status}") + info!(target: "response", "{status}"); } } } @@ -354,9 +360,6 @@ pub fn get_uuid() -> String { // // String util methods // - -use std::str::FromStr; - #[inline] pub fn upcase_first(s: &str) -> String { let mut c = s.chars(); @@ -390,9 +393,6 @@ where // // Env methods // - -use std::env; - pub fn get_env_str_value(key: &str) -> Option { let key_file = format!("{key}_FILE"); let value_from_env = env::var(key); @@ -402,7 +402,7 @@ pub fn get_env_str_value(key: &str) -> Option { (Ok(_), Ok(_)) => panic!("You should not define both {key} and {key_file}!"), (Ok(v_env), Err(_)) => Some(v_env), (Err(_), Ok(v_file)) => match std::fs::read_to_string(v_file) { - Ok(content) => Some(content.trim().to_string()), + Ok(content) => Some(content.trim().to_owned()), Err(e) => panic!("Failed to load {key}: {e:?}"), }, _ => None, @@ -431,8 +431,6 @@ pub fn get_env_bool(key: &str) -> Option { // Date util methods // -use chrono::{DateTime, Local, NaiveDateTime, TimeZone}; - /// Formats a UTC-offset `NaiveDateTime` in the format used by Bitwarden API /// responses with "date" fields (`CreationDate`, `RevisionDate`, etc.). pub fn format_date(dt: &NaiveDateTime) -> String { @@ -457,10 +455,10 @@ pub fn validate_and_format_date(dt: &str) -> String { pub fn format_datetime_local(dt: &DateTime, fmt: &str) -> String { // Try parsing the `TZ` environment variable to enable formatting `%Z` as // a time zone abbreviation. - if let Ok(tz) = env::var("TZ") { - if let Ok(tz) = tz.parse::() { - return dt.with_timezone(&tz).format(fmt).to_string(); - } + if let Ok(tz) = env::var("TZ") + && let Ok(tz) = tz.parse::() + { + return dt.with_timezone(&tz).format(fmt).to_string(); } // Otherwise, fall back to formatting `%Z` as a UTC offset. @@ -512,6 +510,7 @@ pub fn is_valid_email(email: &str) -> bool { // /// Returns true if the program is running in Docker, Podman or Kubernetes. +#[must_use] pub fn is_running_in_container() -> bool { Path::new("/.dockerenv").exists() || Path::new("/run/.containerenv").exists() @@ -543,10 +542,10 @@ pub fn get_active_web_release() -> String { ]; for version_file in version_files { - if let Ok(version_str) = std::fs::read_to_string(&version_file) { - if let Ok(version) = serde_json::from_str::(&version_str) { - return String::from(version.version.trim_start_matches('v')); - } + if let Ok(version_str) = std::fs::read_to_string(&version_file) + && let Ok(version) = serde_json::from_str::(&version_str) + { + return String::from(version.version.trim_start_matches('v')); } } @@ -556,12 +555,6 @@ pub fn get_active_web_release() -> String { // // Deserialization methods // - -use std::fmt; - -use serde::de::{self, DeserializeOwned, Deserializer, MapAccess, SeqAccess, Visitor}; -use serde_json::Value; - pub type JsonMap = serde_json::Map; #[derive(Serialize, Deserialize)] @@ -605,7 +598,7 @@ impl<'de> Visitor<'de> for LowerCaseVisitor { let mut result_map = JsonMap::new(); while let Some((key, value)) = map.next_entry()? { - result_map.insert(_process_key(key), convert_json_key_lcase_first(value)); + result_map.insert(process_json_key(key), convert_json_key_lcase_first(value)); } Ok(Value::Object(result_map)) @@ -627,7 +620,7 @@ impl<'de> Visitor<'de> for LowerCaseVisitor { // Inner function to handle a special case for the 'ssn' key. // This key is part of the Identity Cipher (Social Security Number) -fn _process_key(key: &str) -> String { +fn process_json_key(key: &str) -> String { match key.to_lowercase().as_ref() { "ssn" => "ssn".into(), _ => lcase_first(key), @@ -664,21 +657,24 @@ impl NumberOrString { } } - #[allow(clippy::wrong_self_convention)] + #[expect(clippy::wrong_self_convention)] pub fn into_i32(&self) -> Result { use std::num::ParseIntError as PIE; match self { - NumberOrString::Number(n) => match n.to_i32() { - Some(n) => Ok(n), - None => err!("Number does not fit in i32"), - }, + NumberOrString::Number(n) => { + if let Some(n) = n.to_i32() { + Ok(n) + } else { + err!("Number does not fit in i32") + } + } NumberOrString::String(s) => { s.parse().map_err(|e: PIE| crate::Error::new("Can't convert to number", e.to_string())) } } } - #[allow(clippy::wrong_self_convention)] + #[expect(clippy::wrong_self_convention)] pub fn into_i64(&self) -> Result { use std::num::ParseIntError as PIE; match self { @@ -753,11 +749,11 @@ pub fn convert_json_key_lcase_first(src_json: Value) -> Value { Value::Object(obj) => { let mut json_map = JsonMap::new(); - for (key, value) in obj.into_iter() { + for (key, value) in obj { match (key, value) { (key, Value::Object(elm)) => { let inner_value = convert_json_key_lcase_first(Value::Object(elm)); - json_map.insert(_process_key(&key), inner_value); + json_map.insert(process_json_key(&key), inner_value); } (key, Value::Array(elm)) => { @@ -767,11 +763,11 @@ pub fn convert_json_key_lcase_first(src_json: Value) -> Value { inner_array.push(convert_json_key_lcase_first(inner_obj)); } - json_map.insert(_process_key(&key), Value::Array(inner_array)); + json_map.insert(process_json_key(&key), Value::Array(inner_array)); } (key, value) => { - json_map.insert(_process_key(&key), value); + json_map.insert(process_json_key(&key), value); } } } @@ -793,7 +789,7 @@ pub enum FeatureFlagFilter { /// Parses the experimental client feature flags string into a HashMap. pub fn parse_experimental_client_feature_flags( experimental_client_feature_flags: &str, - filter_mode: FeatureFlagFilter, + filter_mode: &FeatureFlagFilter, ) -> HashMap { experimental_client_feature_flags .split(',') @@ -811,7 +807,8 @@ pub fn parse_experimental_client_feature_flags( /// TODO: This is extracted from IpAddr::is_global, which is unstable: /// https://doc.rust-lang.org/nightly/std/net/enum.IpAddr.html#method.is_global /// Remove once https://github.com/rust-lang/rust/issues/27709 is merged -#[allow(clippy::nonminimal_bool)] +// #[expect(clippy::nonminimal_bool, reason = "Mostly copy/paste from std, keep as-is")] +#[expect(clippy::decimal_bitwise_operands, reason = "Mostly copy/paste from std, keep as-is")] #[cfg(any(not(feature = "unstable"), test))] pub fn is_global_hardcoded(ip: std::net::IpAddr) -> bool { match ip { From d626ea81abf8504b7e54be14b0cfaf45134171b6 Mon Sep 17 00:00:00 2001 From: user71424q <127671064+user71424q@users.noreply.github.com> Date: Sun, 17 May 2026 19:46:10 +0000 Subject: [PATCH 88/89] Serve Apple app site association file (#7191) --- src/api/web.rs | 28 +++++++++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/src/api/web.rs b/src/api/web.rs index 8c628f96..5bd4c85d 100644 --- a/src/api/web.rs +++ b/src/api/web.rs @@ -26,7 +26,15 @@ pub fn routes() -> Vec { // crate::utils::LOGGED_ROUTES to make sure they appear in the log let mut routes = routes![attachments, alive, alive_head, static_files]; if CONFIG.web_vault_enabled() { - routes.append(&mut routes![web_index, web_index_direct, web_index_head, app_id, web_files, vaultwarden_css]); + routes.append(&mut routes![ + web_index, + web_index_direct, + web_index_head, + app_id, + apple_app_site_association, + web_files, + vaultwarden_css + ]); } #[cfg(debug_assertions)] @@ -163,6 +171,24 @@ fn app_id() -> Cached<(ContentType, Json)> { ) } +#[get("/.well-known/apple-app-site-association")] +fn apple_app_site_association() -> Cached<(ContentType, Json)> { + Cached::long( + ( + ContentType::JSON, + Json(json!({ + "webcredentials": { + "apps": [ + "LTZ2PFU5D6.com.8bit.bitwarden", + "LTZ2PFU5D6.com.8bit.bitwarden.beta" + ] + } + })), + ), + true, + ) +} + #[get("/", rank = 10)] // Only match this if the other routes don't match async fn web_files(p: PathBuf) -> Cached> { Cached::long(NamedFile::open(Path::new(&CONFIG.web_vault_folder()).join(p)).await.ok(), true) From d6a3d539ed13352085ca7dfa63c49017d86c419b Mon Sep 17 00:00:00 2001 From: Mathijs van Veluw Date: Fri, 5 Jun 2026 21:52:52 +0200 Subject: [PATCH 89/89] Update Rust, Crates and GHA (#7307) - Updated Rust to v1.96.0 - Updated all the crates, and adjusted code where needed - Fixed some nightly reported clippy lints - Updated all the GitHub actions Signed-off-by: BlackDex --- .github/workflows/build.yml | 2 +- .github/workflows/check-templates.yml | 2 +- .github/workflows/hadolint.yml | 4 +- .github/workflows/release.yml | 20 +- .github/workflows/trivy.yml | 4 +- .github/workflows/typos.yml | 4 +- .github/workflows/zizmor.yml | 4 +- .pre-commit-config.yaml | 2 +- Cargo.lock | 553 ++++++++++++++++---------- Cargo.toml | 41 +- docker/DockerSettings.yaml | 2 +- docker/Dockerfile.alpine | 8 +- docker/Dockerfile.debian | 2 +- rust-toolchain.toml | 2 +- src/api/admin.rs | 4 +- src/config.rs | 2 +- src/sso_client.rs | 2 +- 17 files changed, 397 insertions(+), 261 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 6269e595..79bbddfa 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -62,7 +62,7 @@ jobs: # Checkout the repo - name: "Checkout" - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: persist-credentials: false fetch-depth: 0 diff --git a/.github/workflows/check-templates.yml b/.github/workflows/check-templates.yml index 57b53bf4..6be812c4 100644 --- a/.github/workflows/check-templates.yml +++ b/.github/workflows/check-templates.yml @@ -20,7 +20,7 @@ jobs: steps: # Checkout the repo - name: "Checkout" - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: persist-credentials: false # End Checkout the repo diff --git a/.github/workflows/hadolint.yml b/.github/workflows/hadolint.yml index 2b476904..074bf2fc 100644 --- a/.github/workflows/hadolint.yml +++ b/.github/workflows/hadolint.yml @@ -20,7 +20,7 @@ jobs: steps: # Start Docker Buildx - name: Setup Docker Buildx - uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0 + uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0 # https://github.com/moby/buildkit/issues/3969 # Also set max parallelism to 2, the default of 4 breaks GitHub Actions and causes OOMKills with: @@ -40,7 +40,7 @@ jobs: # End Download hadolint # Checkout the repo - name: Checkout - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: persist-credentials: false # End Checkout the repo diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 8db56c38..3c6d8574 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -58,13 +58,13 @@ jobs: steps: - name: Initialize QEMU binfmt support - uses: docker/setup-qemu-action@ce360397dd3f832beb865e1373c09c0e9f86d70a # v4.0.0 + uses: docker/setup-qemu-action@06116385d9baf250c9f4dcb4858b16962ea869c3 # v4.1.0 with: platforms: "arm64,arm" # Start Docker Buildx - name: Setup Docker Buildx - uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0 + uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0 # https://github.com/moby/buildkit/issues/3969 # Also set max parallelism to 2, the default of 4 breaks GitHub Actions and causes OOMKills with: @@ -77,7 +77,7 @@ jobs: # Checkout the repo - name: Checkout - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 # We need fetch-depth of 0 so we also get all the tag metadata with: persist-credentials: false @@ -106,7 +106,7 @@ jobs: # Login to Docker Hub - name: Login to Docker Hub - uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0 + uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} @@ -121,7 +121,7 @@ jobs: # Login to GitHub Container Registry - name: Login to GitHub Container Registry - uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0 + uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0 with: registry: ghcr.io username: ${{ github.repository_owner }} @@ -137,7 +137,7 @@ jobs: # Login to Quay.io - name: Login to Quay.io - uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0 + uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0 with: registry: quay.io username: ${{ secrets.QUAY_USERNAME }} @@ -185,7 +185,7 @@ jobs: - name: Bake ${{ matrix.base_image }} containers id: bake_vw - uses: docker/bake-action@a66e1c87e2eca0503c343edf1d208c716d54b8a8 # v7.1.0 + uses: docker/bake-action@6614cfa25eff9a0b2b2697efb0b6159e7680d584 # v7.2.0 env: BASE_TAGS: "${{ steps.determine-version.outputs.BASE_TAGS }}" SOURCE_COMMIT: "${{ env.SOURCE_COMMIT }}" @@ -272,7 +272,7 @@ jobs: # Login to Docker Hub - name: Login to Docker Hub - uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0 + uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} @@ -287,7 +287,7 @@ jobs: # Login to GitHub Container Registry - name: Login to GitHub Container Registry - uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0 + uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0 with: registry: ghcr.io username: ${{ github.repository_owner }} @@ -303,7 +303,7 @@ jobs: # Login to Quay.io - name: Login to Quay.io - uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0 + uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0 with: registry: quay.io username: ${{ secrets.QUAY_USERNAME }} diff --git a/.github/workflows/trivy.yml b/.github/workflows/trivy.yml index 83d00210..542be807 100644 --- a/.github/workflows/trivy.yml +++ b/.github/workflows/trivy.yml @@ -33,7 +33,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: persist-credentials: false @@ -50,6 +50,6 @@ jobs: severity: CRITICAL,HIGH - name: Upload Trivy scan results to GitHub Security tab - uses: github/codeql-action/upload-sarif@9e0d7b8d25671d64c341c19c0152d693099fb5ba # v4.35.5 + uses: github/codeql-action/upload-sarif@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2 with: sarif_file: 'trivy-results.sarif' diff --git a/.github/workflows/typos.yml b/.github/workflows/typos.yml index eaf77896..a574641c 100644 --- a/.github/workflows/typos.yml +++ b/.github/workflows/typos.yml @@ -16,11 +16,11 @@ jobs: steps: # Checkout the repo - name: Checkout - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: persist-credentials: false # End Checkout the repo # When this version is updated, do not forget to update this in `.pre-commit-config.yaml` too - name: Spell Check Repo - uses: crate-ci/typos@5374cbf686e897b15713110e233094e2874de7ef # v1.46.1 + uses: crate-ci/typos@37bb98842b0d8c4ffebdb75301a13db0267cef89 # v1.47.2 diff --git a/.github/workflows/zizmor.yml b/.github/workflows/zizmor.yml index d5bc7464..1036b1ce 100644 --- a/.github/workflows/zizmor.yml +++ b/.github/workflows/zizmor.yml @@ -19,12 +19,12 @@ jobs: security-events: write # To write the security report steps: - name: Checkout repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: persist-credentials: false - name: Run zizmor - uses: zizmorcore/zizmor-action@b572f7b1a1c2d41efaab43d504f68d215c3cd727 # v0.5.4 + uses: zizmorcore/zizmor-action@5f14fd08f7cf1cb1609c1e344975f152c7ee938d # v0.5.6 with: # intentionally not scanning the entire repository, # since it contains integration tests. diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 11f0b439..8010e67f 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -18,7 +18,7 @@ repos: # When this version is updated, do not forget to update this in `.github/workflows/typos.yaml` too - repo: https://github.com/crate-ci/typos - rev: 5374cbf686e897b15713110e233094e2874de7ef # v1.46.1 + rev: 37bb98842b0d8c4ffebdb75301a13db0267cef89 # v1.47.2 hooks: - id: typos diff --git a/Cargo.lock b/Cargo.lock index f4d87f9b..2826b092 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8,6 +8,17 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" +[[package]] +name = "aes" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures 0.2.17", +] + [[package]] name = "ahash" version = "0.8.12" @@ -65,6 +76,15 @@ version = "1.0.102" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" +[[package]] +name = "arc-swap" +version = "1.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a3a1fd6f75306b68087b831f025c712524bcb19aad54e557b1129cfa0a2b207" +dependencies = [ + "rustversion", +] + [[package]] name = "argon2" version = "0.5.3" @@ -334,15 +354,15 @@ checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" [[package]] name = "autocfg" -version = "1.5.0" +version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" [[package]] name = "aws-config" -version = "1.8.16" +version = "1.8.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50f156acdd2cf55f5aa53ee416c4ac851cf1222694506c0b1f78c85695e9ca9d" +checksum = "e33f815b73a3899c03b380d543532e5865f230dce9678d108dc10732a8682275" dependencies = [ "aws-credential-types", "aws-runtime", @@ -354,13 +374,14 @@ dependencies = [ "aws-smithy-json", "aws-smithy-runtime", "aws-smithy-runtime-api", + "aws-smithy-schema", "aws-smithy-types", "aws-types", "bytes", "fastrand", "hex", - "http 1.4.0", - "sha1", + "http 1.4.1", + "sha1 0.10.6", "time", "tokio", "tracing", @@ -382,9 +403,9 @@ dependencies = [ [[package]] name = "aws-runtime" -version = "1.7.3" +version = "1.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5dcd93c82209ac7413532388067dce79be5a8780c1786e5fae3df22e4dee2864" +checksum = "77ed8e8c52d2dc2390ad9f15647fe663f71e9780b4262c190fbb823a32721566" dependencies = [ "aws-credential-types", "aws-sigv4", @@ -397,7 +418,7 @@ dependencies = [ "bytes", "bytes-utils", "fastrand", - "http 1.4.0", + "http 1.4.1", "http-body 1.0.1", "percent-encoding", "pin-project-lite", @@ -407,10 +428,11 @@ dependencies = [ [[package]] name = "aws-sdk-sso" -version = "1.98.0" +version = "1.101.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d69c77aafa20460c68b6b3213c84f6423b6e76dbf89accd3e1789a686ffd9489" +checksum = "b647baea49ff551960b904f905681e9b4765a6c4ea08631e89dc52d8bd3f5896" dependencies = [ + "arc-swap", "aws-credential-types", "aws-runtime", "aws-smithy-async", @@ -424,17 +446,18 @@ dependencies = [ "bytes", "fastrand", "http 0.2.12", - "http 1.4.0", + "http 1.4.1", "regex-lite", "tracing", ] [[package]] name = "aws-sdk-ssooidc" -version = "1.100.0" +version = "1.103.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c7e7b09346d5ca22a2a08267555843a6a0127fb20d8964cb6ecfb8fdb190225" +checksum = "7ae401c65ff288aa7873117fe535cd32b7b1bb0bc43751d28901a1d5f20636b9" dependencies = [ + "arc-swap", "aws-credential-types", "aws-runtime", "aws-smithy-async", @@ -448,17 +471,18 @@ dependencies = [ "bytes", "fastrand", "http 0.2.12", - "http 1.4.0", + "http 1.4.1", "regex-lite", "tracing", ] [[package]] name = "aws-sdk-sts" -version = "1.103.0" +version = "1.106.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2249b81a2e73a8027c41c378463a81ec39b8510f184f2caab87de912af0f49b" +checksum = "4c80de7bb7d03e9ca8c9fd7b489f20f3948d3f3be91a7953591347d238115408" dependencies = [ + "arc-swap", "aws-credential-types", "aws-runtime", "aws-smithy-async", @@ -473,16 +497,16 @@ dependencies = [ "aws-types", "fastrand", "http 0.2.12", - "http 1.4.0", + "http 1.4.1", "regex-lite", "tracing", ] [[package]] name = "aws-sigv4" -version = "1.4.3" +version = "1.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68dc0b907359b120170613b5c09ccc61304eac3998ff6274b97d93ee6490115a" +checksum = "bae38512beae0ffee7010fc24e7a8a123c53efdfef42a61e80fda4882418dc71" dependencies = [ "aws-credential-types", "aws-smithy-http", @@ -493,7 +517,7 @@ dependencies = [ "hex", "hmac 0.13.0", "http 0.2.12", - "http 1.4.0", + "http 1.4.1", "percent-encoding", "sha2 0.11.0", "time", @@ -523,7 +547,7 @@ dependencies = [ "bytes-utils", "futures-core", "futures-util", - "http 1.4.0", + "http 1.4.1", "http-body 1.0.1", "http-body-util", "percent-encoding", @@ -534,10 +558,12 @@ dependencies = [ [[package]] name = "aws-smithy-json" -version = "0.62.5" +version = "0.62.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9648b0bb82a2eedd844052c6ad2a1a822d1f8e3adee5fbf668366717e428856a" +checksum = "701a947f4797e52a911e114a898667c746c39feea467bbd1abd7b3721f702ffa" dependencies = [ + "aws-smithy-runtime-api", + "aws-smithy-schema", "aws-smithy-types", ] @@ -562,19 +588,20 @@ dependencies = [ [[package]] name = "aws-smithy-runtime" -version = "1.11.1" +version = "1.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0504b1ab12debb5959e5165ee5fe97dd387e7aa7ea6a477bfd7635dfe769a4f5" +checksum = "b8e6f5caf6fea86f8c2206541ab5857cfcda9013426cdbe8fa0098b9e2d32182" dependencies = [ "aws-smithy-async", "aws-smithy-http", "aws-smithy-observability", "aws-smithy-runtime-api", + "aws-smithy-schema", "aws-smithy-types", "bytes", "fastrand", "http 0.2.12", - "http 1.4.0", + "http 1.4.1", "http-body 0.4.6", "http-body 1.0.1", "http-body-util", @@ -586,16 +613,16 @@ dependencies = [ [[package]] name = "aws-smithy-runtime-api" -version = "1.12.0" +version = "1.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b71a13df6ada0aafbf21a73bdfcdf9324cfa9df77d96b8446045be3cde61b42e" +checksum = "9db177daa6ba8afb9ee1aefcf548c907abcf52065e394ee11a92780057fe0e8c" dependencies = [ "aws-smithy-async", "aws-smithy-runtime-api-macros", "aws-smithy-types", "bytes", "http 0.2.12", - "http 1.4.0", + "http 1.4.1", "pin-project-lite", "tokio", "tracing", @@ -614,16 +641,27 @@ dependencies = [ ] [[package]] -name = "aws-smithy-types" -version = "1.4.7" +name = "aws-smithy-schema" +version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d73dbfbaa8e4bc57b9045137680b958d274823509a360abfd8e1d514d40c95c" +checksum = "7442cb268338f0eb8278140a107c046756aa01093d8ef5e99628d34ae09c94f5" +dependencies = [ + "aws-smithy-runtime-api", + "aws-smithy-types", + "http 1.4.1", +] + +[[package]] +name = "aws-smithy-types" +version = "1.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53f93074121a1be41317b9aa607143ae17900631f7f59a99f2b905d519d6783b" dependencies = [ "base64-simd", "bytes", "bytes-utils", "http 0.2.12", - "http 1.4.0", + "http 1.4.1", "http-body 0.4.6", "http-body 1.0.1", "http-body-util", @@ -647,13 +685,14 @@ dependencies = [ [[package]] name = "aws-types" -version = "1.3.15" +version = "1.3.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f4bbcaa9304ea40902d3d5f42a0428d1bd895a2b0f6999436fb279ffddc58ac" +checksum = "d16bf10b03a3c01e6b3b7d47cd964e873ffe9e7d4e80fad16bd4c077cb068531" dependencies = [ "aws-credential-types", "aws-smithy-async", "aws-smithy-runtime-api", + "aws-smithy-schema", "aws-smithy-types", "rustc_version", "tracing", @@ -725,9 +764,9 @@ checksum = "383d29d513d8764dcdc42ea295d979eb99c3c9f00607b3692cf68a431f7dca72" [[package]] name = "bitflags" -version = "2.11.1" +version = "2.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" +checksum = "84d7ced0ae9557296835c32bf1b1e02b44c746701f898460fb000d7eaa84f00a" [[package]] name = "blake2" @@ -756,6 +795,15 @@ dependencies = [ "hybrid-array", ] +[[package]] +name = "block-padding" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8894febbff9f758034a5b8e12d87918f56dfc64a8e1fe757d65e29041538d93" +dependencies = [ + "generic-array", +] + [[package]] name = "blocking" version = "1.6.2" @@ -771,9 +819,9 @@ dependencies = [ [[package]] name = "brotli" -version = "8.0.2" +version = "8.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4bd8b9603c7aa97359dbd97ecf258968c95f3adddd6db2f7e7a5bef101c84560" +checksum = "8119e4516436f5708bbc474a9d395bf12f1b5395e93a92a56e647ac3388c8610" dependencies = [ "alloc-no-stdlib", "alloc-stdlib", @@ -782,9 +830,9 @@ dependencies = [ [[package]] name = "brotli-decompressor" -version = "5.0.0" +version = "5.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "874bb8112abecc98cbd6d81ea4fa7e94fb9449648c93cc89aa40c81c24d7de03" +checksum = "5962523e1b92ce1b5e793d9169b9943eece10d39f62550bc04bb605d75b94924" dependencies = [ "alloc-no-stdlib", "alloc-stdlib", @@ -801,9 +849,9 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.20.2" +version = "3.20.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" [[package]] name = "bytemuck" @@ -835,17 +883,14 @@ dependencies = [ [[package]] name = "cached" -version = "0.59.0" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53b6f5d101f0f6322c8646a45b7c581a673e476329040d97565815c2461dd0c4" +checksum = "4863037a22757575c62f4f52c71bc833c7989c696c35eda5c83a4f36599b2bbd" dependencies = [ "ahash", - "async-trait", "cached_proc_macro", "cached_proc_macro_types", - "futures", "hashbrown 0.16.1", - "once_cell", "parking_lot", "thiserror 2.0.18", "tokio", @@ -854,9 +899,9 @@ dependencies = [ [[package]] name = "cached_proc_macro" -version = "0.27.0" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ebcf9c75f17a17d55d11afc98e46167d4790a263f428891b8705ab2f793eca3" +checksum = "8b7a89b3ceb2f9166826b0d21b670a8720dbaeb3397428d1c7603e0ffacdfd37" dependencies = [ "darling 0.20.11", "proc-macro2", @@ -866,15 +911,24 @@ dependencies = [ [[package]] name = "cached_proc_macro_types" -version = "0.1.1" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ade8366b8bd5ba243f0a58f036cc0ca8a2f069cff1a2351ef1cac6b083e16fc0" +checksum = "26cf465651fa6ad902a2d327ba60c3a6bc61c6a2f4ad70d091cf20dfda0074ef" + +[[package]] +name = "cbc" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26b52a9543ae338f279b96b0b9fed9c8093744685043739079ce85cd58f289a6" +dependencies = [ + "cipher", +] [[package]] name = "cc" -version = "1.2.62" +version = "1.2.63" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1dce859f0832a7d088c4f1119888ab94ef4b5d6795d1ce05afb7fe159d79f98" +checksum = "556e016178bb5662a08681bbe0f00f8e17631781a4dfc8c45e466e4b185ec27f" dependencies = [ "find-msvc-tools", "jobserver", @@ -901,9 +955,9 @@ dependencies = [ [[package]] name = "chrono" -version = "0.4.44" +version = "0.4.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0" +checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327" dependencies = [ "iana-time-zone", "js-sys", @@ -924,10 +978,20 @@ dependencies = [ ] [[package]] -name = "cmov" -version = "0.5.3" +name = "cipher" +version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f88a43d011fc4a6876cb7344703e297c71dda42494fee094d5f7c76bf13f746" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common 0.1.6", + "inout", +] + +[[package]] +name = "cmov" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c9ea0ac24bc397ab3c98583a3c9ba74fa56b09a4449bbe172b9b1ddb016027a" [[package]] name = "codemap" @@ -1177,9 +1241,9 @@ dependencies = [ [[package]] name = "crypto-common" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77727bb15fa921304124b128af125e7e3b968275d1b108b379190264f4423710" +checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453" dependencies = [ "hybrid-array", ] @@ -1326,9 +1390,9 @@ dependencies = [ [[package]] name = "dashmap" -version = "6.1.0" +version = "6.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5041cc499144891f3790297212f32a74fb938e5136a14943f338ef9e0ae276cf" +checksum = "e6361d5c062261c78a176addb82d4c821ae42bed6089de0e12603cd25de2059c" dependencies = [ "cfg-if", "crossbeam-utils", @@ -1474,9 +1538,9 @@ dependencies = [ [[package]] name = "diesel" -version = "2.3.9" +version = "2.3.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9940fb8467a0a06312218ed384185cb8536aa10d8ec017d0ce7fad2c1bd882d5" +checksum = "29fe29a87fb84c631ffb3ba21798c4b1f3a964701ba78f0dce4bf8668562ec88" dependencies = [ "bigdecimal", "bitflags", @@ -1562,15 +1626,15 @@ checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2" dependencies = [ "block-buffer 0.12.0", "const-oid 0.10.2", - "crypto-common 0.2.1", + "crypto-common 0.2.2", "ctutils", ] [[package]] name = "displaydoc" -version = "0.2.5" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +checksum = "1ac70aa55017e108007fbaf5aa0f54b021c98f92ff8af59d42eda9da96e3dd4f" dependencies = [ "proc-macro2", "quote", @@ -1667,9 +1731,9 @@ dependencies = [ [[package]] name = "either" -version = "1.15.0" +version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" +checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e" [[package]] name = "elliptic-curve" @@ -1955,9 +2019,9 @@ checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" [[package]] name = "futures-timer" -version = "3.0.3" +version = "3.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f288b0a4f20f9a56b5d1da57e2227c661b7b16168e2f72365f57b63326e29b24" +checksum = "af43fadb8a98512d547e37b4e92e0ced13e205c061b87b4623eff01d918d6968" [[package]] name = "futures-util" @@ -2117,7 +2181,7 @@ dependencies = [ "fnv", "futures-core", "futures-sink", - "http 1.4.0", + "http 1.4.1", "indexmap 2.14.0", "slab", "tokio", @@ -2138,9 +2202,9 @@ dependencies = [ [[package]] name = "handlebars" -version = "6.4.0" +version = "6.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b3f9296c208515b87bd915a2f5d1163d4b3f863ba83337d7713cf478055948e" +checksum = "d43ccdfe15a81ab0a8af639e90254227c9a46afd9c5f5b6ec7efaa345c4b0f00" dependencies = [ "derive_builder", "log", @@ -2343,9 +2407,9 @@ dependencies = [ [[package]] name = "http" -version = "1.4.0" +version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" +checksum = "8be7462df143984c4598a256ef469b251d7d7f9e271135073e78fc535414f3d0" dependencies = [ "bytes", "itoa", @@ -2369,7 +2433,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" dependencies = [ "bytes", - "http 1.4.0", + "http 1.4.1", ] [[package]] @@ -2380,7 +2444,7 @@ checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" dependencies = [ "bytes", "futures-core", - "http 1.4.0", + "http 1.4.1", "http-body 1.0.1", "pin-project-lite", ] @@ -2431,16 +2495,16 @@ dependencies = [ [[package]] name = "hyper" -version = "1.9.0" +version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6299f016b246a94207e63da54dbe807655bf9e00044f73ded42c3ac5305fbcca" +checksum = "55281c53a1894c864990125767da440a4e630446785086f52523b20033b74498" dependencies = [ "atomic-waker", "bytes", "futures-channel", "futures-core", "h2", - "http 1.4.0", + "http 1.4.1", "http-body 1.0.1", "httparse", "itoa", @@ -2456,8 +2520,8 @@ version = "0.27.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f" dependencies = [ - "http 1.4.0", - "hyper 1.9.0", + "http 1.4.1", + "hyper 1.10.1", "hyper-util", "rustls 0.23.40", "tokio", @@ -2475,14 +2539,14 @@ dependencies = [ "bytes", "futures-channel", "futures-util", - "http 1.4.0", + "http 1.4.1", "http-body 1.0.1", - "hyper 1.9.0", + "hyper 1.10.1", "ipnet", "libc", "percent-encoding", "pin-project-lite", - "socket2 0.6.3", + "socket2 0.6.4", "system-configuration", "tokio", "tower-service", @@ -2658,13 +2722,23 @@ version = "0.1.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c8fae54786f62fb2918dcfae3d568594e50eb9b5c25bf04371af6fe7516452fb" +[[package]] +name = "inout" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" +dependencies = [ + "block-padding", + "generic-array", +] + [[package]] name = "ipconfig" version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4d40460c0ce33d6ce4b0630ad68ff63d6661961c48b6dba35e5a4d81cfb48222" dependencies = [ - "socket2 0.6.3", + "socket2 0.6.4", "widestring", "windows-registry", "windows-result", @@ -2714,9 +2788,9 @@ checksum = "47f142fe24a9c9944451e8349de0a56af5f3e7226dc46f3ed4d4ecc0b85af75e" [[package]] name = "jiff" -version = "0.2.24" +version = "0.2.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f00b5dbd620d61dfdcb6007c9c1f6054ebd75319f163d886a9055cec1155073d" +checksum = "4603d3033e49e2b0e31229fcab20a5d40089c607d975cd9c80551dc69eed9102" dependencies = [ "jiff-static", "jiff-tzdb-platform", @@ -2726,14 +2800,14 @@ dependencies = [ "portable-atomic-util", "serde_core", "wasm-bindgen", - "windows-sys 0.61.2", + "windows-link", ] [[package]] name = "jiff-static" -version = "0.2.24" +version = "0.2.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e000de030ff8022ea1da3f466fbb0f3a809f5e51ed31f6dd931c35181ad8e6d7" +checksum = "782d32378dddf207193ac91cefb848ad41abb58195c95168e1291227a0832b47" dependencies = [ "proc-macro2", "quote", @@ -2827,9 +2901,9 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.98" +version = "0.3.99" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67df7112613f8bfd9150013a0314e196f4800d3201ae742489d999db2f979f08" +checksum = "142bc4740e452c1e57ade0cbc129f139c9093e354346f0872ef985f4f5cf5f11" dependencies = [ "cfg-if", "futures-util", @@ -2918,7 +2992,7 @@ dependencies = [ "rustls 0.23.40", "rustls-native-certs", "serde", - "socket2 0.6.3", + "socket2 0.6.4", "tokio", "tokio-rustls 0.26.4", "tracing", @@ -2939,9 +3013,9 @@ checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" [[package]] name = "libmimalloc-sys" -version = "0.1.47" +version = "0.1.49" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d1eacfa31c33ec25e873c136ba5669f00f9866d0688bea7be4d3f7e43067df6" +checksum = "6a45a52f43e1c16f667ccfe4dd8c85b7f7c204fd5e3bf46c5b0db9a5c3c0b8e9" dependencies = [ "cc", ] @@ -2986,9 +3060,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.29" +version = "0.4.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" +checksum = "953f07c43838f8e6f9758cab68bf5bed85465e7587ebe0b823f1bcd81978ad3a" dependencies = [ "value-bag", ] @@ -3027,28 +3101,28 @@ dependencies = [ [[package]] name = "md-5" -version = "0.10.6" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" +checksum = "69b6441f590336821bb897fb28fc622898ccceb1d6cea3fde5ea86b090c4de98" dependencies = [ "cfg-if", - "digest 0.10.7", + "digest 0.11.3", ] [[package]] name = "mea" -version = "0.6.3" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6747f54621d156e1b47eb6b25f39a941b9fc347f98f67d25d8881ff99e8ed832" +checksum = "2640d335e7273dacdcf51044026139b2e269c3bb0dfc3f8cb3496b85e3f6a42c" dependencies = [ "slab", ] [[package]] name = "memchr" -version = "2.8.0" +version = "2.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" +checksum = "6b947ae49db0d222b1dbc6b113ce7248a3fc3a6ca21b696717bfc000ba4484d8" [[package]] name = "migrations_internals" @@ -3073,9 +3147,9 @@ dependencies = [ [[package]] name = "mimalloc" -version = "0.1.50" +version = "0.1.52" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3627c4272df786b9260cabaa46aec1d59c93ede723d4c3ef646c503816b0640" +checksum = "2d4139bb28d14ad1facf21d5eb8825051b326e172d216b39f6d31df53cc97862" dependencies = [ "libmimalloc-sys", ] @@ -3104,9 +3178,9 @@ dependencies = [ [[package]] name = "mio" -version = "1.2.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50b7e5b27aa02a74bac8c3f23f448f8d87ff11f92d3aac1a6ed369ee08cc56c1" +checksum = "02bd0af71c67b473010cbbc60715ee815645a4dc942899111f494b4b737d6fda" dependencies = [ "libc", "wasi", @@ -3142,7 +3216,7 @@ dependencies = [ "bytes", "encoding_rs", "futures-util", - "http 1.4.0", + "http 1.4.1", "httparse", "memchr", "mime", @@ -3231,9 +3305,9 @@ dependencies = [ [[package]] name = "num-conv" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6673768db2d862beb9b39a78fdcb1a69439615d5794a1be50caa9bc92c81967" +checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" [[package]] name = "num-derive" @@ -3319,7 +3393,7 @@ dependencies = [ "base64 0.22.1", "chrono", "getrandom 0.2.17", - "http 1.4.0", + "http 1.4.1", "rand 0.8.6", "serde", "serde_json", @@ -3350,9 +3424,9 @@ dependencies = [ [[package]] name = "opendal" -version = "0.56.0" +version = "0.57.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97b31d3d8e99a85d83b73ec26647f5607b80578ed9375810b6e44ffa3590a236" +checksum = "96c9c85ce253ff87225e7669979d877a20c98a06604ec9d6dd5f4473e08f1ae1" dependencies = [ "opendal-core", "opendal-service-fs", @@ -3361,22 +3435,22 @@ dependencies = [ [[package]] name = "opendal-core" -version = "0.56.0" +version = "0.57.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1849dd2687e173e776d3af5fce1ba3ae47b9dd37a09d1c4deba850ef45fe00ca" +checksum = "c4f8607c90e2c963a91467f50fb49fbc7fb3d573f88cea219ca59ccd3740b309" dependencies = [ "anyhow", "base64 0.22.1", "bytes", "futures", - "http 1.4.0", + "http 1.4.1", "http-body 1.0.1", "jiff", "log", "md-5", "mea", "percent-encoding", - "quick-xml 0.38.4", + "quick-xml 0.39.4", "reqsign-core", "reqwest", "serde", @@ -3389,9 +3463,9 @@ dependencies = [ [[package]] name = "opendal-service-fs" -version = "0.56.0" +version = "0.57.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf0be0417abeeb0053376d816b90fceb9ca98f20dfb54ebf1f2a282729f83663" +checksum = "22e89a665fef0e6bd249cf5ea47fc174b7ba892159bee4b9382528b1ca873a2c" dependencies = [ "bytes", "log", @@ -3403,18 +3477,18 @@ dependencies = [ [[package]] name = "opendal-service-s3" -version = "0.56.0" +version = "0.57.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9dadddeb9bb50b0d30927dd914c298c4ddca47e4c1cfa7674d311f0cf9b051c8" +checksum = "313d46c9f5ae70bca26b7c3e3fbb9b639292625f28af73aa016f47e788af9deb" dependencies = [ "base64 0.22.1", "bytes", "crc32c", - "http 1.4.0", + "http 1.4.1", "log", "md-5", "opendal-core", - "quick-xml 0.38.4", + "quick-xml 0.39.4", "reqsign-aws-v4", "reqsign-core", "reqsign-file-read-tokio", @@ -3433,7 +3507,7 @@ dependencies = [ "dyn-clone", "ed25519-dalek", "hmac 0.12.1", - "http 1.4.0", + "http 1.4.1", "itertools", "log", "oauth2", @@ -3455,9 +3529,9 @@ dependencies = [ [[package]] name = "openssl" -version = "0.10.79" +version = "0.10.80" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf0b434746ee2832f4f0baf10137e1cabb18cbe6912c69e2e33263c45250f542" +checksum = "a45fa2aa886c42762255da344f0a0d313e254066c46aad76f300c3d3da62d967" dependencies = [ "bitflags", "cfg-if", @@ -3495,9 +3569,9 @@ dependencies = [ [[package]] name = "openssl-sys" -version = "0.9.115" +version = "0.9.116" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "158fe5b292746440aa6e7a7e690e55aeb72d41505e2804c23c6973ad0e9c9781" +checksum = "f28a22dc7140cda5f096e5e7724a6962ca81a7f8bfd2979f9b18c11af56318c4" dependencies = [ "cc", "libc", @@ -3603,9 +3677,19 @@ checksum = "35fb2e5f958ec131621fdd531e9fc186ed768cbe395337403ae56c17a74c68ec" [[package]] name = "pastey" -version = "0.2.2" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5a797f0e07bdf071d15742978fc3128ec6c22891c31a3a931513263904c982a" +checksum = "2ee67f1008b1ba2321834326597b8e186293b049a023cdef258527550b9935b4" + +[[package]] +name = "pbkdf2" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2" +dependencies = [ + "digest 0.10.7", + "hmac 0.12.1", +] [[package]] name = "pear" @@ -3798,6 +3882,21 @@ dependencies = [ "spki", ] +[[package]] +name = "pkcs5" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e847e2c91a18bfa887dd028ec33f2fe6f25db77db3619024764914affe8b69a6" +dependencies = [ + "aes", + "cbc", + "der", + "pbkdf2", + "scrypt", + "sha2 0.10.9", + "spki", +] + [[package]] name = "pkcs8" version = "0.10.2" @@ -3805,6 +3904,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" dependencies = [ "der", + "pkcs5", + "rand_core 0.6.4", "spki", ] @@ -3969,9 +4070,9 @@ checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" [[package]] name = "quick-xml" -version = "0.38.4" +version = "0.39.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b66c2058c55a409d601666cffe35f04333cf1013010882cec174a7467cd4e21c" +checksum = "cdcc8dd4e2f670d309a5f0e83fe36dfdc05af317008fea29144da1a2ac858e5e" dependencies = [ "memchr", "serde", @@ -3979,9 +4080,9 @@ dependencies = [ [[package]] name = "quick-xml" -version = "0.39.4" +version = "0.40.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cdcc8dd4e2f670d309a5f0e83fe36dfdc05af317008fea29144da1a2ac858e5e" +checksum = "2474bd2e5029e7ccb6abb2ba48cf2383a333851dedf495901544281590c7da7f" dependencies = [ "memchr", "serde", @@ -4187,30 +4288,31 @@ dependencies = [ [[package]] name = "reqsign-aws-v4" -version = "3.0.0" +version = "3.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44eaca382e94505a49f1a4849658d153aebf79d9c1a58e5dd3b10361511e9f43" +checksum = "7b75624bd8a466e37ddc0a7b6c33ac859a85347c153a916e1dd9d0b68338f74a" dependencies = [ "anyhow", "bytes", "form_urlencoded", - "http 1.4.0", + "hex", + "http 1.4.1", "log", "percent-encoding", - "quick-xml 0.39.4", + "quick-xml 0.40.1", "reqsign-core", "rust-ini", "serde", "serde_json", "serde_urlencoded", - "sha1", + "sha1 0.11.0", ] [[package]] name = "reqsign-core" -version = "3.0.0" +version = "3.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b10302cf0a7d7e7352ba211fc92c3c5bebf1286153e49cc5aa87348078a8e102" +checksum = "a5fa5cb48808693614d1701fcd3db0b30fa292e0f18e122ae068b6d32eaeed3f" dependencies = [ "anyhow", "base64 0.22.1", @@ -4218,21 +4320,24 @@ dependencies = [ "form_urlencoded", "futures", "hex", - "hmac 0.12.1", - "http 1.4.0", + "hmac 0.13.0", + "http 1.4.1", "jiff", "log", "percent-encoding", - "sha1", - "sha2 0.10.9", + "rsa", + "serde", + "serde_json", + "sha1 0.11.0", + "sha2 0.11.0", "windows-sys 0.61.2", ] [[package]] name = "reqsign-file-read-tokio" -version = "3.0.0" +version = "3.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2d89295b3d17abea31851cc8de55d843d89c52132c864963c38d41920613dc5" +checksum = "6a4b6f3a3fd29ffcc99a90aec585a65217783badfd73acddf847b63ae683bda9" dependencies = [ "anyhow", "reqsign-core", @@ -4241,9 +4346,9 @@ dependencies = [ [[package]] name = "reqwest" -version = "0.13.3" +version = "0.13.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62e0021ea2c22aed41653bc7e1419abb2c97e038ff2c33d0e1309e49a97deec0" +checksum = "219c5811de6525e5416c7d5d53bb656d3afdbc6c5af816e0802bcfa42dbdc1c3" dependencies = [ "base64 0.22.1", "bytes", @@ -4254,10 +4359,10 @@ dependencies = [ "futures-core", "futures-util", "h2", - "http 1.4.0", + "http 1.4.1", "http-body 1.0.1", "http-body-util", - "hyper 1.9.0", + "hyper 1.10.1", "hyper-rustls", "hyper-util", "js-sys", @@ -4430,9 +4535,9 @@ dependencies = [ [[package]] name = "rpassword" -version = "7.5.2" +version = "7.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ac5b223d9738ef56e0b98305410be40fa0941bf6036c56f1506751e43552d64" +checksum = "2da316a15f47e3d053de9cb2c439650bd8fa4aaeb9365f2e5f27f492ff73c196" dependencies = [ "libc", "rtoolbox", @@ -4453,6 +4558,7 @@ dependencies = [ "pkcs1", "pkcs8", "rand_core 0.6.4", + "sha2 0.10.9", "signature", "spki", "subtle", @@ -4461,9 +4567,9 @@ dependencies = [ [[package]] name = "rsqlite-vfs" -version = "0.1.0" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8a1f2315036ef6b1fbacd1972e8ee7688030b0a2121edfc2a6550febd41574d" +checksum = "c51c9ae4df8a7fba42103df5c621fa3c37eccf3a3c650879e90fc48b11cc192c" dependencies = [ "hashbrown 0.16.1", "thiserror 2.0.18", @@ -4549,9 +4655,9 @@ dependencies = [ [[package]] name = "rustls-native-certs" -version = "0.8.3" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "612460d5f7bea540c490b2b6395d8e34a953e52b491accd6c86c8164c5932a63" +checksum = "dab5152771c58876a2146916e53e35057e1a4dfa2b9df0f0305b07f611fdea4d" dependencies = [ "openssl-probe", "rustls-pki-types", @@ -4637,6 +4743,15 @@ version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" +[[package]] +name = "salsa20" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97a22f5af31f73a954c10289c93e8a50cc23d971e80ee446f1f6f7137a088213" +dependencies = [ + "cipher", +] + [[package]] name = "same-file" version = "1.0.6" @@ -4700,6 +4815,17 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "scrypt" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0516a385866c09368f0b5bcd1caff3366aace790fcd46e2bb032697bb172fd1f" +dependencies = [ + "pbkdf2", + "salsa20", + "sha2 0.10.9", +] + [[package]] name = "sct" version = "0.7.1" @@ -4805,9 +4931,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.149" +version = "1.0.150" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" dependencies = [ "itoa", "memchr", @@ -4868,9 +4994,9 @@ dependencies = [ [[package]] name = "serde_with" -version = "3.20.0" +version = "3.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e72c1c2cb7b223fafb600a619537a871c2818583d619401b785e7c0b746ccde2" +checksum = "76a5c54c7310e7b8b9577c286d7e399ddd876c3e12b3ed917a8aabc4b96e9e8c" dependencies = [ "base64 0.22.1", "bs58", @@ -4888,9 +5014,9 @@ dependencies = [ [[package]] name = "serde_with_macros" -version = "3.20.0" +version = "3.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b90c488738ecb4fb0262f41f43bc40efc5868d9fb744319ddf5f5317f417bfac" +checksum = "84d57bc0c8b9a17920c178daa6bb924850d54a9c97ab45194bb8c17ad66bb660" dependencies = [ "darling 0.23.0", "proc-macro2", @@ -4909,6 +5035,17 @@ dependencies = [ "digest 0.10.7", ] +[[package]] +name = "sha1" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aacc4cc499359472b4abe1bf11d0b12e688af9a805fa5e3016f9a386dc2d0214" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.0", + "digest 0.11.3", +] + [[package]] name = "sha2" version = "0.10.9" @@ -4942,9 +5079,9 @@ dependencies = [ [[package]] name = "shlex" -version = "1.3.0" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" [[package]] name = "signal-hook" @@ -5040,9 +5177,9 @@ dependencies = [ [[package]] name = "socket2" -version = "0.6.3" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" +checksum = "52d1cfed4120b4d927bf7c0f86d2087a4a7d6027c906d9f9d525a80573b9be51" dependencies = [ "libc", "windows-sys 0.61.2", @@ -5075,9 +5212,9 @@ dependencies = [ [[package]] name = "sqlite-wasm-rs" -version = "0.5.3" +version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b2c760607300407ddeaee518acf28c795661b7108c75421303dbefb237d3a36" +checksum = "dc3efc0da82635d7e1ced0053bbbfa8c7ab9645d0bf36ceb4f7127bb85315d75" dependencies = [ "cc", "js-sys", @@ -5354,7 +5491,7 @@ dependencies = [ "parking_lot", "pin-project-lite", "signal-hook-registry", - "socket2 0.6.3", + "socket2 0.6.4", "tokio-macros", "windows-sys 0.61.2", ] @@ -5507,7 +5644,7 @@ checksum = "f8e43134db17199f7f721803383ac5854edd0d3d523cc34dba321d6acfbe76c3" dependencies = [ "digest 0.10.7", "hmac 0.12.1", - "sha1", + "sha1 0.10.6", "sha2 0.10.9", ] @@ -5528,16 +5665,16 @@ dependencies = [ [[package]] name = "tower-http" -version = "0.6.10" +version = "0.6.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68d6fdd9f81c2819c9a8b0e0cd91660e7746a8e6ea2ba7c6b2b057985f6bcb51" +checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840" dependencies = [ "async-compression", "bitflags", "bytes", "futures-core", "futures-util", - "http 1.4.0", + "http 1.4.1", "http-body 1.0.1", "http-body-util", "pin-project-lite", @@ -5638,11 +5775,11 @@ dependencies = [ "byteorder", "bytes", "data-encoding", - "http 1.4.0", + "http 1.4.1", "httparse", "log", "rand 0.8.6", - "sha1", + "sha1 0.10.6", "thiserror 1.0.69", "url", "utf-8", @@ -5650,9 +5787,9 @@ dependencies = [ [[package]] name = "typenum" -version = "1.20.0" +version = "1.20.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40ce102ab67701b8526c123c1bab5cbe42d7040ccfd0f64af1a385808d2f43de" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" [[package]] name = "ubyte" @@ -5687,9 +5824,9 @@ checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" [[package]] name = "unicode-segmentation" -version = "1.13.2" +version = "1.13.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9629274872b2bfaf8d66f5f15725007f635594914870f65218920345aa11aa8c" +checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8" [[package]] name = "unicode-xid" @@ -5736,9 +5873,9 @@ checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" [[package]] name = "uuid" -version = "1.23.1" +version = "1.23.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddd74a9687298c6858e9b88ec8935ec45d22e8fd5e6394fa1bd4e99a87789c76" +checksum = "d258b83ceec21034727ecee8c382cfa6c3e133699b0742c64571814fb420c9f7" dependencies = [ "getrandom 0.4.2", "js-sys", @@ -5789,7 +5926,7 @@ dependencies = [ "handlebars", "hickory-resolver", "html5gum", - "http 1.4.0", + "http 1.4.1", "job_scheduler_ng", "jsonwebtoken", "lettre", @@ -5803,7 +5940,7 @@ dependencies = [ "opendal", "openidconnect", "openssl", - "pastey 0.2.2", + "pastey 0.2.3", "percent-encoding", "pico-args", "rand 0.10.1", @@ -5900,9 +6037,9 @@ dependencies = [ [[package]] name = "wasm-bindgen" -version = "0.2.121" +version = "0.2.122" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49ace1d07c165b0864824eee619580c4689389afa9dc9ed3a4c75040d82e6790" +checksum = "3ed04576f974d2b2fba0f38c51dbc5518011e38c36bf1143164be765528fd409" dependencies = [ "cfg-if", "once_cell", @@ -5913,9 +6050,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.71" +version = "0.4.72" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96492d0d3ffba25305a7dc88720d250b1401d7edca02cc3bcd50633b424673b8" +checksum = "9473dbd2991ae90b6291c3c32c30c6187ac49aa32f9905d1cce280ec1e110b0f" dependencies = [ "js-sys", "wasm-bindgen", @@ -5923,9 +6060,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.121" +version = "0.2.122" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e68e6f4afd367a562002c05637acb8578ff2dea1943df76afb9e83d177c8578" +checksum = "916151b09da36bd82f6615cbf3a419e2f0ba23a03c6160e8e92eb6bd4aa1dec6" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -5933,9 +6070,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.121" +version = "0.2.122" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d95a9ec35c64b2a7cb35d3fead40c4238d0940c86d107136999567a4703259f2" +checksum = "299047362ccbfce148b67ab7e73349f77748e00c8296f9542adfad2ad82c5c5e" dependencies = [ "bumpalo", "proc-macro2", @@ -5946,9 +6083,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.121" +version = "0.2.122" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4e0100b01e9f0d03189a92b96772a1fb998639d981193d7dbab487302513441" +checksum = "9a929b2c61f11ba3e9bc35b50c1f25cb38e0e892c0c231ae2b8cf78d5dad4437" dependencies = [ "unicode-ident", ] @@ -6002,9 +6139,9 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.98" +version = "0.3.99" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b572dff8bcf38bad0fa19729c89bb5748b2b9b1d8be70cf90df697e3a8f32aa" +checksum = "6d621441cfc37b84979402712047321980c178f299193a3589d05b99e8763436" dependencies = [ "js-sys", "wasm-bindgen", @@ -6544,9 +6681,9 @@ dependencies = [ [[package]] name = "yoke" -version = "0.8.2" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "abe8c5fda708d9ca3df187cae8bfb9ceda00dd96231bed36e445a1a48e66f9ca" +checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5" dependencies = [ "stable_deref_trait", "yoke-derive", @@ -6577,24 +6714,24 @@ dependencies = [ "hmac 0.12.1", "rand 0.9.4", "reqwest", - "sha1", + "sha1 0.10.6", "threadpool", ] [[package]] name = "zerocopy" -version = "0.8.48" +version = "0.8.50" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eed437bf9d6692032087e337407a86f04cd8d6a16a37199ed57949d415bd68e9" +checksum = "3b065d4f0e55f82fae73202e189638116a87c55ab6b8e6c2721e13dd9d854ad1" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.48" +version = "0.8.50" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4" +checksum = "0b631b19d36a892ab55420c92dbc83ccd79274f25be714855d3074aa71cab639" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index 599af5c8..47c17f59 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [workspace.package] edition = "2024" -rust-version = "1.93.0" +rust-version = "1.94.0" license = "AGPL-3.0-only" repository = "https://github.com/dani-garcia/vaultwarden" publish = false @@ -14,7 +14,6 @@ version = "1.0.0" authors = ["Daniel GarcĂ­a "] readme = "README.md" build = "build.rs" -resolver = "2" repository.workspace = true edition.workspace = true rust-version.workspace = true @@ -66,7 +65,7 @@ syslog = "7.0.0" macros = { path = "./macros" } # Logging -log = "0.4.29" +log = "0.4.32" fern = { version = "0.7.1", features = ["syslog-7", "reopen-1"] } # We need the `log` feature for `tracing` to enable logging for several crates to work, like lettre or webauthn-rs tracing = { version = "0.1.44", features = ["log"] } @@ -87,7 +86,7 @@ rocket_ws = { version = "0.1.1" } rmpv = "1.3.1" # MessagePack library # Concurrent HashMap used for WebSocket messaging and favicons -dashmap = "6.1.0" +dashmap = "6.2.1" # Async futures futures = "0.3.32" @@ -104,10 +103,10 @@ tokio-util = { version = "0.7.18", features = ["compat"] } # A generic serialization/deserialization framework serde = { version = "1.0.228", features = ["derive"] } -serde_json = "1.0.149" +serde_json = "1.0.150" # A safe, extensible ORM and Query builder -diesel = { version = "2.3.9", features = ["chrono", "r2d2", "numeric"] } +diesel = { version = "2.3.10", features = ["chrono", "r2d2", "numeric"] } diesel_migrations = "2.3.2" derive_more = { version = "2.1.1", features = [ @@ -129,10 +128,10 @@ rustls = { version = "0.23.40", features = ["ring", "std"], default-features = f subtle = "2.6.1" # UUID generation -uuid = { version = "1.23.1", features = ["v4"] } +uuid = { version = "1.23.2", features = ["v4"] } # Date and time libraries -chrono = { version = "0.4.44", default-features = false, features = ["clock", "serde"] } +chrono = { version = "0.4.45", default-features = false, features = ["clock", "serde"] } chrono-tz = "0.10.4" time = "0.3.47" @@ -180,10 +179,10 @@ percent-encoding = "2.3.2" # URL encoding library used for URL's in the emails email_address = "0.2.9" # HTML Template library -handlebars = { version = "6.4.0", features = ["dir_source"] } +handlebars = { version = "6.4.1", features = ["dir_source"] } # HTTP client (Used for favicons, version check, DUO and HIBP API) -reqwest = { version = "0.13.3", default-features = false, features = [ +reqwest = { version = "0.13.4", default-features = false, features = [ # Misc "charset", "cookies", @@ -215,20 +214,20 @@ bytes = "1.11.1" svg-hush = "0.9.6" # Cache function results (Used for version check and favicon fetching) -cached = { version = "0.59.0", features = ["async"] } +cached = { version = "1.1.0", features = ["async"] } # Used for custom short lived cookie jar during favicon extraction cookie = "0.18.1" cookie_store = "0.22.1" # Used by U2F, JWT and PostgreSQL -openssl = "0.10.79" +openssl = "0.10.80" # CLI argument parsing pico-args = "0.5.0" # Macro ident concatenation -pastey = "0.2.2" +pastey = "0.2.3" governor = "0.10.4" # OIDC for SSO @@ -240,7 +239,7 @@ semver = "1.0.28" # Allow overriding the default memory allocator # Mainly used for the musl builds, since the default musl malloc is very slow -mimalloc = { version = "0.1.50", optional = true, default-features = false, features = ["secure"] } +mimalloc = { version = "0.1.52", optional = true, default-features = false, features = ["secure"] } which = "8.0.2" @@ -248,26 +247,26 @@ which = "8.0.2" argon2 = "0.5.3" # Reading a password from the cli for generating the Argon2id ADMIN_TOKEN -rpassword = "7.5.2" +rpassword = "7.5.4" # Loading a dynamic CSS Stylesheet grass_compiler = { version = "0.13.4", default-features = false } # File are accessed through Apache OpenDAL -opendal = { version = "0.56.0", default-features = false, features = ["services-fs"] } +opendal = { version = "0.57.0", default-features = false, features = ["services-fs"] } # For retrieving AWS credentials, including temporary SSO credentials -aws-config = { version = "1.8.16", optional = true, default-features = false, features = [ +aws-config = { version = "1.8.18", optional = true, default-features = false, features = [ "behavior-version-latest", "credentials-process", "rt-tokio", "sso", ] } aws-credential-types = { version = "1.2.14", optional = true } -aws-smithy-runtime-api = { version = "1.12.0", optional = true } -http = { version = "1.4.0", optional = true } -reqsign-aws-v4 = { version = "3.0.0", optional = true } -reqsign-core = { version = "3.0.0", optional = true } +aws-smithy-runtime-api = { version = "1.12.3", optional = true } +http = { version = "1.4.1", optional = true } +reqsign-aws-v4 = { version = "3.0.1", optional = true } +reqsign-core = { version = "3.0.1", optional = true } # Strip debuginfo from the release builds # The debug symbols are to provide better panic traces diff --git a/docker/DockerSettings.yaml b/docker/DockerSettings.yaml index 9d4a563a..5f2c16cf 100644 --- a/docker/DockerSettings.yaml +++ b/docker/DockerSettings.yaml @@ -5,7 +5,7 @@ vault_image_digest: "sha256:ca2a4251c4e63c9ad428262b4dd452789a1b9f6fce71da351e93 # We use the linux/amd64 platform shell scripts since there is no difference between the different platform scripts # https://github.com/tonistiigi/xx | https://hub.docker.com/r/tonistiigi/xx/tags xx_image_digest: "sha256:c64defb9ed5a91eacb37f96ccc3d4cd72521c4bd18d5442905b95e2226b0e707" -rust_version: 1.95.0 # Rust version to be used +rust_version: 1.96.0 # Rust version to be used debian_version: trixie # Debian release name to be used alpine_version: "3.23" # Alpine version to be used # For which platforms/architectures will we try to build images diff --git a/docker/Dockerfile.alpine b/docker/Dockerfile.alpine index 88492d94..373d944b 100644 --- a/docker/Dockerfile.alpine +++ b/docker/Dockerfile.alpine @@ -32,10 +32,10 @@ FROM --platform=linux/amd64 docker.io/vaultwarden/web-vault@sha256:ca2a4251c4e63 ########################## ALPINE BUILD IMAGES ########################## ## NOTE: The Alpine Base Images do not support other platforms then linux/amd64 and linux/arm64 ## And for Alpine we define all build images here, they will only be loaded when actually used -FROM --platform=$BUILDPLATFORM ghcr.io/blackdex/rust-musl:x86_64-musl-stable-1.95.0 AS build_amd64 -FROM --platform=$BUILDPLATFORM ghcr.io/blackdex/rust-musl:aarch64-musl-stable-1.95.0 AS build_arm64 -FROM --platform=$BUILDPLATFORM ghcr.io/blackdex/rust-musl:armv7-musleabihf-stable-1.95.0 AS build_armv7 -FROM --platform=$BUILDPLATFORM ghcr.io/blackdex/rust-musl:arm-musleabi-stable-1.95.0 AS build_armv6 +FROM --platform=$BUILDPLATFORM ghcr.io/blackdex/rust-musl:x86_64-musl-stable-1.96.0 AS build_amd64 +FROM --platform=$BUILDPLATFORM ghcr.io/blackdex/rust-musl:aarch64-musl-stable-1.96.0 AS build_arm64 +FROM --platform=$BUILDPLATFORM ghcr.io/blackdex/rust-musl:armv7-musleabihf-stable-1.96.0 AS build_armv7 +FROM --platform=$BUILDPLATFORM ghcr.io/blackdex/rust-musl:arm-musleabi-stable-1.96.0 AS build_armv6 ########################## BUILD IMAGE ########################## # hadolint ignore=DL3006 diff --git a/docker/Dockerfile.debian b/docker/Dockerfile.debian index 04f93361..85afb0f5 100644 --- a/docker/Dockerfile.debian +++ b/docker/Dockerfile.debian @@ -36,7 +36,7 @@ FROM --platform=linux/amd64 docker.io/tonistiigi/xx@sha256:c64defb9ed5a91eacb37f ########################## BUILD IMAGE ########################## # hadolint ignore=DL3006 -FROM --platform=$BUILDPLATFORM docker.io/library/rust:1.95.0-slim-trixie AS build +FROM --platform=$BUILDPLATFORM docker.io/library/rust:1.96.0-slim-trixie AS build COPY --from=xx / / ARG TARGETARCH ARG TARGETVARIANT diff --git a/rust-toolchain.toml b/rust-toolchain.toml index 775ded5a..2a813a32 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -1,4 +1,4 @@ [toolchain] -channel = "1.95.0" +channel = "1.96.0" components = [ "rustfmt", "clippy" ] profile = "minimal" diff --git a/src/api/admin.rs b/src/api/admin.rs index cb31a353..7037bfb1 100644 --- a/src/api/admin.rs +++ b/src/api/admin.rs @@ -643,11 +643,11 @@ async fn has_http_access() -> bool { } } -use cached::proc_macro::cached; +use cached::macros::cached; /// Cache this function to prevent API call rate limit. Github only allows 60 requests per hour, and we use 3 here already /// It will cache this function for 600 seconds (10 minutes) which should prevent the exhaustion of the rate limit /// Any cache will be lost if Vaultwarden is restarted -#[cached(time = 600, sync_writes = "default")] +#[cached(ttl = 600, sync_writes = "default")] async fn get_release_info(has_http_access: bool) -> (String, String, String) { // If the HTTP Check failed, do not even attempt to check for new versions since we were not able to connect with github.com anyway. if has_http_access { diff --git a/src/config.rs b/src/config.rs index 5c826fe9..3656d0d9 100644 --- a/src/config.rs +++ b/src/config.rs @@ -956,7 +956,7 @@ fn validate_config(cfg: &ConfigItems, on_update: bool) -> Result<(), Error> { } if cfg.database_min_conns > cfg.database_max_conns { - err!(format!("`DATABASE_MIN_CONNS` must be smaller than or equal to `DATABASE_MAX_CONNS`.",)); + err!("`DATABASE_MIN_CONNS` must be smaller than or equal to `DATABASE_MAX_CONNS`."); } if let Some(log_file) = &cfg.log_file diff --git a/src/sso_client.rs b/src/sso_client.rs index 5aa77750..355fffcb 100644 --- a/src/sso_client.rs +++ b/src/sso_client.rs @@ -219,7 +219,7 @@ impl Client { } else { let challenge = PkceCodeChallenge::from_code_verifier_sha256(&verifier); if challenge.as_str() != String::from(sso_auth.client_challenge.clone()) { - err!(format!("PKCE client challenge failed")) + err!("PKCE client challenge failed") // Might need to notify admin ? how ? } }