Skip to main content

kanidmd_lib/idm/
accountpolicy.rs

1use crate::prelude::*;
2use crate::value::CredentialType;
3use kanidm_lib_crypto::{PW_MAX_LENGTH_NIST, PW_MFA_MIN_LENGTH, PW_SFA_MIN_LENGTH_NIST};
4use webauthn_rs::prelude::AttestationCaList;
5
6#[derive(Clone)]
7#[cfg_attr(test, derive(Default))]
8pub(crate) struct AccountPolicy {
9    privilege_expiry: u32,
10    authsession_expiry: u32,
11    pw_min_length: u32,
12    credential_policy: CredentialType,
13    webauthn_att_ca_list: Option<AttestationCaList>,
14    limit_search_max_filter_test: Option<u64>,
15    limit_search_max_results: Option<u64>,
16    allow_primary_cred_fallback: Option<bool>,
17}
18
19impl From<&EntrySealedCommitted> for Option<AccountPolicy> {
20    fn from(val: &EntrySealedCommitted) -> Self {
21        if !val.attribute_equality(
22            Attribute::Class,
23            &EntryClass::AccountPolicy.to_partialvalue(),
24        ) {
25            return None;
26        }
27
28        let authsession_expiry = val
29            .get_ava_single_uint32(Attribute::AuthSessionExpiry)
30            .unwrap_or(MAXIMUM_AUTH_SESSION_EXPIRY);
31
32        let privilege_expiry = val
33            .get_ava_single_uint32(Attribute::PrivilegeExpiry)
34            .unwrap_or(MAXIMUM_AUTH_PRIVILEGE_EXPIRY);
35
36        let pw_min_length = val
37            .get_ava_single_uint32(Attribute::AuthPasswordMinimumLength)
38            .unwrap_or(PW_MFA_MIN_LENGTH);
39
40        let credential_policy = val
41            .get_ava_single_credential_type(Attribute::CredentialTypeMinimum)
42            .unwrap_or(CredentialType::Any);
43
44        let webauthn_att_ca_list = val
45            .get_ava_webauthn_attestation_ca_list(Attribute::WebauthnAttestationCaList)
46            .cloned();
47
48        let limit_search_max_results = val
49            .get_ava_single_uint32(Attribute::LimitSearchMaxResults)
50            .map(|u| u as u64);
51
52        let limit_search_max_filter_test = val
53            .get_ava_single_uint32(Attribute::LimitSearchMaxFilterTest)
54            .map(|u| u as u64);
55
56        let allow_primary_cred_fallback =
57            val.get_ava_single_bool(Attribute::AllowPrimaryCredFallback);
58
59        Some(AccountPolicy {
60            privilege_expiry,
61            authsession_expiry,
62            pw_min_length,
63            credential_policy,
64            webauthn_att_ca_list,
65            limit_search_max_filter_test,
66            limit_search_max_results,
67            allow_primary_cred_fallback,
68        })
69    }
70}
71
72#[derive(Clone, Debug)]
73#[cfg_attr(test, derive(Default))]
74pub(crate) struct ResolvedAccountPolicy {
75    privilege_expiry: u32,
76    authsession_expiry: u32,
77    pw_min_length: u32,
78    pw_max_length: u32,
79    credential_policy: CredentialType,
80    webauthn_att_ca_list: Option<AttestationCaList>,
81    limit_search_max_filter_test: Option<u64>,
82    limit_search_max_results: Option<u64>,
83    allow_primary_cred_fallback: Option<bool>,
84}
85
86impl ResolvedAccountPolicy {
87    #[cfg(test)]
88    pub(crate) fn test_policy() -> Self {
89        ResolvedAccountPolicy {
90            privilege_expiry: DEFAULT_AUTH_PRIVILEGE_EXPIRY,
91            authsession_expiry: DEFAULT_AUTH_SESSION_EXPIRY,
92            pw_min_length: PW_MFA_MIN_LENGTH,
93            pw_max_length: PW_MAX_LENGTH_NIST,
94            credential_policy: CredentialType::Any,
95            webauthn_att_ca_list: None,
96            limit_search_max_filter_test: Some(DEFAULT_LIMIT_SEARCH_MAX_FILTER_TEST),
97            limit_search_max_results: Some(DEFAULT_LIMIT_SEARCH_MAX_RESULTS),
98            allow_primary_cred_fallback: None,
99        }
100    }
101
102    pub(crate) fn fold_from<I>(iter: I) -> Self
103    where
104        I: Iterator<Item = AccountPolicy>,
105    {
106        // Start with our maximums
107        let mut accumulate = ResolvedAccountPolicy {
108            privilege_expiry: MAXIMUM_AUTH_PRIVILEGE_EXPIRY,
109            authsession_expiry: MAXIMUM_AUTH_SESSION_EXPIRY,
110            pw_min_length: PW_MFA_MIN_LENGTH,
111            pw_max_length: PW_MAX_LENGTH_NIST,
112            credential_policy: CredentialType::Any,
113            webauthn_att_ca_list: None,
114            limit_search_max_filter_test: None,
115            limit_search_max_results: None,
116            allow_primary_cred_fallback: None,
117        };
118
119        iter.for_each(|acc_pol| {
120            // Take the smaller expiry
121            if acc_pol.privilege_expiry < accumulate.privilege_expiry {
122                accumulate.privilege_expiry = acc_pol.privilege_expiry
123            }
124
125            // Take the smaller expiry
126            if acc_pol.authsession_expiry < accumulate.authsession_expiry {
127                accumulate.authsession_expiry = acc_pol.authsession_expiry
128            }
129
130            // Take larger pw min len
131            if acc_pol.pw_min_length > accumulate.pw_min_length {
132                accumulate.pw_min_length = acc_pol.pw_min_length
133            }
134
135            // Take the greater credential type policy
136            if acc_pol.credential_policy > accumulate.credential_policy {
137                accumulate.credential_policy = acc_pol.credential_policy
138            }
139
140            if let Some(pol_lim) = acc_pol.limit_search_max_results {
141                if let Some(acc_lim) = accumulate.limit_search_max_results {
142                    if pol_lim > acc_lim {
143                        accumulate.limit_search_max_results = Some(pol_lim);
144                    }
145                } else {
146                    accumulate.limit_search_max_results = Some(pol_lim);
147                }
148            }
149
150            if let Some(pol_lim) = acc_pol.limit_search_max_filter_test {
151                if let Some(acc_lim) = accumulate.limit_search_max_filter_test {
152                    if pol_lim > acc_lim {
153                        accumulate.limit_search_max_filter_test = Some(pol_lim);
154                    }
155                } else {
156                    accumulate.limit_search_max_filter_test = Some(pol_lim);
157                }
158            }
159
160            if let Some(acc_pol_w_att_ca) = acc_pol.webauthn_att_ca_list {
161                if let Some(res_w_att_ca) = accumulate.webauthn_att_ca_list.as_mut() {
162                    res_w_att_ca.intersection(&acc_pol_w_att_ca);
163                } else {
164                    accumulate.webauthn_att_ca_list = Some(acc_pol_w_att_ca);
165                }
166            }
167
168            if let Some(allow_primary_cred_fallback) = acc_pol.allow_primary_cred_fallback {
169                accumulate.allow_primary_cred_fallback =
170                    match accumulate.allow_primary_cred_fallback {
171                        Some(acc_fallback) => Some(allow_primary_cred_fallback && acc_fallback),
172                        None => Some(allow_primary_cred_fallback),
173                    };
174            }
175        });
176
177        // Per NIST, if a password can be used as a single factor authenticator,
178        // then we need to set the minimum length to a greater value.
179        if accumulate.credential_policy < CredentialType::Mfa
180            && accumulate.pw_min_length < PW_SFA_MIN_LENGTH_NIST
181        {
182            accumulate.pw_min_length = PW_SFA_MIN_LENGTH_NIST;
183        }
184
185        accumulate
186    }
187
188    pub(crate) fn privilege_expiry(&self) -> u32 {
189        self.privilege_expiry
190    }
191
192    pub(crate) fn authsession_expiry(&self) -> u32 {
193        self.authsession_expiry
194    }
195
196    pub(crate) fn pw_min_length(&self) -> u32 {
197        self.pw_min_length
198    }
199
200    pub(crate) fn pw_max_length(&self) -> u32 {
201        self.pw_max_length
202    }
203
204    pub(crate) fn credential_policy(&self) -> CredentialType {
205        self.credential_policy
206    }
207
208    pub(crate) fn webauthn_attestation_ca_list(&self) -> Option<&AttestationCaList> {
209        self.webauthn_att_ca_list.as_ref()
210    }
211
212    pub(crate) fn limit_search_max_results(&self) -> Option<u64> {
213        self.limit_search_max_results
214    }
215
216    pub(crate) fn limit_search_max_filter_test(&self) -> Option<u64> {
217        self.limit_search_max_filter_test
218    }
219
220    pub(crate) fn allow_primary_cred_fallback(&self) -> Option<bool> {
221        self.allow_primary_cred_fallback
222    }
223}
224
225#[cfg(test)]
226mod tests {
227    use super::{AccountPolicy, CredentialType, ResolvedAccountPolicy};
228    use crate::prelude::*;
229    use webauthn_rs_core::proto::AttestationCaListBuilder;
230
231    #[test]
232    fn test_idm_account_policy_resolve() {
233        sketching::test_init();
234
235        // Yubico U2F Root CA Serial 457200631
236        let ca_root_a: &[u8] = b"-----BEGIN CERTIFICATE-----
237MIIDHjCCAgagAwIBAgIEG0BT9zANBgkqhkiG9w0BAQsFADAuMSwwKgYDVQQDEyNZ
238dWJpY28gVTJGIFJvb3QgQ0EgU2VyaWFsIDQ1NzIwMDYzMTAgFw0xNDA4MDEwMDAw
239MDBaGA8yMDUwMDkwNDAwMDAwMFowLjEsMCoGA1UEAxMjWXViaWNvIFUyRiBSb290
240IENBIFNlcmlhbCA0NTcyMDA2MzEwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEK
241AoIBAQC/jwYuhBVlqaiYWEMsrWFisgJ+PtM91eSrpI4TK7U53mwCIawSDHy8vUmk
2425N2KAj9abvT9NP5SMS1hQi3usxoYGonXQgfO6ZXyUA9a+KAkqdFnBnlyugSeCOep
2438EdZFfsaRFtMjkwz5Gcz2Py4vIYvCdMHPtwaz0bVuzneueIEz6TnQjE63Rdt2zbw
244nebwTG5ZybeWSwbzy+BJ34ZHcUhPAY89yJQXuE0IzMZFcEBbPNRbWECRKgjq//qT
2459nmDOFVlSRCt2wiqPSzluwn+v+suQEBsUjTGMEd25tKXXTkNW21wIWbxeSyUoTXw
246LvGS6xlwQSgNpk2qXYwf8iXg7VWZAgMBAAGjQjBAMB0GA1UdDgQWBBQgIvz0bNGJ
247hjgpToksyKpP9xv9oDAPBgNVHRMECDAGAQH/AgEAMA4GA1UdDwEB/wQEAwIBBjAN
248BgkqhkiG9w0BAQsFAAOCAQEAjvjuOMDSa+JXFCLyBKsycXtBVZsJ4Ue3LbaEsPY4
249MYN/hIQ5ZM5p7EjfcnMG4CtYkNsfNHc0AhBLdq45rnT87q/6O3vUEtNMafbhU6kt
250hX7Y+9XFN9NpmYxr+ekVY5xOxi8h9JDIgoMP4VB1uS0aunL1IGqrNooL9mmFnL2k
251LVVee6/VR6C5+KSTCMCWppMuJIZII2v9o4dkoZ8Y7QRjQlLfYzd3qGtKbw7xaF1U
252sG/5xUb/Btwb2X2g4InpiB/yt/3CpQXpiWX/K4mBvUKiGn05ZsqeY1gx4g0xLBqc
253U9psmyPzK+Vsgw2jeRQ5JlKDyqE0hebfC1tvFu0CCrJFcw==
254-----END CERTIFICATE-----";
255
256        // Defunct Apple WebAuthn Root CA
257        let ca_root_b: &[u8] = b"-----BEGIN CERTIFICATE-----
258MIICEjCCAZmgAwIBAgIQaB0BbHo84wIlpQGUKEdXcTAKBggqhkjOPQQDAzBLMR8w
259HQYDVQQDDBZBcHBsZSBXZWJBdXRobiBSb290IENBMRMwEQYDVQQKDApBcHBsZSBJ
260bmMuMRMwEQYDVQQIDApDYWxpZm9ybmlhMB4XDTIwMDMxODE4MjEzMloXDTQ1MDMx
261NTAwMDAwMFowSzEfMB0GA1UEAwwWQXBwbGUgV2ViQXV0aG4gUm9vdCBDQTETMBEG
262A1UECgwKQXBwbGUgSW5jLjETMBEGA1UECAwKQ2FsaWZvcm5pYTB2MBAGByqGSM49
263AgEGBSuBBAAiA2IABCJCQ2pTVhzjl4Wo6IhHtMSAzO2cv+H9DQKev3//fG59G11k
264xu9eI0/7o6V5uShBpe1u6l6mS19S1FEh6yGljnZAJ+2GNP1mi/YK2kSXIuTHjxA/
265pcoRf7XkOtO4o1qlcaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUJtdk
2662cV4wlpn0afeaxLQG2PxxtcwDgYDVR0PAQH/BAQDAgEGMAoGCCqGSM49BAMDA2cA
267MGQCMFrZ+9DsJ1PW9hfNdBywZDsWDbWFp28it1d/5w2RPkRX3Bbn/UbDTNLx7Jr3
268jAGGiQIwHFj+dJZYUJR786osByBelJYsVZd2GbHQu209b5RCmGQ21gpSAk9QZW4B
2691bWeT0vT
270-----END CERTIFICATE-----";
271
272        let aaguid_a = Uuid::new_v4();
273        let aaguid_b = Uuid::new_v4();
274        let aaguid_c = Uuid::new_v4();
275        let aaguid_d = Uuid::new_v4();
276        let aaguid_e = Uuid::new_v4();
277
278        let mut att_ca_builder = AttestationCaListBuilder::new();
279
280        att_ca_builder
281            .insert_device_pem(ca_root_a, aaguid_a, "A".to_string(), Default::default())
282            .unwrap();
283        att_ca_builder
284            .insert_device_pem(ca_root_a, aaguid_b, "B".to_string(), Default::default())
285            .unwrap();
286        att_ca_builder
287            .insert_device_pem(ca_root_a, aaguid_c, "C".to_string(), Default::default())
288            .unwrap();
289        att_ca_builder
290            .insert_device_pem(ca_root_b, aaguid_d, "D".to_string(), Default::default())
291            .unwrap();
292
293        let att_ca_list_a = att_ca_builder.build();
294
295        let policy_a = AccountPolicy {
296            privilege_expiry: 100,
297            authsession_expiry: 100,
298            pw_min_length: 11,
299            credential_policy: CredentialType::Mfa,
300            webauthn_att_ca_list: Some(att_ca_list_a),
301            limit_search_max_filter_test: Some(10),
302            limit_search_max_results: Some(10),
303            allow_primary_cred_fallback: None,
304        };
305
306        let mut att_ca_builder = AttestationCaListBuilder::new();
307
308        att_ca_builder
309            .insert_device_pem(ca_root_a, aaguid_b, "B".to_string(), Default::default())
310            .unwrap();
311        att_ca_builder
312            .insert_device_pem(ca_root_b, aaguid_e, "E".to_string(), Default::default())
313            .unwrap();
314
315        let att_ca_list_b = att_ca_builder.build();
316
317        let policy_b = AccountPolicy {
318            privilege_expiry: 150,
319            authsession_expiry: 50,
320            pw_min_length: 15,
321            credential_policy: CredentialType::Passkey,
322            webauthn_att_ca_list: Some(att_ca_list_b),
323            limit_search_max_filter_test: Some(5),
324            limit_search_max_results: Some(15),
325            allow_primary_cred_fallback: Some(false),
326        };
327
328        let rap = ResolvedAccountPolicy::fold_from([policy_a, policy_b].into_iter());
329
330        assert_eq!(rap.privilege_expiry(), 100);
331        assert_eq!(rap.authsession_expiry(), 50);
332        assert_eq!(rap.pw_min_length(), 15);
333        assert_eq!(rap.credential_policy, CredentialType::Passkey);
334        assert_eq!(rap.limit_search_max_results(), Some(15));
335        assert_eq!(rap.limit_search_max_filter_test(), Some(10));
336        assert_eq!(rap.allow_primary_cred_fallback(), Some(false));
337
338        let mut att_ca_builder = AttestationCaListBuilder::new();
339
340        att_ca_builder
341            .insert_device_pem(ca_root_a, aaguid_b, "B".to_string(), Default::default())
342            .unwrap();
343
344        let att_ca_list_ex = att_ca_builder.build();
345
346        assert_eq!(rap.webauthn_att_ca_list, Some(att_ca_list_ex));
347    }
348}