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::{Policy, PolicyType, policy_type::PolicyDataType};
6
7/// Master Password policy.
8pub struct MasterPasswordPolicy;
9
10impl Policy 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#[cfg_attr(
37    feature = "wasm",
38    derive(tsify::Tsify),
39    tsify(into_wasm_abi, from_wasm_abi)
40)]
41pub struct MasterPasswordPolicyData {
42    /// The minimum complexity score required for the master password.
43    /// Complexity is calculated based on password strength metrics.
44    #[serde(skip_serializing_if = "Option::is_none")]
45    pub min_complexity: Option<i32>,
46
47    /// The minimum length required for the master password.
48    #[serde(skip_serializing_if = "Option::is_none")]
49    pub min_length: Option<i32>,
50
51    /// Whether the master password must contain at least one lowercase letter.
52    #[serde(skip_serializing_if = "Option::is_none")]
53    pub require_lower: Option<bool>,
54
55    /// Whether the master password must contain at least one uppercase letter.
56    #[serde(skip_serializing_if = "Option::is_none")]
57    pub require_upper: Option<bool>,
58
59    /// Whether the master password must contain at least one numeric digit.
60    #[serde(skip_serializing_if = "Option::is_none")]
61    pub require_numbers: Option<bool>,
62
63    /// Whether the master password must contain at least one special character.
64    #[serde(skip_serializing_if = "Option::is_none")]
65    pub require_special: Option<bool>,
66
67    /// Whether this policy should be enforced when the user logs in.
68    /// If true, the user will be required to update their master password
69    /// if it doesn't meet the policy requirements.
70    #[serde(skip_serializing_if = "Option::is_none")]
71    pub enforce_on_login: Option<bool>,
72}
73
74impl From<MasterPasswordPolicyResponseModel> for MasterPasswordPolicyData {
75    fn from(api: MasterPasswordPolicyResponseModel) -> Self {
76        Self {
77            min_complexity: api.min_complexity,
78            min_length: api.min_length,
79            require_lower: api.require_lower,
80            require_upper: api.require_upper,
81            require_numbers: api.require_numbers,
82            require_special: api.require_special,
83            enforce_on_login: api.enforce_on_login,
84        }
85    }
86}
87
88#[cfg(test)]
89mod tests {
90    use super::*;
91
92    #[test]
93    fn round_trips() {
94        let data = MasterPasswordPolicyData {
95            min_complexity: Some(3),
96            min_length: Some(12),
97            require_lower: Some(true),
98            require_upper: Some(true),
99            require_numbers: Some(false),
100            require_special: Some(false),
101            enforce_on_login: Some(true),
102        };
103        let json = serde_json::to_string(&data).unwrap();
104        assert!(json.contains(r#""minComplexity":3"#));
105        assert!(json.contains(r#""enforceOnLogin":true"#));
106        assert_eq!(
107            serde_json::from_str::<MasterPasswordPolicyData>(&json).unwrap(),
108            data
109        );
110    }
111
112    #[test]
113    fn empty_serializes_to_empty_object() {
114        assert_eq!(
115            serde_json::to_string(&MasterPasswordPolicyData::default()).unwrap(),
116            "{}"
117        );
118    }
119
120    #[test]
121    fn from_api_model_full() {
122        let api = MasterPasswordPolicyResponseModel {
123            object: Some("masterPasswordPolicy".to_string()),
124            min_complexity: Some(4),
125            min_length: Some(12),
126            require_lower: Some(true),
127            require_upper: Some(true),
128            require_numbers: Some(true),
129            require_special: Some(true),
130            enforce_on_login: Some(true),
131        };
132
133        let domain: MasterPasswordPolicyData = api.into();
134
135        assert_eq!(domain.min_complexity, Some(4));
136        assert_eq!(domain.min_length, Some(12));
137        assert_eq!(domain.require_lower, Some(true));
138        assert_eq!(domain.require_upper, Some(true));
139        assert_eq!(domain.require_numbers, Some(true));
140        assert_eq!(domain.require_special, Some(true));
141        assert_eq!(domain.enforce_on_login, Some(true));
142    }
143
144    #[test]
145    fn from_api_model_minimal() {
146        let api = MasterPasswordPolicyResponseModel {
147            object: Some("masterPasswordPolicy".to_string()),
148            min_complexity: None,
149            min_length: Some(8),
150            require_lower: None,
151            require_upper: None,
152            require_numbers: None,
153            require_special: None,
154            enforce_on_login: Some(false),
155        };
156
157        let domain: MasterPasswordPolicyData = api.into();
158
159        assert_eq!(domain.min_complexity, None);
160        assert_eq!(domain.min_length, Some(8));
161        assert_eq!(domain.require_lower, None);
162        assert_eq!(domain.require_upper, None);
163        assert_eq!(domain.require_numbers, None);
164        assert_eq!(domain.require_special, None);
165        assert_eq!(domain.enforce_on_login, Some(false));
166    }
167
168    #[test]
169    fn from_api_model_empty() {
170        let api = MasterPasswordPolicyResponseModel {
171            object: Some("masterPasswordPolicy".to_string()),
172            min_complexity: None,
173            min_length: None,
174            require_lower: None,
175            require_upper: None,
176            require_numbers: None,
177            require_special: None,
178            enforce_on_login: None,
179        };
180
181        let domain: MasterPasswordPolicyData = api.into();
182
183        assert_eq!(domain.min_complexity, None);
184        assert_eq!(domain.min_length, None);
185        assert_eq!(domain.require_lower, None);
186        assert_eq!(domain.require_upper, None);
187        assert_eq!(domain.require_numbers, None);
188        assert_eq!(domain.require_special, None);
189        assert_eq!(domain.enforce_on_login, None);
190    }
191}