From 650defac75571b9ec74e38d92bed0c49f64e3ba2 Mon Sep 17 00:00:00 2001 From: Mathijs van Veluw Date: Mon, 23 Mar 2026 21:21:21 +0100 Subject: [PATCH 01/18] 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 02/18] 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 03/18] 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 04/18] 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 05/18] 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 06/18] 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 07/18] 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 08/18] 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 | 112 +++++++++++--------- src/api/core/events.rs | 2 +- src/api/core/organizations.rs | 186 +++++++++++++++++++++++----------- 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, 340 insertions(+), 191 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,67 +1643,75 @@ 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: Headers, + 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 mut user = headers.user; + let user = headers.user; 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?; + 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, + nt: Notify<'_>, +) -> EmptyResult { + let data: PasswordOrOtpData = data.into_inner(); + let mut user = headers.user; + + data.validate(&user, true, &conn).await?; + + 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)] pub enum CipherDeleteOptions { SoftSingle, @@ -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; +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; - member.delete(&conn).await - } + 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 09/18] 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 10/18] 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 11/18] 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 12/18] 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 13/18] 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 14/18] 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 15/18] 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 16/18] 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 17/18] 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 18/18] 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 => (),