Browse Source

Merge 2a335ad3b4 into 904b80115d

pull/7915/merge
bonderaustria 1 week ago
committed by GitHub
parent
commit
5cd1766f5e
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 5
      .env.template
  2. 9
      src/api/core/ciphers.rs
  3. 19
      src/api/core/organizations.rs
  4. 11
      src/api/mod.rs
  5. 28
      src/config.rs
  6. 13
      src/db/models/org_policy.rs

5
.env.template

@ -244,6 +244,11 @@
## 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 master password policy, enforced for all users in addition to any
## organization policies. Uses the same JSON format as SSO_MASTER_PASSWORD_POLICY.
## Note that the Bitwarden clients do not allow a minimum length below 12 characters.
# MASTER_PASSWORD_POLICY='{"enforceOnLogin":false,"minComplexity":3,"minLength":12,"requireLower":false,"requireNumbers":false,"requireSpecial":false,"requireUpper":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_master_password_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 {

19
src/api/core/organizations.rs

@ -2000,8 +2000,8 @@ async fn list_policies_token(org_id: OrganizationId, token: &str, conn: DbConn)
// Called during the SSO enrollment return the default policy // Called during the SSO enrollment return the default policy
#[get("/organizations/00000000-01DC-01DC-01DC-000000000000/policies/master-password", rank = 1)] #[get("/organizations/00000000-01DC-01DC-01DC-000000000000/policies/master-password", rank = 1)]
fn get_dummy_master_password_policy() -> JsonResult { fn get_dummy_master_password_policy() -> JsonResult {
let (enabled, data) = match CONFIG.sso_master_password_policy_value() { let (enabled, data) = match config_master_password_policy() {
Some(policy) if CONFIG.sso_enabled() => (true, policy.to_string()), Some(policy) => (true, policy.to_string()),
_ => (false, "null".to_owned()), _ => (false, "null".to_owned()),
}; };
let policy = OrgPolicy::new(FAKE_SSO_IDENTIFIER.into(), OrgPolicyType::MasterPassword, enabled, data); let policy = OrgPolicy::new(FAKE_SSO_IDENTIFIER.into(), OrgPolicyType::MasterPassword, enabled, data);
@ -2013,8 +2013,8 @@ fn get_dummy_master_password_policy() -> JsonResult {
async fn get_master_password_policy(org_id: OrganizationId, _headers: OrgMemberHeaders, conn: DbConn) -> JsonResult { async fn get_master_password_policy(org_id: OrganizationId, _headers: OrgMemberHeaders, conn: DbConn) -> JsonResult {
let policy = let policy =
OrgPolicy::find_by_org_and_type(&org_id, OrgPolicyType::MasterPassword, &conn).await.unwrap_or_else(|| { 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() { let (enabled, data) = match config_master_password_policy() {
Some(policy) if CONFIG.sso_enabled() => (true, policy.to_string()), Some(policy) => (true, policy.to_string()),
_ => (false, "null".to_owned()), _ => (false, "null".to_owned()),
}; };
@ -2024,6 +2024,17 @@ async fn get_master_password_policy(org_id: OrganizationId, _headers: OrgMemberH
Ok(Json(policy.to_json())) Ok(Json(policy.to_json()))
} }
// The global policy wins over the SSO one since it applies to all users
fn config_master_password_policy() -> Option<Value> {
CONFIG.master_password_policy_value().or_else(|| {
if CONFIG.sso_enabled() {
CONFIG.sso_master_password_policy_value()
} else {
None
}
})
}
#[get("/organizations/<org_id>/policies/<pol_type>", rank = 3)] #[get("/organizations/<org_id>/policies/<pol_type>", rank = 3)]
async fn get_policy(org_id: OrganizationId, pol_type: i32, headers: AdminHeaders, conn: DbConn) -> JsonResult { async fn get_policy(org_id: OrganizationId, pol_type: i32, headers: AdminHeaders, conn: DbConn) -> JsonResult {
if org_id != headers.org_id { if org_id != headers.org_id {

11
src/api/mod.rs

@ -78,7 +78,7 @@ impl PasswordOrOtpData {
#[expect(clippy::struct_excessive_bools, reason = "Bitwarden clients expect the data in this specific format")] #[expect(clippy::struct_excessive_bools, reason = "Bitwarden clients expect the data in this specific format")]
#[derive(Debug, Default, Deserialize, Serialize)] #[derive(Debug, Default, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase", default)]
pub struct MasterPasswordPolicy { pub struct MasterPasswordPolicy {
min_complexity: Option<u8>, min_complexity: Option<u8>,
min_length: Option<u32>, min_length: Option<u32>,
@ -91,7 +91,7 @@ pub struct MasterPasswordPolicy {
// Fetch all valid Master Password Policies and merge them into one with all trues and largest numbers as one policy // Fetch all valid Master Password Policies and merge them into one with all trues and largest numbers as one policy
async fn master_password_policy(user: &User, conn: &DbConn) -> Value { async fn master_password_policy(user: &User, conn: &DbConn) -> Value {
let master_password_policies: Vec<MasterPasswordPolicy> = let mut master_password_policies: Vec<MasterPasswordPolicy> =
OrgPolicy::find_accepted_and_confirmed_by_user_and_active_policy( OrgPolicy::find_accepted_and_confirmed_by_user_and_active_policy(
&user.uuid, &user.uuid,
OrgPolicyType::MasterPassword, OrgPolicyType::MasterPassword,
@ -102,6 +102,13 @@ async fn master_password_policy(user: &User, conn: &DbConn) -> Value {
.filter_map(|p| serde_json::from_str(&p.data).ok()) .filter_map(|p| serde_json::from_str(&p.data).ok())
.collect(); .collect();
// The global policy applies to all users, on top of whatever their organizations require
if let Some(global_policy) = CONFIG.master_password_policy_value()
&& let Ok(global_policy) = serde_json::from_value::<MasterPasswordPolicy>(global_policy)
{
master_password_policies.push(global_policy);
}
let mut mpp_json = if !master_password_policies.is_empty() { let mut mpp_json = if !master_password_policies.is_empty() {
json!(master_password_policies.into_iter().reduce(|acc, policy| { json!(master_password_policies.into_iter().reduce(|acc, policy| {
MasterPasswordPolicy { MasterPasswordPolicy {

28
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;
/// Master password policy |> Optional global master password policy, enforced for all users
/// in addition to any organization policies. Uses the same JSON format as the SSO master
/// password policy. Ex format: '{"enforceOnLogin":false,"minComplexity":3,"minLength":12,"requireLower":false,"requireNumbers":false,"requireSpecial":false,"requireUpper":false}'
master_password_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
@ -1158,9 +1163,11 @@ fn validate_config(cfg: &ConfigItems, on_update: bool) -> Result<(), Error> {
validate_internal_sso_issuer_url(&cfg.sso_authority)?; validate_internal_sso_issuer_url(&cfg.sso_authority)?;
validate_internal_sso_redirect_url(&cfg.sso_callback_path)?; validate_internal_sso_redirect_url(&cfg.sso_callback_path)?;
validate_sso_master_password_policy(cfg.sso_master_password_policy.as_ref())?; validate_master_password_policy("sso_master_password_policy", cfg.sso_master_password_policy.as_ref())?;
} }
validate_master_password_policy("master_password_policy", cfg.master_password_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")
@ -1389,17 +1396,18 @@ fn validate_internal_sso_redirect_url(sso_callback_path: &String) -> Result<open
} }
} }
fn validate_sso_master_password_policy( fn validate_master_password_policy(
sso_master_password_policy: Option<&String>, setting_name: &str,
master_password_policy: Option<&String>,
) -> Result<Option<serde_json::Value>, Error> { ) -> Result<Option<serde_json::Value>, Error> {
let policy = sso_master_password_policy.as_ref().map(|mpp| serde_json::from_str::<serde_json::Value>(mpp)); let policy = master_password_policy.as_ref().map(|mpp| serde_json::from_str::<serde_json::Value>(mpp));
match policy { match policy {
None => Ok(None), None => Ok(None),
Some(Ok(jsobject @ serde_json::Value::Object(_))) => Ok(Some(jsobject)), Some(Ok(jsobject @ serde_json::Value::Object(_))) => Ok(Some(jsobject)),
Some(Ok(_)) => err!("Invalid sso_master_password_policy: parsed value is not a JSON object"), Some(Ok(_)) => err!(format!("Invalid {setting_name}: parsed value is not a JSON object")),
Some(Err(error)) => { Some(Err(error)) => {
err!(format!("Invalid sso_master_password_policy ({error}), Ensure that it's correctly escaped with ''")) err!(format!("Invalid {setting_name} ({error}), Ensure that it's correctly escaped with ''"))
} }
} }
} }
@ -1771,7 +1779,13 @@ impl Config {
} }
pub fn sso_master_password_policy_value(&self) -> Option<serde_json::Value> { pub fn sso_master_password_policy_value(&self) -> Option<serde_json::Value> {
validate_sso_master_password_policy(self.sso_master_password_policy().as_ref()).ok().flatten() validate_master_password_policy("sso_master_password_policy", self.sso_master_password_policy().as_ref())
.ok()
.flatten()
}
pub fn master_password_policy_value(&self) -> Option<serde_json::Value> {
validate_master_password_policy("master_password_policy", self.master_password_policy().as_ref()).ok().flatten()
} }
pub fn sso_scopes_vec(&self) -> Vec<String> { pub fn sso_scopes_vec(&self) -> Vec<String> {

13
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,16 @@ impl OrgPolicy {
policy policy
} }
// Synthetic policy built from the MASTER_PASSWORD_POLICY setting. Serving it alongside the
// organization policies makes the clients enforce it on password changes as well, without
// requiring an organization.
pub fn global_master_password_policy() -> Option<Value> {
CONFIG.master_password_policy_value().map(|data| {
OrgPolicy::new(FAKE_GLOBAL_POLICY_ORG_ID.into(), OrgPolicyType::MasterPassword, true, data.to_string())
.to_json()
})
}
} }
/// Database methods /// Database methods

Loading…
Cancel
Save