From 863395f5e28eab5f939e341bb700320c5eb8e0d7 Mon Sep 17 00:00:00 2001 From: Tom <83423411+tom27052006@users.noreply.github.com> Date: Thu, 29 Oct 2026 22:25:23 +0100 Subject: [PATCH] Add configurable Fill Assist rules URL (#7791) --- .env.template | 5 +++ src/api/core/mod.rs | 1 + src/api/core/organizations.rs | 62 ++++++++++++++++++++++++++++++++--- src/config.rs | 19 +++++++++++ src/db/models/mod.rs | 2 +- src/db/models/org_policy.rs | 10 ++++++ 6 files changed, 94 insertions(+), 5 deletions(-) diff --git a/.env.template b/.env.template index f0695a16..66cb2f64 100644 --- a/.env.template +++ b/.env.template @@ -386,6 +386,11 @@ # HTTP_REQUEST_BLOCK_NON_GLOBAL_IPS=true ## Client Settings +## Server-wide Fill Assist rules feed base directory URL, not a JSON file. +## Defaults to https://fillassist.bitwarden.com. +## Must be a valid HTTPS URL without credentials, query parameters, fragments, or surrounding whitespace. +# FILL_ASSIST_RULES_URL=https://fillassist.bitwarden.com + ## Enable experimental feature flags for clients. ## This is a comma-separated list of flags, e.g. "flag1,flag2,flag3". ## Note that clients cache the /api/config endpoint for about 1 hour and it could take some time before they are enabled or disabled! diff --git a/src/api/core/mod.rs b/src/api/core/mod.rs index e6a184dd..b7923982 100644 --- a/src/api/core/mod.rs +++ b/src/api/core/mod.rs @@ -247,6 +247,7 @@ fn config() -> Json { "notifications": format!("{domain}/notifications"), "sso": "", "cloudRegion": null, + "fillAssistRules": CONFIG.fill_assist_rules_url(), }, // Bitwarden uses this for the self-hosted servers to indicate the default push technology "push": { diff --git a/src/api/core/organizations.rs b/src/api/core/organizations.rs index 9209d891..3328423e 100644 --- a/src/api/core/organizations.rs +++ b/src/api/core/organizations.rs @@ -16,9 +16,9 @@ use crate::{ DbConn, models::{ Cipher, CipherId, Collection, CollectionCipher, CollectionGroup, CollectionId, CollectionUser, Device, - EventType, Group, GroupId, GroupUser, Invitation, Membership, MembershipId, MembershipStatus, - MembershipType, OrgPolicy, OrgPolicyType, Organization, OrganizationApiKey, OrganizationId, TwoFactor, - TwoFactorType, User, UserId, + EventType, FillAssistPolicyData, Group, GroupId, GroupUser, Invitation, Membership, MembershipId, + MembershipStatus, MembershipType, OrgPolicy, OrgPolicyType, Organization, OrganizationApiKey, + OrganizationId, TwoFactor, TwoFactorType, User, UserId, }, }, mail, @@ -2048,6 +2048,29 @@ async fn put_policy( err!("Invalid or unsupported policy type") }; + let current_policy = OrgPolicy::find_by_org_and_type(&org_id, pol_type_enum, &conn).await; + let is_currently_enabled = current_policy.as_ref().is_some_and(|policy| policy.enabled); + + // Fill Assist depends on Single Organization. Match Bitwarden's dependency checks by only checking when the + // policy changes state. + if pol_type_enum == OrgPolicyType::FillAssist && data.enabled && !is_currently_enabled { + let single_org_policy_enabled = + OrgPolicy::find_by_org_and_type(&org_id, OrgPolicyType::SingleOrg, &conn).await.is_some_and(|p| p.enabled); + + if !single_org_policy_enabled { + err!("Turn on the Single organization policy because it is required for the Activate fill assist policy.") + } + } + + if pol_type_enum == OrgPolicyType::SingleOrg && !data.enabled && is_currently_enabled { + let fill_assist_policy_enabled = + OrgPolicy::find_by_org_and_type(&org_id, OrgPolicyType::FillAssist, &conn).await.is_some_and(|p| p.enabled); + + if fill_assist_policy_enabled { + err!("Turn off the Activate fill assist policy because it requires the Single organization policy.") + } + } + // Bitwarden only allows the Reset Password policy when Single Org policy is enabled // Vaultwarden encouraged to use multiple orgs instead of groups because groups were not available in the past // Now that groups are available we can enforce this option when wanted. @@ -2080,6 +2103,37 @@ async fn put_policy( } } + if pol_type_enum == OrgPolicyType::FillAssist { + let fill_assist_data = match data.data.as_ref() { + None | Some(Value::Null) => FillAssistPolicyData { + rules_url: None, + }, + Some(data) => { + let Ok(data) = serde_json::from_value::(data.clone()) else { + err!("Invalid data for FillAssist policy: rulesUrl has an invalid value.") + }; + data + } + }; + + if data.enabled { + let Some(rules_url) = fill_assist_data.rules_url.filter(|url| !url.trim().is_empty()) else { + err!("The RulesUrl field is required.") + }; + let Ok(rules_url) = url::Url::parse(&rules_url) else { + err!("The RulesUrl field is not a valid fully-qualified http, https, or ftp URL.") + }; + + if !rules_url.has_host() || !matches!(rules_url.scheme(), "http" | "https" | "ftp") { + err!("The RulesUrl field is not a valid fully-qualified http, https, or ftp URL.") + } + + if rules_url.scheme() != "https" { + err!("RulesUrl must use HTTPS.") + } + } + } + // When enabling the TwoFactorAuthentication policy, revoke all members that do not have 2FA if pol_type_enum == OrgPolicyType::TwoFactorAuthentication && data.enabled { two_factor::enforce_2fa_policy_for_org( @@ -2127,7 +2181,7 @@ async fn put_policy( } } - let mut policy = match OrgPolicy::find_by_org_and_type(&org_id, pol_type_enum, &conn).await { + let mut policy = match current_policy { Some(p) => p, None => OrgPolicy::new(org_id.clone(), pol_type_enum, false, "{}".to_owned()), }; diff --git a/src/config.rs b/src/config.rs index 54b3d056..35657d2c 100644 --- a/src/config.rs +++ b/src/config.rs @@ -603,6 +603,9 @@ make_config! { /// This setting applies globally to all users. To control this on a per-org basis instead, use the "Disable Send" org policy. sends_allowed: bool, true, def, true; + /// Fill Assist Rules URL |> HTTPS URL for the server-wide Fill Assist rules feed base directory. + fill_assist_rules_url: String, true, def, "https://fillassist.bitwarden.com".to_owned(); + /// HIBP Api Key |> HaveIBeenPwned API Key, request it here: https://haveibeenpwned.com/API/Key hibp_api_key: Pass, true, option; @@ -1023,6 +1026,21 @@ fn validate_config(cfg: &ConfigItems, on_update: bool) -> Result<(), Error> { ); } + let valid_fill_assist_rules_url = Url::parse(&cfg.fill_assist_rules_url).is_ok_and(|url| { + url.scheme() == "https" + && url.has_host() + && url.username().is_empty() + && url.password().is_none() + && url.query().is_none() + && url.fragment().is_none() + && cfg.fill_assist_rules_url.trim() == cfg.fill_assist_rules_url + }); + if !valid_fill_assist_rules_url { + err!( + "`FILL_ASSIST_RULES_URL` must be a valid HTTPS URL without credentials, query parameters, fragments, or surrounding whitespace." + ); + } + let connect_src = cfg.allowed_connect_src.to_lowercase(); for url in connect_src.split_whitespace() { if !url.starts_with("https://") || Url::parse(url).is_err() { @@ -1477,6 +1495,7 @@ pub const SUPPORTED_FEATURE_FLAGS: &[&str] = &[ // Autofill Team "undetermined-cipher-scenario-logic", "enable-basic-auth-response", + "fill-assist-targeting-rules", "ssh-agent-v2", "windows-desktop-autotype", "windows-desktop-autotype-ga", diff --git a/src/db/models/mod.rs b/src/db/models/mod.rs index 3a592b34..fda76df4 100644 --- a/src/db/models/mod.rs +++ b/src/db/models/mod.rs @@ -31,7 +31,7 @@ pub use self::event::{Event, EventType}; pub use self::favorite::Favorite; pub use self::folder::{Folder, FolderCipher, FolderId}; pub use self::group::{CollectionGroup, Group, GroupId, GroupUser}; -pub use self::org_policy::{OrgPolicy, OrgPolicyId, OrgPolicyType}; +pub use self::org_policy::{FillAssistPolicyData, OrgPolicy, OrgPolicyId, OrgPolicyType}; pub use self::organization::{ Membership, MembershipId, MembershipStatus, MembershipType, OrgApiKeyId, Organization, OrganizationApiKey, OrganizationId, diff --git a/src/db/models/org_policy.rs b/src/db/models/org_policy.rs index 31938f76..554f51fc 100644 --- a/src/db/models/org_policy.rs +++ b/src/db/models/org_policy.rs @@ -53,6 +53,8 @@ pub enum OrgPolicyType { // AutoConfirm = 18, // Not supported (not implemented yet) // BlockClaimedDomainAccountCreation = 19, // Not supported (Not AGPLv3 Licensed) OrganizationUserNotification = 20, + // SendControls = 21, // Not supported yet + FillAssist = 22, } // https://github.com/bitwarden/server/blob/9ebe16587175b1c0e9208f84397bb75d0d595510/src/Core/AdminConsole/Models/Data/Organizations/Policies/SendOptionsPolicyData.cs#L5 @@ -71,6 +73,14 @@ pub struct ResetPasswordDataModel { pub auto_enroll_enabled: bool, } +// https://github.com/bitwarden/server/blob/13bc813de8e715a71d3410f1e584e87ccdf76025/src/Core/AdminConsole/Models/Data/Organizations/Policies/FillAssistPolicyData.cs +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct FillAssistPolicyData { + #[serde(alias = "RulesUrl")] + pub rules_url: Option, +} + /// Local methods impl OrgPolicy { pub fn new(org_uuid: OrganizationId, atype: OrgPolicyType, enabled: bool, data: String) -> Self {