From 7dff07c3182796e27406627e1482c15b6394d34a Mon Sep 17 00:00:00 2001 From: BlackDex Date: Sat, 21 Mar 2026 16:57:38 +0100 Subject: [PATCH] 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 --- src/api/core/mod.rs | 31 +++++++++-------- src/config.rs | 81 +++++++++++++++++++++++++-------------------- src/util.rs | 31 ++++++++++------- 3 files changed, 80 insertions(+), 63 deletions(-) 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..6a90c3c3 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,19 @@ 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 configured_flags = + parse_experimental_client_feature_flags(&cfg.experimental_client_feature_flags, FeatureFlagFilter::Unfiltered); + let invalid_flags: Vec<&str> = + configured_flags.keys().map(String::as_str).filter(|flag| !SUPPORTED_FEATURE_FLAGS.contains(flag)).collect(); 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 +1460,34 @@ 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", + // 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 +1501,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 +1537,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/util.rs b/src/util.rs index 6da1c3df..a9bbdbc8 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,25 @@ pub fn convert_json_key_lcase_first(src_json: Value) -> Value { } } +pub enum FeatureFlagFilter { + Unfiltered, + ValidOnly, +} + /// 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), }) + .map(|flag| (flag.to_owned(), true)) .collect() }