diff --git a/Cargo.lock b/Cargo.lock index ca88c75fcb..1d267747b4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1036,8 +1036,11 @@ dependencies = [ "bitwarden-api-api", "bitwarden-core", "bitwarden-organizations", + "bitwarden-send", + "bitwarden-vault", "chrono", "serde", + "serde_json", "serde_repr", "tsify", "uniffi", diff --git a/crates/bitwarden-auth/src/login/models/login_success_response.rs b/crates/bitwarden-auth/src/login/models/login_success_response.rs index 4f0c4268d4..a7d80064ad 100644 --- a/crates/bitwarden-auth/src/login/models/login_success_response.rs +++ b/crates/bitwarden-auth/src/login/models/login_success_response.rs @@ -10,7 +10,7 @@ use bitwarden_core::{ }, require, }; -use bitwarden_policies::MasterPasswordPolicyResponse; +use bitwarden_policies::MasterPasswordPolicy; use thiserror::Error; use crate::login::{api::response::LoginSuccessApiResponse, models::UserDecryptionOptionsResponse}; @@ -71,7 +71,7 @@ pub struct LoginSuccessResponse { /// If the user is subject to an organization master password policy, /// this field contains the requirements of that policy. - pub master_password_policy: Option, + pub master_password_policy: Option, /// The user's account cryptographic keys (wrapped with the user key). pub wrapped_account_crypto_state: Option, diff --git a/crates/bitwarden-policies/Cargo.toml b/crates/bitwarden-policies/Cargo.toml index 8e79431f0c..5738cfcc8f 100644 --- a/crates/bitwarden-policies/Cargo.toml +++ b/crates/bitwarden-policies/Cargo.toml @@ -31,8 +31,11 @@ wasm = [ bitwarden-api-api = { workspace = true } bitwarden-core = { workspace = true } bitwarden-organizations = { workspace = true } +bitwarden-send = { workspace = true } +bitwarden-vault = { workspace = true } chrono = { workspace = true } serde = { workspace = true } +serde_json = { workspace = true } serde_repr = { workspace = true } tsify = { workspace = true, optional = true } uniffi = { workspace = true, optional = true } diff --git a/crates/bitwarden-policies/src/enriched_policy.rs b/crates/bitwarden-policies/src/enriched_policy.rs new file mode 100644 index 0000000000..cfd18c0d43 --- /dev/null +++ b/crates/bitwarden-policies/src/enriched_policy.rs @@ -0,0 +1,71 @@ +//! The [`EnrichedPolicy`] model. +//! +//! An [`EnrichedPolicy`] is the strongly-typed counterpart to a +//! [`PolicyView`]: it carries the deserialized `policy.data` +//! payload (via [`EnrichedPolicyType`]) and knows how to evaluate whether it is +//! enforced against a given user. + +use std::collections::HashMap; + +use chrono::{DateTime, Utc}; +use serde::{Deserialize, Serialize}; +#[cfg(feature = "wasm")] +use tsify::Tsify; +use uuid::Uuid; + +use crate::{EnrichedPolicyType, OrganizationUserPolicyContext, PolicyView}; + +/// An organization policy - strongly typed with its data. +#[derive(Serialize, Deserialize, Debug, Clone)] +#[serde(rename_all = "camelCase")] +#[cfg_attr(feature = "uniffi", derive(uniffi::Record))] +#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))] +pub struct EnrichedPolicy { + /// The policy's unique ID. + pub id: Uuid, + /// The organization this policy belongs to. + pub organization_id: Uuid, + /// The type of policy, with its policy definition if applicable. + pub r#type: EnrichedPolicyType, + /// Whether the policy is enabled. + pub enabled: bool, + /// When the policy was last modified. + pub revision_date: Option>, +} + +impl EnrichedPolicy { + /// Builds an [`EnrichedPolicy`] from a raw [`PolicyView`], deserializing its + /// `data` payload into the strongly-typed [`EnrichedPolicyType`]. + pub fn from_policy_view(view: &PolicyView) -> EnrichedPolicy { + EnrichedPolicy { + id: view.id, + organization_id: view.organization_id, + enabled: view.enabled, + revision_date: view.revision_date, + r#type: EnrichedPolicyType::from_policy_type(view.r#type, view.data.as_deref()), + } + } + + /// Returns whether this policy is enforced against the user described by the + /// given organization contexts, applying the policy definition's exemption + /// and applicability rules. + pub fn enforced( + &self, + organization_user_policy_contexts: &HashMap, + ) -> bool { + let org = organization_user_policy_contexts.get(&self.organization_id); + let definition = self.r#type.to_policy_definition(); + + self.enabled + && match org { + Some(org) => { + org.enabled + && org.use_policies + && definition.applicable_statuses().contains(&org.status) + && !definition.exempt_roles().contains(&org.role) + && !(org.is_provider_user && definition.exempt_providers()) + } + None => true, // Unknown org: enforce by default + } + } +} diff --git a/crates/bitwarden-policies/src/enriched_policy_type.rs b/crates/bitwarden-policies/src/enriched_policy_type.rs new file mode 100644 index 0000000000..9e8f30b115 --- /dev/null +++ b/crates/bitwarden-policies/src/enriched_policy_type.rs @@ -0,0 +1,150 @@ +//! The [`EnrichedPolicyType`] enum. +//! +//! [`PolicyType`] is a bare discriminant that matches the +//! server's numeric wire format. `EnrichedPolicyType` mirrors every variant of +//! that enum but additionally carries the strongly-typed `policy.data` payload +//! (see [`policy_definitions`](crate::policy_definitions)) for the policies that have one. +//! Toggle-only policies (whose `data` is always `null`) are unit variants. + +use serde::{Deserialize, Serialize}; +#[cfg(feature = "wasm")] +use tsify::Tsify; + +use crate::{ + PolicyType, + policy_definition::{DefaultPolicyDefinition, PolicyDefinition}, + policy_definitions::{ + AutomaticAppLoginPolicy, AutomaticUserConfirmationPolicy, FreeFamiliesSponsorshipPolicy, + MasterPasswordPolicy, MaximumSessionTimeoutPolicy, OrganizationDataOwnershipPolicy, + OrganizationUserNotificationPolicy, PasswordGeneratorPolicy, RemoveUnlockWithPinPolicy, + ResetPasswordPolicy, RestrictedItemTypesPolicy, SendControlsPolicy, SendOptionsPolicy, + UriMatchDefaultPolicy, + }, +}; + +/// Helper function to parse policy data. +fn parse_data(data: Option<&str>) -> T { + match data { + Some(d) => serde_json::from_str(d).unwrap_or_default(), /* TODO: log deserialization */ + // failures + None => T::default(), + } +} + +/// A [`PolicyType`] paired with its strongly-typed +/// `policy.data` payload. +/// +/// Variants mirror [`PolicyType`] one-to-one. Policies that +/// carry configuration wrap their payload struct; toggle-only policies (whose +/// `data` is always `null`) are unit variants. +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] +#[serde(rename_all = "camelCase")] +#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))] +#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))] +pub enum EnrichedPolicyType { + /// Requires members to have two-step login enabled on their account. + TwoFactorAuthentication, + /// Sets minimum requirements for members' master passwords. + MasterPassword(MasterPasswordPolicy), + /// Sets minimum requirements for the password generator. + PasswordGenerator(PasswordGeneratorPolicy), + /// Restricts members to being part of a single organization. + SingleOrg, + /// Requires members to authenticate with single sign-on. + RequireSso, + /// Forces newly added or cloned items to be owned by the organization. + OrganizationDataOwnership(OrganizationDataOwnershipPolicy), + /// Disables the ability to create and edit Bitwarden Sends. + DisableSend, + /// Sets restrictions or defaults for Bitwarden Sends. + SendOptions(SendOptionsPolicy), + /// Allows administrators to recover member accounts. + ResetPassword(ResetPasswordPolicy), + /// Sets the maximum allowed vault timeout for members. + MaximumVaultTimeout(MaximumSessionTimeoutPolicy), + /// Disables members' ability to export their personal vault. + DisablePersonalVaultExport, + /// Activates autofill on page load in the browser extension. + ActivateAutofill, + /// Automatically logs members into apps using single sign-on. + AutomaticAppLogIn(AutomaticAppLoginPolicy), + /// Removes members' access to the free Bitwarden Families sponsorship benefit. + FreeFamiliesSponsorship, + /// Prevents members from unlocking the app with a PIN. + RemoveUnlockWithPin, + /// Restricts the item types that members can create. + RestrictedItemTypes, + /// Sets the default URI match detection strategy for autofill. + UriMatchDefaults(UriMatchDefaultPolicy), + /// Sets the default behavior for the autotype feature. + AutotypeDefaultSetting, + /// Automatically confirms invited users into the organization. + AutomaticUserConfirmation, + /// Blocks account creation for users with email addresses on claimed domains. + BlockClaimedDomainAccountCreation, + /// Displays an organization-configured banner message to members. + OrganizationUserNotification(OrganizationUserNotificationPolicy), + /// Configures Send-related behavior (disabling Sends, email visibility, + /// access controls, Send types, and deletion). + SendControls(SendControlsPolicy), +} + +impl EnrichedPolicyType { + /// Returns the [`PolicyDefinition`] trait + /// implementer for this policy type. + /// + /// Policies with custom rules return their own definition; policies without custom rules fall + /// back to a default definition. + pub fn to_policy_definition(&self) -> &dyn PolicyDefinition { + match self { + EnrichedPolicyType::MasterPassword(p) => p, + EnrichedPolicyType::PasswordGenerator(p) => p, + EnrichedPolicyType::MaximumVaultTimeout(p) => p, + EnrichedPolicyType::FreeFamiliesSponsorship => &FreeFamiliesSponsorshipPolicy, + EnrichedPolicyType::RemoveUnlockWithPin => &RemoveUnlockWithPinPolicy, + EnrichedPolicyType::RestrictedItemTypes => &RestrictedItemTypesPolicy, + EnrichedPolicyType::AutomaticUserConfirmation => &AutomaticUserConfirmationPolicy, + EnrichedPolicyType::OrganizationUserNotification(p) => p, + // Policies without custom rules use the default definition + _ => &DefaultPolicyDefinition, + } + } + + /// Constructs an `EnrichedPolicyType` from a `PolicyType` and optional JSON data. + /// + /// For policies with configuration data, the JSON string is deserialized into the + /// appropriate data structure. If deserialization fails or data is missing, the + /// policy defaults are used. + pub fn from_policy_type(policy_type: PolicyType, data: Option<&str>) -> Self { + match policy_type { + PolicyType::TwoFactorAuthentication => Self::TwoFactorAuthentication, + PolicyType::MasterPassword => Self::MasterPassword(parse_data(data)), + PolicyType::PasswordGenerator => Self::PasswordGenerator(parse_data(data)), + PolicyType::SingleOrg => Self::SingleOrg, + PolicyType::RequireSso => Self::RequireSso, + PolicyType::OrganizationDataOwnership => { + Self::OrganizationDataOwnership(parse_data(data)) + } + PolicyType::DisableSend => Self::DisableSend, + PolicyType::SendOptions => Self::SendOptions(parse_data(data)), + PolicyType::ResetPassword => Self::ResetPassword(parse_data(data)), + PolicyType::MaximumVaultTimeout => Self::MaximumVaultTimeout(parse_data(data)), + PolicyType::DisablePersonalVaultExport => Self::DisablePersonalVaultExport, + PolicyType::ActivateAutofill => Self::ActivateAutofill, + PolicyType::AutomaticAppLogIn => Self::AutomaticAppLogIn(parse_data(data)), + PolicyType::FreeFamiliesSponsorship => Self::FreeFamiliesSponsorship, + PolicyType::RemoveUnlockWithPin => Self::RemoveUnlockWithPin, + PolicyType::RestrictedItemTypes => Self::RestrictedItemTypes, + PolicyType::UriMatchDefaults => Self::UriMatchDefaults(parse_data(data)), + PolicyType::AutotypeDefaultSetting => Self::AutotypeDefaultSetting, + PolicyType::AutomaticUserConfirmation => Self::AutomaticUserConfirmation, + PolicyType::BlockClaimedDomainAccountCreation => { + Self::BlockClaimedDomainAccountCreation + } + PolicyType::OrganizationUserNotification => { + Self::OrganizationUserNotification(parse_data(data)) + } + PolicyType::SendControls => Self::SendControls(parse_data(data)), + } + } +} diff --git a/crates/bitwarden-policies/src/filter.rs b/crates/bitwarden-policies/src/filter.rs deleted file mode 100644 index db95a6f3aa..0000000000 --- a/crates/bitwarden-policies/src/filter.rs +++ /dev/null @@ -1,285 +0,0 @@ -#![allow(dead_code)] - -//! Policy filtering logic. -//! -//! Provides the [`Policy`] trait for determining which policies -//! should be enforced against the current user based on business rules. - -use std::collections::HashMap; - -use bitwarden_organizations::{OrganizationUserStatusType, OrganizationUserType}; -use uuid::Uuid; - -use crate::{ - models::{OrganizationUserPolicyContext, PolicyView}, - policy_type::PolicyType, -}; - -/// Defines the filtering behavior for a specific policy type. -/// -/// Implement this trait to control how a policy is enforced. -pub trait Policy: Send + Sync + 'static { - /// Returns the policy type this definition handles. - fn policy_type(&self) -> PolicyType; - - /// Returns the organization roles that are exempt from this policy. - /// - /// Defaults to [`Owner`](OrganizationUserType::Owner) and - /// [`Admin`](OrganizationUserType::Admin). - fn exempt_roles(&self) -> &[OrganizationUserType] { - &[OrganizationUserType::Owner, OrganizationUserType::Admin] - } - - /// Returns whether provider users are exempt from this policy. - /// - /// Defaults to `true`. - fn exempt_providers(&self) -> bool { - true - } - - /// Returns the organization membership statuses for which this policy applies. - /// - /// Defaults to [`Accepted`](OrganizationUserStatusType::Accepted) and - /// [`Confirmed`](OrganizationUserStatusType::Confirmed). - fn applicable_statuses(&self) -> &[OrganizationUserStatusType] { - &[ - OrganizationUserStatusType::Accepted, - OrganizationUserStatusType::Confirmed, - ] - } -} - -/// Extension trait that adds a [`filter`](PolicyFilter::filter) method to every [`Policy`]. -/// -/// Implemented automatically for all `T: Policy`. -pub trait PolicyFilter: Policy { - /// Filters `policies` to those that should be enforced against the user. - /// This evaluates common business rules (e.g. the policy is enabled), - /// as well as policy-specific rules according to its [`Policy`]. - /// - /// If a policy's organization is not present in `organization_user_policy_contexts`, the policy - /// is enforced by default. - fn filter<'a>( - &self, - policies: &'a [PolicyView], - organization_user_policy_contexts: &[OrganizationUserPolicyContext], - ) -> Vec<&'a PolicyView> { - let org_map: HashMap<&Uuid, &OrganizationUserPolicyContext> = - organization_user_policy_contexts - .iter() - .map(|o| (&o.id, o)) - .collect(); - - policies - .iter() - .filter(|p| p.r#type == self.policy_type()) - .filter(|p| p.enabled) - .filter(|p| { - match org_map.get(&p.organization_id) { - Some(org) => { - org.enabled - && org.use_policies - && self.applicable_statuses().contains(&org.status) - && !self.exempt_roles().contains(&org.role) - && !(org.is_provider_user && self.exempt_providers()) - } - None => true, // Unknown org: enforce by default - } - }) - .collect() - } -} - -impl PolicyFilter for T {} - -#[cfg(test)] -mod tests { - use super::*; - - fn policy_view(organization_id: Uuid, policy_type: PolicyType, enabled: bool) -> PolicyView { - PolicyView { - id: Uuid::new_v4(), - organization_id, - r#type: policy_type, - data: None, - enabled, - revision_date: Default::default(), - } - } - - fn organization( - id: Uuid, - user_type: OrganizationUserType, - status: OrganizationUserStatusType, - provider: bool, - ) -> OrganizationUserPolicyContext { - OrganizationUserPolicyContext { - id, - role: user_type, - status, - enabled: true, - use_policies: true, - is_provider_user: provider, - } - } - - struct TestPolicy; - impl Policy for TestPolicy { - fn policy_type(&self) -> PolicyType { - PolicyType::MasterPassword - } - - // These happen to match the default impl, but repeating here - // to decouple the filter tests from the default impl - fn exempt_roles(&self) -> &[OrganizationUserType] { - &[OrganizationUserType::Owner, OrganizationUserType::Admin] - } - - fn exempt_providers(&self) -> bool { - true - } - - fn applicable_statuses(&self) -> &[OrganizationUserStatusType] { - &[ - OrganizationUserStatusType::Accepted, - OrganizationUserStatusType::Confirmed, - ] - } - } - - #[test] - fn matching_policy_is_returned() { - let org_id = Uuid::new_v4(); - let policies = [policy_view(org_id, PolicyType::MasterPassword, true)]; - let orgs = [organization( - org_id, - OrganizationUserType::User, - OrganizationUserStatusType::Confirmed, - false, - )]; - - let result = TestPolicy.filter(&policies, &orgs); - assert_eq!(result.len(), 1); - } - - #[test] - fn disabled_organization_is_filtered_out() { - let org_id = Uuid::new_v4(); - let orgs = [OrganizationUserPolicyContext { - enabled: false, - id: org_id, - role: OrganizationUserType::User, - status: OrganizationUserStatusType::Confirmed, - use_policies: true, - is_provider_user: false, - }]; - let policies = [policy_view(org_id, PolicyType::MasterPassword, true)]; - - let result = TestPolicy.filter(&policies, &orgs); - assert!(result.is_empty()); - } - - #[test] - fn disabled_policy_is_filtered_out() { - let org_id = Uuid::new_v4(); - let policies = [policy_view(org_id, PolicyType::MasterPassword, false)]; - let orgs = [organization( - org_id, - OrganizationUserType::User, - OrganizationUserStatusType::Confirmed, - false, - )]; - - let result = TestPolicy.filter(&policies, &orgs); - assert!(result.is_empty()); - } - - #[test] - fn wrong_policy_type_is_filtered_out() { - let org_id = Uuid::new_v4(); - let policies = [policy_view(org_id, PolicyType::PasswordGenerator, true)]; - let orgs = [organization( - org_id, - OrganizationUserType::User, - OrganizationUserStatusType::Confirmed, - false, - )]; - - let result = TestPolicy.filter(&policies, &orgs); - assert!(result.is_empty()); - } - - #[test] - fn use_policies_false_is_filtered_out() { - let org_id = Uuid::new_v4(); - let orgs = [OrganizationUserPolicyContext { - id: org_id, - role: OrganizationUserType::User, - status: OrganizationUserStatusType::Confirmed, - enabled: true, - use_policies: false, - is_provider_user: false, - }]; - let policies = [policy_view(org_id, PolicyType::MasterPassword, true)]; - - let result = TestPolicy.filter(&policies, &orgs); - assert!(result.is_empty()); - } - - #[test] - fn exempt_role_is_filtered_out() { - let org_id = Uuid::new_v4(); - let policies = [policy_view(org_id, PolicyType::MasterPassword, true)]; - let orgs = [organization( - org_id, - OrganizationUserType::Owner, - OrganizationUserStatusType::Confirmed, - false, - )]; - - let result = TestPolicy.filter(&policies, &orgs); - assert!(result.is_empty()); - } - - #[test] - fn non_applicable_status_is_filtered_out() { - let org_id = Uuid::new_v4(); - let policies = [policy_view(org_id, PolicyType::MasterPassword, true)]; - let orgs = [organization( - org_id, - OrganizationUserType::User, - OrganizationUserStatusType::Revoked, - false, - )]; - - let result = TestPolicy.filter(&policies, &orgs); - assert!(result.is_empty()); - } - - #[test] - fn provider_is_filtered_out() { - let org_id = Uuid::new_v4(); - let policies = [policy_view(org_id, PolicyType::MasterPassword, true)]; - let orgs = [organization( - org_id, - OrganizationUserType::User, - OrganizationUserStatusType::Confirmed, - true, - )]; - - let result = TestPolicy.filter(&policies, &orgs); - assert!(result.is_empty()); - } - - #[test] - fn missing_org_enforces_by_default() { - let policies = [policy_view( - Uuid::new_v4(), - PolicyType::MasterPassword, - true, - )]; - - let result = TestPolicy.filter(&policies, &[]); - assert_eq!(result.len(), 1); - } -} diff --git a/crates/bitwarden-policies/src/lib.rs b/crates/bitwarden-policies/src/lib.rs index 69221bd892..d2e543995d 100644 --- a/crates/bitwarden-policies/src/lib.rs +++ b/crates/bitwarden-policies/src/lib.rs @@ -5,17 +5,18 @@ uniffi::setup_scaffolding!(); #[cfg(feature = "uniffi")] mod uniffi_support; -pub mod filter; -mod master_password_policy_response; +mod enriched_policy; +mod enriched_policy_type; mod models; mod policy_client; -pub mod policy_overrides; +pub mod policy_definition; +pub mod policy_definitions; mod policy_type; -mod registry; -pub use filter::Policy; -pub use master_password_policy_response::MasterPasswordPolicyResponse; +pub use enriched_policy::EnrichedPolicy; +pub use enriched_policy_type::EnrichedPolicyType; pub use models::{OrganizationUserPolicyContext, PolicyView}; pub use policy_client::{PoliciesClientExt, PolicyClient}; -pub use policy_overrides::*; +pub use policy_definition::PolicyDefinition; +pub use policy_definitions::*; pub use policy_type::PolicyType; diff --git a/crates/bitwarden-policies/src/master_password_policy_response.rs b/crates/bitwarden-policies/src/master_password_policy_response.rs deleted file mode 100644 index 3538df3577..0000000000 --- a/crates/bitwarden-policies/src/master_password_policy_response.rs +++ /dev/null @@ -1,137 +0,0 @@ -use bitwarden_api_api::models::MasterPasswordPolicyResponseModel; -use serde::{Deserialize, Serialize}; - -/// SDK domain model for master password policy requirements. -/// Defines the complexity requirements for a user's master password -/// when enforced by an organization policy. -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] -#[serde(rename_all = "camelCase")] -#[cfg_attr(feature = "uniffi", derive(uniffi::Record))] -#[cfg_attr( - feature = "wasm", - derive(tsify::Tsify), - tsify(into_wasm_abi, from_wasm_abi) -)] -pub struct MasterPasswordPolicyResponse { - /// The minimum complexity score required for the master password. - /// Complexity is calculated based on password strength metrics. - #[serde(skip_serializing_if = "Option::is_none")] - pub min_complexity: Option, - - /// The minimum length required for the master password. - #[serde(skip_serializing_if = "Option::is_none")] - pub min_length: Option, - - /// Whether the master password must contain at least one lowercase letter. - #[serde(skip_serializing_if = "Option::is_none")] - pub require_lower: Option, - - /// Whether the master password must contain at least one uppercase letter. - #[serde(skip_serializing_if = "Option::is_none")] - pub require_upper: Option, - - /// Whether the master password must contain at least one numeric digit. - #[serde(skip_serializing_if = "Option::is_none")] - pub require_numbers: Option, - - /// Whether the master password must contain at least one special character. - #[serde(skip_serializing_if = "Option::is_none")] - pub require_special: Option, - - /// Whether this policy should be enforced when the user logs in. - /// If true, the user will be required to update their master password - /// if it doesn't meet the policy requirements. - #[serde(skip_serializing_if = "Option::is_none")] - pub enforce_on_login: Option, -} - -impl From for MasterPasswordPolicyResponse { - fn from(api: MasterPasswordPolicyResponseModel) -> Self { - Self { - min_complexity: api.min_complexity, - min_length: api.min_length, - require_lower: api.require_lower, - require_upper: api.require_upper, - require_numbers: api.require_numbers, - require_special: api.require_special, - enforce_on_login: api.enforce_on_login, - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_master_password_policy_conversion_full() { - let api = MasterPasswordPolicyResponseModel { - object: Some("masterPasswordPolicy".to_string()), - min_complexity: Some(4), - min_length: Some(12), - require_lower: Some(true), - require_upper: Some(true), - require_numbers: Some(true), - require_special: Some(true), - enforce_on_login: Some(true), - }; - - let domain: MasterPasswordPolicyResponse = api.into(); - - assert_eq!(domain.min_complexity, Some(4)); - assert_eq!(domain.min_length, Some(12)); - assert_eq!(domain.require_lower, Some(true)); - assert_eq!(domain.require_upper, Some(true)); - assert_eq!(domain.require_numbers, Some(true)); - assert_eq!(domain.require_special, Some(true)); - assert_eq!(domain.enforce_on_login, Some(true)); - } - - #[test] - fn test_master_password_policy_conversion_minimal() { - let api = MasterPasswordPolicyResponseModel { - object: Some("masterPasswordPolicy".to_string()), - min_complexity: None, - min_length: Some(8), - require_lower: None, - require_upper: None, - require_numbers: None, - require_special: None, - enforce_on_login: Some(false), - }; - - let domain: MasterPasswordPolicyResponse = api.into(); - - assert_eq!(domain.min_complexity, None); - assert_eq!(domain.min_length, Some(8)); - assert_eq!(domain.require_lower, None); - assert_eq!(domain.require_upper, None); - assert_eq!(domain.require_numbers, None); - assert_eq!(domain.require_special, None); - assert_eq!(domain.enforce_on_login, Some(false)); - } - - #[test] - fn test_master_password_policy_conversion_empty() { - let api = MasterPasswordPolicyResponseModel { - object: Some("masterPasswordPolicy".to_string()), - min_complexity: None, - min_length: None, - require_lower: None, - require_upper: None, - require_numbers: None, - require_special: None, - enforce_on_login: None, - }; - - let domain: MasterPasswordPolicyResponse = api.into(); - - assert_eq!(domain.min_complexity, None); - assert_eq!(domain.min_length, None); - assert_eq!(domain.require_lower, None); - assert_eq!(domain.require_upper, None); - assert_eq!(domain.require_numbers, None); - assert_eq!(domain.require_special, None); - assert_eq!(domain.enforce_on_login, None); - } -} diff --git a/crates/bitwarden-policies/src/models.rs b/crates/bitwarden-policies/src/models.rs index c379a0bae8..d858788fca 100644 --- a/crates/bitwarden-policies/src/models.rs +++ b/crates/bitwarden-policies/src/models.rs @@ -12,7 +12,7 @@ use uuid::Uuid; use crate::policy_type::PolicyType; -/// An organization policy. +/// An organization policy - raw format that comes over the wire, with untyped json data. #[derive(Serialize, Deserialize, Debug, Clone)] #[serde(rename_all = "camelCase")] #[cfg_attr(feature = "uniffi", derive(uniffi::Record))] diff --git a/crates/bitwarden-policies/src/policy_client.rs b/crates/bitwarden-policies/src/policy_client.rs index 8a08c96123..30005d5f62 100644 --- a/crates/bitwarden-policies/src/policy_client.rs +++ b/crates/bitwarden-policies/src/policy_client.rs @@ -1,34 +1,19 @@ //! [`PolicyClient`] and its associated extension trait. +use std::collections::HashMap; + use bitwarden_core::Client; +use uuid::Uuid; #[cfg(feature = "wasm")] use wasm_bindgen::prelude::wasm_bindgen; -use crate::{ - OrganizationUserPolicyContext, PolicyType, PolicyView, policy_overrides::*, - registry::PolicyRegistry, -}; - -fn build_policy_registry() -> PolicyRegistry { - PolicyRegistry::builder() - .register(MasterPasswordPolicy) - .register(PasswordGeneratorPolicy) - .register(MaximumVaultTimeoutPolicy) - .register(FreeFamiliesSponsorshipPolicy) - .register(RemoveUnlockWithPinPolicy) - .register(RestrictedItemTypesPolicy) - .register(AutomaticUserConfirmationPolicy) - .register(OrganizationUserNotificationPolicy) - .build() -} +use crate::{EnrichedPolicy, OrganizationUserPolicyContext, PolicyType, PolicyView}; /// Client for policy domain operations. /// /// Obtained via [`PoliciesClientExt::policies`] on a [`Client`]. #[cfg_attr(feature = "wasm", wasm_bindgen)] -pub struct PolicyClient { - registry: PolicyRegistry, -} +pub struct PolicyClient {} impl Default for PolicyClient { fn default() -> Self { @@ -36,31 +21,49 @@ impl Default for PolicyClient { } } +#[cfg_attr(feature = "wasm", wasm_bindgen)] impl PolicyClient { - /// Creates a new [`PolicyClient`] with a freshly built registry. + /// Create a new PolicyClient instance. pub fn new() -> Self { - Self { - registry: build_policy_registry(), - } + Self {} } -} -#[cfg_attr(feature = "wasm", wasm_bindgen)] -impl PolicyClient { /// Filter policies of the given type for the current user. + /// Returns only those policies that should be enforced against the user. /// /// Untyped FFI path: native/WASM callers pass a runtime `policy_type` integer. - /// Delegates to the registry, falling back to default rules for unknown types. pub fn filter_by_type( + &self, + _policies: Vec, + _organization_user_policy_contexts: Vec, + _policy_type: PolicyType, + ) -> Vec { + todo!( + "Avoid breaking changes: call the new filter fn and map the result back to a PolicyView" + ) + } + + /// Filter policies of the given type for the current user. + /// Returns only those policies that should be enforced against the user. + /// + /// Includes strongly typed policy configuration data. + pub fn filter( &self, policies: Vec, organization_user_policy_contexts: Vec, policy_type: PolicyType, - ) -> Vec { - self.registry - .filter_by_type(&policies, &organization_user_policy_contexts, policy_type) - .into_iter() - .cloned() + ) -> Vec { + let org_map: HashMap = + organization_user_policy_contexts + .into_iter() + .map(|o| (o.id, o)) + .collect(); + + policies + .iter() + .filter(|p| p.r#type == policy_type) + .map(EnrichedPolicy::from_policy_view) + .filter(|ep| ep.enforced(&org_map)) .collect() } } @@ -73,7 +76,7 @@ pub trait PoliciesClientExt { impl PoliciesClientExt for Client { fn policies(&self) -> PolicyClient { - PolicyClient::new() + PolicyClient {} } } @@ -83,7 +86,7 @@ mod tests { use uuid::Uuid; use super::*; - use crate::filter::Policy; + use crate::EnrichedPolicyType; fn policy_view(organization_id: Uuid, policy_type: PolicyType, enabled: bool) -> PolicyView { PolicyView { @@ -108,7 +111,7 @@ mod tests { } #[test] - fn filter_by_type_delegates_to_registry() { + fn filter_new_returns_matching_policy_type() { let org_id = Uuid::new_v4(); let policies = vec![ policy_view(org_id, PolicyType::MasterPassword, true), @@ -117,38 +120,32 @@ mod tests { let orgs = vec![organization(org_id)]; let client = PolicyClient::new(); - let result = client.filter_by_type(policies, orgs, PolicyType::MasterPassword); + let result = client.filter(policies, orgs, PolicyType::MasterPassword); assert_eq!(result.len(), 1); - assert_eq!(result[0].r#type, PolicyType::MasterPassword); + assert!(matches!( + result[0].r#type, + EnrichedPolicyType::MasterPassword(_) + )); } #[test] - fn filter_by_type_returns_empty_for_no_match() { + fn filter_returns_empty_for_no_match() { let org_id = Uuid::new_v4(); let policies = vec![policy_view(org_id, PolicyType::MasterPassword, true)]; let orgs = vec![organization(org_id)]; let client = PolicyClient::new(); - let result = client.filter_by_type(policies, orgs, PolicyType::TwoFactorAuthentication); + let result = client.filter(policies, orgs, PolicyType::TwoFactorAuthentication); assert!(result.is_empty()); } #[test] - fn filter_by_type_uses_registered_policy_definition() { - struct NoExemptionPolicy; - impl Policy for NoExemptionPolicy { - fn policy_type(&self) -> PolicyType { - PolicyType::MasterPassword - } - fn exempt_roles(&self) -> &[OrganizationUserType] { - &[] - } - } - + fn filter_does_not_exempt_owner_from_master_password() { let org_id = Uuid::new_v4(); - // Owner — normally exempt, but NoExemptionPolicy removes the exemption + // Master Password applies to everyone (its definition has no exempt roles), + // so an Owner is still enforced. let policies = vec![policy_view(org_id, PolicyType::MasterPassword, true)]; let orgs = vec![OrganizationUserPolicyContext { id: org_id, @@ -159,11 +156,8 @@ mod tests { is_provider_user: false, }]; - let registry = PolicyRegistry::builder() - .register(NoExemptionPolicy) - .build(); - let client = PolicyClient { registry }; - let result = client.filter_by_type(policies, orgs, PolicyType::MasterPassword); + let client = PolicyClient::new(); + let result = client.filter(policies, orgs, PolicyType::MasterPassword); assert_eq!(result.len(), 1); } diff --git a/crates/bitwarden-policies/src/policy_definition.rs b/crates/bitwarden-policies/src/policy_definition.rs new file mode 100644 index 0000000000..f52da953bc --- /dev/null +++ b/crates/bitwarden-policies/src/policy_definition.rs @@ -0,0 +1,44 @@ +#![allow(dead_code)] + +//! Policy filtering logic. +//! +//! Provides the [`PolicyDefinition`] trait for determining which policies +//! should be enforced against the current user based on business rules. + +use bitwarden_organizations::{OrganizationUserStatusType, OrganizationUserType}; + +/// Defines the filtering behavior for a specific policy type. +/// +/// Implement this trait to control how a policy is enforced. +pub trait PolicyDefinition: Send + Sync + 'static { + /// Returns the organization roles that are exempt from this policy. + /// + /// Defaults to [`Owner`](OrganizationUserType::Owner) and + /// [`Admin`](OrganizationUserType::Admin). + fn exempt_roles(&self) -> &[OrganizationUserType] { + &[OrganizationUserType::Owner, OrganizationUserType::Admin] + } + + /// Returns whether provider users are exempt from this policy. + /// + /// Defaults to `true`. + fn exempt_providers(&self) -> bool { + true + } + + /// Returns the organization membership statuses for which this policy applies. + /// + /// Defaults to [`Accepted`](OrganizationUserStatusType::Accepted) and + /// [`Confirmed`](OrganizationUserStatusType::Confirmed). + fn applicable_statuses(&self) -> &[OrganizationUserStatusType] { + &[ + OrganizationUserStatusType::Accepted, + OrganizationUserStatusType::Confirmed, + ] + } +} + +/// A default policy definition that will use the default enforcement rules. +/// Used where no policy-specific definition is provided. +pub(crate) struct DefaultPolicyDefinition; +impl PolicyDefinition for DefaultPolicyDefinition {} diff --git a/crates/bitwarden-policies/src/policy_definitions.rs b/crates/bitwarden-policies/src/policy_definitions.rs new file mode 100644 index 0000000000..0a8885783a --- /dev/null +++ b/crates/bitwarden-policies/src/policy_definitions.rs @@ -0,0 +1,455 @@ +//! Policy definitions with strongly-typed payloads and trait implementations. +//! +//! Each policy type is defined here with its associated data structure (if any) +//! and its [`PolicyDefinition`] trait implementation. +//! Organized by policy type numeric value. + +use std::str::FromStr; + +use bitwarden_api_api::models::MasterPasswordPolicyResponseModel; +use bitwarden_organizations::OrganizationUserType; +use bitwarden_send::SendType; +use bitwarden_vault::UriMatchType; +use serde::{Deserialize, Serialize}; +#[cfg(feature = "wasm")] +use tsify::Tsify; + +use crate::policy_definition::PolicyDefinition; + +impl FromStr for MasterPasswordPolicy { + type Err = serde_json::Error; + + fn from_str(s: &str) -> Result { + serde_json::from_str(s) + } +} + +// ============================================================================= +// Supporting enums +// ============================================================================= + +// ============================================================================= +// Policy type 0: TwoFactorAuthentication (no data) +// ============================================================================= + +// (Unit variant in EnrichedPolicyType, no data structure needed) + +// ============================================================================= +// Policy type 1: MasterPassword +// ============================================================================= + +/// SDK domain model for master password policy requirements. +/// Defines the complexity requirements for a user's master password +/// when enforced by an organization policy. +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default)] +#[serde(rename_all = "camelCase")] +#[cfg_attr(feature = "uniffi", derive(uniffi::Record))] +#[cfg_attr( + feature = "wasm", + derive(tsify::Tsify), + tsify(into_wasm_abi, from_wasm_abi) +)] +pub struct MasterPasswordPolicy { + /// The minimum complexity score required for the master password. + /// Complexity is calculated based on password strength metrics. + #[serde(skip_serializing_if = "Option::is_none")] + pub min_complexity: Option, + + /// The minimum length required for the master password. + #[serde(skip_serializing_if = "Option::is_none")] + pub min_length: Option, + + /// Whether the master password must contain at least one lowercase letter. + #[serde(skip_serializing_if = "Option::is_none")] + pub require_lower: Option, + + /// Whether the master password must contain at least one uppercase letter. + #[serde(skip_serializing_if = "Option::is_none")] + pub require_upper: Option, + + /// Whether the master password must contain at least one numeric digit. + #[serde(skip_serializing_if = "Option::is_none")] + pub require_numbers: Option, + + /// Whether the master password must contain at least one special character. + #[serde(skip_serializing_if = "Option::is_none")] + pub require_special: Option, + + /// Whether this policy should be enforced when the user logs in. + /// If true, the user will be required to update their master password + /// if it doesn't meet the policy requirements. + #[serde(skip_serializing_if = "Option::is_none")] + pub enforce_on_login: Option, +} + +impl From for MasterPasswordPolicy { + fn from(api: MasterPasswordPolicyResponseModel) -> Self { + Self { + min_complexity: api.min_complexity, + min_length: api.min_length, + require_lower: api.require_lower, + require_upper: api.require_upper, + require_numbers: api.require_numbers, + require_special: api.require_special, + enforce_on_login: api.enforce_on_login, + } + } +} + +impl PolicyDefinition for MasterPasswordPolicy { + fn exempt_roles(&self) -> &[OrganizationUserType] { + &[] + } +} + +// ============================================================================= +// Policy type 2: PasswordGenerator +// ============================================================================= + +/// `policy.data` for the password generator policy. +/// +/// Sets restrictions and defaults for the password/passphrase generator. +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default)] +#[serde(rename_all = "camelCase")] +#[cfg_attr(feature = "uniffi", derive(uniffi::Record))] +#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))] +pub struct PasswordGeneratorPolicy { + /// Forces the generator to a specific type (e.g. `"password"` or `"passphrase"`). + #[serde(skip_serializing_if = "Option::is_none")] + pub override_password_type: Option, + /// The minimum generated password length. + #[serde(skip_serializing_if = "Option::is_none")] + pub min_length: Option, + /// Requires uppercase characters. + #[serde(skip_serializing_if = "Option::is_none")] + pub use_upper: Option, + /// Requires lowercase characters. + #[serde(skip_serializing_if = "Option::is_none")] + pub use_lower: Option, + /// Requires numeric characters. + #[serde(skip_serializing_if = "Option::is_none")] + pub use_numbers: Option, + /// Requires special characters. + #[serde(skip_serializing_if = "Option::is_none")] + pub use_special: Option, + /// The minimum number of numeric characters. + #[serde(skip_serializing_if = "Option::is_none")] + pub min_numbers: Option, + /// The minimum number of special characters. + #[serde(skip_serializing_if = "Option::is_none")] + pub min_special: Option, + /// The minimum number of words in a generated passphrase. + #[serde(skip_serializing_if = "Option::is_none")] + pub min_number_words: Option, + /// Whether generated passphrase words are capitalized. + #[serde(skip_serializing_if = "Option::is_none")] + pub capitalize: Option, + /// Whether a number is included in a generated passphrase. + #[serde(skip_serializing_if = "Option::is_none")] + pub include_number: Option, +} + +impl PolicyDefinition for PasswordGeneratorPolicy { + fn exempt_roles(&self) -> &[OrganizationUserType] { + &[] + } +} + +// ============================================================================= +// Policy type 3: SingleOrg (no data) +// ============================================================================= + +// (Unit variant in EnrichedPolicyType, no data structure needed) + +// ============================================================================= +// Policy type 4: RequireSso (no data) +// ============================================================================= + +// (Unit variant in EnrichedPolicyType, no data structure needed) + +// ============================================================================= +// Policy type 5: OrganizationDataOwnership +// ============================================================================= + +/// `policy.data` for the organization data ownership policy. +/// +/// Note: this policy also sends an encrypted `metadata.defaultUserCollectionName` +/// separately from `policy.data`; that value is not part of this struct. +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default)] +#[serde(rename_all = "camelCase")] +#[cfg_attr(feature = "uniffi", derive(uniffi::Record))] +#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))] +pub struct OrganizationDataOwnershipPolicy { + /// Whether members may transfer individual items into their personal vault. + pub enable_individual_items_transfer: bool, +} + +// ============================================================================= +// Policy type 6: DisableSend (no data) +// ============================================================================= + +// (Unit variant in EnrichedPolicyType, no data structure needed) + +// ============================================================================= +// Policy type 7: SendOptions +// ============================================================================= + +/// `policy.data` for the Send options policy. +/// +/// Superseded by [`SendControlsPolicy`] when the `pm-31885-send-controls` +/// feature flag is active on the server. +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default)] +#[serde(rename_all = "camelCase")] +#[cfg_attr(feature = "uniffi", derive(uniffi::Record))] +#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))] +pub struct SendOptionsPolicy { + /// Disables the "hide my email" option when creating a Send. + pub disable_hide_email: bool, +} + +// ============================================================================= +// Policy type 8: ResetPassword +// ============================================================================= + +/// `policy.data` for the reset password (account recovery) policy. +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default)] +#[serde(rename_all = "camelCase")] +#[cfg_attr(feature = "uniffi", derive(uniffi::Record))] +#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))] +pub struct ResetPasswordPolicy { + /// Whether new members are automatically enrolled in account recovery. + pub auto_enroll_enabled: bool, +} + +// ============================================================================= +// Policy type 9: MaximumVaultTimeout +// ============================================================================= + +/// The action to take when the maximum session timeout elapses, used by +/// [`MaximumSessionTimeoutPolicy`]. +/// +/// Serialized as a camelCase string to match the server's wire format. +// TODO: The variant set (`Lock`/`LogOut`) and their camelCase serialization are +// assumed to mirror the client `VaultTimeoutAction`. Verify against the server's +// wire format before relying on it. +#[derive(Serialize, Deserialize, PartialEq, Eq, Debug, Copy, Clone, Default)] +#[serde(rename_all = "camelCase")] +#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))] +#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))] +pub enum SessionTimeoutAction { + /// Lock the vault, requiring the user to unlock again. + #[default] + Lock, + /// Log the user out entirely. + LogOut, +} + +/// `policy.data` for the maximum session timeout policy. +/// +/// Backs both the v1 and v2 maximum session timeout policies (both use +/// [`PolicyType::MaximumVaultTimeout`](crate::PolicyType::MaximumVaultTimeout)). +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default)] +#[serde(rename_all = "camelCase")] +#[cfg_attr(feature = "uniffi", derive(uniffi::Record))] +#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))] +pub struct MaximumSessionTimeoutPolicy { + /// The kind of timeout being enforced. + // TODO: This is the `SessionTimeoutType` discriminant, modeled as a raw + // string until the server's enum domain is confirmed. Tighten to a dedicated + // enum once the allowed values are known. + pub r#type: String, + /// The maximum allowed session timeout, in minutes. + pub minutes: i32, + /// The action taken when the timeout elapses. + pub action: SessionTimeoutAction, +} + +impl PolicyDefinition for MaximumSessionTimeoutPolicy { + fn exempt_roles(&self) -> &[OrganizationUserType] { + &[OrganizationUserType::Owner] + } +} + +// ============================================================================= +// Policy type 10: DisablePersonalVaultExport (no data) +// ============================================================================= + +// (Unit variant in EnrichedPolicyType, no data structure needed) + +// ============================================================================= +// Policy type 11: ActivateAutofill (no data) +// ============================================================================= + +// (Unit variant in EnrichedPolicyType, no data structure needed) + +// ============================================================================= +// Policy type 12: AutomaticAppLogIn +// ============================================================================= + +/// `policy.data` for the automatic app login policy. +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default)] +#[serde(rename_all = "camelCase")] +#[cfg_attr(feature = "uniffi", derive(uniffi::Record))] +#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))] +pub struct AutomaticAppLoginPolicy { + /// The identity provider host used to automatically log members into apps. + pub idp_host: String, +} + +// ============================================================================= +// Policy type 13: FreeFamiliesSponsorship +// ============================================================================= + +/// Free Families Sponsorship policy. +/// +/// Applies to **everyone**, including Owners and Admins. +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] +pub struct FreeFamiliesSponsorshipPolicy; + +impl PolicyDefinition for FreeFamiliesSponsorshipPolicy { + fn exempt_roles(&self) -> &[OrganizationUserType] { + &[] + } +} + +// ============================================================================= +// Policy type 14: RemoveUnlockWithPin +// ============================================================================= + +/// Remove Unlock with PIN policy. +/// +/// Applies to **everyone**, including Owners and Admins. +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] +pub struct RemoveUnlockWithPinPolicy; + +impl PolicyDefinition for RemoveUnlockWithPinPolicy { + fn exempt_roles(&self) -> &[OrganizationUserType] { + &[] + } +} + +// ============================================================================= +// Policy type 15: RestrictedItemTypes +// ============================================================================= + +/// Restricted Item Types policy. +/// +/// Applies to **everyone**, including Owners and Admins. +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] +pub struct RestrictedItemTypesPolicy; + +impl PolicyDefinition for RestrictedItemTypesPolicy { + fn exempt_roles(&self) -> &[OrganizationUserType] { + &[] + } +} + +// ============================================================================= +// Policy type 16: UriMatchDefaults +// ============================================================================= + +/// `policy.data` for the URI match defaults policy. +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default)] +#[serde(rename_all = "camelCase")] +#[cfg_attr(feature = "uniffi", derive(uniffi::Record))] +#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))] +pub struct UriMatchDefaultPolicy { + /// The default URI match detection strategy for autofill. + pub uri_match_detection: UriMatchType, +} + +// ============================================================================= +// Policy type 17: AutotypeDefaultSetting (no data) +// ============================================================================= + +// (Unit variant in EnrichedPolicyType, no data structure needed) + +// ============================================================================= +// Policy type 18: AutomaticUserConfirmation +// ============================================================================= + +/// Automatic User Confirmation policy. +/// +/// Applies to **everyone**, including Owners and Admins. +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] +pub struct AutomaticUserConfirmationPolicy; + +impl PolicyDefinition for AutomaticUserConfirmationPolicy { + fn exempt_roles(&self) -> &[OrganizationUserType] { + &[] + } +} + +// ============================================================================= +// Policy type 19: BlockClaimedDomainAccountCreation (no data) +// ============================================================================= + +// (Unit variant in EnrichedPolicyType, no data structure needed) + +// ============================================================================= +// Policy type 20: OrganizationUserNotification +// ============================================================================= + +/// `policy.data` for the organization user notification policy. +/// +/// The server returns `null` (i.e. no data) when the policy is disabled. +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default)] +#[serde(rename_all = "camelCase")] +#[cfg_attr(feature = "uniffi", derive(uniffi::Record))] +#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))] +pub struct OrganizationUserNotificationPolicy { + /// The banner header text. + #[serde(skip_serializing_if = "Option::is_none")] + pub header: Option, + /// The banner body text. + #[serde(skip_serializing_if = "Option::is_none")] + pub description: Option, + /// The banner call-to-action button label. + #[serde(skip_serializing_if = "Option::is_none")] + pub button_text: Option, + /// Whether the banner is shown after every login rather than once. + #[serde(skip_serializing_if = "Option::is_none")] + pub show_after_every_login: Option, +} + +impl PolicyDefinition for OrganizationUserNotificationPolicy { + fn exempt_roles(&self) -> &[OrganizationUserType] { + &[] + } +} + +// ============================================================================= +// Policy type 21: SendControls +// ============================================================================= + +/// `policy.data` for the Send controls policy. +/// +/// Configures Send-related behavior. Supersedes [`SendOptionsPolicy`] (and the +/// toggle-only `DisableSend` policy) when the `pm-31885-send-controls` feature flag +/// is active on the server. +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default)] +#[serde(rename_all = "camelCase")] +#[cfg_attr(feature = "uniffi", derive(uniffi::Record))] +#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))] +pub struct SendControlsPolicy { + /// Disables the ability to create and edit Sends. + #[serde(skip_serializing_if = "Option::is_none")] + pub disable_send: Option, + /// Restricts who can access created Sends. + // TODO: Modeled as a raw string until the server's enum domain is confirmed. + // Tighten to a dedicated enum once the allowed values are known. + #[serde(skip_serializing_if = "Option::is_none")] + pub who_can_access: Option, + /// The domains a Send may be shared with, when access is restricted. + #[serde(skip_serializing_if = "Option::is_none")] + pub allowed_domains: Option>, + /// Disables the "hide my email" option when creating a Send. + #[serde(skip_serializing_if = "Option::is_none")] + pub disable_hide_email: Option, + /// The Send types members are allowed to create. + #[serde(skip_serializing_if = "Option::is_none")] + pub allowed_send_types: Option>, + /// The maximum number of hours before a Send is automatically deleted. + #[serde(skip_serializing_if = "Option::is_none")] + pub deletion_hours: Option, +} diff --git a/crates/bitwarden-policies/src/policy_overrides.rs b/crates/bitwarden-policies/src/policy_overrides.rs deleted file mode 100644 index ee4a0ac21e..0000000000 --- a/crates/bitwarden-policies/src/policy_overrides.rs +++ /dev/null @@ -1,304 +0,0 @@ -//! Custom policy implementations that override the default rules. - -use bitwarden_organizations::OrganizationUserType; - -use crate::{PolicyType, filter::Policy}; - -/// Master Password policy (type 1). -/// -/// Applies to **everyone**, including Owners and Admins. -pub struct MasterPasswordPolicy; - -impl Policy for MasterPasswordPolicy { - fn policy_type(&self) -> PolicyType { - PolicyType::MasterPassword - } - - fn exempt_roles(&self) -> &[OrganizationUserType] { - &[] - } -} - -/// Password Generator policy. -/// -/// Applies to **everyone**, including Owners and Admins. -pub struct PasswordGeneratorPolicy; - -impl Policy for PasswordGeneratorPolicy { - fn policy_type(&self) -> PolicyType { - PolicyType::PasswordGenerator - } - - fn exempt_roles(&self) -> &[OrganizationUserType] { - &[] - } -} - -/// Maximum Vault Timeout policy. -/// -/// Applies to everyone **except Owners**. Admins are not exempt. -pub struct MaximumVaultTimeoutPolicy; - -impl Policy for MaximumVaultTimeoutPolicy { - fn policy_type(&self) -> PolicyType { - PolicyType::MaximumVaultTimeout - } - - fn exempt_roles(&self) -> &[OrganizationUserType] { - &[OrganizationUserType::Owner] - } -} - -/// Free Families Sponsorship policy. -/// -/// Applies to **everyone**, including Owners and Admins. -pub struct FreeFamiliesSponsorshipPolicy; - -impl Policy for FreeFamiliesSponsorshipPolicy { - fn policy_type(&self) -> PolicyType { - PolicyType::FreeFamiliesSponsorship - } - - fn exempt_roles(&self) -> &[OrganizationUserType] { - &[] - } -} - -/// Remove Unlock with PIN policy. -/// -/// Applies to **everyone**, including Owners and Admins. -pub struct RemoveUnlockWithPinPolicy; - -impl Policy for RemoveUnlockWithPinPolicy { - fn policy_type(&self) -> PolicyType { - PolicyType::RemoveUnlockWithPin - } - - fn exempt_roles(&self) -> &[OrganizationUserType] { - &[] - } -} - -/// Restricted Item Types policy. -/// -/// Applies to **everyone**, including Owners and Admins. -pub struct RestrictedItemTypesPolicy; - -impl Policy for RestrictedItemTypesPolicy { - fn policy_type(&self) -> PolicyType { - PolicyType::RestrictedItemTypes - } - - fn exempt_roles(&self) -> &[OrganizationUserType] { - &[] - } -} - -/// Automatic User Confirmation policy. -/// -/// Applies to **everyone**, including Owners and Admins. -pub struct AutomaticUserConfirmationPolicy; - -impl Policy for AutomaticUserConfirmationPolicy { - fn policy_type(&self) -> PolicyType { - PolicyType::AutomaticUserConfirmation - } - - fn exempt_roles(&self) -> &[OrganizationUserType] { - &[] - } -} - -/// Organization User Notification policy. -/// -/// Applies to **everyone**, including Owners and Admins. -pub struct OrganizationUserNotificationPolicy; - -impl Policy for OrganizationUserNotificationPolicy { - fn policy_type(&self) -> PolicyType { - PolicyType::OrganizationUserNotification - } - - fn exempt_roles(&self) -> &[OrganizationUserType] { - &[] - } -} - -#[cfg(test)] -mod tests { - use bitwarden_organizations::{OrganizationUserStatusType, OrganizationUserType}; - use uuid::Uuid; - - use super::*; - use crate::{OrganizationUserPolicyContext, PolicyView, filter::PolicyFilter}; - - fn policy_view(organization_id: Uuid, policy_type: PolicyType) -> PolicyView { - PolicyView { - id: Uuid::new_v4(), - organization_id, - r#type: policy_type, - data: None, - enabled: true, - revision_date: Default::default(), - } - } - - fn org(id: Uuid, user_type: OrganizationUserType) -> OrganizationUserPolicyContext { - OrganizationUserPolicyContext { - id, - role: user_type, - status: OrganizationUserStatusType::Confirmed, - enabled: true, - use_policies: true, - is_provider_user: false, - } - } - - // --- MasterPasswordPolicy --- - - #[test] - fn master_password_applies_to_owner() { - let org_id = Uuid::new_v4(); - let policies = [policy_view(org_id, PolicyType::MasterPassword)]; - let orgs = [org(org_id, OrganizationUserType::Owner)]; - assert_eq!(MasterPasswordPolicy.filter(&policies, &orgs).len(), 1); - } - - #[test] - fn master_password_applies_to_admin() { - let org_id = Uuid::new_v4(); - let policies = [policy_view(org_id, PolicyType::MasterPassword)]; - let orgs = [org(org_id, OrganizationUserType::Admin)]; - assert_eq!(MasterPasswordPolicy.filter(&policies, &orgs).len(), 1); - } - - // --- PasswordGeneratorPolicy --- - - #[test] - fn password_generator_applies_to_owner() { - let org_id = Uuid::new_v4(); - let policies = [policy_view(org_id, PolicyType::PasswordGenerator)]; - let orgs = [org(org_id, OrganizationUserType::Owner)]; - assert_eq!(PasswordGeneratorPolicy.filter(&policies, &orgs).len(), 1); - } - - #[test] - fn password_generator_applies_to_admin() { - let org_id = Uuid::new_v4(); - let policies = [policy_view(org_id, PolicyType::PasswordGenerator)]; - let orgs = [org(org_id, OrganizationUserType::Admin)]; - assert_eq!(PasswordGeneratorPolicy.filter(&policies, &orgs).len(), 1); - } - - // --- MaximumVaultTimeoutPolicy --- - - #[test] - fn maximum_vault_timeout_exempts_owner() { - let org_id = Uuid::new_v4(); - let policies = [policy_view(org_id, PolicyType::MaximumVaultTimeout)]; - let orgs = [org(org_id, OrganizationUserType::Owner)]; - assert!( - MaximumVaultTimeoutPolicy - .filter(&policies, &orgs) - .is_empty() - ); - } - - #[test] - fn maximum_vault_timeout_applies_to_admin() { - let org_id = Uuid::new_v4(); - let policies = [policy_view(org_id, PolicyType::MaximumVaultTimeout)]; - let orgs = [org(org_id, OrganizationUserType::Admin)]; - assert_eq!(MaximumVaultTimeoutPolicy.filter(&policies, &orgs).len(), 1); - } - - #[test] - fn maximum_vault_timeout_applies_to_user() { - let org_id = Uuid::new_v4(); - let policies = [policy_view(org_id, PolicyType::MaximumVaultTimeout)]; - let orgs = [org(org_id, OrganizationUserType::User)]; - assert_eq!(MaximumVaultTimeoutPolicy.filter(&policies, &orgs).len(), 1); - } - - // --- FreeFamiliesSponsorshipPolicy --- - - #[test] - fn free_families_applies_to_owner() { - let org_id = Uuid::new_v4(); - let policies = [policy_view(org_id, PolicyType::FreeFamiliesSponsorship)]; - let orgs = [org(org_id, OrganizationUserType::Owner)]; - assert_eq!( - FreeFamiliesSponsorshipPolicy.filter(&policies, &orgs).len(), - 1 - ); - } - - // --- RemoveUnlockWithPinPolicy --- - - #[test] - fn remove_unlock_with_pin_applies_to_owner() { - let org_id = Uuid::new_v4(); - let policies = [policy_view(org_id, PolicyType::RemoveUnlockWithPin)]; - let orgs = [org(org_id, OrganizationUserType::Owner)]; - assert_eq!(RemoveUnlockWithPinPolicy.filter(&policies, &orgs).len(), 1); - } - - // --- RestrictedItemTypesPolicy --- - - #[test] - fn restricted_item_types_applies_to_owner() { - let org_id = Uuid::new_v4(); - let policies = [policy_view(org_id, PolicyType::RestrictedItemTypes)]; - let orgs = [org(org_id, OrganizationUserType::Owner)]; - assert_eq!(RestrictedItemTypesPolicy.filter(&policies, &orgs).len(), 1); - } - - // --- AutomaticUserConfirmationPolicy --- - - #[test] - fn automatic_user_confirmation_applies_to_owner() { - let org_id = Uuid::new_v4(); - let policies = [policy_view(org_id, PolicyType::AutomaticUserConfirmation)]; - let orgs = [org(org_id, OrganizationUserType::Owner)]; - assert_eq!( - AutomaticUserConfirmationPolicy - .filter(&policies, &orgs) - .len(), - 1 - ); - } - - // --- OrganizationUserNotificationPolicy --- - - #[test] - fn organization_user_notification_applies_to_owner() { - let org_id = Uuid::new_v4(); - let policies = [policy_view( - org_id, - PolicyType::OrganizationUserNotification, - )]; - let orgs = [org(org_id, OrganizationUserType::Owner)]; - assert_eq!( - OrganizationUserNotificationPolicy - .filter(&policies, &orgs) - .len(), - 1 - ); - } - - #[test] - fn organization_user_notification_applies_to_admin() { - let org_id = Uuid::new_v4(); - let policies = [policy_view( - org_id, - PolicyType::OrganizationUserNotification, - )]; - let orgs = [org(org_id, OrganizationUserType::Admin)]; - assert_eq!( - OrganizationUserNotificationPolicy - .filter(&policies, &orgs) - .len(), - 1 - ); - } -} diff --git a/crates/bitwarden-policies/src/registry.rs b/crates/bitwarden-policies/src/registry.rs deleted file mode 100644 index bcf3de1aa7..0000000000 --- a/crates/bitwarden-policies/src/registry.rs +++ /dev/null @@ -1,188 +0,0 @@ -#![allow(dead_code)] - -//! Policy registry for managing [`Policy`] implementations. -//! -//! The [`PolicyRegistry`] maps policy types to their definitions -//! and provides an interface for filtering policies according to their registered definition. -//! Unregistered policy types fall back to [`DefaultPolicy`]. - -use std::collections::HashMap; - -use crate::{ - OrganizationUserPolicyContext, PolicyType, PolicyView, - filter::{Policy, PolicyFilter}, -}; - -/// A [`Policy`] that uses the default filtering behavior for any policy type. -struct DefaultPolicy(PolicyType); - -impl Policy for DefaultPolicy { - fn policy_type(&self) -> PolicyType { - self.0 - } -} - -/// A registry mapping each [`PolicyType`] to its [`Policy`] implementation. -/// -/// This is for FFI callers where the [`Policy`] implementation is unknown. -/// Rust callers should call [`filter`](PolicyFilter::filter) -/// directly on their desired [`Policy`]. -/// -/// Use [`PolicyRegistry::builder`] to construct an instance. -pub struct PolicyRegistry { - policies: HashMap>, -} - -impl PolicyRegistry { - /// Returns a [`PolicyRegistryBuilder`] for constructing a registry. - pub fn builder() -> PolicyRegistryBuilder { - PolicyRegistryBuilder { - policies: HashMap::new(), - } - } - - /// Filters `policies` to those of `policy_type` that should be enforced. - /// - /// Uses the registered [`Policy`] for `policy_type` if one exists, - /// otherwise falls back to [`DefaultPolicy`]. - pub(crate) fn filter_by_type<'a>( - &self, - policies: &'a [PolicyView], - organization_user_policy_contexts: &[OrganizationUserPolicyContext], - policy_type: PolicyType, - ) -> Vec<&'a PolicyView> { - match self.policies.get(&policy_type) { - Some(p) => p.filter(policies, organization_user_policy_contexts), - None => DefaultPolicy(policy_type).filter(policies, organization_user_policy_contexts), - } - } -} - -/// Builder for [`PolicyRegistry`]. -pub struct PolicyRegistryBuilder { - policies: HashMap>, -} - -impl PolicyRegistryBuilder { - /// Registers a [`Policy`] for its policy type. - /// - /// # Panics - /// - /// Panics if a [`Policy`] for the same [`PolicyType`] has already been registered. - pub fn register(mut self, policy: P) -> Self { - let policy_type = policy.policy_type(); - if self.policies.contains_key(&policy_type) { - panic!("policy already registered for type {:?}", policy_type); - } - self.policies.insert(policy_type, Box::new(policy)); - self - } - - /// Builds the [`PolicyRegistry`]. - pub fn build(self) -> PolicyRegistry { - PolicyRegistry { - policies: self.policies, - } - } -} - -#[cfg(test)] -mod tests { - use bitwarden_organizations::{OrganizationUserStatusType, OrganizationUserType}; - use uuid::Uuid; - - use super::*; - - fn policy_view(organization_id: Uuid, policy_type: PolicyType, enabled: bool) -> PolicyView { - PolicyView { - id: Uuid::new_v4(), - organization_id, - r#type: policy_type, - data: None, - enabled, - revision_date: Default::default(), - } - } - - fn organization( - id: Uuid, - user_type: OrganizationUserType, - status: OrganizationUserStatusType, - provider: bool, - ) -> OrganizationUserPolicyContext { - OrganizationUserPolicyContext { - id, - role: user_type, - status, - enabled: true, - use_policies: true, - is_provider_user: provider, - } - } - - #[test] - #[should_panic(expected = "policy already registered for type")] - fn registry_panics_on_duplicate_registration() { - struct AnyPolicy; - impl Policy for AnyPolicy { - fn policy_type(&self) -> PolicyType { - PolicyType::MasterPassword - } - } - - PolicyRegistry::builder() - .register(AnyPolicy) - .register(AnyPolicy) - .build(); - } - - #[test] - fn registry_uses_registered_definition() { - let org_id = Uuid::new_v4(); - let policies = [policy_view(org_id, PolicyType::MasterPassword, true)]; - // Owner — exempt under default rules, not exempt under NoExemptionPolicy - let orgs = [organization( - org_id, - OrganizationUserType::Owner, - OrganizationUserStatusType::Confirmed, - false, - )]; - - struct NoExemptionPolicy; - impl Policy for NoExemptionPolicy { - fn policy_type(&self) -> PolicyType { - PolicyType::MasterPassword - } - fn exempt_roles(&self) -> &[OrganizationUserType] { - &[] - } - } - - let registry = PolicyRegistry::builder() - .register(NoExemptionPolicy) - .build(); - - let result = registry.filter_by_type(&policies, &orgs, PolicyType::MasterPassword); - - assert_eq!(result.len(), 1); - } - - #[test] - fn registry_uses_default_policy_definition() { - let org_id = Uuid::new_v4(); - let policies = [policy_view(org_id, PolicyType::MasterPassword, true)]; - let orgs = [organization( - org_id, - OrganizationUserType::User, - OrganizationUserStatusType::Confirmed, - false, - )]; - - // empty registry - let registry = PolicyRegistry::builder().build(); - - let result = registry.filter_by_type(&policies, &orgs, PolicyType::MasterPassword); - - assert_eq!(result.len(), 1); - } -} diff --git a/crates/bitwarden-uniffi/src/policies.rs b/crates/bitwarden-uniffi/src/policies.rs index 39e51b4b8c..e36d7d54d6 100644 --- a/crates/bitwarden-uniffi/src/policies.rs +++ b/crates/bitwarden-uniffi/src/policies.rs @@ -1,4 +1,6 @@ -use bitwarden_policies::{OrganizationUserPolicyContext, PolicyClient, PolicyType, PolicyView}; +use bitwarden_policies::{ + EnrichedPolicy, OrganizationUserPolicyContext, PolicyClient, PolicyType, PolicyView, +}; /// Client for policy domain operations. #[derive(uniffi::Object)] @@ -7,16 +9,14 @@ pub struct PoliciesClient(pub(crate) PolicyClient); #[uniffi::export] impl PoliciesClient { /// Filter policies of the given type for the current user. - /// - /// Returns the subset of `policies` that should be enforced against the user, - /// based on their organization memberships and roles. - pub fn filter_by_type( + /// POC code path that uses an enum to wrap strongly typed data and the policy definition. + pub fn filter( &self, policies: Vec, organization_user_policy_contexts: Vec, policy_type: PolicyType, - ) -> Vec { + ) -> Vec { self.0 - .filter_by_type(policies, organization_user_policy_contexts, policy_type) + .filter(policies, organization_user_policy_contexts, policy_type) } } diff --git a/crates/bitwarden-vault/src/cipher/login.rs b/crates/bitwarden-vault/src/cipher/login.rs index b963e10ac2..79ec0a5a68 100644 --- a/crates/bitwarden-vault/src/cipher/login.rs +++ b/crates/bitwarden-vault/src/cipher/login.rs @@ -21,12 +21,13 @@ use super::cipher::{CipherKind, StrictDecrypt}; use crate::{Cipher, PasswordHistoryView, VaultParseError, cipher::cipher::CopyableCipherFields}; #[allow(missing_docs)] -#[derive(Clone, Copy, Serialize_repr, Deserialize_repr, Debug, PartialEq)] +#[derive(Clone, Copy, Serialize_repr, Deserialize_repr, Debug, PartialEq, Default)] #[repr(u8)] #[serde(rename_all = "camelCase", deny_unknown_fields)] #[cfg_attr(feature = "uniffi", derive(uniffi::Enum))] #[cfg_attr(feature = "wasm", wasm_bindgen)] pub enum UriMatchType { + #[default] //TODO; arbitrary, confirm Domain = 0, Host = 1, StartsWith = 2,