kanidmd_lib/idm/
accountpolicy.rs

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