diff --git a/.env.template b/.env.template index 2d5c5d8e..3f2062c8 100644 --- a/.env.template +++ b/.env.template @@ -244,6 +244,10 @@ ## This setting applies globally to all users. # INCOMPLETE_2FA_TIME_LIMIT=3 +## Optional global password generator policy, enforced for all users as if set by an +## organization. Uses the Bitwarden policy data format. +# PASSWORD_GENERATOR_POLICY='{"overridePasswordType":null,"minLength":16,"useUpper":true,"useLower":true,"useNumbers":true,"useSpecial":false,"minNumbers":1,"minSpecial":1,"minNumberWords":3,"capitalize":false,"includeNumber":false}' + ## Disable icon downloading ## Set to true to disable icon downloading in the internal icon service. ## This still serves existing icons from $ICON_CACHE_FOLDER, without generating any external diff --git a/src/api/core/ciphers.rs b/src/api/core/ciphers.rs index 97f5fd49..47c1c604 100644 --- a/src/api/core/ciphers.rs +++ b/src/api/core/ciphers.rs @@ -159,15 +159,20 @@ async fn sync(data: SyncData, headers: Headers, client_version: Option = Send::find_by_user(&headers.user.uuid, &conn).await.iter().map(Send::to_json).collect(); - let policies_json: Vec = + let mut policies_json: Vec = OrgPolicy::find_confirmed_by_user(&headers.user.uuid, &conn).await.iter().map(OrgPolicy::to_json).collect(); - let policies_new_json: Vec = OrgPolicy::find_accepted_and_confirmed_by_user(&headers.user.uuid, &conn) + let mut policies_new_json: Vec = OrgPolicy::find_accepted_and_confirmed_by_user(&headers.user.uuid, &conn) .await .iter() .map(OrgPolicy::to_json) .collect(); + if let Some(global_policy) = OrgPolicy::global_password_generator_policy() { + policies_json.push(global_policy.clone()); + policies_new_json.push(global_policy); + } + let domains_json = if data.exclude_domains { Value::Null } else { diff --git a/src/config.rs b/src/config.rs index 1d75d342..9b260039 100644 --- a/src/config.rs +++ b/src/config.rs @@ -628,6 +628,11 @@ make_config! { /// This setting applies globally to all users. incomplete_2fa_time_limit: u16, true, def, 3; + /// Password generator policy |> Optional global password generator policy, enforced for all + /// users as if set by an organization. Uses the Bitwarden policy data format. Ex format: + /// '{"overridePasswordType":null,"minLength":16,"useUpper":true,"useLower":true,"useNumbers":true,"useSpecial":false,"minNumbers":1,"minSpecial":1,"minNumberWords":3,"capitalize":false,"includeNumber":false}' + password_generator_policy: String, true, option; + /// Disable icon downloads |> Set to true to disable icon downloading in the internal icon service. /// This still serves existing icons from $ICON_CACHE_FOLDER, without generating any external /// network requests. $ICON_CACHE_TTL must also be set to 0; otherwise, the existing icons @@ -1161,6 +1166,8 @@ fn validate_config(cfg: &ConfigItems, on_update: bool) -> Result<(), Error> { validate_sso_master_password_policy(cfg.sso_master_password_policy.as_ref())?; } + validate_password_generator_policy(cfg.password_generator_policy.as_ref())?; + if cfg._enable_yubico { if cfg.yubico_client_id.is_some() != cfg.yubico_secret_key.is_some() { err!("Both `YUBICO_CLIENT_ID` and `YUBICO_SECRET_KEY` must be set for Yubikey OTP support") @@ -1404,6 +1411,21 @@ fn validate_sso_master_password_policy( } } +fn validate_password_generator_policy( + password_generator_policy: Option<&String>, +) -> Result, Error> { + let policy = password_generator_policy.as_ref().map(|pgp| serde_json::from_str::(pgp)); + + match policy { + None => Ok(None), + Some(Ok(jsobject @ serde_json::Value::Object(_))) => Ok(Some(jsobject)), + Some(Ok(_)) => err!("Invalid password_generator_policy: parsed value is not a JSON object"), + Some(Err(error)) => { + err!(format!("Invalid password_generator_policy ({error}), Ensure that it's correctly escaped with ''")) + } + } +} + /// Extracts an RFC 6454 web origin from a URL. fn extract_url_origin(url: &str) -> String { match Url::parse(url) { @@ -1774,6 +1796,10 @@ impl Config { validate_sso_master_password_policy(self.sso_master_password_policy().as_ref()).ok().flatten() } + pub fn password_generator_policy_value(&self) -> Option { + validate_password_generator_policy(self.password_generator_policy().as_ref()).ok().flatten() + } + pub fn sso_scopes_vec(&self) -> Vec { self.sso_scopes().split_whitespace().map(str::to_owned).collect() } diff --git a/src/db/models/org_policy.rs b/src/db/models/org_policy.rs index 554f51fc..df8f775f 100644 --- a/src/db/models/org_policy.rs +++ b/src/db/models/org_policy.rs @@ -29,6 +29,9 @@ pub struct OrgPolicy { pub revision_date: NaiveDateTime, } +// Fake organization id for the synthetic server-wide policy, analogous to FAKE_SSO_IDENTIFIER +pub static FAKE_GLOBAL_POLICY_ORG_ID: &str = "00000000-01DC-01DC-01DC-000000000001"; + // https://github.com/bitwarden/server/blob/9ebe16587175b1c0e9208f84397bb75d0d595510/src/Core/AdminConsole/Enums/PolicyType.cs #[derive(Copy, Clone, Eq, PartialEq, num_derive::FromPrimitive)] pub enum OrgPolicyType { @@ -120,6 +123,15 @@ impl OrgPolicy { policy } + + // Synthetic policy built from the PASSWORD_GENERATOR_POLICY setting. It is served alongside + // the organization policies so the clients enforce it without requiring an organization. + pub fn global_password_generator_policy() -> Option { + CONFIG.password_generator_policy_value().map(|data| { + OrgPolicy::new(FAKE_GLOBAL_POLICY_ORG_ID.into(), OrgPolicyType::PasswordGenerator, true, data.to_string()) + .to_json() + }) + } } /// Database methods