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 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 if acc_pol.privilege_expiry < accumulate.privilege_expiry {
118 accumulate.privilege_expiry = acc_pol.privilege_expiry
119 }
120
121 if acc_pol.authsession_expiry < accumulate.authsession_expiry {
123 accumulate.authsession_expiry = acc_pol.authsession_expiry
124 }
125
126 if acc_pol.pw_min_length > accumulate.pw_min_length {
128 accumulate.pw_min_length = acc_pol.pw_min_length
129 }
130
131 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 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 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}