Skip to main content

bitwarden_policies/policies/
master_password.rs

1use bitwarden_api_api::models::MasterPasswordPolicyResponseModel;
2use bitwarden_organizations::OrganizationUserType;
3use serde::{Deserialize, Serialize};
4
5use crate::{PolicyDefinition, PolicyType, policy_type::PolicyDataType};
6
7/// Master Password policy.
8pub struct MasterPasswordPolicy;
9
10impl PolicyDefinition for MasterPasswordPolicy {
11    type Data = MasterPasswordPolicyData;
12
13    fn policy_type(&self) -> PolicyType {
14        PolicyType::MasterPassword
15    }
16
17    fn to_erased(&self, data: Self::Data) -> PolicyDataType {
18        PolicyDataType::MasterPassword(data)
19    }
20
21    fn exempt_roles(&self) -> &[OrganizationUserType] {
22        &[]
23    }
24}
25
26/// The complexity requirements an organization enforces on members' master
27/// passwords.
28///
29/// This models the same information the server delivers two ways: as the
30/// policy's saved JSON `data` blob (parsed for enforcement) and as the typed
31/// [`MasterPasswordPolicyResponseModel`] returned at login (bridged via
32/// [`From`]).
33#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default)]
34#[serde(rename_all = "camelCase")]
35#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
36#[bitwarden_ffi::wasm_record]
37pub struct MasterPasswordPolicyData {
38    /// The minimum complexity score required for the master password.
39    /// Complexity is calculated based on password strength metrics.
40    #[serde(skip_serializing_if = "Option::is_none")]
41    pub min_complexity: Option<i32>,
42
43    /// The minimum length required for the master password.
44    #[serde(skip_serializing_if = "Option::is_none")]
45    pub min_length: Option<i32>,
46
47    /// Whether the master password must contain at least one lowercase letter.
48    #[serde(skip_serializing_if = "Option::is_none")]
49    pub require_lower: Option<bool>,
50
51    /// Whether the master password must contain at least one uppercase letter.
52    #[serde(skip_serializing_if = "Option::is_none")]
53    pub require_upper: Option<bool>,
54
55    /// Whether the master password must contain at least one numeric digit.
56    #[serde(skip_serializing_if = "Option::is_none")]
57    pub require_numbers: Option<bool>,
58
59    /// Whether the master password must contain at least one special character.
60    #[serde(skip_serializing_if = "Option::is_none")]
61    pub require_special: Option<bool>,
62
63    /// Whether this policy should be enforced when the user logs in.
64    /// If true, the user will be required to update their master password
65    /// if it doesn't meet the policy requirements.
66    #[serde(skip_serializing_if = "Option::is_none")]
67    pub enforce_on_login: Option<bool>,
68}
69
70impl From<MasterPasswordPolicyResponseModel> for MasterPasswordPolicyData {
71    fn from(api: MasterPasswordPolicyResponseModel) -> Self {
72        Self {
73            min_complexity: api.min_complexity,
74            min_length: api.min_length,
75            require_lower: api.require_lower,
76            require_upper: api.require_upper,
77            require_numbers: api.require_numbers,
78            require_special: api.require_special,
79            enforce_on_login: api.enforce_on_login,
80        }
81    }
82}
83
84#[cfg(test)]
85mod tests {
86    use super::*;
87
88    #[test]
89    fn round_trips() {
90        let data = MasterPasswordPolicyData {
91            min_complexity: Some(3),
92            min_length: Some(12),
93            require_lower: Some(true),
94            require_upper: Some(true),
95            require_numbers: Some(false),
96            require_special: Some(false),
97            enforce_on_login: Some(true),
98        };
99        let json = serde_json::to_string(&data).unwrap();
100        assert!(json.contains(r#""minComplexity":3"#));
101        assert!(json.contains(r#""enforceOnLogin":true"#));
102        assert_eq!(
103            serde_json::from_str::<MasterPasswordPolicyData>(&json).unwrap(),
104            data
105        );
106    }
107
108    #[test]
109    fn empty_serializes_to_empty_object() {
110        assert_eq!(
111            serde_json::to_string(&MasterPasswordPolicyData::default()).unwrap(),
112            "{}"
113        );
114    }
115
116    #[test]
117    fn from_api_model_full() {
118        let api = MasterPasswordPolicyResponseModel {
119            object: Some("masterPasswordPolicy".to_string()),
120            min_complexity: Some(4),
121            min_length: Some(12),
122            require_lower: Some(true),
123            require_upper: Some(true),
124            require_numbers: Some(true),
125            require_special: Some(true),
126            enforce_on_login: Some(true),
127        };
128
129        let domain: MasterPasswordPolicyData = api.into();
130
131        assert_eq!(domain.min_complexity, Some(4));
132        assert_eq!(domain.min_length, Some(12));
133        assert_eq!(domain.require_lower, Some(true));
134        assert_eq!(domain.require_upper, Some(true));
135        assert_eq!(domain.require_numbers, Some(true));
136        assert_eq!(domain.require_special, Some(true));
137        assert_eq!(domain.enforce_on_login, Some(true));
138    }
139
140    #[test]
141    fn from_api_model_minimal() {
142        let api = MasterPasswordPolicyResponseModel {
143            object: Some("masterPasswordPolicy".to_string()),
144            min_complexity: None,
145            min_length: Some(8),
146            require_lower: None,
147            require_upper: None,
148            require_numbers: None,
149            require_special: None,
150            enforce_on_login: Some(false),
151        };
152
153        let domain: MasterPasswordPolicyData = api.into();
154
155        assert_eq!(domain.min_complexity, None);
156        assert_eq!(domain.min_length, Some(8));
157        assert_eq!(domain.require_lower, None);
158        assert_eq!(domain.require_upper, None);
159        assert_eq!(domain.require_numbers, None);
160        assert_eq!(domain.require_special, None);
161        assert_eq!(domain.enforce_on_login, Some(false));
162    }
163
164    #[test]
165    fn from_api_model_empty() {
166        let api = MasterPasswordPolicyResponseModel {
167            object: Some("masterPasswordPolicy".to_string()),
168            min_complexity: None,
169            min_length: None,
170            require_lower: None,
171            require_upper: None,
172            require_numbers: None,
173            require_special: None,
174            enforce_on_login: None,
175        };
176
177        let domain: MasterPasswordPolicyData = api.into();
178
179        assert_eq!(domain.min_complexity, None);
180        assert_eq!(domain.min_length, None);
181        assert_eq!(domain.require_lower, None);
182        assert_eq!(domain.require_upper, None);
183        assert_eq!(domain.require_numbers, None);
184        assert_eq!(domain.require_special, None);
185        assert_eq!(domain.enforce_on_login, None);
186    }
187}