use chrono::{NaiveDateTime, Utc}; use derive_more::{AsRef, From}; use diesel::prelude::*; use serde::Deserialize; use serde_json::Value; use crate::{ CONFIG, api::{EmptyResult, core::two_factor}, db::{ DbConn, schema::{org_policies, users_organizations}, }, error::MapResult, util::format_date, }; use super::{Membership, MembershipId, MembershipStatus, MembershipType, OrganizationId, TwoFactor, UserId}; #[derive(Identifiable, Queryable, Insertable, AsChangeset)] #[diesel(table_name = org_policies)] #[diesel(primary_key(uuid))] pub struct OrgPolicy { pub uuid: OrgPolicyId, pub org_uuid: OrganizationId, pub atype: i32, pub enabled: bool, pub data: String, pub revision_date: NaiveDateTime, } // https://github.com/bitwarden/server/blob/9ebe16587175b1c0e9208f84397bb75d0d595510/src/Core/AdminConsole/Enums/PolicyType.cs #[derive(Copy, Clone, Eq, PartialEq, num_derive::FromPrimitive)] pub enum OrgPolicyType { TwoFactorAuthentication = 0, MasterPassword = 1, PasswordGenerator = 2, SingleOrg = 3, // RequireSso = 4, // Not supported PersonalOwnership = 5, DisableSend = 6, SendOptions = 7, ResetPassword = 8, // MaximumVaultTimeout = 9, // Not supported (Not AGPLv3 Licensed) // DisablePersonalVaultExport = 10, // Not supported (Not AGPLv3 Licensed) // ActivateAutofill = 11, // AutomaticAppLogIn = 12, // FreeFamiliesSponsorshipPolicy = 13, RemoveUnlockWithPin = 14, RestrictedItemTypes = 15, UriMatchDefaults = 16, AutotypeDefaultSetting = 17, AutomaticUserConfirmation = 18, // 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 #[derive(Deserialize)] #[serde(rename_all = "camelCase")] pub struct SendOptionsPolicyData { #[serde(rename = "disableHideEmail", alias = "DisableHideEmail")] pub disable_hide_email: bool, } // https://github.com/bitwarden/server/blob/9ebe16587175b1c0e9208f84397bb75d0d595510/src/Core/AdminConsole/Models/Data/Organizations/Policies/ResetPasswordDataModel.cs #[derive(Deserialize)] #[serde(rename_all = "camelCase")] pub struct ResetPasswordDataModel { #[serde(rename = "autoEnrollEnabled", alias = "AutoEnrollEnabled")] 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 { Self { uuid: OrgPolicyId(crate::util::get_uuid()), org_uuid, atype: atype as i32, enabled, data, revision_date: Utc::now().naive_utc(), } } pub fn has_type(&self, policy_type: OrgPolicyType) -> bool { self.atype == policy_type as i32 } pub fn to_json(&self) -> Value { let data_json: Value = serde_json::from_str(&self.data).unwrap_or(Value::Null); let mut policy = json!({ "id": self.uuid, "organizationId": self.org_uuid, "type": self.atype, "data": data_json, "enabled": self.enabled, "revisionDate": format_date(&self.revision_date), "object": "policy", }); // Upstream adds this key/value // Allow enabling Single Org policy when the organization has claimed domains. // See: (https://github.com/bitwarden/server/pull/5565) // We return the same to prevent possible issues if self.atype == 8i32 { policy["canToggleState"] = json!(true); } policy } } /// Database methods impl OrgPolicy { pub async fn save(&mut self, conn: &DbConn) -> EmptyResult { self.revision_date = Utc::now().naive_utc(); db_run! { conn: sqlite, mysql { match diesel::replace_into(org_policies::table) .values(&*self) .execute(conn) { Ok(_) => Ok(()), // Record already exists and causes a Foreign Key Violation because replace_into() wants to delete the record first. Err(diesel::result::Error::DatabaseError(diesel::result::DatabaseErrorKind::ForeignKeyViolation, _)) => { diesel::update(org_policies::table) .filter(org_policies::uuid.eq(&self.uuid)) .set(&*self) .execute(conn) .map_res("Error saving org_policy") } Err(e) => Err(e.into()), }.map_res("Error saving org_policy") } postgresql { // We need to make sure we're not going to violate the unique constraint on org_uuid and atype. // This happens automatically on other DBMS backends due to replace_into(). PostgreSQL does // not support multiple constraints on ON CONFLICT clauses. let _: () = diesel::delete( org_policies::table .filter(org_policies::org_uuid.eq(&self.org_uuid)) .filter(org_policies::atype.eq(&self.atype)), ) .execute(conn) .map_res("Error deleting org_policy for insert")?; diesel::insert_into(org_policies::table) .values(&*self) .on_conflict(org_policies::uuid) .do_update() .set(&*self) .execute(conn) .map_res("Error saving org_policy") } } } pub async fn delete(self, conn: &DbConn) -> EmptyResult { conn.run(move |conn| { diesel::delete(org_policies::table.filter(org_policies::uuid.eq(self.uuid))) .execute(conn) .map_res("Error deleting org_policy") }) .await } pub async fn find_by_org(org_uuid: &OrganizationId, conn: &DbConn) -> Vec { conn.run(move |conn| { org_policies::table .filter(org_policies::org_uuid.eq(org_uuid)) .load::(conn) .expect("Error loading org_policy") }) .await } pub async fn find_confirmed_by_user(user_uuid: &UserId, conn: &DbConn) -> Vec { conn.run(move |conn| { org_policies::table .inner_join( users_organizations::table.on(users_organizations::org_uuid .eq(org_policies::org_uuid) .and(users_organizations::user_uuid.eq(user_uuid))), ) .filter(users_organizations::status.eq(MembershipStatus::Confirmed as i32)) .select(org_policies::all_columns) .load::(conn) .expect("Error loading org_policy") }) .await } pub async fn find_accepted_and_confirmed_by_user(user_uuid: &UserId, conn: &DbConn) -> Vec { conn.run(move |conn| { org_policies::table .inner_join( users_organizations::table.on(users_organizations::org_uuid .eq(org_policies::org_uuid) .and(users_organizations::user_uuid.eq(user_uuid))), ) .filter( users_organizations::status .eq(MembershipStatus::Accepted as i32) .or(users_organizations::status.eq(MembershipStatus::Confirmed as i32)), ) .select(org_policies::all_columns) .load::(conn) .expect("Error loading org_policy") }) .await } pub async fn find_by_org_and_type( org_uuid: &OrganizationId, policy_type: OrgPolicyType, conn: &DbConn, ) -> Option { conn.run(move |conn| { org_policies::table .filter(org_policies::org_uuid.eq(org_uuid)) .filter(org_policies::atype.eq(policy_type as i32)) .first::(conn) .ok() }) .await } pub async fn delete_all_by_organization(org_uuid: &OrganizationId, conn: &DbConn) -> EmptyResult { conn.run(move |conn| { diesel::delete(org_policies::table.filter(org_policies::org_uuid.eq(org_uuid))) .execute(conn) .map_res("Error deleting org_policy") }) .await } pub async fn find_accepted_and_confirmed_by_user_and_active_policy( user_uuid: &UserId, policy_type: OrgPolicyType, conn: &DbConn, ) -> Vec { conn.run(move |conn| { org_policies::table .inner_join( users_organizations::table.on(users_organizations::org_uuid .eq(org_policies::org_uuid) .and(users_organizations::user_uuid.eq(user_uuid))), ) .filter(users_organizations::status.eq(MembershipStatus::Accepted as i32)) .or_filter(users_organizations::status.eq(MembershipStatus::Confirmed as i32)) .filter(org_policies::atype.eq(policy_type as i32)) .filter(org_policies::enabled.eq(true)) .select(org_policies::all_columns) .load::(conn) .expect("Error loading org_policy") }) .await } pub async fn find_confirmed_by_user_and_active_policy( user_uuid: &UserId, policy_type: OrgPolicyType, conn: &DbConn, ) -> Vec { conn.run(move |conn| { org_policies::table .inner_join( users_organizations::table.on(users_organizations::org_uuid .eq(org_policies::org_uuid) .and(users_organizations::user_uuid.eq(user_uuid))), ) .filter(users_organizations::status.eq(MembershipStatus::Confirmed as i32)) .filter(org_policies::atype.eq(policy_type as i32)) .filter(org_policies::enabled.eq(true)) .select(org_policies::all_columns) .load::(conn) .expect("Error loading org_policy") }) .await } /// Returns true if the user belongs to an org that has enabled the specified policy type, /// and the user is not an owner or admin of that org. This is only useful for checking /// applicability of policy types that have these particular semantics. pub async fn is_applicable_to_user( user_uuid: &UserId, policy_type: OrgPolicyType, exclude_org_uuid: Option<&OrganizationId>, conn: &DbConn, ) -> bool { for policy in OrgPolicy::find_accepted_and_confirmed_by_user_and_active_policy(user_uuid, policy_type, conn).await { // Check if we need to skip this organization. if exclude_org_uuid.is_some() && *exclude_org_uuid.unwrap() == policy.org_uuid { continue; } if let Some(user) = Membership::find_confirmed_by_user_and_org(user_uuid, &policy.org_uuid, conn).await && user.atype < MembershipType::Admin { return true; } } false } /// Whether the policy is stored as enabled for this organization, regardless of any config option. pub async fn is_enabled(org_uuid: &OrganizationId, policy_type: OrgPolicyType, conn: &DbConn) -> bool { Self::find_by_org_and_type(org_uuid, policy_type, conn).await.is_some_and(|p| p.enabled) } /// Requires both the server-wide option and this organization's policy. pub async fn is_auto_confirm_enabled(org_uuid: &OrganizationId, conn: &DbConn) -> bool { CONFIG.org_auto_confirm_enabled() && Self::is_enabled(org_uuid, OrgPolicyType::AutomaticUserConfirmation, conn).await } pub async fn check_user_allowed(m: &Membership, action: &str, conn: &DbConn) -> EmptyResult { if m.atype < MembershipType::Admin && m.status > (MembershipStatus::Invited as i32) { // Enforce TwoFactor/TwoStep login if let Some(p) = Self::find_by_org_and_type(&m.org_uuid, OrgPolicyType::TwoFactorAuthentication, conn).await && p.enabled && TwoFactor::find_by_user(&m.user_uuid, conn).await.is_empty() { if CONFIG.email_2fa_auto_fallback() { two_factor::email::find_and_activate_email_2fa(&m.user_uuid, conn).await?; } else { err!(format!("Cannot {} because 2FA is required (membership {})", action, m.uuid)); } } // Check if the user is part of another Organization with SingleOrg activated if Self::is_applicable_to_user(&m.user_uuid, OrgPolicyType::SingleOrg, Some(&m.org_uuid), conn).await { err!(format!( "Cannot {} because another organization policy forbids it (membership {})", action, m.uuid )); } if let Some(p) = Self::find_by_org_and_type(&m.org_uuid, OrgPolicyType::SingleOrg, conn).await && p.enabled && Membership::count_accepted_and_confirmed_by_user(&m.user_uuid, &m.org_uuid, conn).await > 0 { err!(format!( "Cannot {} because the organization policy forbids being part of other organization (membership {})", action, m.uuid )); } } // Stricter than the SingleOrg block above: this policy exempts no role and no status. // https://github.com/bitwarden/server/blob/b3d1eb9a7854322f106efa55c191c1a4da9f8645/src/Core/AdminConsole/OrganizationFeatures/Policies/Enforcement/AutoConfirm/AutomaticUserConfirmationPolicyEnforcementHandler.cs if AutoConfirmRequirement::for_user(&m.user_uuid, conn).await.forbids_membership_outside(&m.org_uuid) { err!(format!( "Cannot {} because another organization confirms its members automatically and forbids other memberships (membership {})", action, m.uuid )); } if Self::is_auto_confirm_enabled(&m.org_uuid, conn).await && Membership::count_accepted_confirmed_and_revoked_by_user(&m.user_uuid, &m.org_uuid, conn).await > 0 { err!(format!( "Cannot {} because the organization confirms its members automatically and forbids being part of other organizations (membership {})", action, m.uuid )); } Ok(()) } pub async fn org_is_reset_password_auto_enroll(org_uuid: &OrganizationId, conn: &DbConn) -> bool { // Account recovery depends on outbound mail. When SMTP is disabled, treat the // auto-enroll policy as inactive so invites/registration are not forced to // supply a reset-password key (see check_reset_password_applicable). if !CONFIG.mail_enabled() { return false; } match OrgPolicy::find_by_org_and_type(org_uuid, OrgPolicyType::ResetPassword, conn).await { Some(policy) => match serde_json::from_str::(&policy.data) { Ok(opts) => { return policy.enabled && opts.auto_enroll_enabled; } _ => error!("Failed to deserialize ResetPasswordDataModel: {}", policy.data), }, None => return false, } false } /// Returns true if the user belongs to an org that has enabled the `DisableHideEmail` /// option of the `Send Options` policy, and the user is not an owner or admin of that org. pub async fn is_hide_email_disabled(user_uuid: &UserId, conn: &DbConn) -> bool { for policy in OrgPolicy::find_confirmed_by_user_and_active_policy(user_uuid, OrgPolicyType::SendOptions, conn).await { if let Some(user) = Membership::find_confirmed_by_user_and_org(user_uuid, &policy.org_uuid, conn).await && user.atype < MembershipType::Admin { match serde_json::from_str::(&policy.data) { Ok(opts) => { if opts.disable_hide_email { return true; } } _ => error!("Failed to deserialize SendOptionsPolicyData: {}", policy.data), } } } false } pub async fn is_enabled_for_member(member_uuid: &MembershipId, policy_type: OrgPolicyType, conn: &DbConn) -> bool { if let Some(member) = Membership::find_by_uuid(member_uuid, conn).await && let Some(policy) = OrgPolicy::find_by_org_and_type(&member.org_uuid, policy_type, conn).await { return policy.enabled; } false } } #[derive(Clone, Debug, AsRef, DieselNewType, From, FromForm, PartialEq, Eq, Hash, Serialize, Deserialize)] pub struct OrgPolicyId(String); /// Active auto-confirm policy memberships. No role or status is exempt; each check below decides which /// statuses matter for its operation. /// https://github.com/bitwarden/server/blob/b3d1eb9a7854322f106efa55c191c1a4da9f8645/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/AutomaticUserConfirmationPolicyRequirement.cs pub struct AutoConfirmRequirement(Vec); impl AutoConfirmRequirement { /// Loads every role and status in organizations with this policy; empty when the server option is off. /// https://github.com/bitwarden/server/blob/b3d1eb9a7854322f106efa55c191c1a4da9f8645/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/BasePolicyRequirementFactory.cs pub async fn for_user(user_uuid: &UserId, conn: &DbConn) -> Self { if !CONFIG.org_auto_confirm_enabled() { return Self(Vec::new()); } Self( conn.run(move |conn| { org_policies::table .inner_join( users_organizations::table.on(users_organizations::org_uuid .eq(org_policies::org_uuid) .and(users_organizations::user_uuid.eq(user_uuid))), ) .filter(org_policies::atype.eq(OrgPolicyType::AutomaticUserConfirmation as i32)) .filter(org_policies::enabled.eq(true)) .select(users_organizations::all_columns) .load::(conn) .expect("Error loading memberships by org_policy") }) .await, ) } /// Any membership, including Invited and elevated roles, forbids creating another organization. pub fn forbids_creating_organization(&self) -> bool { !self.0.is_empty() } /// Any policy membership outside `org_uuid` forbids membership in `org_uuid`. pub fn forbids_membership_outside(&self, org_uuid: &OrganizationId) -> bool { self.0.iter().any(|m| &m.org_uuid != org_uuid) } /// Accepted, Confirmed, or Revoked members may neither grant nor accept emergency access; Invited is exempt. pub fn forbids_emergency_access(&self) -> bool { self.0.iter().any(Membership::counts_for_auto_confirm) } } #[cfg(test)] mod tests { use super::*; fn membership(org_uuid: &str, atype: MembershipType, status: MembershipStatus) -> Membership { let mut member = Membership::new("user".to_owned().into(), org_uuid.to_owned().into(), None); member.atype = atype as i32; member.status = status as i32; member } /// Every role and status is covered; only emergency access exempts Invited. #[test] fn auto_confirm_requirement() { let auto_confirm_org = OrganizationId::from(String::from("auto-confirm")); let other_org = OrganizationId::from(String::from("other")); let none = AutoConfirmRequirement(Vec::new()); assert!(!none.forbids_creating_organization()); assert!(!none.forbids_membership_outside(&other_org)); assert!(!none.forbids_emergency_access()); let invited_owner = AutoConfirmRequirement(vec![membership("auto-confirm", MembershipType::Owner, MembershipStatus::Invited)]); assert!(invited_owner.forbids_creating_organization()); assert!(invited_owner.forbids_membership_outside(&other_org)); assert!(!invited_owner.forbids_membership_outside(&auto_confirm_org)); assert!(!invited_owner.forbids_emergency_access()); // Revoked counts because restore has no second accept step; Invited alone does not restrict emergency access. let mut revoked_owner = membership("auto-confirm", MembershipType::Owner, MembershipStatus::Confirmed); assert!(revoked_owner.revoke()); let mut requirement = AutoConfirmRequirement(vec![revoked_owner]); assert!(requirement.forbids_creating_organization()); requirement.0.push(membership("invited-to", MembershipType::User, MembershipStatus::Invited)); assert!(requirement.forbids_emergency_access()); } }