Browse Source

Add global password generator policy setting

Adds PASSWORD_GENERATOR_POLICY, an optional server-wide password
generator policy in the Bitwarden policy data format. It is served
in the sync response as a synthetic policy next to the organization
policies, so the clients enforce it for all users without requiring
an organization. Follows the FAKE_SSO_IDENTIFIER pattern for the
synthetic organization id.
pull/7916/head
Justin Troebinger 2 weeks ago
parent
commit
47c91074d7
  1. 4
      .env.template
  2. 9
      src/api/core/ciphers.rs
  3. 26
      src/config.rs
  4. 12
      src/db/models/org_policy.rs

4
.env.template

@ -244,6 +244,10 @@
## This setting applies globally to all users. ## This setting applies globally to all users.
# INCOMPLETE_2FA_TIME_LIMIT=3 # 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 ## Disable icon downloading
## Set to true to disable icon downloading in the internal icon service. ## 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 ## This still serves existing icons from $ICON_CACHE_FOLDER, without generating any external

9
src/api/core/ciphers.rs

@ -159,15 +159,20 @@ async fn sync(data: SyncData, headers: Headers, client_version: Option<ClientVer
let sends_json: Vec<Value> = let sends_json: Vec<Value> =
Send::find_by_user(&headers.user.uuid, &conn).await.iter().map(Send::to_json).collect(); Send::find_by_user(&headers.user.uuid, &conn).await.iter().map(Send::to_json).collect();
let policies_json: Vec<Value> = let mut policies_json: Vec<Value> =
OrgPolicy::find_confirmed_by_user(&headers.user.uuid, &conn).await.iter().map(OrgPolicy::to_json).collect(); OrgPolicy::find_confirmed_by_user(&headers.user.uuid, &conn).await.iter().map(OrgPolicy::to_json).collect();
let policies_new_json: Vec<Value> = OrgPolicy::find_accepted_and_confirmed_by_user(&headers.user.uuid, &conn) let mut policies_new_json: Vec<Value> = OrgPolicy::find_accepted_and_confirmed_by_user(&headers.user.uuid, &conn)
.await .await
.iter() .iter()
.map(OrgPolicy::to_json) .map(OrgPolicy::to_json)
.collect(); .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 { let domains_json = if data.exclude_domains {
Value::Null Value::Null
} else { } else {

26
src/config.rs

@ -628,6 +628,11 @@ make_config! {
/// This setting applies globally to all users. /// This setting applies globally to all users.
incomplete_2fa_time_limit: u16, true, def, 3; 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. /// 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 /// 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 /// network requests. $ICON_CACHE_TTL must also be set to 0; otherwise, the existing icons
@ -1157,6 +1162,8 @@ fn validate_config(cfg: &ConfigItems, on_update: bool) -> Result<(), Error> {
validate_sso_master_password_policy(cfg.sso_master_password_policy.as_ref())?; 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._enable_yubico {
if cfg.yubico_client_id.is_some() != cfg.yubico_secret_key.is_some() { 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") err!("Both `YUBICO_CLIENT_ID` and `YUBICO_SECRET_KEY` must be set for Yubikey OTP support")
@ -1400,6 +1407,21 @@ fn validate_sso_master_password_policy(
} }
} }
fn validate_password_generator_policy(
password_generator_policy: Option<&String>,
) -> Result<Option<serde_json::Value>, Error> {
let policy = password_generator_policy.as_ref().map(|pgp| serde_json::from_str::<serde_json::Value>(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. /// Extracts an RFC 6454 web origin from a URL.
fn extract_url_origin(url: &str) -> String { fn extract_url_origin(url: &str) -> String {
match Url::parse(url) { match Url::parse(url) {
@ -1770,6 +1792,10 @@ impl Config {
validate_sso_master_password_policy(self.sso_master_password_policy().as_ref()).ok().flatten() validate_sso_master_password_policy(self.sso_master_password_policy().as_ref()).ok().flatten()
} }
pub fn password_generator_policy_value(&self) -> Option<serde_json::Value> {
validate_password_generator_policy(self.password_generator_policy().as_ref()).ok().flatten()
}
pub fn sso_scopes_vec(&self) -> Vec<String> { pub fn sso_scopes_vec(&self) -> Vec<String> {
self.sso_scopes().split_whitespace().map(str::to_owned).collect() self.sso_scopes().split_whitespace().map(str::to_owned).collect()
} }

12
src/db/models/org_policy.rs

@ -29,6 +29,9 @@ pub struct OrgPolicy {
pub revision_date: NaiveDateTime, 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 // https://github.com/bitwarden/server/blob/9ebe16587175b1c0e9208f84397bb75d0d595510/src/Core/AdminConsole/Enums/PolicyType.cs
#[derive(Copy, Clone, Eq, PartialEq, num_derive::FromPrimitive)] #[derive(Copy, Clone, Eq, PartialEq, num_derive::FromPrimitive)]
pub enum OrgPolicyType { pub enum OrgPolicyType {
@ -120,6 +123,15 @@ impl OrgPolicy {
policy 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<Value> {
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 /// Database methods

Loading…
Cancel
Save