1use super::accountpolicy::ResolvedAccountPolicy;
2use crate::credential::totp::{Totp, TOTP_DEFAULT_STEP};
3use crate::credential::{BackupCodes, Credential};
4use crate::idm::account::Account;
5use crate::idm::server::{IdmServerCredUpdateTransaction, IdmServerProxyWriteTransaction};
6use crate::prelude::*;
7use crate::server::access::Access;
8use crate::utils::{backup_code_from_random, readable_password_from_random, uuid_from_duration};
9use crate::value::{CredUpdateSessionPerms, CredentialType, IntentTokenState, LABEL_RE};
10use compact_jwt::compact::JweCompact;
11use compact_jwt::jwe::JweBuilder;
12use core::ops::Deref;
13use hashbrown::HashSet;
14use kanidm_proto::internal::{
15 CUCredState, CUExtPortal, CURegState, CURegWarning, CUStatus, CredentialDetail, PasskeyDetail,
16 PasswordFeedback, TotpSecret,
17};
18use kanidm_proto::v1::OutboundMessage;
19use serde::{Deserialize, Serialize};
20use sshkey_attest::proto::PublicKey as SshPublicKey;
21use std::collections::BTreeMap;
22use std::fmt::{self, Display};
23use std::sync::{Arc, Mutex};
24use std::time::Duration;
25use time::OffsetDateTime;
26use webauthn_rs::prelude::{
27 AttestedPasskey as AttestedPasskeyV4, AttestedPasskeyRegistration, CreationChallengeResponse,
28 Passkey as PasskeyV4, PasskeyRegistration, RegisterPublicKeyCredential, WebauthnError,
29};
30use zxcvbn::{zxcvbn, Score};
31
32const MAXIMUM_CRED_UPDATE_TTL: Duration = Duration::from_secs(900);
35const MINIMUM_INTENT_TTL: Duration = Duration::from_secs(300);
37const DEFAULT_INTENT_TTL: Duration = Duration::from_secs(3600);
39const MAXIMUM_INTENT_TTL: Duration = Duration::from_secs(86400);
41
42#[derive(Debug)]
43pub enum PasswordQuality {
44 TooShort(u32),
45 BadListed,
46 DontReusePasswords,
47 Feedback(Vec<PasswordFeedback>),
48}
49
50#[derive(Clone, Debug)]
51pub struct CredentialUpdateIntentToken {
52 pub intent_id: String,
53 pub expiry_time: OffsetDateTime,
54}
55
56#[derive(Clone, Debug)]
57pub struct CredentialUpdateIntentTokenExchange {
58 pub intent_id: String,
59}
60
61impl From<CredentialUpdateIntentToken> for CredentialUpdateIntentTokenExchange {
62 fn from(tok: CredentialUpdateIntentToken) -> Self {
63 CredentialUpdateIntentTokenExchange {
64 intent_id: tok.intent_id,
65 }
66 }
67}
68
69#[derive(Serialize, Deserialize, Debug)]
70struct CredentialUpdateSessionTokenInner {
71 pub sessionid: Uuid,
72 pub max_ttl: Duration,
74}
75
76#[derive(Debug)]
77pub struct CredentialUpdateSessionToken {
78 pub token_enc: JweCompact,
79}
80
81#[derive(Clone)]
83enum MfaRegState {
84 None,
85 TotpInit(Totp),
86 TotpTryAgain(Totp),
87 TotpNameTryAgain(Totp, String),
88 TotpInvalidSha1(Totp, Totp, String),
89 Passkey(Box<CreationChallengeResponse>, PasskeyRegistration),
90 #[allow(dead_code)]
91 AttestedPasskey(Box<CreationChallengeResponse>, AttestedPasskeyRegistration),
92}
93
94impl fmt::Debug for MfaRegState {
95 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
96 let t = match self {
97 MfaRegState::None => "MfaRegState::None",
98 MfaRegState::TotpInit(_) => "MfaRegState::TotpInit",
99 MfaRegState::TotpTryAgain(_) => "MfaRegState::TotpTryAgain",
100 MfaRegState::TotpNameTryAgain(_, _) => "MfaRegState::TotpNameTryAgain",
101 MfaRegState::TotpInvalidSha1(_, _, _) => "MfaRegState::TotpInvalidSha1",
102 MfaRegState::Passkey(_, _) => "MfaRegState::Passkey",
103 MfaRegState::AttestedPasskey(_, _) => "MfaRegState::AttestedPasskey",
104 };
105 write!(f, "{t}")
106 }
107}
108
109#[derive(Debug, Clone, Copy)]
110enum CredentialState {
111 Modifiable,
112 DeleteOnly,
113 AccessDeny,
114 PolicyDeny,
115 }
117
118impl From<CredentialState> for CUCredState {
119 fn from(val: CredentialState) -> CUCredState {
120 match val {
121 CredentialState::Modifiable => CUCredState::Modifiable,
122 CredentialState::DeleteOnly => CUCredState::DeleteOnly,
123 CredentialState::AccessDeny => CUCredState::AccessDeny,
124 CredentialState::PolicyDeny => CUCredState::PolicyDeny,
125 }
127 }
128}
129
130#[derive(Clone)]
131pub(crate) struct CredentialUpdateSession {
132 issuer: String,
133 account: Account,
135 resolved_account_policy: ResolvedAccountPolicy,
137 intent_token_id: Option<String>,
139
140 ext_cred_portal: CUExtPortal,
142
143 primary_state: CredentialState,
145 primary: Option<Credential>,
146
147 unixcred: Option<Credential>,
149 unixcred_state: CredentialState,
150
151 sshkeys: BTreeMap<String, SshPublicKey>,
153 sshkeys_state: CredentialState,
154
155 passkeys: BTreeMap<Uuid, (String, PasskeyV4)>,
157 passkeys_state: CredentialState,
158
159 attested_passkeys: BTreeMap<Uuid, (String, AttestedPasskeyV4)>,
161 attested_passkeys_state: CredentialState,
162
163 mfaregstate: MfaRegState,
165}
166
167impl fmt::Debug for CredentialUpdateSession {
168 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
169 let primary: Option<CredentialDetail> = self.primary.as_ref().map(|c| c.into());
170 let passkeys: Vec<PasskeyDetail> = self
171 .passkeys
172 .iter()
173 .map(|(uuid, (tag, _pk))| PasskeyDetail {
174 tag: tag.clone(),
175 uuid: *uuid,
176 })
177 .collect();
178 let attested_passkeys: Vec<PasskeyDetail> = self
179 .attested_passkeys
180 .iter()
181 .map(|(uuid, (tag, _pk))| PasskeyDetail {
182 tag: tag.clone(),
183 uuid: *uuid,
184 })
185 .collect();
186 f.debug_struct("CredentialUpdateSession")
187 .field("account.spn", &self.account.spn())
188 .field("account.unix", &self.account.unix_extn().is_some())
189 .field("resolved_account_policy", &self.resolved_account_policy)
190 .field("intent_token_id", &self.intent_token_id)
191 .field("primary.detail()", &primary)
192 .field("primary.state", &self.primary_state)
193 .field("passkeys.list()", &passkeys)
194 .field("passkeys.state", &self.passkeys_state)
195 .field("attested_passkeys.list()", &attested_passkeys)
196 .field("attested_passkeys.state", &self.attested_passkeys_state)
197 .field("mfaregstate", &self.mfaregstate)
198 .finish()
199 }
200}
201
202impl CredentialUpdateSession {
203 fn can_commit(&self) -> (bool, Vec<CredentialUpdateSessionStatusWarnings>) {
205 let mut warnings = Vec::with_capacity(0);
206 let mut can_commit = true;
207
208 let cred_type_min = self.resolved_account_policy.credential_policy();
209
210 debug!(?cred_type_min);
211
212 match cred_type_min {
213 CredentialType::Any => {}
214 CredentialType::External | CredentialType::Mfa => {
215 if self
216 .primary
217 .as_ref()
218 .map(|cred| !cred.is_mfa())
219 .unwrap_or(false)
222 {
223 can_commit = false;
224 warnings.push(CredentialUpdateSessionStatusWarnings::MfaRequired);
225 }
226 }
227 CredentialType::Passkey => {
228 if self.primary.is_some() {
231 can_commit = false;
232 warnings.push(CredentialUpdateSessionStatusWarnings::PasskeyRequired);
233 }
234 }
235 CredentialType::AttestedPasskey => {
236 if !self.passkeys.is_empty() || self.primary.is_some() {
238 can_commit = false;
239 warnings.push(CredentialUpdateSessionStatusWarnings::AttestedPasskeyRequired);
240 }
241 }
242 CredentialType::AttestedResidentkey => {
243 if !self.attested_passkeys.is_empty()
245 || !self.passkeys.is_empty()
246 || self.primary.is_some()
247 {
248 can_commit = false;
249 warnings
250 .push(CredentialUpdateSessionStatusWarnings::AttestedResidentKeyRequired);
251 }
252 }
253 CredentialType::Invalid => {
254 can_commit = false;
256 warnings.push(CredentialUpdateSessionStatusWarnings::Unsatisfiable)
257 }
258 }
259
260 if let Some(att_ca_list) = self.resolved_account_policy.webauthn_attestation_ca_list() {
261 if att_ca_list.is_empty() {
262 warnings
263 .push(CredentialUpdateSessionStatusWarnings::WebauthnAttestationUnsatisfiable)
264 }
265 }
266
267 if can_commit
269 && self.attested_passkeys.is_empty()
270 && self.passkeys.is_empty()
271 && self.primary.is_none()
272 {
273 can_commit = false;
275 warnings.push(CredentialUpdateSessionStatusWarnings::NoValidCredentials)
276 }
277
278 (can_commit, warnings)
279 }
280}
281
282pub enum MfaRegStateStatus {
283 None,
285 TotpCheck(TotpSecret),
286 TotpTryAgain,
287 TotpNameTryAgain(String),
288 TotpInvalidSha1,
289 BackupCodes(HashSet<String>),
290 Passkey(CreationChallengeResponse),
291 AttestedPasskey(CreationChallengeResponse),
292}
293
294impl fmt::Debug for MfaRegStateStatus {
295 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
296 let t = match self {
297 MfaRegStateStatus::None => "MfaRegStateStatus::None",
298 MfaRegStateStatus::TotpCheck(_) => "MfaRegStateStatus::TotpCheck",
299 MfaRegStateStatus::TotpTryAgain => "MfaRegStateStatus::TotpTryAgain",
300 MfaRegStateStatus::TotpNameTryAgain(_) => "MfaRegStateStatus::TotpNameTryAgain",
301 MfaRegStateStatus::TotpInvalidSha1 => "MfaRegStateStatus::TotpInvalidSha1",
302 MfaRegStateStatus::BackupCodes(_) => "MfaRegStateStatus::BackupCodes",
303 MfaRegStateStatus::Passkey(_) => "MfaRegStateStatus::Passkey",
304 MfaRegStateStatus::AttestedPasskey(_) => "MfaRegStateStatus::AttestedPasskey",
305 };
306 write!(f, "{t}")
307 }
308}
309
310#[derive(Debug, PartialEq, Eq, Clone, Copy)]
311pub enum CredentialUpdateSessionStatusWarnings {
312 MfaRequired,
313 PasskeyRequired,
314 AttestedPasskeyRequired,
315 AttestedResidentKeyRequired,
316 Unsatisfiable,
317 WebauthnAttestationUnsatisfiable,
318 WebauthnUserVerificationRequired,
319 NoValidCredentials,
320}
321
322impl Display for CredentialUpdateSessionStatusWarnings {
323 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
324 write!(f, "{self:?}")
325 }
326}
327
328impl From<CredentialUpdateSessionStatusWarnings> for CURegWarning {
329 fn from(val: CredentialUpdateSessionStatusWarnings) -> CURegWarning {
330 match val {
331 CredentialUpdateSessionStatusWarnings::MfaRequired => CURegWarning::MfaRequired,
332 CredentialUpdateSessionStatusWarnings::PasskeyRequired => CURegWarning::PasskeyRequired,
333 CredentialUpdateSessionStatusWarnings::AttestedPasskeyRequired => {
334 CURegWarning::AttestedPasskeyRequired
335 }
336 CredentialUpdateSessionStatusWarnings::AttestedResidentKeyRequired => {
337 CURegWarning::AttestedResidentKeyRequired
338 }
339 CredentialUpdateSessionStatusWarnings::Unsatisfiable => CURegWarning::Unsatisfiable,
340 CredentialUpdateSessionStatusWarnings::WebauthnAttestationUnsatisfiable => {
341 CURegWarning::WebauthnAttestationUnsatisfiable
342 }
343 CredentialUpdateSessionStatusWarnings::WebauthnUserVerificationRequired => {
344 CURegWarning::WebauthnUserVerificationRequired
345 }
346 CredentialUpdateSessionStatusWarnings::NoValidCredentials => {
347 CURegWarning::NoValidCredentials
348 }
349 }
350 }
351}
352
353#[derive(Debug)]
354pub struct CredentialUpdateSessionStatus {
355 spn: String,
356 displayname: String,
358 ext_cred_portal: CUExtPortal,
359 mfaregstate: MfaRegStateStatus,
361 can_commit: bool,
362 warnings: Vec<CredentialUpdateSessionStatusWarnings>,
364 primary: Option<CredentialDetail>,
365 primary_state: CredentialState,
366 passkeys: Vec<PasskeyDetail>,
367 passkeys_state: CredentialState,
368 attested_passkeys: Vec<PasskeyDetail>,
369 attested_passkeys_state: CredentialState,
370 attested_passkeys_allowed_devices: Vec<String>,
371
372 unixcred: Option<CredentialDetail>,
373 unixcred_state: CredentialState,
374
375 sshkeys: BTreeMap<String, SshPublicKey>,
376 sshkeys_state: CredentialState,
377}
378
379impl CredentialUpdateSessionStatus {
380 pub fn append_ephemeral_warning(&mut self, warning: CredentialUpdateSessionStatusWarnings) {
384 self.warnings.push(warning)
385 }
386
387 pub fn can_commit(&self) -> bool {
388 self.can_commit
389 }
390
391 pub fn mfaregstate(&self) -> &MfaRegStateStatus {
392 &self.mfaregstate
393 }
394}
395
396#[allow(clippy::from_over_into)]
399impl Into<CUStatus> for CredentialUpdateSessionStatus {
400 fn into(self) -> CUStatus {
401 CUStatus {
402 spn: self.spn,
403 displayname: self.displayname,
404 ext_cred_portal: self.ext_cred_portal,
405 mfaregstate: match self.mfaregstate {
406 MfaRegStateStatus::None => CURegState::None,
407 MfaRegStateStatus::TotpCheck(c) => CURegState::TotpCheck(c),
408 MfaRegStateStatus::TotpTryAgain => CURegState::TotpTryAgain,
409 MfaRegStateStatus::TotpNameTryAgain(label) => CURegState::TotpNameTryAgain(label),
410 MfaRegStateStatus::TotpInvalidSha1 => CURegState::TotpInvalidSha1,
411 MfaRegStateStatus::BackupCodes(s) => {
412 CURegState::BackupCodes(s.into_iter().collect())
413 }
414 MfaRegStateStatus::Passkey(r) => CURegState::Passkey(r),
415 MfaRegStateStatus::AttestedPasskey(r) => CURegState::AttestedPasskey(r),
416 },
417 can_commit: self.can_commit,
418 warnings: self.warnings.into_iter().map(|w| w.into()).collect(),
419 primary: self.primary,
420 primary_state: self.primary_state.into(),
421 passkeys: self.passkeys,
422 passkeys_state: self.passkeys_state.into(),
423 attested_passkeys: self.attested_passkeys,
424 attested_passkeys_state: self.attested_passkeys_state.into(),
425 attested_passkeys_allowed_devices: self.attested_passkeys_allowed_devices,
426 unixcred: self.unixcred,
427 unixcred_state: self.unixcred_state.into(),
428 sshkeys: self.sshkeys,
429 sshkeys_state: self.sshkeys_state.into(),
430 }
431 }
432}
433
434impl From<&CredentialUpdateSession> for CredentialUpdateSessionStatus {
435 fn from(session: &CredentialUpdateSession) -> Self {
436 let (can_commit, warnings) = session.can_commit();
437
438 let attested_passkeys_allowed_devices: Vec<String> = session
439 .resolved_account_policy
440 .webauthn_attestation_ca_list()
441 .iter()
442 .flat_map(|att_ca_list: &&webauthn_rs::prelude::AttestationCaList| {
443 att_ca_list.cas().values().flat_map(|ca| {
444 ca.aaguids()
445 .values()
446 .map(|device| device.description_en().to_string())
447 })
448 })
449 .collect();
450
451 CredentialUpdateSessionStatus {
452 spn: session.account.spn().into(),
453 displayname: session.account.displayname.clone(),
454 ext_cred_portal: session.ext_cred_portal.clone(),
455 can_commit,
456 warnings,
457 primary: session.primary.as_ref().map(|c| c.into()),
458 primary_state: session.primary_state,
459 passkeys: session
460 .passkeys
461 .iter()
462 .map(|(uuid, (tag, _pk))| PasskeyDetail {
463 tag: tag.clone(),
464 uuid: *uuid,
465 })
466 .collect(),
467 passkeys_state: session.passkeys_state,
468 attested_passkeys: session
469 .attested_passkeys
470 .iter()
471 .map(|(uuid, (tag, _pk))| PasskeyDetail {
472 tag: tag.clone(),
473 uuid: *uuid,
474 })
475 .collect(),
476 attested_passkeys_state: session.attested_passkeys_state,
477 attested_passkeys_allowed_devices,
478
479 unixcred: session.unixcred.as_ref().map(|c| c.into()),
480 unixcred_state: session.unixcred_state,
481
482 sshkeys: session.sshkeys.clone(),
483 sshkeys_state: session.sshkeys_state,
484
485 mfaregstate: match &session.mfaregstate {
486 MfaRegState::None => MfaRegStateStatus::None,
487 MfaRegState::TotpInit(token) => MfaRegStateStatus::TotpCheck(
488 token.to_proto(session.account.spn(), session.issuer.as_str()),
489 ),
490 MfaRegState::TotpNameTryAgain(_, name) => {
491 MfaRegStateStatus::TotpNameTryAgain(name.clone())
492 }
493 MfaRegState::TotpTryAgain(_) => MfaRegStateStatus::TotpTryAgain,
494 MfaRegState::TotpInvalidSha1(_, _, _) => MfaRegStateStatus::TotpInvalidSha1,
495 MfaRegState::Passkey(r, _) => MfaRegStateStatus::Passkey(r.as_ref().clone()),
496 MfaRegState::AttestedPasskey(r, _) => {
497 MfaRegStateStatus::AttestedPasskey(r.as_ref().clone())
498 }
499 },
500 }
501 }
502}
503
504pub(crate) type CredentialUpdateSessionMutex = Arc<Mutex<CredentialUpdateSession>>;
505
506pub struct InitCredentialUpdateIntentEvent {
507 pub ident: Identity,
509 pub target: Uuid,
511 pub max_ttl: Option<Duration>,
513}
514
515impl InitCredentialUpdateIntentEvent {
516 pub fn new(ident: Identity, target: Uuid, max_ttl: Option<Duration>) -> Self {
517 InitCredentialUpdateIntentEvent {
518 ident,
519 target,
520 max_ttl,
521 }
522 }
523
524 #[cfg(test)]
525 pub fn new_impersonate_entry(
526 e: std::sync::Arc<Entry<EntrySealed, EntryCommitted>>,
527 target: Uuid,
528 max_ttl: Duration,
529 ) -> Self {
530 let ident = Identity::from_impersonate_entry_readwrite(e);
531 InitCredentialUpdateIntentEvent {
532 ident,
533 target,
534 max_ttl: Some(max_ttl),
535 }
536 }
537}
538
539pub struct CredentialUpdateAccountRecovery {
540 pub email: String,
542 pub max_ttl: Option<Duration>,
544}
545
546pub struct InitCredentialUpdateIntentSendEvent {
547 pub ident: Identity,
549 pub target: Uuid,
551 pub max_ttl: Option<Duration>,
553 pub email: Option<String>,
555}
556
557pub struct InitCredentialUpdateEvent {
558 pub ident: Identity,
559 pub target: Uuid,
560}
561
562impl InitCredentialUpdateEvent {
563 pub fn new(ident: Identity, target: Uuid) -> Self {
564 InitCredentialUpdateEvent { ident, target }
565 }
566
567 #[cfg(test)]
568 pub fn new_impersonate_entry(e: std::sync::Arc<Entry<EntrySealed, EntryCommitted>>) -> Self {
569 let ident = Identity::from_impersonate_entry_readwrite(e);
570 let target = ident.get_uuid();
571 InitCredentialUpdateEvent { ident, target }
572 }
573}
574
575impl IdmServerProxyWriteTransaction<'_> {
576 fn validate_init_credential_update(
577 &mut self,
578 target: Uuid,
579 ident: &Identity,
580 ) -> Result<(Account, ResolvedAccountPolicy, CredUpdateSessionPerms), OperationError> {
581 let entry = self.qs_write.internal_search_uuid(target)?;
582
583 security_info!(
584 %target,
585 "Initiating Credential Update Session",
586 );
587
588 if ident.access_scope() != AccessScope::ReadWrite {
591 security_access!("identity access scope is not permitted to modify");
592 security_access!("denied ❌");
593 return Err(OperationError::AccessDenied);
594 }
595
596 let (account, resolved_account_policy) =
598 Account::try_from_entry_with_policy(entry.as_ref(), &mut self.qs_write)?;
599
600 let effective_perms = self
601 .qs_write
602 .get_accesscontrols()
603 .effective_permission_check(
604 ident,
605 Some(btreeset![
606 Attribute::PrimaryCredential,
607 Attribute::PassKeys,
608 Attribute::AttestedPasskeys,
609 Attribute::UnixPassword,
610 Attribute::SshPublicKey
611 ]),
612 &[entry],
613 )?;
614
615 let eperm = effective_perms.first().ok_or_else(|| {
616 error!("Effective Permission check returned no results");
617 OperationError::InvalidState
618 })?;
619
620 if eperm.target != account.uuid {
624 error!("Effective Permission check target differs from requested entry uuid");
625 return Err(OperationError::InvalidEntryState);
626 }
627
628 let eperm_search_primary_cred = match &eperm.search {
629 Access::Deny => false,
630 Access::Grant => true,
631 Access::Allow(attrs) => attrs.contains(&Attribute::PrimaryCredential),
632 };
633
634 let eperm_mod_primary_cred = match &eperm.modify_pres {
635 Access::Deny => false,
636 Access::Grant => true,
637 Access::Allow(attrs) => attrs.contains(&Attribute::PrimaryCredential),
638 };
639
640 let eperm_rem_primary_cred = match &eperm.modify_rem {
641 Access::Deny => false,
642 Access::Grant => true,
643 Access::Allow(attrs) => attrs.contains(&Attribute::PrimaryCredential),
644 };
645
646 let primary_can_edit =
647 eperm_search_primary_cred && eperm_mod_primary_cred && eperm_rem_primary_cred;
648
649 let eperm_search_passkeys = match &eperm.search {
650 Access::Deny => false,
651 Access::Grant => true,
652 Access::Allow(attrs) => attrs.contains(&Attribute::PassKeys),
653 };
654
655 let eperm_mod_passkeys = match &eperm.modify_pres {
656 Access::Deny => false,
657 Access::Grant => true,
658 Access::Allow(attrs) => attrs.contains(&Attribute::PassKeys),
659 };
660
661 let eperm_rem_passkeys = match &eperm.modify_rem {
662 Access::Deny => false,
663 Access::Grant => true,
664 Access::Allow(attrs) => attrs.contains(&Attribute::PassKeys),
665 };
666
667 let passkeys_can_edit = eperm_search_passkeys && eperm_mod_passkeys && eperm_rem_passkeys;
668
669 let eperm_search_attested_passkeys = match &eperm.search {
670 Access::Deny => false,
671 Access::Grant => true,
672 Access::Allow(attrs) => attrs.contains(&Attribute::AttestedPasskeys),
673 };
674
675 let eperm_mod_attested_passkeys = match &eperm.modify_pres {
676 Access::Deny => false,
677 Access::Grant => true,
678 Access::Allow(attrs) => attrs.contains(&Attribute::AttestedPasskeys),
679 };
680
681 let eperm_rem_attested_passkeys = match &eperm.modify_rem {
682 Access::Deny => false,
683 Access::Grant => true,
684 Access::Allow(attrs) => attrs.contains(&Attribute::AttestedPasskeys),
685 };
686
687 let attested_passkeys_can_edit = eperm_search_attested_passkeys
688 && eperm_mod_attested_passkeys
689 && eperm_rem_attested_passkeys;
690
691 let eperm_search_unixcred = match &eperm.search {
692 Access::Deny => false,
693 Access::Grant => true,
694 Access::Allow(attrs) => attrs.contains(&Attribute::UnixPassword),
695 };
696
697 let eperm_mod_unixcred = match &eperm.modify_pres {
698 Access::Deny => false,
699 Access::Grant => true,
700 Access::Allow(attrs) => attrs.contains(&Attribute::UnixPassword),
701 };
702
703 let eperm_rem_unixcred = match &eperm.modify_rem {
704 Access::Deny => false,
705 Access::Grant => true,
706 Access::Allow(attrs) => attrs.contains(&Attribute::UnixPassword),
707 };
708
709 let unixcred_can_edit = account.unix_extn().is_some()
710 && eperm_search_unixcred
711 && eperm_mod_unixcred
712 && eperm_rem_unixcred;
713
714 let eperm_search_sshpubkey = match &eperm.search {
715 Access::Deny => false,
716 Access::Grant => true,
717 Access::Allow(attrs) => attrs.contains(&Attribute::SshPublicKey),
718 };
719
720 let eperm_mod_sshpubkey = match &eperm.modify_pres {
721 Access::Deny => false,
722 Access::Grant => true,
723 Access::Allow(attrs) => attrs.contains(&Attribute::SshPublicKey),
724 };
725
726 let eperm_rem_sshpubkey = match &eperm.modify_rem {
727 Access::Deny => false,
728 Access::Grant => true,
729 Access::Allow(attrs) => attrs.contains(&Attribute::SshPublicKey),
730 };
731
732 let sshpubkey_can_edit = account.unix_extn().is_some()
733 && eperm_search_sshpubkey
734 && eperm_mod_sshpubkey
735 && eperm_rem_sshpubkey;
736
737 let ext_cred_portal_can_view = if let Some(sync_parent_uuid) = account.sync_parent_uuid {
738 let entry = self.qs_write.internal_search_uuid(sync_parent_uuid)?;
740
741 let effective_perms = self
742 .qs_write
743 .get_accesscontrols()
744 .effective_permission_check(
745 ident,
746 Some(btreeset![Attribute::SyncCredentialPortal]),
747 &[entry],
748 )?;
749
750 let eperm = effective_perms.first().ok_or_else(|| {
751 admin_error!("Effective Permission check returned no results");
752 OperationError::InvalidState
753 })?;
754
755 match &eperm.search {
756 Access::Deny => false,
757 Access::Grant => true,
758 Access::Allow(attrs) => attrs.contains(&Attribute::SyncCredentialPortal),
759 }
760 } else {
761 false
762 };
763
764 if !(primary_can_edit
766 || passkeys_can_edit
767 || attested_passkeys_can_edit
768 || ext_cred_portal_can_view
769 || sshpubkey_can_edit
770 || unixcred_can_edit)
771 {
772 error!("Unable to proceed with credential update intent - at least one type of credential must be modifiable or visible.");
773 Err(OperationError::NotAuthorised)
774 } else {
775 security_info!(%primary_can_edit, %passkeys_can_edit, %unixcred_can_edit, %sshpubkey_can_edit, %ext_cred_portal_can_view, "Proceeding");
776 Ok((
777 account,
778 resolved_account_policy,
779 CredUpdateSessionPerms {
780 ext_cred_portal_can_view,
781 passkeys_can_edit,
782 attested_passkeys_can_edit,
783 primary_can_edit,
784 unixcred_can_edit,
785 sshpubkey_can_edit,
786 },
787 ))
788 }
789 }
790
791 fn create_credupdate_session(
792 &mut self,
793 sessionid: Uuid,
794 intent_token_id: Option<String>,
795 account: Account,
796 resolved_account_policy: ResolvedAccountPolicy,
797 perms: CredUpdateSessionPerms,
798 ct: Duration,
799 ) -> Result<(CredentialUpdateSessionToken, CredentialUpdateSessionStatus), OperationError> {
800 let ext_cred_portal_can_view = perms.ext_cred_portal_can_view;
801
802 let cred_type_min = resolved_account_policy.credential_policy();
803
804 let passkey_attestation_required = resolved_account_policy
808 .webauthn_attestation_ca_list()
809 .is_some();
810
811 let primary_state = if cred_type_min > CredentialType::Mfa {
812 CredentialState::PolicyDeny
813 } else if perms.primary_can_edit {
814 CredentialState::Modifiable
815 } else {
816 CredentialState::AccessDeny
817 };
818
819 let passkeys_state =
820 if cred_type_min > CredentialType::Passkey || passkey_attestation_required {
821 CredentialState::PolicyDeny
822 } else if perms.passkeys_can_edit {
823 CredentialState::Modifiable
824 } else {
825 CredentialState::AccessDeny
826 };
827
828 let attested_passkeys_state = if cred_type_min > CredentialType::AttestedPasskey {
829 CredentialState::PolicyDeny
830 } else if perms.attested_passkeys_can_edit {
831 if passkey_attestation_required {
832 CredentialState::Modifiable
833 } else {
834 CredentialState::DeleteOnly
836 }
837 } else {
838 CredentialState::AccessDeny
839 };
840
841 let unixcred_state = if account.unix_extn().is_none() {
842 CredentialState::PolicyDeny
843 } else if perms.unixcred_can_edit {
844 CredentialState::Modifiable
845 } else {
846 CredentialState::AccessDeny
847 };
848
849 let sshkeys_state = if perms.sshpubkey_can_edit {
850 CredentialState::Modifiable
851 } else {
852 CredentialState::AccessDeny
853 };
854
855 let primary = if matches!(primary_state, CredentialState::Modifiable) {
857 account.primary.clone()
858 } else {
859 None
860 };
861
862 let passkeys = if matches!(passkeys_state, CredentialState::Modifiable) {
863 account.passkeys.clone()
864 } else {
865 BTreeMap::default()
866 };
867
868 let unixcred: Option<Credential> = if matches!(unixcred_state, CredentialState::Modifiable)
869 {
870 account.unix_extn().and_then(|uext| uext.ucred()).cloned()
871 } else {
872 None
873 };
874
875 let sshkeys = if matches!(sshkeys_state, CredentialState::Modifiable) {
876 account.sshkeys().clone()
877 } else {
878 BTreeMap::default()
879 };
880
881 let attested_passkeys = if matches!(attested_passkeys_state, CredentialState::Modifiable)
885 || matches!(attested_passkeys_state, CredentialState::DeleteOnly)
886 {
887 if let Some(att_ca_list) = resolved_account_policy.webauthn_attestation_ca_list() {
888 let mut attested_passkeys = BTreeMap::default();
889
890 for (uuid, (label, apk)) in account.attested_passkeys.iter() {
891 match apk.verify_attestation(att_ca_list) {
892 Ok(_) => {
893 attested_passkeys.insert(*uuid, (label.clone(), apk.clone()));
895 }
896 Err(e) => {
897 warn!(eclass=?e, emsg=%e, "credential no longer meets attestation criteria");
898 }
899 }
900 }
901
902 attested_passkeys
903 } else {
904 account.attested_passkeys.clone()
908 }
909 } else {
910 BTreeMap::default()
911 };
912
913 let ext_cred_portal = match (account.sync_parent_uuid, ext_cred_portal_can_view) {
915 (Some(sync_parent_uuid), true) => {
916 let sync_entry = self.qs_write.internal_search_uuid(sync_parent_uuid)?;
917 sync_entry
918 .get_ava_single_url(Attribute::SyncCredentialPortal)
919 .cloned()
920 .map(CUExtPortal::Some)
921 .unwrap_or(CUExtPortal::Hidden)
922 }
923 (Some(_), false) => CUExtPortal::Hidden,
924 (None, _) => CUExtPortal::None,
925 };
926
927 let issuer = self.qs_write.get_domain_display_name().to_string();
929
930 let session = CredentialUpdateSession {
932 account,
933 resolved_account_policy,
934 issuer,
935 intent_token_id,
936 ext_cred_portal,
937 primary,
938 primary_state,
939 unixcred,
940 unixcred_state,
941 sshkeys,
942 sshkeys_state,
943 passkeys,
944 passkeys_state,
945 attested_passkeys,
946 attested_passkeys_state,
947 mfaregstate: MfaRegState::None,
948 };
949
950 let max_ttl = ct + MAXIMUM_CRED_UPDATE_TTL;
951
952 let token = CredentialUpdateSessionTokenInner { sessionid, max_ttl };
953
954 let token_data = serde_json::to_vec(&token).map_err(|e| {
955 admin_error!(err = ?e, "Unable to encode token data");
956 OperationError::SerdeJsonError
957 })?;
958
959 let token_jwe = JweBuilder::from(token_data).build();
960
961 let token_enc = self
962 .qs_write
963 .get_domain_key_object_handle()?
964 .jwe_a128gcm_encrypt(&token_jwe, ct)?;
965
966 let status: CredentialUpdateSessionStatus = (&session).into();
967
968 let session = Arc::new(Mutex::new(session));
969
970 self.expire_credential_update_sessions(ct);
974
975 self.cred_update_sessions.insert(sessionid, session);
977 trace!("cred_update_sessions.insert - {}", sessionid);
978
979 Ok((CredentialUpdateSessionToken { token_enc }, status))
981 }
982
983 pub fn credential_update_account_recovery(
984 &mut self,
985 event: CredentialUpdateAccountRecovery,
986 ct: Duration,
987 ) -> Result<(), OperationError> {
988 if !self.qs_write.domain_info().allow_account_recovery() {
989 error!("Account Recovery is Disabled, Rejecting Attempt");
990 return Err(OperationError::CU0010AccountRecoveryDisabled);
991 }
992
993 let ident = Identity::account_request();
996
997 let filter = filter!(f_eq(
999 Attribute::Mail,
1000 PartialValue::EmailAddress(event.email.clone())
1001 ));
1002
1003 let mut entries = self
1004 .qs_write
1005 .impersonate_search(filter.clone(), filter, &ident)?;
1006
1007 let entry = entries
1008 .pop()
1009 .and_then(|entry| entries.is_empty().then_some(entry))
1010 .ok_or(OperationError::CU0009AccountEmailNotFound)?;
1011
1012 let target = entry.get_uuid();
1013
1014 let target_ident = Identity::from_impersonate_entry_readwrite(entry);
1023
1024 let (account, _resolved_account_policy, perms) =
1025 self.validate_init_credential_update(target, &target_ident)?;
1026
1027 self.process_credential_update_send(&account, event.max_ttl, perms, event.email, ct)
1030 }
1031
1032 #[instrument(level = "debug", skip_all)]
1033 pub fn init_credential_update_intent_send(
1034 &mut self,
1035 event: InitCredentialUpdateIntentSendEvent,
1036 ct: Duration,
1037 ) -> Result<(), OperationError> {
1038 let (account, _resolved_account_policy, perms) =
1039 self.validate_init_credential_update(event.target, &event.ident)?;
1040
1041 let to_email = if let Some(to_email) = event.email {
1044 account.mail().contains(&to_email)
1045 .then_some(to_email)
1046 .ok_or_else(|| {
1047 error!(spn = %account.spn(), "Requested email address is not present on account, unable to send credential reset.");
1048 OperationError::CU0007AccountEmailNotFound
1049 })
1050 } else {
1051 let maybe_to_email = account.mail_primary().map(String::from);
1052
1053 maybe_to_email.ok_or_else(|| {
1054 error!(spn = %account.spn(), "account does not have a primary email address, unable to send credential reset.");
1055 OperationError::CU0008AccountMissingEmail
1056 })
1057 }?;
1058
1059 self.process_credential_update_send(&account, event.max_ttl, perms, to_email, ct)
1060 }
1061
1062 fn process_credential_update_send(
1063 &mut self,
1064 account: &Account,
1065 max_ttl: Option<Duration>,
1066 perms: CredUpdateSessionPerms,
1067 to_email: String,
1068 ct: Duration,
1069 ) -> Result<(), OperationError> {
1070 let (intent_id, expiry_time) =
1072 self.build_credential_update_intent(max_ttl, account, perms, ct)?;
1073
1074 let display_name = account.display_name().to_owned();
1076
1077 let message = OutboundMessage::CredentialResetV1 {
1078 display_name,
1079 intent_id,
1080 expiry_time,
1081 };
1082
1083 let ident = Identity::message_queue();
1084
1085 self.qs_write.queue_message(
1086 &ident, message, to_email,
1089 )
1090 }
1091
1092 #[instrument(level = "debug", skip_all)]
1093 pub fn init_credential_update_intent(
1094 &mut self,
1095 event: &InitCredentialUpdateIntentEvent,
1096 ct: Duration,
1097 ) -> Result<CredentialUpdateIntentToken, OperationError> {
1098 let (account, _resolved_account_policy, perms) =
1099 self.validate_init_credential_update(event.target, &event.ident)?;
1100
1101 let (intent_id, expiry_time) =
1106 self.build_credential_update_intent(event.max_ttl, &account, perms, ct)?;
1107
1108 Ok(CredentialUpdateIntentToken {
1109 intent_id,
1110 expiry_time,
1111 })
1112 }
1113
1114 fn build_credential_update_intent(
1115 &mut self,
1116 max_ttl: Option<Duration>,
1117 account: &Account,
1118 perms: CredUpdateSessionPerms,
1119 ct: Duration,
1120 ) -> Result<(String, OffsetDateTime), OperationError> {
1121 let mttl = max_ttl.unwrap_or(DEFAULT_INTENT_TTL);
1125 let clamped_mttl = mttl.clamp(MINIMUM_INTENT_TTL, MAXIMUM_INTENT_TTL);
1126 debug!(?clamped_mttl, "clamped update intent validity");
1127 let max_ttl = ct + clamped_mttl;
1129
1130 let expiry_time = OffsetDateTime::UNIX_EPOCH + max_ttl;
1132
1133 let intent_id = readable_password_from_random();
1134
1135 let mut modlist = ModifyList::new_append(
1141 Attribute::CredentialUpdateIntentToken,
1142 Value::IntentToken(
1143 intent_id.clone(),
1144 IntentTokenState::Valid { max_ttl, perms },
1145 ),
1146 );
1147
1148 account
1150 .credential_update_intent_tokens
1151 .iter()
1152 .for_each(|(existing_intent_id, state)| {
1153 let max_ttl = match state {
1154 IntentTokenState::Valid { max_ttl, perms: _ }
1155 | IntentTokenState::InProgress {
1156 max_ttl,
1157 perms: _,
1158 session_id: _,
1159 session_ttl: _,
1160 }
1161 | IntentTokenState::Consumed { max_ttl } => *max_ttl,
1162 };
1163
1164 if ct >= max_ttl {
1165 modlist.push_mod(Modify::Removed(
1166 Attribute::CredentialUpdateIntentToken,
1167 PartialValue::IntentToken(existing_intent_id.clone()),
1168 ));
1169 }
1170 });
1171
1172 self.qs_write
1173 .internal_modify(
1174 &filter!(f_eq(Attribute::Uuid, PartialValue::Uuid(account.uuid))),
1176 &modlist,
1177 )
1178 .inspect_err(|err| {
1179 error!(?err);
1180 })
1181 .map(|_| (intent_id, expiry_time))
1182 }
1183
1184 #[instrument(level = "debug", skip_all)]
1185 pub fn revoke_credential_update_intent(
1186 &mut self,
1187 token: CredentialUpdateIntentTokenExchange,
1188 _current_time: Duration,
1189 ) -> Result<(), OperationError> {
1190 let CredentialUpdateIntentTokenExchange { intent_id } = token;
1191 let entries = self.qs_write.internal_search(filter!(f_eq(
1198 Attribute::CredentialUpdateIntentToken,
1199 PartialValue::IntentToken(intent_id.clone())
1200 )))?;
1201
1202 let batch_mod = entries
1204 .iter()
1205 .filter_map(|entry| {
1206 let intenttokens = entry
1207 .get_ava_set(Attribute::CredentialUpdateIntentToken)
1208 .and_then(|vs| vs.as_intenttoken_map());
1209
1210 let Some(intenttoken) = intenttokens.and_then(|m| m.get(&intent_id)) else {
1212 debug_assert!(false);
1213 return None;
1214 };
1215
1216 let max_ttl = match intenttoken {
1217 IntentTokenState::Consumed { max_ttl: _ } => return None,
1219 IntentTokenState::InProgress { max_ttl, .. }
1221 | IntentTokenState::Valid { max_ttl, .. } => *max_ttl,
1222 };
1223
1224 let entry_uuid = entry.get_uuid();
1225
1226 let mut modlist = ModifyList::new();
1227
1228 modlist.push_mod(Modify::Removed(
1229 Attribute::CredentialUpdateIntentToken,
1230 PartialValue::IntentToken(intent_id.clone()),
1231 ));
1232
1233 modlist.push_mod(Modify::Present(
1234 Attribute::CredentialUpdateIntentToken,
1235 Value::IntentToken(intent_id.clone(), IntentTokenState::Consumed { max_ttl }),
1236 ));
1237
1238 Some((entry_uuid, modlist))
1239 })
1240 .collect::<Vec<_>>();
1241
1242 self.qs_write.internal_batch_modify(batch_mod.into_iter())
1243 }
1244
1245 pub fn exchange_intent_credential_update(
1246 &mut self,
1247 token: CredentialUpdateIntentTokenExchange,
1248 current_time: Duration,
1249 ) -> Result<(CredentialUpdateSessionToken, CredentialUpdateSessionStatus), OperationError> {
1250 let CredentialUpdateIntentTokenExchange { intent_id } = token;
1251
1252 let mut vs = self.qs_write.internal_search(filter!(f_eq(
1262 Attribute::CredentialUpdateIntentToken,
1263 PartialValue::IntentToken(intent_id.clone())
1264 )))?;
1265
1266 let entry = match vs.pop() {
1267 Some(entry) => {
1268 if vs.is_empty() {
1269 entry
1271 } else {
1272 let matched_uuids = std::iter::once(entry.get_uuid())
1274 .chain(vs.iter().map(|e| e.get_uuid()))
1275 .collect::<Vec<_>>();
1276
1277 security_error!("Multiple entries had identical intent_id - for safety, rejecting the use of this intent_id! {:?}", matched_uuids);
1278
1279 return Err(OperationError::InvalidState);
1304 }
1305 }
1306 None => {
1307 security_info!(
1308 "Rejecting Update Session - Intent Token does not exist (replication delay?)",
1309 );
1310 return Err(OperationError::Wait(
1311 OffsetDateTime::UNIX_EPOCH + (current_time + Duration::from_secs(150)),
1312 ));
1313 }
1314 };
1315
1316 let (account, resolved_account_policy) =
1318 Account::try_from_entry_with_policy(entry.as_ref(), &mut self.qs_write)?;
1319
1320 let (max_ttl, perms) = match account.credential_update_intent_tokens.get(&intent_id) {
1324 Some(IntentTokenState::Consumed { max_ttl: _ }) => {
1325 security_info!(
1326 %entry,
1327 %account.uuid,
1328 "Rejecting Update Session - Intent Token has already been exchanged",
1329 );
1330 return Err(OperationError::SessionExpired);
1331 }
1332 Some(IntentTokenState::InProgress {
1333 max_ttl,
1334 perms,
1335 session_id,
1336 session_ttl,
1337 }) => {
1338 if current_time > *session_ttl {
1339 security_info!(
1341 %entry,
1342 %account.uuid,
1343 "Initiating Credential Update Session - Previous session {} has expired", session_id
1344 );
1345 } else {
1346 security_info!(
1356 %entry,
1357 %account.uuid,
1358 "Initiating Update Session - Intent Token was in use {} - this will be invalidated.", session_id
1359 );
1360 };
1361 (*max_ttl, *perms)
1362 }
1363 Some(IntentTokenState::Valid { max_ttl, perms }) => (*max_ttl, *perms),
1364 None => {
1365 admin_error!("Corruption may have occurred - index yielded an entry for intent_id, but the entry does not contain that intent_id");
1366 return Err(OperationError::InvalidState);
1367 }
1368 };
1369
1370 if current_time >= max_ttl {
1371 security_info!(?current_time, ?max_ttl, %account.uuid, "intent has expired");
1372 return Err(OperationError::SessionExpired);
1373 }
1374
1375 security_info!(
1376 %entry,
1377 %account.uuid,
1378 "Initiating Credential Update Session",
1379 );
1380
1381 let session_id = uuid_from_duration(current_time + MAXIMUM_CRED_UPDATE_TTL, self.sid);
1391
1392 let mut modlist = ModifyList::new();
1393
1394 modlist.push_mod(Modify::Removed(
1395 Attribute::CredentialUpdateIntentToken,
1396 PartialValue::IntentToken(intent_id.clone()),
1397 ));
1398 modlist.push_mod(Modify::Present(
1399 Attribute::CredentialUpdateIntentToken,
1400 Value::IntentToken(
1401 intent_id.clone(),
1402 IntentTokenState::InProgress {
1403 max_ttl,
1404 perms,
1405 session_id,
1406 session_ttl: current_time + MAXIMUM_CRED_UPDATE_TTL,
1407 },
1408 ),
1409 ));
1410
1411 self.qs_write
1412 .internal_modify(
1413 &filter!(f_eq(Attribute::Uuid, PartialValue::Uuid(account.uuid))),
1415 &modlist,
1416 )
1417 .map_err(|e| {
1418 request_error!(error = ?e);
1419 e
1420 })?;
1421
1422 self.create_credupdate_session(
1426 session_id,
1427 Some(intent_id),
1428 account,
1429 resolved_account_policy,
1430 perms,
1431 current_time,
1432 )
1433 }
1434
1435 #[instrument(level = "debug", skip_all)]
1436 pub fn init_credential_update(
1437 &mut self,
1438 event: &InitCredentialUpdateEvent,
1439 current_time: Duration,
1440 ) -> Result<(CredentialUpdateSessionToken, CredentialUpdateSessionStatus), OperationError> {
1441 let (account, resolved_account_policy, perms) =
1442 self.validate_init_credential_update(event.target, &event.ident)?;
1443
1444 let sessionid = uuid_from_duration(current_time + MAXIMUM_CRED_UPDATE_TTL, self.sid);
1448
1449 self.create_credupdate_session(
1451 sessionid,
1452 None,
1453 account,
1454 resolved_account_policy,
1455 perms,
1456 current_time,
1457 )
1458 }
1459
1460 #[instrument(level = "trace", skip(self))]
1461 pub fn expire_credential_update_sessions(&mut self, ct: Duration) {
1462 let before = self.cred_update_sessions.len();
1463 let split_at = uuid_from_duration(ct, self.sid);
1464 trace!(?split_at, "expiring less than");
1465 self.cred_update_sessions.split_off_lt(&split_at);
1466 let removed = before - self.cred_update_sessions.len();
1467 trace!(?removed);
1468 }
1469
1470 fn credential_update_commit_common(
1472 &mut self,
1473 cust: &CredentialUpdateSessionToken,
1474 ct: Duration,
1475 ) -> Result<
1476 (
1477 ModifyList<ModifyInvalid>,
1478 CredentialUpdateSession,
1479 CredentialUpdateSessionTokenInner,
1480 ),
1481 OperationError,
1482 > {
1483 let session_token: CredentialUpdateSessionTokenInner = self
1484 .qs_write
1485 .get_domain_key_object_handle()?
1486 .jwe_decrypt(&cust.token_enc)
1487 .map_err(|e| {
1488 admin_error!(?e, "Failed to decrypt credential update session request");
1489 OperationError::SessionExpired
1490 })
1491 .and_then(|data| {
1492 data.from_json().map_err(|e| {
1493 admin_error!(err = ?e, "Failed to deserialise credential update session request");
1494 OperationError::SerdeJsonError
1495 })
1496 })?;
1497
1498 if ct >= session_token.max_ttl {
1499 trace!(?ct, ?session_token.max_ttl);
1500 security_info!(%session_token.sessionid, "session expired");
1501 return Err(OperationError::SessionExpired);
1502 }
1503
1504 let session_handle = self.cred_update_sessions.remove(&session_token.sessionid)
1505 .ok_or_else(|| {
1506 admin_error!("No such sessionid exists on this server - may be due to a load balancer failover or replay? {:?}", session_token.sessionid);
1507 OperationError::InvalidState
1508 })?;
1509
1510 let session = session_handle
1511 .try_lock()
1512 .map(|guard| (*guard).clone())
1513 .map_err(|_| {
1514 admin_error!("Session already locked, unable to proceed.");
1515 OperationError::InvalidState
1516 })?;
1517
1518 trace!(?session);
1519
1520 let modlist = ModifyList::new();
1521
1522 Ok((modlist, session, session_token))
1523 }
1524
1525 pub fn commit_credential_update(
1526 &mut self,
1527 cust: &CredentialUpdateSessionToken,
1528 ct: Duration,
1529 ) -> Result<(), OperationError> {
1530 let (mut modlist, session, session_token) =
1531 self.credential_update_commit_common(cust, ct)?;
1532
1533 let can_commit = session.can_commit();
1535 if !can_commit.0 {
1536 let commit_failure_reasons = can_commit
1537 .1
1538 .iter()
1539 .map(|e| e.to_string())
1540 .collect::<Vec<String>>()
1541 .join(", ");
1542 admin_error!(
1543 "Session is unable to commit due to: {}",
1544 commit_failure_reasons
1545 );
1546 return Err(OperationError::CU0004SessionInconsistent);
1547 }
1548
1549 let entry = self.qs_write.internal_search_uuid(session.account.uuid)?;
1552 let account = Account::try_from_entry_rw(entry.as_ref(), &mut self.qs_write)?;
1553
1554 if let Some(intent_token_id) = &session.intent_token_id {
1564 let max_ttl = match account.credential_update_intent_tokens.get(intent_token_id) {
1565 Some(IntentTokenState::InProgress {
1566 max_ttl,
1567 perms: _,
1568 session_id,
1569 session_ttl: _,
1570 }) => {
1571 if *session_id != session_token.sessionid {
1572 security_info!("Session originated from an intent token, but the intent token has initiated a conflicting second update session. Refusing to commit changes.");
1573 return Err(OperationError::CU0005IntentTokenConflict);
1574 } else {
1575 *max_ttl
1576 }
1577 }
1578 Some(IntentTokenState::Consumed { max_ttl: _ })
1579 | Some(IntentTokenState::Valid {
1580 max_ttl: _,
1581 perms: _,
1582 })
1583 | None => {
1584 security_info!("Session originated from an intent token, but the intent token has transitioned to an invalid state. Refusing to commit changes.");
1585 return Err(OperationError::CU0006IntentTokenInvalidated);
1586 }
1587 };
1588
1589 modlist.push_mod(Modify::Removed(
1590 Attribute::CredentialUpdateIntentToken,
1591 PartialValue::IntentToken(intent_token_id.clone()),
1592 ));
1593 modlist.push_mod(Modify::Present(
1594 Attribute::CredentialUpdateIntentToken,
1595 Value::IntentToken(
1596 intent_token_id.clone(),
1597 IntentTokenState::Consumed { max_ttl },
1598 ),
1599 ));
1600 };
1601
1602 let mut cred_changed: Option<OffsetDateTime> = None;
1603
1604 match session.unixcred_state {
1605 CredentialState::DeleteOnly | CredentialState::Modifiable => {
1606 modlist.push_mod(Modify::Purged(Attribute::UnixPassword));
1607
1608 if let Some(ncred) = &session.unixcred {
1609 let vcred = Value::new_credential("unix", ncred.clone());
1610 modlist.push_mod(Modify::Present(Attribute::UnixPassword, vcred));
1611 cred_changed = Some(ncred.timestamp());
1612 }
1613 }
1614 CredentialState::PolicyDeny => {
1615 modlist.push_mod(Modify::Purged(Attribute::UnixPassword));
1616 }
1617 CredentialState::AccessDeny => {}
1618 };
1619
1620 if cred_changed.is_none()
1622 && session
1623 .resolved_account_policy
1624 .allow_primary_cred_fallback()
1625 != Some(true)
1626 {
1627 cred_changed = Some(OffsetDateTime::UNIX_EPOCH);
1629 }
1630
1631 match session.primary_state {
1632 CredentialState::Modifiable => {
1633 modlist.push_mod(Modify::Purged(Attribute::PrimaryCredential));
1634 if let Some(ncred) = &session.primary {
1635 let vcred = Value::new_credential("primary", ncred.clone());
1636 modlist.push_mod(Modify::Present(Attribute::PrimaryCredential, vcred));
1637
1638 cred_changed.get_or_insert(ncred.timestamp());
1639 };
1640 }
1641 CredentialState::DeleteOnly | CredentialState::PolicyDeny => {
1642 modlist.push_mod(Modify::Purged(Attribute::PrimaryCredential));
1643 }
1644 CredentialState::AccessDeny => {}
1645 };
1646
1647 cred_changed.get_or_insert(OffsetDateTime::UNIX_EPOCH);
1648
1649 if let Some(timestamp) = cred_changed {
1650 modlist.push_mod(Modify::Purged(Attribute::PasswordChangedTime));
1651 modlist.push_mod(Modify::Present(
1652 Attribute::PasswordChangedTime,
1653 Value::DateTime(timestamp),
1654 ));
1655 }
1656
1657 match session.passkeys_state {
1658 CredentialState::DeleteOnly | CredentialState::Modifiable => {
1659 modlist.push_mod(Modify::Purged(Attribute::PassKeys));
1660 session.passkeys.iter().for_each(|(uuid, (tag, pk))| {
1663 let v_pk = Value::Passkey(*uuid, tag.clone(), pk.clone());
1664 modlist.push_mod(Modify::Present(Attribute::PassKeys, v_pk));
1665 });
1666 }
1667 CredentialState::PolicyDeny => {
1668 modlist.push_mod(Modify::Purged(Attribute::PassKeys));
1669 }
1670 CredentialState::AccessDeny => {}
1671 };
1672
1673 match session.attested_passkeys_state {
1674 CredentialState::DeleteOnly | CredentialState::Modifiable => {
1675 modlist.push_mod(Modify::Purged(Attribute::AttestedPasskeys));
1676 session
1679 .attested_passkeys
1680 .iter()
1681 .for_each(|(uuid, (tag, pk))| {
1682 let v_pk = Value::AttestedPasskey(*uuid, tag.clone(), pk.clone());
1683 modlist.push_mod(Modify::Present(Attribute::AttestedPasskeys, v_pk));
1684 });
1685 }
1686 CredentialState::PolicyDeny => {
1687 modlist.push_mod(Modify::Purged(Attribute::AttestedPasskeys));
1688 }
1689 CredentialState::AccessDeny => {}
1691 };
1692
1693 match session.sshkeys_state {
1694 CredentialState::DeleteOnly | CredentialState::Modifiable => {
1695 modlist.push_mod(Modify::Purged(Attribute::SshPublicKey));
1696 for (tag, pk) in &session.sshkeys {
1697 let v_sk = Value::SshKey(tag.clone(), pk.clone());
1698 modlist.push_mod(Modify::Present(Attribute::SshPublicKey, v_sk));
1699 }
1700 }
1701 CredentialState::PolicyDeny => {
1702 modlist.push_mod(Modify::Purged(Attribute::SshPublicKey));
1703 }
1704 CredentialState::AccessDeny => {}
1705 };
1706
1707 trace!(?modlist, "processing change");
1709
1710 if modlist.is_empty() {
1711 trace!("no changes to apply");
1712 Ok(())
1713 } else {
1714 self.qs_write
1715 .internal_modify(
1716 &filter!(f_eq(
1718 Attribute::Uuid,
1719 PartialValue::Uuid(session.account.uuid)
1720 )),
1721 &modlist,
1722 )
1723 .map_err(|e| {
1724 request_error!(error = ?e);
1725 e
1726 })
1727 }
1728 }
1729
1730 pub fn cancel_credential_update(
1731 &mut self,
1732 cust: &CredentialUpdateSessionToken,
1733 ct: Duration,
1734 ) -> Result<(), OperationError> {
1735 let (mut modlist, session, session_token) =
1736 self.credential_update_commit_common(cust, ct)?;
1737
1738 if let Some(intent_token_id) = &session.intent_token_id {
1740 let entry = self.qs_write.internal_search_uuid(session.account.uuid)?;
1741 let account = Account::try_from_entry_rw(entry.as_ref(), &mut self.qs_write)?;
1742
1743 let (max_ttl, perms) = match account
1744 .credential_update_intent_tokens
1745 .get(intent_token_id)
1746 {
1747 Some(IntentTokenState::InProgress {
1748 max_ttl,
1749 perms,
1750 session_id,
1751 session_ttl: _,
1752 }) => {
1753 if *session_id != session_token.sessionid {
1754 security_info!("Session originated from an intent token, but the intent token has initiated a conflicting second update session. Refusing to commit changes.");
1755 return Err(OperationError::InvalidState);
1756 } else {
1757 (*max_ttl, *perms)
1758 }
1759 }
1760 Some(IntentTokenState::Consumed { max_ttl: _ })
1761 | Some(IntentTokenState::Valid {
1762 max_ttl: _,
1763 perms: _,
1764 })
1765 | None => {
1766 security_info!("Session originated from an intent token, but the intent token has transitioned to an invalid state. Refusing to commit changes.");
1767 return Err(OperationError::InvalidState);
1768 }
1769 };
1770
1771 modlist.push_mod(Modify::Removed(
1772 Attribute::CredentialUpdateIntentToken,
1773 PartialValue::IntentToken(intent_token_id.clone()),
1774 ));
1775 modlist.push_mod(Modify::Present(
1776 Attribute::CredentialUpdateIntentToken,
1777 Value::IntentToken(
1778 intent_token_id.clone(),
1779 IntentTokenState::Valid { max_ttl, perms },
1780 ),
1781 ));
1782 };
1783
1784 if !modlist.is_empty() {
1786 trace!(?modlist, "processing change");
1787
1788 self.qs_write
1789 .internal_modify(
1790 &filter!(f_eq(
1792 Attribute::Uuid,
1793 PartialValue::Uuid(session.account.uuid)
1794 )),
1795 &modlist,
1796 )
1797 .map_err(|e| {
1798 request_error!(error = ?e);
1799 e
1800 })
1801 } else {
1802 Ok(())
1803 }
1804 }
1805}
1806
1807impl IdmServerCredUpdateTransaction<'_> {
1808 #[cfg(test)]
1809 pub fn get_origin(&self) -> &Url {
1810 &self.webauthn.get_allowed_origins()[0]
1811 }
1812
1813 fn get_current_session(
1814 &self,
1815 cust: &CredentialUpdateSessionToken,
1816 ct: Duration,
1817 ) -> Result<CredentialUpdateSessionMutex, OperationError> {
1818 let session_token: CredentialUpdateSessionTokenInner = self
1819 .qs_read
1820 .get_domain_key_object_handle()?
1821 .jwe_decrypt(&cust.token_enc)
1822 .map_err(|e| {
1823 admin_error!(?e, "Failed to decrypt credential update session request");
1824 OperationError::SessionExpired
1825 })
1826 .and_then(|data| {
1827 data.from_json().map_err(|e| {
1828 admin_error!(err = ?e, "Failed to deserialise credential update session request");
1829 OperationError::SerdeJsonError
1830 })
1831 })?;
1832
1833 if ct >= session_token.max_ttl {
1835 trace!(?ct, ?session_token.max_ttl);
1836 security_info!(%session_token.sessionid, "session expired");
1837 return Err(OperationError::SessionExpired);
1838 }
1839
1840 self.cred_update_sessions.get(&session_token.sessionid)
1841 .ok_or_else(|| {
1842 admin_error!("No such sessionid exists on this server - may be due to a load balancer failover or token replay? {}", session_token.sessionid);
1843 OperationError::InvalidState
1844 })
1845 .cloned()
1846 }
1847
1848 pub fn credential_update_status(
1851 &self,
1852 cust: &CredentialUpdateSessionToken,
1853 ct: Duration,
1854 ) -> Result<CredentialUpdateSessionStatus, OperationError> {
1855 let session_handle = self.get_current_session(cust, ct)?;
1856 let session = session_handle.try_lock().map_err(|_| {
1857 admin_error!("Session already locked, unable to proceed.");
1858 OperationError::InvalidState
1859 })?;
1860 trace!(?session);
1861
1862 let status: CredentialUpdateSessionStatus = session.deref().into();
1863 Ok(status)
1864 }
1865
1866 #[instrument(level = "trace", skip(self))]
1867 fn check_password_quality(
1868 &self,
1869 cleartext: &str,
1870 resolved_account_policy: &ResolvedAccountPolicy,
1871 related_inputs: &[&str],
1872 radius_secret: Option<&str>,
1873 ) -> Result<(), PasswordQuality> {
1874 let pw_min_length = resolved_account_policy.pw_min_length();
1879 if cleartext.len() < pw_min_length as usize {
1880 return Err(PasswordQuality::TooShort(pw_min_length));
1881 }
1882
1883 if let Some(some_radius_secret) = radius_secret {
1884 if cleartext.contains(some_radius_secret) {
1885 return Err(PasswordQuality::DontReusePasswords);
1886 }
1887 }
1888
1889 for related in related_inputs {
1893 if cleartext.contains(related) {
1894 return Err(PasswordQuality::Feedback(vec![
1895 PasswordFeedback::NamesAndSurnamesByThemselvesAreEasyToGuess,
1896 PasswordFeedback::AvoidDatesAndYearsThatAreAssociatedWithYou,
1897 ]));
1898 }
1899 }
1900
1901 let entropy = zxcvbn(cleartext, related_inputs);
1903
1904 if entropy.score() < Score::Four {
1906 let feedback: zxcvbn::feedback::Feedback = entropy
1909 .feedback()
1910 .ok_or(OperationError::InvalidState)
1911 .cloned()
1912 .map_err(|e| {
1913 security_info!("zxcvbn returned no feedback when score < 3 -> {:?}", e);
1914 PasswordQuality::Feedback(vec![
1916 PasswordFeedback::UseAFewWordsAvoidCommonPhrases,
1917 PasswordFeedback::AddAnotherWordOrTwo,
1918 PasswordFeedback::NoNeedForSymbolsDigitsOrUppercaseLetters,
1919 ])
1920 })?;
1921
1922 security_info!(?feedback, "pw quality feedback");
1923
1924 let feedback: Vec<_> = feedback
1925 .suggestions()
1926 .iter()
1927 .map(|s| {
1928 match s {
1929 zxcvbn::feedback::Suggestion::UseAFewWordsAvoidCommonPhrases => {
1930 PasswordFeedback::UseAFewWordsAvoidCommonPhrases
1931 }
1932 zxcvbn::feedback::Suggestion::NoNeedForSymbolsDigitsOrUppercaseLetters => {
1933 PasswordFeedback::NoNeedForSymbolsDigitsOrUppercaseLetters
1934 }
1935 zxcvbn::feedback::Suggestion::AddAnotherWordOrTwo => {
1936 PasswordFeedback::AddAnotherWordOrTwo
1937 }
1938 zxcvbn::feedback::Suggestion::CapitalizationDoesntHelpVeryMuch => {
1939 PasswordFeedback::CapitalizationDoesntHelpVeryMuch
1940 }
1941 zxcvbn::feedback::Suggestion::AllUppercaseIsAlmostAsEasyToGuessAsAllLowercase => {
1942 PasswordFeedback::AllUppercaseIsAlmostAsEasyToGuessAsAllLowercase
1943 }
1944 zxcvbn::feedback::Suggestion::ReversedWordsArentMuchHarderToGuess => {
1945 PasswordFeedback::ReversedWordsArentMuchHarderToGuess
1946 }
1947 zxcvbn::feedback::Suggestion::PredictableSubstitutionsDontHelpVeryMuch => {
1948 PasswordFeedback::PredictableSubstitutionsDontHelpVeryMuch
1949 }
1950 zxcvbn::feedback::Suggestion::UseALongerKeyboardPatternWithMoreTurns => {
1951 PasswordFeedback::UseALongerKeyboardPatternWithMoreTurns
1952 }
1953 zxcvbn::feedback::Suggestion::AvoidRepeatedWordsAndCharacters => {
1954 PasswordFeedback::AvoidRepeatedWordsAndCharacters
1955 }
1956 zxcvbn::feedback::Suggestion::AvoidSequences => {
1957 PasswordFeedback::AvoidSequences
1958 }
1959 zxcvbn::feedback::Suggestion::AvoidRecentYears => {
1960 PasswordFeedback::AvoidRecentYears
1961 }
1962 zxcvbn::feedback::Suggestion::AvoidYearsThatAreAssociatedWithYou => {
1963 PasswordFeedback::AvoidYearsThatAreAssociatedWithYou
1964 }
1965 zxcvbn::feedback::Suggestion::AvoidDatesAndYearsThatAreAssociatedWithYou => {
1966 PasswordFeedback::AvoidDatesAndYearsThatAreAssociatedWithYou
1967 }
1968 }
1969 })
1970 .chain(feedback.warning().map(|w| match w {
1971 zxcvbn::feedback::Warning::StraightRowsOfKeysAreEasyToGuess => {
1972 PasswordFeedback::StraightRowsOfKeysAreEasyToGuess
1973 }
1974 zxcvbn::feedback::Warning::ShortKeyboardPatternsAreEasyToGuess => {
1975 PasswordFeedback::ShortKeyboardPatternsAreEasyToGuess
1976 }
1977 zxcvbn::feedback::Warning::RepeatsLikeAaaAreEasyToGuess => {
1978 PasswordFeedback::RepeatsLikeAaaAreEasyToGuess
1979 }
1980 zxcvbn::feedback::Warning::RepeatsLikeAbcAbcAreOnlySlightlyHarderToGuess => {
1981 PasswordFeedback::RepeatsLikeAbcAbcAreOnlySlightlyHarderToGuess
1982 }
1983 zxcvbn::feedback::Warning::ThisIsATop10Password => {
1984 PasswordFeedback::ThisIsATop10Password
1985 }
1986 zxcvbn::feedback::Warning::ThisIsATop100Password => {
1987 PasswordFeedback::ThisIsATop100Password
1988 }
1989 zxcvbn::feedback::Warning::ThisIsACommonPassword => {
1990 PasswordFeedback::ThisIsACommonPassword
1991 }
1992 zxcvbn::feedback::Warning::ThisIsSimilarToACommonlyUsedPassword => {
1993 PasswordFeedback::ThisIsSimilarToACommonlyUsedPassword
1994 }
1995 zxcvbn::feedback::Warning::SequencesLikeAbcAreEasyToGuess => {
1996 PasswordFeedback::SequencesLikeAbcAreEasyToGuess
1997 }
1998 zxcvbn::feedback::Warning::RecentYearsAreEasyToGuess => {
1999 PasswordFeedback::RecentYearsAreEasyToGuess
2000 }
2001 zxcvbn::feedback::Warning::AWordByItselfIsEasyToGuess => {
2002 PasswordFeedback::AWordByItselfIsEasyToGuess
2003 }
2004 zxcvbn::feedback::Warning::DatesAreOftenEasyToGuess => {
2005 PasswordFeedback::DatesAreOftenEasyToGuess
2006 }
2007 zxcvbn::feedback::Warning::NamesAndSurnamesByThemselvesAreEasyToGuess => {
2008 PasswordFeedback::NamesAndSurnamesByThemselvesAreEasyToGuess
2009 }
2010 zxcvbn::feedback::Warning::CommonNamesAndSurnamesAreEasyToGuess => {
2011 PasswordFeedback::CommonNamesAndSurnamesAreEasyToGuess
2012 }
2013 }))
2014 .collect();
2015
2016 return Err(PasswordQuality::Feedback(feedback));
2017 }
2018
2019 if self
2023 .qs_read
2024 .pw_badlist()
2025 .contains(&cleartext.to_lowercase())
2026 {
2027 security_info!("Password found in badlist, rejecting");
2028 Err(PasswordQuality::BadListed)
2029 } else {
2030 Ok(())
2031 }
2032 }
2033
2034 #[instrument(level = "trace", skip(cust, self))]
2035 pub fn credential_check_password_quality(
2036 &self,
2037 cust: &CredentialUpdateSessionToken,
2038 ct: Duration,
2039 pw: &str,
2040 ) -> Result<CredentialUpdateSessionStatus, OperationError> {
2041 let session_handle = self.get_current_session(cust, ct)?;
2042 let session = session_handle.try_lock().map_err(|_| {
2043 admin_error!("Session already locked, unable to proceed.");
2044 OperationError::InvalidState
2045 })?;
2046 trace!(?session);
2047
2048 self.check_password_quality(
2049 pw,
2050 &session.resolved_account_policy,
2051 session.account.related_inputs().as_slice(),
2052 session.account.radius_secret.as_deref(),
2053 )
2054 .map_err(|e| match e {
2055 PasswordQuality::TooShort(sz) => {
2056 OperationError::PasswordQuality(vec![PasswordFeedback::TooShort(sz)])
2057 }
2058 PasswordQuality::BadListed => {
2059 OperationError::PasswordQuality(vec![PasswordFeedback::BadListed])
2060 }
2061 PasswordQuality::DontReusePasswords => {
2062 OperationError::PasswordQuality(vec![PasswordFeedback::DontReusePasswords])
2063 }
2064 PasswordQuality::Feedback(feedback) => OperationError::PasswordQuality(feedback),
2065 })?;
2066
2067 Ok(session.deref().into())
2068 }
2069
2070 #[instrument(level = "trace", skip(cust, self))]
2071 pub fn credential_primary_set_password(
2072 &self,
2073 cust: &CredentialUpdateSessionToken,
2074 ct: Duration,
2075 pw: &str,
2076 ) -> Result<CredentialUpdateSessionStatus, OperationError> {
2077 let session_handle = self.get_current_session(cust, ct)?;
2078 let mut session = session_handle.try_lock().map_err(|_| {
2079 admin_error!("Session already locked, unable to proceed.");
2080 OperationError::InvalidState
2081 })?;
2082 trace!(?session);
2083
2084 if !matches!(session.primary_state, CredentialState::Modifiable) {
2085 error!("Session does not have permission to modify primary credential");
2086 return Err(OperationError::AccessDenied);
2087 };
2088
2089 let timestamp = OffsetDateTime::UNIX_EPOCH + ct;
2090
2091 self.check_password_quality(
2092 pw,
2093 &session.resolved_account_policy,
2094 session.account.related_inputs().as_slice(),
2095 session.account.radius_secret.as_deref(),
2096 )
2097 .map_err(|e| match e {
2098 PasswordQuality::TooShort(sz) => {
2099 OperationError::PasswordQuality(vec![PasswordFeedback::TooShort(sz)])
2100 }
2101 PasswordQuality::BadListed => {
2102 OperationError::PasswordQuality(vec![PasswordFeedback::BadListed])
2103 }
2104 PasswordQuality::DontReusePasswords => {
2105 OperationError::PasswordQuality(vec![PasswordFeedback::DontReusePasswords])
2106 }
2107 PasswordQuality::Feedback(feedback) => OperationError::PasswordQuality(feedback),
2108 })?;
2109
2110 let ncred = match &session.primary {
2111 Some(primary) => {
2112 primary.set_password(self.crypto_policy, pw, timestamp)?
2114 }
2115 None => Credential::new_password_only(self.crypto_policy, pw, timestamp)?,
2116 };
2117
2118 session.primary = Some(ncred);
2119 Ok(session.deref().into())
2120 }
2121
2122 pub fn credential_primary_init_totp(
2123 &self,
2124 cust: &CredentialUpdateSessionToken,
2125 ct: Duration,
2126 ) -> Result<CredentialUpdateSessionStatus, OperationError> {
2127 let session_handle = self.get_current_session(cust, ct)?;
2128 let mut session = session_handle.try_lock().map_err(|_| {
2129 admin_error!("Session already locked, unable to proceed.");
2130 OperationError::InvalidState
2131 })?;
2132 trace!(?session);
2133
2134 if !matches!(session.primary_state, CredentialState::Modifiable) {
2135 error!("Session does not have permission to modify primary credential");
2136 return Err(OperationError::AccessDenied);
2137 };
2138
2139 if !matches!(session.mfaregstate, MfaRegState::None) {
2141 debug!("Clearing incomplete mfareg");
2142 }
2143
2144 let totp_token = Totp::generate_secure(TOTP_DEFAULT_STEP);
2146
2147 session.mfaregstate = MfaRegState::TotpInit(totp_token);
2148 Ok(session.deref().into())
2150 }
2151
2152 pub fn credential_primary_check_totp(
2153 &self,
2154 cust: &CredentialUpdateSessionToken,
2155 ct: Duration,
2156 totp_chal: u32,
2157 label: &str,
2158 ) -> Result<CredentialUpdateSessionStatus, OperationError> {
2159 let session_handle = self.get_current_session(cust, ct)?;
2160 let mut session = session_handle.try_lock().map_err(|_| {
2161 admin_error!("Session already locked, unable to proceed.");
2162 OperationError::InvalidState
2163 })?;
2164 trace!(?session);
2165
2166 if !matches!(session.primary_state, CredentialState::Modifiable) {
2167 error!("Session does not have permission to modify primary credential");
2168 return Err(OperationError::AccessDenied);
2169 };
2170
2171 let timestamp = OffsetDateTime::UNIX_EPOCH + ct;
2172
2173 match &session.mfaregstate {
2175 MfaRegState::TotpInit(totp_token)
2176 | MfaRegState::TotpTryAgain(totp_token)
2177 | MfaRegState::TotpNameTryAgain(totp_token, _)
2178 | MfaRegState::TotpInvalidSha1(totp_token, _, _) => {
2179 if session
2180 .primary
2181 .as_ref()
2182 .map(|cred| cred.has_totp_by_name(label))
2183 .unwrap_or_default()
2184 || label.trim().is_empty()
2185 || !Value::validate_str_escapes(label)
2186 {
2187 session.mfaregstate =
2189 MfaRegState::TotpNameTryAgain(totp_token.clone(), label.into());
2190 return Ok(session.deref().into());
2191 }
2192
2193 if totp_token.verify(totp_chal, ct) {
2194 let ncred = session
2196 .primary
2197 .as_ref()
2198 .map(|cred| {
2199 cred.append_totp(label.to_string(), totp_token.clone(), timestamp)
2200 })
2201 .ok_or_else(|| {
2202 admin_error!("A TOTP was added, but no primary credential stub exists");
2203 OperationError::InvalidState
2204 })?;
2205
2206 session.primary = Some(ncred);
2207
2208 session.mfaregstate = MfaRegState::None;
2210 Ok(session.deref().into())
2211 } else {
2212 let token_sha1 = totp_token.clone().downgrade_to_legacy();
2216
2217 if token_sha1.verify(totp_chal, ct) {
2218 session.mfaregstate = MfaRegState::TotpInvalidSha1(
2221 totp_token.clone(),
2222 token_sha1,
2223 label.to_string(),
2224 );
2225 Ok(session.deref().into())
2226 } else {
2227 session.mfaregstate = MfaRegState::TotpTryAgain(totp_token.clone());
2229 Ok(session.deref().into())
2230 }
2231 }
2232 }
2233 _ => Err(OperationError::InvalidRequestState),
2234 }
2235 }
2236
2237 pub fn credential_primary_accept_sha1_totp(
2238 &self,
2239 cust: &CredentialUpdateSessionToken,
2240 ct: Duration,
2241 ) -> Result<CredentialUpdateSessionStatus, OperationError> {
2242 let session_handle = self.get_current_session(cust, ct)?;
2243 let mut session = session_handle.try_lock().map_err(|_| {
2244 admin_error!("Session already locked, unable to proceed.");
2245 OperationError::InvalidState
2246 })?;
2247 trace!(?session);
2248
2249 if !matches!(session.primary_state, CredentialState::Modifiable) {
2250 error!("Session does not have permission to modify primary credential");
2251 return Err(OperationError::AccessDenied);
2252 };
2253
2254 let timestamp = OffsetDateTime::UNIX_EPOCH + ct;
2255
2256 match &session.mfaregstate {
2258 MfaRegState::TotpInvalidSha1(_, token_sha1, label) => {
2259 let ncred = session
2261 .primary
2262 .as_ref()
2263 .map(|cred| cred.append_totp(label.to_string(), token_sha1.clone(), timestamp))
2264 .ok_or_else(|| {
2265 admin_error!("A TOTP was added, but no primary credential stub exists");
2266 OperationError::InvalidState
2267 })?;
2268
2269 security_info!("A SHA1 TOTP credential was accepted");
2270
2271 session.primary = Some(ncred);
2272
2273 session.mfaregstate = MfaRegState::None;
2275 Ok(session.deref().into())
2276 }
2277 _ => Err(OperationError::InvalidRequestState),
2278 }
2279 }
2280
2281 pub fn credential_primary_remove_totp(
2282 &self,
2283 cust: &CredentialUpdateSessionToken,
2284 ct: Duration,
2285 label: &str,
2286 ) -> Result<CredentialUpdateSessionStatus, OperationError> {
2287 let session_handle = self.get_current_session(cust, ct)?;
2288 let mut session = session_handle.try_lock().map_err(|_| {
2289 admin_error!("Session already locked, unable to proceed.");
2290 OperationError::InvalidState
2291 })?;
2292 trace!(?session);
2293
2294 if !matches!(session.primary_state, CredentialState::Modifiable) {
2295 error!("Session does not have permission to modify primary credential");
2296 return Err(OperationError::AccessDenied);
2297 };
2298
2299 if !matches!(session.mfaregstate, MfaRegState::None) {
2300 admin_info!("Invalid TOTP state, another update is in progress");
2301 return Err(OperationError::InvalidState);
2302 }
2303
2304 let timestamp = OffsetDateTime::UNIX_EPOCH + ct;
2305
2306 let ncred = session
2307 .primary
2308 .as_ref()
2309 .map(|cred| cred.remove_totp(label, timestamp))
2310 .ok_or_else(|| {
2311 admin_error!("Try to remove TOTP, but no primary credential stub exists");
2312 OperationError::InvalidState
2313 })?;
2314
2315 session.primary = Some(ncred);
2316
2317 session.mfaregstate = MfaRegState::None;
2319 Ok(session.deref().into())
2320 }
2321
2322 pub fn credential_primary_init_backup_codes(
2323 &self,
2324 cust: &CredentialUpdateSessionToken,
2325 ct: Duration,
2326 ) -> Result<CredentialUpdateSessionStatus, OperationError> {
2327 let session_handle = self.get_current_session(cust, ct)?;
2328 let mut session = session_handle.try_lock().map_err(|_| {
2329 error!("Session already locked, unable to proceed.");
2330 OperationError::InvalidState
2331 })?;
2332 trace!(?session);
2333
2334 if !matches!(session.primary_state, CredentialState::Modifiable) {
2335 error!("Session does not have permission to modify primary credential");
2336 return Err(OperationError::AccessDenied);
2337 };
2338
2339 let timestamp = OffsetDateTime::UNIX_EPOCH + ct;
2340
2341 let codes = backup_code_from_random();
2344
2345 let ncred = session
2346 .primary
2347 .as_ref()
2348 .ok_or_else(|| {
2349 error!("Tried to add backup codes, but no primary credential stub exists");
2350 OperationError::InvalidState
2351 })
2352 .and_then(|cred|
2353 cred.update_backup_code(BackupCodes::new(codes.clone()), timestamp)
2354 .map_err(|_| {
2355 error!("Tried to add backup codes, but MFA is not enabled on this credential yet");
2356 OperationError::InvalidState
2357 })
2358 )
2359 ?;
2360
2361 session.primary = Some(ncred);
2362
2363 Ok(session.deref().into()).map(|mut status: CredentialUpdateSessionStatus| {
2364 status.mfaregstate = MfaRegStateStatus::BackupCodes(codes);
2365 status
2366 })
2367 }
2368
2369 pub fn credential_primary_remove_backup_codes(
2370 &self,
2371 cust: &CredentialUpdateSessionToken,
2372 ct: Duration,
2373 ) -> Result<CredentialUpdateSessionStatus, OperationError> {
2374 let session_handle = self.get_current_session(cust, ct)?;
2375 let mut session = session_handle.try_lock().map_err(|_| {
2376 admin_error!("Session already locked, unable to proceed.");
2377 OperationError::InvalidState
2378 })?;
2379 trace!(?session);
2380
2381 if !matches!(session.primary_state, CredentialState::Modifiable) {
2382 error!("Session does not have permission to modify primary credential");
2383 return Err(OperationError::AccessDenied);
2384 };
2385
2386 let timestamp = OffsetDateTime::UNIX_EPOCH + ct;
2387
2388 let ncred = session
2389 .primary
2390 .as_ref()
2391 .ok_or_else(|| {
2392 admin_error!("Tried to add backup codes, but no primary credential stub exists");
2393 OperationError::InvalidState
2394 })
2395 .and_then(|cred|
2396 cred.remove_backup_code(timestamp)
2397 .map_err(|_| {
2398 admin_error!("Tried to remove backup codes, but MFA is not enabled on this credential yet");
2399 OperationError::InvalidState
2400 })
2401 )
2402 ?;
2403
2404 session.primary = Some(ncred);
2405
2406 Ok(session.deref().into())
2407 }
2408
2409 pub fn credential_primary_delete(
2410 &self,
2411 cust: &CredentialUpdateSessionToken,
2412 ct: Duration,
2413 ) -> Result<CredentialUpdateSessionStatus, OperationError> {
2414 let session_handle = self.get_current_session(cust, ct)?;
2415 let mut session = session_handle.try_lock().map_err(|_| {
2416 admin_error!("Session already locked, unable to proceed.");
2417 OperationError::InvalidState
2418 })?;
2419 trace!(?session);
2420
2421 if !(matches!(session.primary_state, CredentialState::Modifiable)
2422 || matches!(session.primary_state, CredentialState::DeleteOnly))
2423 {
2424 error!("Session does not have permission to modify primary credential");
2425 return Err(OperationError::AccessDenied);
2426 };
2427
2428 session.primary = None;
2429 Ok(session.deref().into())
2430 }
2431
2432 pub fn credential_passkey_init(
2433 &self,
2434 cust: &CredentialUpdateSessionToken,
2435 ct: Duration,
2436 ) -> Result<CredentialUpdateSessionStatus, OperationError> {
2437 let session_handle = self.get_current_session(cust, ct)?;
2438 let mut session = session_handle.try_lock().map_err(|_| {
2439 admin_error!("Session already locked, unable to proceed.");
2440 OperationError::InvalidState
2441 })?;
2442 trace!(?session);
2443
2444 if !matches!(session.passkeys_state, CredentialState::Modifiable) {
2445 error!("Session does not have permission to modify passkeys");
2446 return Err(OperationError::AccessDenied);
2447 };
2448
2449 if !matches!(session.mfaregstate, MfaRegState::None) {
2450 debug!("Clearing incomplete mfareg");
2451 }
2452
2453 let (ccr, pk_reg) = self
2454 .webauthn
2455 .start_passkey_registration(
2456 session.account.uuid,
2457 session.account.spn(),
2458 &session.account.displayname,
2459 session.account.existing_credential_id_list(),
2460 )
2461 .map_err(|e| {
2462 error!(eclass=?e, emsg=%e, "Unable to start passkey registration");
2463 OperationError::Webauthn
2464 })?;
2465
2466 session.mfaregstate = MfaRegState::Passkey(Box::new(ccr), pk_reg);
2467 Ok(session.deref().into())
2469 }
2470
2471 pub fn credential_passkey_finish(
2472 &self,
2473 cust: &CredentialUpdateSessionToken,
2474 ct: Duration,
2475 label: String,
2476 reg: &RegisterPublicKeyCredential,
2477 ) -> Result<CredentialUpdateSessionStatus, OperationError> {
2478 let session_handle = self.get_current_session(cust, ct)?;
2479 let mut session = session_handle.try_lock().map_err(|_| {
2480 admin_error!("Session already locked, unable to proceed.");
2481 OperationError::InvalidState
2482 })?;
2483 trace!(?session);
2484
2485 if !matches!(session.passkeys_state, CredentialState::Modifiable) {
2486 error!("Session does not have permission to modify passkeys");
2487 return Err(OperationError::AccessDenied);
2488 };
2489
2490 match &session.mfaregstate {
2491 MfaRegState::Passkey(_ccr, pk_reg) => {
2492 let reg_result = self.webauthn.finish_passkey_registration(reg, pk_reg);
2493
2494 session.mfaregstate = MfaRegState::None;
2496
2497 match reg_result {
2498 Ok(passkey) => {
2499 let pk_id = Uuid::new_v4();
2500 session.passkeys.insert(pk_id, (label, passkey));
2501
2502 let cu_status: CredentialUpdateSessionStatus = session.deref().into();
2503 Ok(cu_status)
2504 }
2505 Err(WebauthnError::UserNotVerified) => {
2506 let mut cu_status: CredentialUpdateSessionStatus = session.deref().into();
2507 cu_status.append_ephemeral_warning(
2508 CredentialUpdateSessionStatusWarnings::WebauthnUserVerificationRequired,
2509 );
2510 Ok(cu_status)
2511 }
2512 Err(err) => {
2513 error!(eclass=?err, emsg=%err, "Unable to complete passkey registration");
2514 Err(OperationError::CU0002WebauthnRegistrationError)
2515 }
2516 }
2517 }
2518 invalid_state => {
2519 warn!(?invalid_state);
2520 Err(OperationError::InvalidRequestState)
2521 }
2522 }
2523 }
2524
2525 pub fn credential_passkey_remove(
2526 &self,
2527 cust: &CredentialUpdateSessionToken,
2528 ct: Duration,
2529 uuid: Uuid,
2530 ) -> Result<CredentialUpdateSessionStatus, OperationError> {
2531 let session_handle = self.get_current_session(cust, ct)?;
2532 let mut session = session_handle.try_lock().map_err(|_| {
2533 admin_error!("Session already locked, unable to proceed.");
2534 OperationError::InvalidState
2535 })?;
2536 trace!(?session);
2537
2538 if !(matches!(session.passkeys_state, CredentialState::Modifiable)
2539 || matches!(session.passkeys_state, CredentialState::DeleteOnly))
2540 {
2541 error!("Session does not have permission to modify passkeys");
2542 return Err(OperationError::AccessDenied);
2543 };
2544
2545 session.passkeys.remove(&uuid);
2547
2548 Ok(session.deref().into())
2549 }
2550
2551 pub fn credential_attested_passkey_init(
2552 &self,
2553 cust: &CredentialUpdateSessionToken,
2554 ct: Duration,
2555 ) -> Result<CredentialUpdateSessionStatus, OperationError> {
2556 let session_handle = self.get_current_session(cust, ct)?;
2557 let mut session = session_handle.try_lock().map_err(|_| {
2558 error!("Session already locked, unable to proceed.");
2559 OperationError::InvalidState
2560 })?;
2561 trace!(?session);
2562
2563 if !matches!(session.attested_passkeys_state, CredentialState::Modifiable) {
2564 error!("Session does not have permission to modify attested passkeys");
2565 return Err(OperationError::AccessDenied);
2566 };
2567
2568 if !matches!(session.mfaregstate, MfaRegState::None) {
2569 debug!("Cancelling abandoned mfareg");
2570 }
2571
2572 let att_ca_list = session
2573 .resolved_account_policy
2574 .webauthn_attestation_ca_list()
2575 .cloned()
2576 .ok_or_else(|| {
2577 error!(
2578 "No attestation CA list is available, can not proceed with attested passkeys."
2579 );
2580 OperationError::AccessDenied
2581 })?;
2582
2583 let (ccr, pk_reg) = self
2584 .webauthn
2585 .start_attested_passkey_registration(
2586 session.account.uuid,
2587 session.account.spn(),
2588 &session.account.displayname,
2589 session.account.existing_credential_id_list(),
2590 att_ca_list,
2591 None,
2592 )
2593 .map_err(|e| {
2594 error!(eclass=?e, emsg=%e, "Unable to start passkey registration");
2595 OperationError::Webauthn
2596 })?;
2597
2598 session.mfaregstate = MfaRegState::AttestedPasskey(Box::new(ccr), pk_reg);
2599 Ok(session.deref().into())
2601 }
2602
2603 pub fn credential_attested_passkey_finish(
2604 &self,
2605 cust: &CredentialUpdateSessionToken,
2606 ct: Duration,
2607 label: String,
2608 reg: &RegisterPublicKeyCredential,
2609 ) -> Result<CredentialUpdateSessionStatus, OperationError> {
2610 let session_handle = self.get_current_session(cust, ct)?;
2611 let mut session = session_handle.try_lock().map_err(|_| {
2612 admin_error!("Session already locked, unable to proceed.");
2613 OperationError::InvalidState
2614 })?;
2615 trace!(?session);
2616
2617 if !matches!(session.attested_passkeys_state, CredentialState::Modifiable) {
2618 error!("Session does not have permission to modify attested passkeys");
2619 return Err(OperationError::AccessDenied);
2620 };
2621
2622 match &session.mfaregstate {
2623 MfaRegState::AttestedPasskey(_ccr, pk_reg) => {
2624 let result = self
2625 .webauthn
2626 .finish_attested_passkey_registration(reg, pk_reg)
2627 .map_err(|e| {
2628 error!(eclass=?e, emsg=%e, "Unable to complete attested passkey registration");
2629
2630 match e {
2631 WebauthnError::AttestationChainNotTrusted(_)
2632 | WebauthnError::AttestationNotVerifiable => {
2633 OperationError::CU0001WebauthnAttestationNotTrusted
2634 },
2635 WebauthnError::UserNotVerified => {
2636 OperationError::CU0003WebauthnUserNotVerified
2637 },
2638 _ => OperationError::CU0002WebauthnRegistrationError,
2639 }
2640 });
2641
2642 session.mfaregstate = MfaRegState::None;
2644
2645 let passkey = result?;
2646 trace!(?passkey);
2647
2648 let pk_id = Uuid::new_v4();
2649 session.attested_passkeys.insert(pk_id, (label, passkey));
2650
2651 trace!(?session.attested_passkeys);
2652
2653 Ok(session.deref().into())
2654 }
2655 _ => Err(OperationError::InvalidRequestState),
2656 }
2657 }
2658
2659 pub fn credential_attested_passkey_remove(
2660 &self,
2661 cust: &CredentialUpdateSessionToken,
2662 ct: Duration,
2663 uuid: Uuid,
2664 ) -> Result<CredentialUpdateSessionStatus, OperationError> {
2665 let session_handle = self.get_current_session(cust, ct)?;
2666 let mut session = session_handle.try_lock().map_err(|_| {
2667 admin_error!("Session already locked, unable to proceed.");
2668 OperationError::InvalidState
2669 })?;
2670 trace!(?session);
2671
2672 if !(matches!(session.attested_passkeys_state, CredentialState::Modifiable)
2673 || matches!(session.attested_passkeys_state, CredentialState::DeleteOnly))
2674 {
2675 error!("Session does not have permission to modify attested passkeys");
2676 return Err(OperationError::AccessDenied);
2677 };
2678
2679 session.attested_passkeys.remove(&uuid);
2681
2682 Ok(session.deref().into())
2683 }
2684
2685 #[instrument(level = "trace", skip(cust, self))]
2686 pub fn credential_unix_set_password(
2687 &self,
2688 cust: &CredentialUpdateSessionToken,
2689 ct: Duration,
2690 pw: &str,
2691 ) -> Result<CredentialUpdateSessionStatus, OperationError> {
2692 let session_handle = self.get_current_session(cust, ct)?;
2693 let mut session = session_handle.try_lock().map_err(|_| {
2694 admin_error!("Session already locked, unable to proceed.");
2695 OperationError::InvalidState
2696 })?;
2697 trace!(?session);
2698
2699 if !matches!(session.unixcred_state, CredentialState::Modifiable) {
2700 error!("Session does not have permission to modify unix credential");
2701 return Err(OperationError::AccessDenied);
2702 };
2703
2704 let timestamp = OffsetDateTime::UNIX_EPOCH + ct;
2705
2706 self.check_password_quality(
2707 pw,
2708 &session.resolved_account_policy,
2709 session.account.related_inputs().as_slice(),
2710 session.account.radius_secret.as_deref(),
2711 )
2712 .map_err(|e| match e {
2713 PasswordQuality::TooShort(sz) => {
2714 OperationError::PasswordQuality(vec![PasswordFeedback::TooShort(sz)])
2715 }
2716 PasswordQuality::BadListed => {
2717 OperationError::PasswordQuality(vec![PasswordFeedback::BadListed])
2718 }
2719 PasswordQuality::DontReusePasswords => {
2720 OperationError::PasswordQuality(vec![PasswordFeedback::DontReusePasswords])
2721 }
2722 PasswordQuality::Feedback(feedback) => OperationError::PasswordQuality(feedback),
2723 })?;
2724
2725 let ncred = match &session.unixcred {
2726 Some(unixcred) => {
2727 unixcred.set_password(self.crypto_policy, pw, timestamp)?
2729 }
2730 None => Credential::new_password_only(self.crypto_policy, pw, timestamp)?,
2731 };
2732
2733 session.unixcred = Some(ncred);
2734 Ok(session.deref().into())
2735 }
2736
2737 pub fn credential_unix_delete(
2738 &self,
2739 cust: &CredentialUpdateSessionToken,
2740 ct: Duration,
2741 ) -> Result<CredentialUpdateSessionStatus, OperationError> {
2742 let session_handle = self.get_current_session(cust, ct)?;
2743 let mut session = session_handle.try_lock().map_err(|_| {
2744 admin_error!("Session already locked, unable to proceed.");
2745 OperationError::InvalidState
2746 })?;
2747 trace!(?session);
2748
2749 if !(matches!(session.unixcred_state, CredentialState::Modifiable)
2750 || matches!(session.unixcred_state, CredentialState::DeleteOnly))
2751 {
2752 error!("Session does not have permission to modify unix credential");
2753 return Err(OperationError::AccessDenied);
2754 };
2755
2756 session.unixcred = None;
2757 Ok(session.deref().into())
2758 }
2759
2760 #[instrument(level = "trace", skip(cust, self))]
2761 pub fn credential_sshkey_add(
2762 &self,
2763 cust: &CredentialUpdateSessionToken,
2764 ct: Duration,
2765 label: String,
2766 sshpubkey: SshPublicKey,
2767 ) -> Result<CredentialUpdateSessionStatus, OperationError> {
2768 let session_handle = self.get_current_session(cust, ct)?;
2769 let mut session = session_handle.try_lock().map_err(|_| {
2770 admin_error!("Session already locked, unable to proceed.");
2771 OperationError::InvalidState
2772 })?;
2773 trace!(?session);
2774
2775 if !matches!(session.unixcred_state, CredentialState::Modifiable) {
2776 error!("Session does not have permission to modify unix credential");
2777 return Err(OperationError::AccessDenied);
2778 };
2779
2780 if !LABEL_RE.is_match(&label) {
2782 error!("SSH Public Key label invalid");
2783 return Err(OperationError::InvalidLabel);
2784 }
2785
2786 if session.sshkeys.contains_key(&label) {
2787 error!("SSH Public Key label duplicate");
2788 return Err(OperationError::DuplicateLabel);
2789 }
2790
2791 if session.sshkeys.values().any(|sk| *sk == sshpubkey) {
2792 error!("SSH Public Key duplicate");
2793 return Err(OperationError::DuplicateKey);
2794 }
2795
2796 session.sshkeys.insert(label, sshpubkey);
2797
2798 Ok(session.deref().into())
2799 }
2800
2801 pub fn credential_sshkey_remove(
2802 &self,
2803 cust: &CredentialUpdateSessionToken,
2804 ct: Duration,
2805 label: &str,
2806 ) -> Result<CredentialUpdateSessionStatus, OperationError> {
2807 let session_handle = self.get_current_session(cust, ct)?;
2808 let mut session = session_handle.try_lock().map_err(|_| {
2809 admin_error!("Session already locked, unable to proceed.");
2810 OperationError::InvalidState
2811 })?;
2812 trace!(?session);
2813
2814 if !(matches!(session.sshkeys_state, CredentialState::Modifiable)
2815 || matches!(session.sshkeys_state, CredentialState::DeleteOnly))
2816 {
2817 error!("Session does not have permission to modify sshkeys");
2818 return Err(OperationError::AccessDenied);
2819 };
2820
2821 session.sshkeys.remove(label).ok_or_else(|| {
2822 error!("No such key for label");
2823 OperationError::NoMatchingEntries
2824 })?;
2825
2826 Ok(session.deref().into())
2829 }
2830
2831 pub fn credential_update_cancel_mfareg(
2832 &self,
2833 cust: &CredentialUpdateSessionToken,
2834 ct: Duration,
2835 ) -> Result<CredentialUpdateSessionStatus, OperationError> {
2836 let session_handle = self.get_current_session(cust, ct)?;
2837 let mut session = session_handle.try_lock().map_err(|_| {
2838 admin_error!("Session already locked, unable to proceed.");
2839 OperationError::InvalidState
2840 })?;
2841 trace!(?session);
2842 session.mfaregstate = MfaRegState::None;
2843 Ok(session.deref().into())
2844 }
2845
2846 }
2848
2849#[cfg(test)]
2850mod tests {
2851 use super::{
2852 CredentialState, CredentialUpdateAccountRecovery, CredentialUpdateSessionStatus,
2853 CredentialUpdateSessionStatusWarnings, CredentialUpdateSessionToken,
2854 InitCredentialUpdateEvent, InitCredentialUpdateIntentEvent,
2855 InitCredentialUpdateIntentSendEvent, MfaRegStateStatus, MAXIMUM_CRED_UPDATE_TTL,
2856 MAXIMUM_INTENT_TTL, MINIMUM_INTENT_TTL,
2857 };
2858 use crate::credential::totp::Totp;
2859 use crate::event::CreateEvent;
2860 use crate::idm::audit::AuditEvent;
2861 use crate::idm::authentication::AuthState;
2862 use crate::idm::delayed::DelayedAction;
2863 use crate::idm::event::{
2864 AuthEvent, AuthResult, RegenerateRadiusSecretEvent, UnixUserAuthEvent,
2865 };
2866 use crate::idm::server::{IdmServer, IdmServerCredUpdateTransaction, IdmServerDelayed};
2867 use crate::prelude::*;
2868 use crate::utils::password_from_random_len;
2869 use crate::value::CredentialType;
2870 use crate::valueset::ValueSetEmailAddress;
2871 use compact_jwt::JwsCompact;
2872 use kanidm_proto::internal::{CUExtPortal, CredentialDetailType, PasswordFeedback};
2873 use kanidm_proto::v1::OutboundMessage;
2874 use kanidm_proto::v1::{AuthAllowed, AuthIssueSession, AuthMech, UnixUserToken};
2875 use sshkey_attest::proto::PublicKey as SshPublicKey;
2876 use std::time::Duration;
2877 use time::OffsetDateTime;
2878 use uuid::uuid;
2879 use webauthn_authenticator_rs::softpasskey::SoftPasskey;
2880 use webauthn_authenticator_rs::softtoken::{self, SoftToken};
2881 use webauthn_authenticator_rs::{AuthenticatorBackend, WebauthnAuthenticator};
2882 use webauthn_rs::prelude::AttestationCaListBuilder;
2883
2884 const TEST_CURRENT_TIME: u64 = 6000;
2885 const TESTPERSON_UUID: Uuid = uuid!("cf231fea-1a8f-4410-a520-fd9b1a379c86");
2886 const TESTPERSON_NAME: &str = "testperson";
2887
2888 const TESTPERSON_PASSWORD: &str = "SSBndWVzcyB5b3UgZGlzY292ZXJlZCB0aGUgc2VjcmV0";
2889
2890 const SSHKEY_VALID_1: &str = "sk-ecdsa-sha2-nistp256@openssh.com AAAAInNrLWVjZHNhLXNoYTItbmlzdHAyNTZAb3BlbnNzaC5jb20AAAAIbmlzdHAyNTYAAABBBENubZikrb8hu+HeVRdZ0pp/VAk2qv4JDbuJhvD0yNdWDL2e3cBbERiDeNPkWx58Q4rVnxkbV1fa8E2waRtT91wAAAAEc3NoOg== testuser@fidokey";
2891 const SSHKEY_VALID_2: &str = "sk-ecdsa-sha2-nistp256@openssh.com AAAAInNrLWVjZHNhLXNoYTItbmlzdHAyNTZAb3BlbnNzaC5jb20AAAAIbmlzdHAyNTYAAABBBIbkSsdGCRoW6v0nO/3vNYPhG20YhWU0wQPY7x52EOb4dmYhC4IJfzVDpEPg313BxWRKQglb5RQ1PPkou7JFyCUAAAAEc3NoOg== testuser@fidokey";
2892 const SSHKEY_INVALID: &str = "sk-ecrsa-sha9000-nistp@openssh.com AAAAInNrLWVjZHNhLXNoYTItbmlzdHAyNTZAb3BlbnNzaC5jb20AAAAIbmlzdHAyNTYAAABBBIbkSsdGCRoW6v0nO/3vNYPhG20YhWU0wQPY7x52EOb4dmYhC4IJfzVDpEPg313BxWRKQglb5RQ1PPkou7JFyCUAAAAEc3NoOg== badkey@rejectme";
2893
2894 #[idm_test]
2895 async fn credential_update_session_init(
2896 idms: &IdmServer,
2897 _idms_delayed: &mut IdmServerDelayed,
2898 ) {
2899 let ct = Duration::from_secs(TEST_CURRENT_TIME);
2900 let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();
2901
2902 let testaccount_uuid = Uuid::new_v4();
2903
2904 let e1 = entry_init!(
2905 (Attribute::Class, EntryClass::Object.to_value()),
2906 (Attribute::Class, EntryClass::Account.to_value()),
2907 (Attribute::Class, EntryClass::ServiceAccount.to_value()),
2908 (Attribute::Name, Value::new_iname("user_account_only")),
2909 (Attribute::Uuid, Value::Uuid(testaccount_uuid)),
2910 (Attribute::Description, Value::new_utf8s("testaccount")),
2911 (Attribute::DisplayName, Value::new_utf8s("testaccount"))
2912 );
2913
2914 let e2 = entry_init!(
2915 (Attribute::Class, EntryClass::Object.to_value()),
2916 (Attribute::Class, EntryClass::Account.to_value()),
2917 (Attribute::Class, EntryClass::PosixAccount.to_value()),
2918 (Attribute::Class, EntryClass::Person.to_value()),
2919 (Attribute::Name, Value::new_iname(TESTPERSON_NAME)),
2920 (Attribute::Uuid, Value::Uuid(TESTPERSON_UUID)),
2921 (Attribute::Description, Value::new_utf8s(TESTPERSON_NAME)),
2922 (Attribute::DisplayName, Value::new_utf8s(TESTPERSON_NAME))
2923 );
2924
2925 let ce = CreateEvent::new_internal(vec![e1, e2]);
2926 let cr = idms_prox_write.qs_write.create(&ce);
2927 assert!(cr.is_ok());
2928
2929 let testaccount = idms_prox_write
2930 .qs_write
2931 .internal_search_uuid(testaccount_uuid)
2932 .expect("failed");
2933
2934 let testperson = idms_prox_write
2935 .qs_write
2936 .internal_search_uuid(TESTPERSON_UUID)
2937 .expect("failed");
2938
2939 let idm_admin = idms_prox_write
2940 .qs_write
2941 .internal_search_uuid(UUID_IDM_ADMIN)
2942 .expect("failed");
2943
2944 let cur = idms_prox_write.init_credential_update(
2948 &InitCredentialUpdateEvent::new_impersonate_entry(testaccount),
2949 ct,
2950 );
2951
2952 assert!(matches!(cur, Err(OperationError::NotAuthorised)));
2953
2954 let cur = idms_prox_write.init_credential_update(
2957 &InitCredentialUpdateEvent::new_impersonate_entry(testperson),
2958 ct,
2959 );
2960
2961 assert!(cur.is_ok());
2962
2963 let cur = idms_prox_write.init_credential_update_intent(
2968 &InitCredentialUpdateIntentEvent::new_impersonate_entry(
2969 idm_admin.clone(),
2970 TESTPERSON_UUID,
2971 MINIMUM_INTENT_TTL,
2972 ),
2973 ct,
2974 );
2975
2976 assert!(cur.is_ok());
2977 let intent_tok = cur.expect("Failed to create intent token!");
2978
2979 let cur = idms_prox_write
2982 .exchange_intent_credential_update(intent_tok.clone().into(), ct + MINIMUM_INTENT_TTL);
2983
2984 assert!(matches!(cur, Err(OperationError::SessionExpired)));
2985
2986 let cur = idms_prox_write
2987 .exchange_intent_credential_update(intent_tok.clone().into(), ct + MAXIMUM_INTENT_TTL);
2988
2989 assert!(matches!(cur, Err(OperationError::SessionExpired)));
2990
2991 let (cust_a, _c_status) = idms_prox_write
2993 .exchange_intent_credential_update(intent_tok.clone().into(), ct)
2994 .unwrap();
2995
2996 let (cust_b, _c_status) = idms_prox_write
2999 .exchange_intent_credential_update(intent_tok.into(), ct + Duration::from_secs(1))
3000 .unwrap();
3001
3002 let cur = idms_prox_write.commit_credential_update(&cust_a, ct);
3003
3004 trace!(?cur);
3006 assert!(cur.is_err());
3007
3008 let _ = idms_prox_write.commit_credential_update(&cust_b, ct);
3010
3011 debug!("Start intent token revoke");
3012
3013 let intent_tok = idms_prox_write
3015 .init_credential_update_intent(
3016 &InitCredentialUpdateIntentEvent::new_impersonate_entry(
3017 idm_admin,
3018 TESTPERSON_UUID,
3019 MINIMUM_INTENT_TTL,
3020 ),
3021 ct,
3022 )
3023 .expect("Failed to create intent token!");
3024
3025 idms_prox_write
3026 .revoke_credential_update_intent(intent_tok.clone().into(), ct)
3027 .expect("Failed to revoke intent");
3028
3029 let cur = idms_prox_write.exchange_intent_credential_update(
3031 intent_tok.clone().into(),
3032 ct + Duration::from_secs(1),
3033 );
3034 debug!(?cur);
3035 assert!(matches!(cur, Err(OperationError::SessionExpired)));
3036
3037 idms_prox_write.commit().expect("Failed to commit txn");
3038 }
3039
3040 async fn setup_test_session(
3041 idms: &IdmServer,
3042 ct: Duration,
3043 ) -> (CredentialUpdateSessionToken, CredentialUpdateSessionStatus) {
3044 setup_test_session_inner(idms, ct, true).await
3045 }
3046
3047 async fn setup_test_session_no_posix(
3048 idms: &IdmServer,
3049 ct: Duration,
3050 ) -> (CredentialUpdateSessionToken, CredentialUpdateSessionStatus) {
3051 setup_test_session_inner(idms, ct, false).await
3052 }
3053
3054 async fn setup_test_session_inner(
3055 idms: &IdmServer,
3056 ct: Duration,
3057 posix: bool,
3058 ) -> (CredentialUpdateSessionToken, CredentialUpdateSessionStatus) {
3059 let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();
3060
3061 let modlist = ModifyList::new_purge(Attribute::CredentialTypeMinimum);
3063 idms_prox_write
3064 .qs_write
3065 .internal_modify_uuid(UUID_IDM_ALL_PERSONS, &modlist)
3066 .expect("Unable to change default session exp");
3067
3068 let mut builder = entry_init!(
3069 (Attribute::Class, EntryClass::Object.to_value()),
3070 (Attribute::Class, EntryClass::Account.to_value()),
3071 (Attribute::Class, EntryClass::Person.to_value()),
3072 (Attribute::Name, Value::new_iname(TESTPERSON_NAME)),
3073 (Attribute::Uuid, Value::Uuid(TESTPERSON_UUID)),
3074 (Attribute::Description, Value::new_utf8s(TESTPERSON_NAME)),
3075 (Attribute::DisplayName, Value::new_utf8s(TESTPERSON_NAME))
3076 );
3077
3078 if posix {
3079 builder.add_ava(Attribute::Class, EntryClass::PosixAccount.to_value());
3080 }
3081
3082 let ce = CreateEvent::new_internal(vec![builder]);
3083 let cr = idms_prox_write.qs_write.create(&ce);
3084 assert!(cr.is_ok());
3085
3086 let testperson = idms_prox_write
3087 .qs_write
3088 .internal_search_uuid(TESTPERSON_UUID)
3089 .expect("failed");
3090
3091 if posix {
3092 let rrse = RegenerateRadiusSecretEvent::new_internal(TESTPERSON_UUID);
3094
3095 let _ = idms_prox_write
3096 .regenerate_radius_secret(&rrse)
3097 .expect("Failed to reset radius credential 1");
3098 }
3099
3100 let cur = idms_prox_write.init_credential_update(
3101 &InitCredentialUpdateEvent::new_impersonate_entry(testperson),
3102 ct,
3103 );
3104
3105 idms_prox_write.commit().expect("Failed to commit txn");
3106
3107 cur.expect("Failed to start update")
3108 }
3109
3110 async fn renew_test_session(
3111 idms: &IdmServer,
3112 ct: Duration,
3113 ) -> (CredentialUpdateSessionToken, CredentialUpdateSessionStatus) {
3114 let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();
3115
3116 let testperson = idms_prox_write
3117 .qs_write
3118 .internal_search_uuid(TESTPERSON_UUID)
3119 .expect("failed");
3120
3121 let cur = idms_prox_write.init_credential_update(
3122 &InitCredentialUpdateEvent::new_impersonate_entry(testperson),
3123 ct,
3124 );
3125
3126 trace!(renew_test_session_result = ?cur);
3127
3128 idms_prox_write.commit().expect("Failed to commit txn");
3129
3130 cur.expect("Failed to start update")
3131 }
3132
3133 async fn commit_session(idms: &IdmServer, ct: Duration, cust: CredentialUpdateSessionToken) {
3134 let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();
3135
3136 idms_prox_write
3137 .commit_credential_update(&cust, ct)
3138 .expect("Failed to commit credential update.");
3139
3140 idms_prox_write.commit().expect("Failed to commit txn");
3141 }
3142
3143 async fn check_testperson_password(
3144 idms: &IdmServer,
3145 idms_delayed: &mut IdmServerDelayed,
3146 pw: &str,
3147 ct: Duration,
3148 ) -> Option<JwsCompact> {
3149 let mut idms_auth = idms.auth().await.unwrap();
3150
3151 let auth_init = AuthEvent::named_init(TESTPERSON_NAME);
3152
3153 let r1 = idms_auth
3154 .auth(&auth_init, ct, Source::Internal.into())
3155 .await;
3156 let ar = r1.unwrap();
3157 let AuthResult { sessionid, state } = ar;
3158
3159 if !matches!(state, AuthState::Choose(_)) {
3160 debug!("Can't proceed - {:?}", state);
3161 return None;
3162 };
3163
3164 let auth_begin = AuthEvent::begin_mech(sessionid, AuthMech::Password);
3165
3166 let r2 = idms_auth
3167 .auth(&auth_begin, ct, Source::Internal.into())
3168 .await;
3169 let ar = r2.unwrap();
3170 let AuthResult { sessionid, state } = ar;
3171
3172 assert!(matches!(state, AuthState::Continue(_)));
3173
3174 let pw_step = AuthEvent::cred_step_password(sessionid, pw);
3175
3176 let r2 = idms_auth.auth(&pw_step, ct, Source::Internal.into()).await;
3178 debug!("r2 ==> {:?}", r2);
3179 idms_auth.commit().expect("Must not fail");
3180
3181 match r2 {
3182 Ok(AuthResult {
3183 sessionid: _,
3184 state: AuthState::Success(token, AuthIssueSession::Token),
3185 }) => {
3186 let da = idms_delayed.try_recv().expect("invalid");
3188 assert!(matches!(da, DelayedAction::AuthSessionRecord(_)));
3189
3190 Some(*token)
3191 }
3192 _ => None,
3193 }
3194 }
3195
3196 async fn check_testperson_unix_password(
3197 idms: &IdmServer,
3198 pw: &str,
3200 ct: Duration,
3201 ) -> Option<UnixUserToken> {
3202 let mut idms_auth = idms.auth().await.unwrap();
3203
3204 let auth_event = UnixUserAuthEvent::new_internal(TESTPERSON_UUID, pw);
3205
3206 idms_auth
3207 .auth_unix(&auth_event, ct)
3208 .await
3209 .expect("Unable to perform unix authentication")
3210 }
3211
3212 async fn check_testperson_password_totp(
3213 idms: &IdmServer,
3214 idms_delayed: &mut IdmServerDelayed,
3215 pw: &str,
3216 token: &Totp,
3217 ct: Duration,
3218 ) -> Option<JwsCompact> {
3219 let mut idms_auth = idms.auth().await.unwrap();
3220
3221 let auth_init = AuthEvent::named_init(TESTPERSON_NAME);
3222
3223 let r1 = idms_auth
3224 .auth(&auth_init, ct, Source::Internal.into())
3225 .await;
3226 let ar = r1.unwrap();
3227 let AuthResult { sessionid, state } = ar;
3228
3229 if !matches!(state, AuthState::Choose(_)) {
3230 debug!("Can't proceed - {:?}", state);
3231 return None;
3232 };
3233
3234 let auth_begin = AuthEvent::begin_mech(sessionid, AuthMech::PasswordTotp);
3235
3236 let r2 = idms_auth
3237 .auth(&auth_begin, ct, Source::Internal.into())
3238 .await;
3239 let ar = r2.unwrap();
3240 let AuthResult { sessionid, state } = ar;
3241
3242 assert!(matches!(state, AuthState::Continue(_)));
3243
3244 let totp = token
3245 .do_totp_duration_from_epoch(&ct)
3246 .expect("Failed to perform totp step");
3247
3248 let totp_step = AuthEvent::cred_step_totp(sessionid, totp);
3249 let r2 = idms_auth
3250 .auth(&totp_step, ct, Source::Internal.into())
3251 .await;
3252 let ar = r2.unwrap();
3253 let AuthResult { sessionid, state } = ar;
3254
3255 assert!(matches!(state, AuthState::Continue(_)));
3256
3257 let pw_step = AuthEvent::cred_step_password(sessionid, pw);
3258
3259 let r3 = idms_auth.auth(&pw_step, ct, Source::Internal.into()).await;
3261 debug!("r3 ==> {:?}", r3);
3262 idms_auth.commit().expect("Must not fail");
3263
3264 match r3 {
3265 Ok(AuthResult {
3266 sessionid: _,
3267 state: AuthState::Success(token, AuthIssueSession::Token),
3268 }) => {
3269 let da = idms_delayed.try_recv().expect("invalid");
3271 assert!(matches!(da, DelayedAction::AuthSessionRecord(_)));
3272 Some(*token)
3273 }
3274 _ => None,
3275 }
3276 }
3277
3278 async fn check_testperson_password_backup_code(
3279 idms: &IdmServer,
3280 idms_delayed: &mut IdmServerDelayed,
3281 pw: &str,
3282 code: &str,
3283 ct: Duration,
3284 ) -> Option<JwsCompact> {
3285 let mut idms_auth = idms.auth().await.unwrap();
3286
3287 let auth_init = AuthEvent::named_init(TESTPERSON_NAME);
3288
3289 let r1 = idms_auth
3290 .auth(&auth_init, ct, Source::Internal.into())
3291 .await;
3292 let ar = r1.unwrap();
3293 let AuthResult { sessionid, state } = ar;
3294
3295 if !matches!(state, AuthState::Choose(_)) {
3296 debug!("Can't proceed - {:?}", state);
3297 return None;
3298 };
3299
3300 let auth_begin = AuthEvent::begin_mech(sessionid, AuthMech::PasswordBackupCode);
3301
3302 let r2 = idms_auth
3303 .auth(&auth_begin, ct, Source::Internal.into())
3304 .await;
3305 let ar = r2.unwrap();
3306 let AuthResult { sessionid, state } = ar;
3307
3308 assert!(matches!(state, AuthState::Continue(_)));
3309
3310 let code_step = AuthEvent::cred_step_backup_code(sessionid, code);
3311 let r2 = idms_auth
3312 .auth(&code_step, ct, Source::Internal.into())
3313 .await;
3314 let ar = r2.unwrap();
3315 let AuthResult { sessionid, state } = ar;
3316
3317 assert!(matches!(state, AuthState::Continue(_)));
3318
3319 let pw_step = AuthEvent::cred_step_password(sessionid, pw);
3320
3321 let r3 = idms_auth.auth(&pw_step, ct, Source::Internal.into()).await;
3323 debug!("r3 ==> {:?}", r3);
3324 idms_auth.commit().expect("Must not fail");
3325
3326 match r3 {
3327 Ok(AuthResult {
3328 sessionid: _,
3329 state: AuthState::Success(token, AuthIssueSession::Token),
3330 }) => {
3331 let da = idms_delayed.try_recv().expect("invalid");
3333 assert!(matches!(da, DelayedAction::BackupCodeRemoval(_)));
3334 let r = idms.delayed_action(ct, da).await;
3335 assert!(r.is_ok());
3336
3337 let da = idms_delayed.try_recv().expect("invalid");
3339 assert!(matches!(da, DelayedAction::AuthSessionRecord(_)));
3340 Some(*token)
3341 }
3342 _ => None,
3343 }
3344 }
3345
3346 async fn check_testperson_passkey<T: AuthenticatorBackend>(
3347 idms: &IdmServer,
3348 idms_delayed: &mut IdmServerDelayed,
3349 wa: &mut WebauthnAuthenticator<T>,
3350 origin: Url,
3351 ct: Duration,
3352 ) -> Option<JwsCompact> {
3353 let mut idms_auth = idms.auth().await.unwrap();
3354
3355 let auth_init = AuthEvent::named_init(TESTPERSON_NAME);
3356
3357 let r1 = idms_auth
3358 .auth(&auth_init, ct, Source::Internal.into())
3359 .await;
3360 let ar = r1.unwrap();
3361 let AuthResult { sessionid, state } = ar;
3362
3363 if !matches!(state, AuthState::Choose(_)) {
3364 debug!("Can't proceed - {:?}", state);
3365 return None;
3366 };
3367
3368 let auth_begin = AuthEvent::begin_mech(sessionid, AuthMech::Passkey);
3369
3370 let ar = idms_auth
3371 .auth(&auth_begin, ct, Source::Internal.into())
3372 .await
3373 .inspect_err(|err| error!(?err))
3374 .ok()?;
3375 let AuthResult { sessionid, state } = ar;
3376
3377 trace!(?state);
3378
3379 let rcr = match state {
3380 AuthState::Continue(mut allowed) => match allowed.pop() {
3381 Some(AuthAllowed::Passkey(rcr)) => rcr,
3382 _ => unreachable!(),
3383 },
3384 _ => unreachable!(),
3385 };
3386
3387 trace!(?rcr);
3388
3389 let resp = wa
3390 .do_authentication(origin, rcr)
3391 .inspect_err(|err| error!(?err))
3392 .ok()?;
3393
3394 let passkey_step = AuthEvent::cred_step_passkey(sessionid, resp);
3395
3396 let r3 = idms_auth
3397 .auth(&passkey_step, ct, Source::Internal.into())
3398 .await;
3399 debug!("r3 ==> {:?}", r3);
3400 idms_auth.commit().expect("Must not fail");
3401
3402 match r3 {
3403 Ok(AuthResult {
3404 sessionid: _,
3405 state: AuthState::Success(token, AuthIssueSession::Token),
3406 }) => {
3407 let da = idms_delayed.try_recv().expect("invalid");
3409 assert!(matches!(da, DelayedAction::WebauthnCounterIncrement(_)));
3410 let r = idms.delayed_action(ct, da).await;
3411 assert!(r.is_ok());
3412
3413 let da = idms_delayed.try_recv().expect("invalid");
3415 assert!(matches!(da, DelayedAction::AuthSessionRecord(_)));
3416
3417 Some(*token)
3418 }
3419 _ => None,
3420 }
3421 }
3422
3423 #[idm_test]
3424 async fn credential_update_session_cleanup(
3425 idms: &IdmServer,
3426 _idms_delayed: &mut IdmServerDelayed,
3427 ) {
3428 let ct = Duration::from_secs(TEST_CURRENT_TIME);
3429 let (cust, _) = setup_test_session(idms, ct).await;
3430
3431 let cutxn = idms.cred_update_transaction().await.unwrap();
3432 let c_status = cutxn.credential_update_status(&cust, ct);
3434 assert!(c_status.is_ok());
3435 drop(cutxn);
3436
3437 let (_cust, _) =
3439 renew_test_session(idms, ct + MAXIMUM_CRED_UPDATE_TTL + Duration::from_secs(1)).await;
3440
3441 let cutxn = idms.cred_update_transaction().await.unwrap();
3442
3443 let c_status = cutxn
3446 .credential_update_status(&cust, ct)
3447 .expect_err("Session is still valid!");
3448 assert!(matches!(c_status, OperationError::InvalidState));
3449 }
3450
3451 #[idm_test]
3452 async fn credential_update_intent_send(idms: &IdmServer, _idms_delayed: &mut IdmServerDelayed) {
3453 let ct = Duration::from_secs(TEST_CURRENT_TIME);
3454
3455 let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();
3456
3457 let email_address = format!("{}@example.com", TESTPERSON_NAME);
3458
3459 let test_entry = EntryInitNew::from_iter([
3460 (
3461 Attribute::Class,
3462 ValueSetIutf8::from_iter([
3463 EntryClass::Object.into(),
3464 EntryClass::Account.into(),
3465 EntryClass::PosixAccount.into(),
3466 EntryClass::Person.into(),
3467 ])
3468 .unwrap() as ValueSet,
3469 ),
3470 (
3471 Attribute::Name,
3472 ValueSetIname::new(TESTPERSON_NAME) as ValueSet,
3473 ),
3474 (
3475 Attribute::Uuid,
3476 ValueSetUuid::new(TESTPERSON_UUID) as ValueSet,
3477 ),
3478 (
3479 Attribute::Description,
3480 ValueSetUtf8::new(TESTPERSON_NAME.into()) as ValueSet,
3481 ),
3482 (
3483 Attribute::DisplayName,
3484 ValueSetUtf8::new(TESTPERSON_NAME.into()) as ValueSet,
3485 ),
3486 ]);
3487
3488 let ce = CreateEvent::new_internal(vec![test_entry]);
3489 let cr = idms_prox_write.qs_write.create(&ce);
3490 assert!(cr.is_ok());
3491
3492 let idm_admin_identity = idms_prox_write
3493 .qs_write
3494 .impersonate_uuid_as_readwrite_identity(UUID_IDM_ADMIN)
3495 .expect("Failed to retrieve identity");
3496
3497 let event = InitCredentialUpdateIntentSendEvent {
3499 ident: idm_admin_identity.clone(),
3500 target: TESTPERSON_UUID,
3501 max_ttl: None,
3502 email: None,
3503 };
3504
3505 let err = idms_prox_write
3506 .init_credential_update_intent_send(event, ct)
3507 .expect_err("Should not succeed!");
3508 assert_eq!(err, OperationError::CU0008AccountMissingEmail);
3509
3510 idms_prox_write
3512 .qs_write
3513 .internal_modify_uuid(
3514 TESTPERSON_UUID,
3515 &ModifyList::new_set(
3516 Attribute::Mail,
3517 ValueSetEmailAddress::new(email_address.clone()) as ValueSet,
3518 ),
3519 )
3520 .expect("Failed to update test person account");
3521
3522 let event = InitCredentialUpdateIntentSendEvent {
3525 ident: idm_admin_identity.clone(),
3526 target: TESTPERSON_UUID,
3527 max_ttl: None,
3528 email: Some("email-that-is-not-present@example.com".into()),
3529 };
3530
3531 let err = idms_prox_write
3532 .init_credential_update_intent_send(event, ct)
3533 .expect_err("Should not succeed!");
3534 assert_eq!(err, OperationError::CU0007AccountEmailNotFound);
3535
3536 let event = InitCredentialUpdateIntentSendEvent {
3540 ident: idm_admin_identity.clone(),
3541 target: TESTPERSON_UUID,
3542 max_ttl: None,
3543 email: Some(email_address.clone()),
3544 };
3545
3546 idms_prox_write
3547 .init_credential_update_intent_send(event, ct)
3548 .expect("Should succeed!");
3549
3550 let filter = filter!(f_and(vec![
3552 f_eq(Attribute::Class, EntryClass::OutboundMessage.into()),
3553 f_eq(
3554 Attribute::MailDestination,
3555 PartialValue::EmailAddress(email_address)
3556 )
3557 ]));
3558
3559 let mut entries = idms_prox_write
3560 .qs_write
3561 .impersonate_search(filter.clone(), filter, &idm_admin_identity)
3562 .expect("Unable to search message queue");
3563
3564 assert_eq!(entries.len(), 1);
3565 let message_entry = entries.pop().unwrap();
3566
3567 let message = message_entry
3568 .get_ava_set(Attribute::MessageTemplate)
3569 .and_then(|vs| vs.as_message())
3570 .unwrap();
3571
3572 match message {
3573 OutboundMessage::CredentialResetV1 { display_name, .. } => {
3574 assert_eq!(display_name, TESTPERSON_NAME);
3575 }
3576 _ => panic!("Wrong message type!"),
3577 }
3578 }
3580
3581 #[idm_test]
3582 async fn account_recovery_basic(idms: &IdmServer, _idms_delayed: &mut IdmServerDelayed) {
3583 let ct = Duration::from_secs(TEST_CURRENT_TIME);
3584
3585 let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();
3586
3587 let idm_admin_identity = idms_prox_write
3588 .qs_write
3589 .impersonate_uuid_as_readwrite_identity(UUID_IDM_ADMIN)
3590 .expect("Failed to retrieve identity");
3591
3592 let email_address = format!("{}@example.com", TESTPERSON_NAME);
3593
3594 let test_entry = EntryInitNew::from_iter([
3595 (
3596 Attribute::Class,
3597 ValueSetIutf8::from_iter([
3598 EntryClass::Object.into(),
3599 EntryClass::Account.into(),
3600 EntryClass::Person.into(),
3601 ])
3602 .unwrap() as ValueSet,
3603 ),
3604 (
3605 Attribute::Name,
3606 ValueSetIname::new(TESTPERSON_NAME) as ValueSet,
3607 ),
3608 (
3609 Attribute::Uuid,
3610 ValueSetUuid::new(TESTPERSON_UUID) as ValueSet,
3611 ),
3612 (
3613 Attribute::DisplayName,
3614 ValueSetUtf8::new(TESTPERSON_NAME.into()) as ValueSet,
3615 ),
3616 (
3617 Attribute::Mail,
3618 ValueSetEmailAddress::new(email_address.clone()) as ValueSet,
3619 ),
3620 ]);
3621
3622 let ce =
3623 CreateEvent::new_impersonate_identity(idm_admin_identity.clone(), vec![test_entry]);
3624 let cr = idms_prox_write.qs_write.create(&ce);
3625 assert!(cr.is_ok());
3626
3627 let event = CredentialUpdateAccountRecovery {
3629 email: "invalid@example.com".into(),
3630 max_ttl: None,
3631 };
3632
3633 let result = idms_prox_write
3634 .credential_update_account_recovery(event, ct)
3635 .expect_err("Must not succeed!");
3636
3637 assert_eq!(result, OperationError::CU0010AccountRecoveryDisabled);
3638
3639 idms_prox_write
3641 .qs_write
3642 .internal_modify_uuid(
3643 UUID_DOMAIN_INFO,
3644 &ModifyList::new_set(
3645 Attribute::DomainAllowAccountRecovery,
3646 ValueSetBool::new(true),
3647 ),
3648 )
3649 .expect("Unable to activate credential reset feature.");
3650
3651 idms_prox_write
3652 .qs_write
3653 .reload()
3654 .expect("Unable to reload domain info.");
3655
3656 let event = CredentialUpdateAccountRecovery {
3658 email: "invalid@example.com".into(),
3659 max_ttl: None,
3660 };
3661
3662 let result = idms_prox_write
3663 .credential_update_account_recovery(event, ct)
3664 .expect_err("Must not succeed!");
3665
3666 assert_eq!(result, OperationError::CU0009AccountEmailNotFound);
3667
3668 let event = CredentialUpdateAccountRecovery {
3670 email: email_address.clone(),
3671 max_ttl: None,
3672 };
3673
3674 idms_prox_write
3675 .credential_update_account_recovery(event, ct)
3676 .expect("Must succeed!");
3677
3678 let filter = filter!(f_and(vec![
3680 f_eq(Attribute::Class, EntryClass::OutboundMessage.into()),
3681 f_eq(
3682 Attribute::MailDestination,
3683 PartialValue::EmailAddress(email_address)
3684 )
3685 ]));
3686
3687 let mut entries = idms_prox_write
3688 .qs_write
3689 .impersonate_search(filter.clone(), filter, &idm_admin_identity)
3690 .expect("Unable to search message queue");
3691
3692 assert_eq!(entries.len(), 1);
3693 let message_entry = entries.pop().unwrap();
3694
3695 let message = message_entry
3696 .get_ava_set(Attribute::MessageTemplate)
3697 .and_then(|vs| vs.as_message())
3698 .unwrap();
3699
3700 match message {
3701 OutboundMessage::CredentialResetV1 { display_name, .. } => {
3702 assert_eq!(display_name, TESTPERSON_NAME);
3703 }
3704 _ => panic!("Wrong message type!"),
3705 }
3706 }
3708
3709 #[idm_test]
3710 async fn credential_update_onboarding_create_new_pw(
3711 idms: &IdmServer,
3712 idms_delayed: &mut IdmServerDelayed,
3713 ) {
3714 let test_pw = "fo3EitierohF9AelaNgiem0Ei6vup4equo1Oogeevaetehah8Tobeengae3Ci0ooh0uki";
3715 let ct = Duration::from_secs(TEST_CURRENT_TIME);
3716
3717 let (cust, _) = setup_test_session(idms, ct).await;
3718
3719 let cutxn = idms.cred_update_transaction().await.unwrap();
3720
3721 let c_status = cutxn
3725 .credential_update_status(&cust, ct)
3726 .expect("Failed to get the current session status.");
3727
3728 trace!(?c_status);
3729 assert!(c_status.primary.is_none());
3730
3731 let c_status = cutxn
3734 .credential_primary_set_password(&cust, ct, test_pw)
3735 .expect("Failed to update the primary cred password");
3736
3737 assert!(c_status.can_commit);
3738
3739 drop(cutxn);
3740 commit_session(idms, ct, cust).await;
3741
3742 assert!(check_testperson_password(idms, idms_delayed, test_pw, ct)
3744 .await
3745 .is_some());
3746
3747 let (cust, _) = renew_test_session(idms, ct).await;
3749 let cutxn = idms.cred_update_transaction().await.unwrap();
3750
3751 let c_status = cutxn
3752 .credential_update_status(&cust, ct)
3753 .expect("Failed to get the current session status.");
3754 trace!(?c_status);
3755 assert!(c_status.primary.is_some());
3756
3757 let c_status = cutxn
3758 .credential_primary_delete(&cust, ct)
3759 .expect("Failed to delete the primary cred");
3760 trace!(?c_status);
3761 assert!(c_status.primary.is_none());
3762 assert!(c_status
3763 .warnings
3764 .contains(&CredentialUpdateSessionStatusWarnings::NoValidCredentials));
3765 assert!(!c_status.can_commit);
3767
3768 drop(cutxn);
3769 }
3770
3771 #[idm_test]
3772 async fn credential_update_password_quality_checks(
3773 idms: &IdmServer,
3774 _idms_delayed: &mut IdmServerDelayed,
3775 ) {
3776 let ct = Duration::from_secs(TEST_CURRENT_TIME);
3777 let (cust, _) = setup_test_session(idms, ct).await;
3778
3779 let mut r_txn = idms.proxy_read().await.unwrap();
3782
3783 let radius_secret = r_txn
3784 .qs_read
3785 .internal_search_uuid(TESTPERSON_UUID)
3786 .expect("No such entry")
3787 .get_ava_single_secret(Attribute::RadiusSecret)
3788 .expect("No radius secret found")
3789 .to_string();
3790
3791 drop(r_txn);
3792
3793 let cutxn = idms.cred_update_transaction().await.unwrap();
3794
3795 let c_status = cutxn
3799 .credential_update_status(&cust, ct)
3800 .expect("Failed to get the current session status.");
3801
3802 trace!(?c_status);
3803
3804 assert!(c_status.primary.is_none());
3805
3806 let err = cutxn
3810 .credential_primary_set_password(&cust, ct, "password")
3811 .unwrap_err();
3812 trace!(?err);
3813 assert!(
3814 matches!(err, OperationError::PasswordQuality(details) if details == vec!(PasswordFeedback::TooShort(PW_MIN_LENGTH),))
3815 );
3816
3817 let err = cutxn
3818 .credential_primary_set_password(&cust, ct, "password1234")
3819 .unwrap_err();
3820 trace!(?err);
3821 assert!(
3822 matches!(err, OperationError::PasswordQuality(details) if details
3823 == vec!(
3824 PasswordFeedback::AddAnotherWordOrTwo,
3825 PasswordFeedback::ThisIsACommonPassword,
3826 ))
3827 );
3828
3829 let err = cutxn
3830 .credential_primary_set_password(&cust, ct, &radius_secret)
3831 .unwrap_err();
3832 trace!(?err);
3833 assert!(
3834 matches!(err, OperationError::PasswordQuality(details) if details == vec!(PasswordFeedback::DontReusePasswords,))
3835 );
3836
3837 let err = cutxn
3838 .credential_primary_set_password(&cust, ct, "testperson2023")
3839 .unwrap_err();
3840 trace!(?err);
3841 assert!(
3842 matches!(err, OperationError::PasswordQuality(details) if details == vec!(
3843 PasswordFeedback::NamesAndSurnamesByThemselvesAreEasyToGuess,
3844 PasswordFeedback::AvoidDatesAndYearsThatAreAssociatedWithYou,
3845 ))
3846 );
3847
3848 let err = cutxn
3849 .credential_primary_set_password(
3850 &cust,
3851 ct,
3852 "demo_badlist_shohfie3aeci2oobur0aru9uushah6EiPi2woh4hohngoighaiRuepieN3ongoo1",
3853 )
3854 .unwrap_err();
3855 trace!(?err);
3856 assert!(
3857 matches!(err, OperationError::PasswordQuality(details) if details == vec!(PasswordFeedback::BadListed))
3858 );
3859
3860 assert!(c_status
3862 .warnings
3863 .contains(&CredentialUpdateSessionStatusWarnings::NoValidCredentials));
3864 assert!(!c_status.can_commit);
3865
3866 drop(cutxn);
3867 }
3868
3869 #[idm_test]
3870 async fn credential_update_password_min_length_account_policy(
3871 idms: &IdmServer,
3872 _idms_delayed: &mut IdmServerDelayed,
3873 ) {
3874 let ct = Duration::from_secs(TEST_CURRENT_TIME);
3875
3876 let test_pw_min_length = PW_MIN_LENGTH * 2;
3878
3879 let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();
3880
3881 let modlist = ModifyList::new_purge_and_set(
3882 Attribute::AuthPasswordMinimumLength,
3883 Value::Uint32(test_pw_min_length),
3884 );
3885 idms_prox_write
3886 .qs_write
3887 .internal_modify_uuid(UUID_IDM_ALL_ACCOUNTS, &modlist)
3888 .expect("Unable to change default session exp");
3889
3890 assert!(idms_prox_write.commit().is_ok());
3891 let (cust, _) = setup_test_session(idms, ct).await;
3894
3895 let cutxn = idms.cred_update_transaction().await.unwrap();
3896
3897 let c_status = cutxn
3901 .credential_update_status(&cust, ct)
3902 .expect("Failed to get the current session status.");
3903
3904 trace!(?c_status);
3905
3906 assert!(c_status.primary.is_none());
3907
3908 let pw = password_from_random_len(8);
3911 let err = cutxn
3912 .credential_primary_set_password(&cust, ct, &pw)
3913 .unwrap_err();
3914 trace!(?err);
3915 assert!(
3916 matches!(err, OperationError::PasswordQuality(details) if details == vec!(PasswordFeedback::TooShort(test_pw_min_length),))
3917 );
3918
3919 let pw = password_from_random_len(test_pw_min_length - 1);
3921 let err = cutxn
3922 .credential_primary_set_password(&cust, ct, &pw)
3923 .unwrap_err();
3924 trace!(?err);
3925 assert!(matches!(err,OperationError::PasswordQuality(details)
3926 if details == vec!(PasswordFeedback::TooShort(test_pw_min_length),)));
3927
3928 let pw = password_from_random_len(test_pw_min_length);
3930 let c_status = cutxn
3931 .credential_primary_set_password(&cust, ct, &pw)
3932 .expect("Failed to update the primary cred password");
3933
3934 assert!(c_status.can_commit);
3935
3936 drop(cutxn);
3937 commit_session(idms, ct, cust).await;
3938 }
3939
3940 #[idm_test]
3946 async fn credential_update_onboarding_create_new_mfa_totp_basic(
3947 idms: &IdmServer,
3948 idms_delayed: &mut IdmServerDelayed,
3949 ) {
3950 let test_pw = "fo3EitierohF9AelaNgiem0Ei6vup4equo1Oogeevaetehah8Tobeengae3Ci0ooh0uki";
3951 let ct = Duration::from_secs(TEST_CURRENT_TIME);
3952
3953 let (cust, _) = setup_test_session(idms, ct).await;
3954 let cutxn = idms.cred_update_transaction().await.unwrap();
3955
3956 let c_status = cutxn
3958 .credential_primary_set_password(&cust, ct, test_pw)
3959 .expect("Failed to update the primary cred password");
3960
3961 assert!(c_status.can_commit);
3963
3964 let c_status = cutxn
3966 .credential_primary_init_totp(&cust, ct)
3967 .expect("Failed to update the primary cred password");
3968
3969 let totp_token: Totp = match c_status.mfaregstate {
3971 MfaRegStateStatus::TotpCheck(secret) => Some(secret.try_into().unwrap()),
3972
3973 _ => None,
3974 }
3975 .expect("Unable to retrieve totp token, invalid state.");
3976
3977 trace!(?totp_token);
3978 let chal = totp_token
3979 .do_totp_duration_from_epoch(&ct)
3980 .expect("Failed to perform totp step");
3981
3982 let c_status = cutxn
3984 .credential_primary_check_totp(&cust, ct, chal + 1, "totp")
3985 .expect("Failed to update the primary cred totp");
3986
3987 assert!(
3988 matches!(c_status.mfaregstate, MfaRegStateStatus::TotpTryAgain),
3989 "{:?}",
3990 c_status.mfaregstate
3991 );
3992
3993 let c_status = cutxn
3995 .credential_primary_check_totp(&cust, ct, chal, "")
3996 .expect("Failed to update the primary cred totp");
3997
3998 assert!(
3999 matches!(
4000 c_status.mfaregstate,
4001 MfaRegStateStatus::TotpNameTryAgain(ref val) if val.is_empty()
4002 ),
4003 "{:?}",
4004 c_status.mfaregstate
4005 );
4006
4007 let c_status = cutxn
4009 .credential_primary_check_totp(&cust, ct, chal, " ")
4010 .expect("Failed to update the primary cred totp");
4011
4012 assert!(
4013 matches!(
4014 c_status.mfaregstate,
4015 MfaRegStateStatus::TotpNameTryAgain(ref val) if val == " "
4016 ),
4017 "{:?}",
4018 c_status.mfaregstate
4019 );
4020
4021 let c_status = cutxn
4022 .credential_primary_check_totp(&cust, ct, chal, "totp")
4023 .expect("Failed to update the primary cred totp");
4024
4025 assert!(matches!(c_status.mfaregstate, MfaRegStateStatus::None));
4026 assert!(match c_status.primary.as_ref().map(|c| &c.type_) {
4027 Some(CredentialDetailType::PasswordMfa(totp, _, 0)) => !totp.is_empty(),
4028 _ => false,
4029 });
4030
4031 {
4032 let c_status = cutxn
4033 .credential_primary_init_totp(&cust, ct)
4034 .expect("Failed to update the primary cred password");
4035
4036 let totp_token: Totp = match c_status.mfaregstate {
4038 MfaRegStateStatus::TotpCheck(secret) => Some(secret.try_into().unwrap()),
4039 _ => None,
4040 }
4041 .expect("Unable to retrieve totp token, invalid state.");
4042
4043 trace!(?totp_token);
4044 let chal = totp_token
4045 .do_totp_duration_from_epoch(&ct)
4046 .expect("Failed to perform totp step");
4047
4048 let c_status = cutxn
4050 .credential_primary_check_totp(&cust, ct, chal, "totp")
4051 .expect("Failed to update the primary cred totp");
4052
4053 assert!(
4054 matches!(
4055 c_status.mfaregstate,
4056 MfaRegStateStatus::TotpNameTryAgain(ref val) if val == "totp"
4057 ),
4058 "{:?}",
4059 c_status.mfaregstate
4060 );
4061
4062 assert!(cutxn.credential_update_cancel_mfareg(&cust, ct).is_ok())
4063 }
4064
4065 drop(cutxn);
4068 commit_session(idms, ct, cust).await;
4069
4070 assert!(
4072 check_testperson_password_totp(idms, idms_delayed, test_pw, &totp_token, ct)
4073 .await
4074 .is_some()
4075 );
4076 let (cust, _) = renew_test_session(idms, ct).await;
4080 let cutxn = idms.cred_update_transaction().await.unwrap();
4081
4082 let c_status = cutxn
4083 .credential_primary_remove_totp(&cust, ct, "totp")
4084 .expect("Failed to update the primary cred password");
4085
4086 assert!(matches!(c_status.mfaregstate, MfaRegStateStatus::None));
4087 assert!(matches!(
4088 c_status.primary.as_ref().map(|c| &c.type_),
4089 Some(CredentialDetailType::Password)
4090 ));
4091
4092 drop(cutxn);
4093 commit_session(idms, ct, cust).await;
4094
4095 assert!(check_testperson_password(idms, idms_delayed, test_pw, ct)
4097 .await
4098 .is_some());
4099 }
4100
4101 #[idm_test]
4103 async fn credential_update_onboarding_create_new_mfa_totp_sha1(
4104 idms: &IdmServer,
4105 idms_delayed: &mut IdmServerDelayed,
4106 ) {
4107 let test_pw = "fo3EitierohF9AelaNgiem0Ei6vup4equo1Oogeevaetehah8Tobeengae3Ci0ooh0uki";
4108 let ct = Duration::from_secs(TEST_CURRENT_TIME);
4109
4110 let (cust, _) = setup_test_session(idms, ct).await;
4111 let cutxn = idms.cred_update_transaction().await.unwrap();
4112
4113 let c_status = cutxn
4115 .credential_primary_set_password(&cust, ct, test_pw)
4116 .expect("Failed to update the primary cred password");
4117
4118 assert!(c_status.can_commit);
4120
4121 let c_status = cutxn
4123 .credential_primary_init_totp(&cust, ct)
4124 .expect("Failed to update the primary cred password");
4125
4126 let totp_token: Totp = match c_status.mfaregstate {
4128 MfaRegStateStatus::TotpCheck(secret) => Some(secret.try_into().unwrap()),
4129
4130 _ => None,
4131 }
4132 .expect("Unable to retrieve totp token, invalid state.");
4133
4134 let totp_token = totp_token.downgrade_to_legacy();
4135
4136 trace!(?totp_token);
4137 let chal = totp_token
4138 .do_totp_duration_from_epoch(&ct)
4139 .expect("Failed to perform totp step");
4140
4141 let c_status = cutxn
4143 .credential_primary_check_totp(&cust, ct, chal, "totp")
4144 .expect("Failed to update the primary cred password");
4145
4146 assert!(matches!(
4147 c_status.mfaregstate,
4148 MfaRegStateStatus::TotpInvalidSha1
4149 ));
4150
4151 let c_status = cutxn
4153 .credential_primary_accept_sha1_totp(&cust, ct)
4154 .expect("Failed to update the primary cred password");
4155
4156 assert!(matches!(c_status.mfaregstate, MfaRegStateStatus::None));
4157 assert!(match c_status.primary.as_ref().map(|c| &c.type_) {
4158 Some(CredentialDetailType::PasswordMfa(totp, _, 0)) => !totp.is_empty(),
4159 _ => false,
4160 });
4161
4162 drop(cutxn);
4165 commit_session(idms, ct, cust).await;
4166
4167 assert!(
4169 check_testperson_password_totp(idms, idms_delayed, test_pw, &totp_token, ct)
4170 .await
4171 .is_some()
4172 );
4173 }
4175
4176 #[idm_test]
4177 async fn credential_update_onboarding_create_new_mfa_totp_backup_codes(
4178 idms: &IdmServer,
4179 idms_delayed: &mut IdmServerDelayed,
4180 ) {
4181 let test_pw = "fo3EitierohF9AelaNgiem0Ei6vup4equo1Oogeevaetehah8Tobeengae3Ci0ooh0uki";
4182 let ct = Duration::from_secs(TEST_CURRENT_TIME);
4183
4184 let (cust, _) = setup_test_session(idms, ct).await;
4185 let cutxn = idms.cred_update_transaction().await.unwrap();
4186
4187 let _c_status = cutxn
4189 .credential_primary_set_password(&cust, ct, test_pw)
4190 .expect("Failed to update the primary cred password");
4191
4192 assert!(matches!(
4194 cutxn.credential_primary_init_backup_codes(&cust, ct),
4195 Err(OperationError::InvalidState)
4196 ));
4197
4198 let c_status = cutxn
4199 .credential_primary_init_totp(&cust, ct)
4200 .expect("Failed to update the primary cred password");
4201
4202 let totp_token: Totp = match c_status.mfaregstate {
4203 MfaRegStateStatus::TotpCheck(secret) => Some(secret.try_into().unwrap()),
4204 _ => None,
4205 }
4206 .expect("Unable to retrieve totp token, invalid state.");
4207
4208 trace!(?totp_token);
4209 let chal = totp_token
4210 .do_totp_duration_from_epoch(&ct)
4211 .expect("Failed to perform totp step");
4212
4213 let c_status = cutxn
4214 .credential_primary_check_totp(&cust, ct, chal, "totp")
4215 .expect("Failed to update the primary cred totp");
4216
4217 assert!(matches!(c_status.mfaregstate, MfaRegStateStatus::None));
4218 assert!(match c_status.primary.as_ref().map(|c| &c.type_) {
4219 Some(CredentialDetailType::PasswordMfa(totp, _, 0)) => !totp.is_empty(),
4220 _ => false,
4221 });
4222
4223 let c_status = cutxn
4226 .credential_primary_init_backup_codes(&cust, ct)
4227 .expect("Failed to update the primary cred password");
4228
4229 let codes = match c_status.mfaregstate {
4230 MfaRegStateStatus::BackupCodes(codes) => Some(codes),
4231 _ => None,
4232 }
4233 .expect("Unable to retrieve backupcodes, invalid state.");
4234
4235 debug!("{:?}", c_status.primary.as_ref().map(|c| &c.type_));
4237 assert!(match c_status.primary.as_ref().map(|c| &c.type_) {
4238 Some(CredentialDetailType::PasswordMfa(totp, _, 8)) => !totp.is_empty(),
4239 _ => false,
4240 });
4241
4242 drop(cutxn);
4244 commit_session(idms, ct, cust).await;
4245
4246 let backup_code = codes.iter().next().expect("No codes available");
4247
4248 assert!(check_testperson_password_backup_code(
4250 idms,
4251 idms_delayed,
4252 test_pw,
4253 backup_code,
4254 ct
4255 )
4256 .await
4257 .is_some());
4258
4259 let (cust, _) = renew_test_session(idms, ct).await;
4261 let cutxn = idms.cred_update_transaction().await.unwrap();
4262
4263 let c_status = cutxn
4265 .credential_update_status(&cust, ct)
4266 .expect("Failed to get the current session status.");
4267
4268 assert!(match c_status.primary.as_ref().map(|c| &c.type_) {
4269 Some(CredentialDetailType::PasswordMfa(totp, _, 7)) => !totp.is_empty(),
4270 _ => false,
4271 });
4272
4273 let c_status = cutxn
4275 .credential_primary_remove_backup_codes(&cust, ct)
4276 .expect("Failed to update the primary cred password");
4277
4278 assert!(matches!(c_status.mfaregstate, MfaRegStateStatus::None));
4279 assert!(match c_status.primary.as_ref().map(|c| &c.type_) {
4280 Some(CredentialDetailType::PasswordMfa(totp, _, 0)) => !totp.is_empty(),
4281 _ => false,
4282 });
4283
4284 let c_status = cutxn
4286 .credential_primary_init_backup_codes(&cust, ct)
4287 .expect("Failed to update the primary cred password");
4288
4289 assert!(matches!(
4290 c_status.mfaregstate,
4291 MfaRegStateStatus::BackupCodes(_)
4292 ));
4293 assert!(match c_status.primary.as_ref().map(|c| &c.type_) {
4294 Some(CredentialDetailType::PasswordMfa(totp, _, 8)) => !totp.is_empty(),
4295 _ => false,
4296 });
4297
4298 let c_status = cutxn
4300 .credential_primary_remove_totp(&cust, ct, "totp")
4301 .expect("Failed to update the primary cred password");
4302
4303 assert!(matches!(c_status.mfaregstate, MfaRegStateStatus::None));
4304 assert!(matches!(
4305 c_status.primary.as_ref().map(|c| &c.type_),
4306 Some(CredentialDetailType::Password)
4307 ));
4308
4309 drop(cutxn);
4310 commit_session(idms, ct, cust).await;
4311 }
4312
4313 #[idm_test]
4314 async fn credential_update_onboarding_cancel_inprogress_totp(
4315 idms: &IdmServer,
4316 idms_delayed: &mut IdmServerDelayed,
4317 ) {
4318 let test_pw = "fo3EitierohF9AelaNgiem0Ei6vup4equo1Oogeevaetehah8Tobeengae3Ci0ooh0uki";
4319 let ct = Duration::from_secs(TEST_CURRENT_TIME);
4320
4321 let (cust, _) = setup_test_session(idms, ct).await;
4322 let cutxn = idms.cred_update_transaction().await.unwrap();
4323
4324 let c_status = cutxn
4326 .credential_primary_set_password(&cust, ct, test_pw)
4327 .expect("Failed to update the primary cred password");
4328
4329 assert!(c_status.can_commit);
4331
4332 let c_status = cutxn
4334 .credential_primary_init_totp(&cust, ct)
4335 .expect("Failed to update the primary cred totp");
4336
4337 assert!(c_status.can_commit);
4339 assert!(matches!(
4340 c_status.mfaregstate,
4341 MfaRegStateStatus::TotpCheck(_)
4342 ));
4343
4344 let c_status = cutxn
4345 .credential_update_cancel_mfareg(&cust, ct)
4346 .expect("Failed to cancel in-flight totp change");
4347
4348 assert!(matches!(c_status.mfaregstate, MfaRegStateStatus::None));
4349 assert!(c_status.can_commit);
4350
4351 drop(cutxn);
4352 commit_session(idms, ct, cust).await;
4353
4354 assert!(check_testperson_password(idms, idms_delayed, test_pw, ct)
4356 .await
4357 .is_some());
4358 }
4359
4360 async fn create_new_passkey(
4367 ct: Duration,
4368 origin: &Url,
4369 cutxn: &IdmServerCredUpdateTransaction<'_>,
4370 cust: &CredentialUpdateSessionToken,
4371 wa: &mut WebauthnAuthenticator<SoftPasskey>,
4372 ) -> CredentialUpdateSessionStatus {
4373 let c_status = cutxn
4375 .credential_passkey_init(cust, ct)
4376 .expect("Failed to initiate passkey registration");
4377
4378 assert!(c_status.passkeys.is_empty());
4379
4380 let passkey_chal = match c_status.mfaregstate {
4381 MfaRegStateStatus::Passkey(c) => Some(c),
4382 _ => None,
4383 }
4384 .expect("Unable to access passkey challenge, invalid state");
4385
4386 let passkey_resp = wa
4387 .do_registration(origin.clone(), passkey_chal)
4388 .expect("Failed to create soft passkey");
4389
4390 let label = "softtoken".to_string();
4392 let c_status = cutxn
4393 .credential_passkey_finish(cust, ct, label, &passkey_resp)
4394 .expect("Failed to initiate passkey registration");
4395
4396 assert!(matches!(c_status.mfaregstate, MfaRegStateStatus::None));
4397 assert!(c_status.primary.as_ref().is_none());
4398
4399 trace!(?c_status);
4401 assert_eq!(c_status.passkeys.len(), 1);
4402
4403 c_status
4404 }
4405
4406 #[idm_test]
4407 async fn credential_update_onboarding_create_new_passkey(
4408 idms: &IdmServer,
4409 idms_delayed: &mut IdmServerDelayed,
4410 ) {
4411 let ct = Duration::from_secs(TEST_CURRENT_TIME);
4412 let test_pw = "fo3EitierohF9AelaNgiem0Ei6vup4equo1Oogeevaetehah8Tobeengae3Ci0ooh0uki";
4413
4414 let (cust, _) = setup_test_session(idms, ct).await;
4415 let cutxn = idms.cred_update_transaction().await.unwrap();
4416 let origin = cutxn.get_origin().clone();
4417
4418 let mut wa = WebauthnAuthenticator::new(SoftPasskey::new(true));
4420
4421 let c_status = create_new_passkey(ct, &origin, &cutxn, &cust, &mut wa).await;
4422
4423 let pk_uuid = c_status.passkeys.first().map(|pkd| pkd.uuid).unwrap();
4425
4426 drop(cutxn);
4428 commit_session(idms, ct, cust).await;
4429
4430 assert!(
4432 check_testperson_passkey(idms, idms_delayed, &mut wa, origin.clone(), ct)
4433 .await
4434 .is_some()
4435 );
4436
4437 let (cust, _) = renew_test_session(idms, ct).await;
4439 let cutxn = idms.cred_update_transaction().await.unwrap();
4440
4441 trace!(?c_status);
4442 assert!(c_status.primary.is_none());
4443 assert_eq!(c_status.passkeys.len(), 1);
4444
4445 let c_status = cutxn
4446 .credential_passkey_remove(&cust, ct, pk_uuid)
4447 .expect("Failed to delete the passkey");
4448
4449 trace!(?c_status);
4450 assert!(c_status.primary.is_none());
4451 assert!(c_status.passkeys.is_empty());
4452
4453 assert!(c_status
4454 .warnings
4455 .contains(&CredentialUpdateSessionStatusWarnings::NoValidCredentials));
4456 assert!(!c_status.can_commit);
4457
4458 let c_status = cutxn
4460 .credential_primary_set_password(&cust, ct, test_pw)
4461 .expect("Failed to update the primary cred password");
4462
4463 assert!(c_status.can_commit);
4465 assert!(!c_status
4466 .warnings
4467 .contains(&CredentialUpdateSessionStatusWarnings::NoValidCredentials));
4468
4469 drop(cutxn);
4470 commit_session(idms, ct, cust).await;
4471
4472 assert!(
4474 check_testperson_passkey(idms, idms_delayed, &mut wa, origin, ct)
4475 .await
4476 .is_none()
4477 );
4478 }
4479
4480 #[idm_test]
4481 async fn credential_update_access_denied(
4482 idms: &IdmServer,
4483 _idms_delayed: &mut IdmServerDelayed,
4484 ) {
4485 let ct = Duration::from_secs(TEST_CURRENT_TIME);
4489
4490 let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();
4491
4492 let sync_uuid = Uuid::new_v4();
4493
4494 let e1 = entry_init!(
4495 (Attribute::Class, EntryClass::Object.to_value()),
4496 (Attribute::Class, EntryClass::SyncAccount.to_value()),
4497 (Attribute::Name, Value::new_iname("test_scim_sync")),
4498 (Attribute::Uuid, Value::Uuid(sync_uuid)),
4499 (
4500 Attribute::Description,
4501 Value::new_utf8s("A test sync agreement")
4502 )
4503 );
4504
4505 let e2 = entry_init!(
4506 (Attribute::Class, EntryClass::Object.to_value()),
4507 (Attribute::Class, EntryClass::SyncObject.to_value()),
4508 (Attribute::Class, EntryClass::Account.to_value()),
4509 (Attribute::Class, EntryClass::PosixAccount.to_value()),
4510 (Attribute::Class, EntryClass::Person.to_value()),
4511 (Attribute::SyncParentUuid, Value::Refer(sync_uuid)),
4512 (Attribute::Name, Value::new_iname(TESTPERSON_NAME)),
4513 (Attribute::Uuid, Value::Uuid(TESTPERSON_UUID)),
4514 (Attribute::Description, Value::new_utf8s(TESTPERSON_NAME)),
4515 (Attribute::DisplayName, Value::new_utf8s(TESTPERSON_NAME))
4516 );
4517
4518 let ce = CreateEvent::new_internal(vec![e1, e2]);
4519 let cr = idms_prox_write.qs_write.create(&ce);
4520 assert!(cr.is_ok());
4521
4522 let testperson = idms_prox_write
4523 .qs_write
4524 .internal_search_uuid(TESTPERSON_UUID)
4525 .expect("failed");
4526
4527 let cur = idms_prox_write.init_credential_update(
4528 &InitCredentialUpdateEvent::new_impersonate_entry(testperson),
4529 ct,
4530 );
4531
4532 idms_prox_write.commit().expect("Failed to commit txn");
4533
4534 let (cust, custatus) = cur.expect("Failed to start update");
4535
4536 trace!(?custatus);
4537
4538 let CredentialUpdateSessionStatus {
4541 spn: _,
4542 displayname: _,
4543 ext_cred_portal,
4544 mfaregstate: _,
4545 can_commit: _,
4546 warnings: _,
4547 primary: _,
4548 primary_state,
4549 passkeys: _,
4550 passkeys_state,
4551 attested_passkeys: _,
4552 attested_passkeys_state,
4553 attested_passkeys_allowed_devices: _,
4554 unixcred_state,
4555 unixcred: _,
4556 sshkeys: _,
4557 sshkeys_state,
4558 } = custatus;
4559
4560 assert!(matches!(ext_cred_portal, CUExtPortal::Hidden));
4561 assert!(matches!(primary_state, CredentialState::AccessDeny));
4562 assert!(matches!(passkeys_state, CredentialState::AccessDeny));
4563 assert!(matches!(
4564 attested_passkeys_state,
4565 CredentialState::AccessDeny
4566 ));
4567 assert!(matches!(unixcred_state, CredentialState::AccessDeny));
4568 assert!(matches!(sshkeys_state, CredentialState::AccessDeny));
4569
4570 let cutxn = idms.cred_update_transaction().await.unwrap();
4571
4572 let err = cutxn
4578 .credential_primary_set_password(&cust, ct, "password")
4579 .unwrap_err();
4580 assert!(matches!(err, OperationError::AccessDenied));
4581
4582 let err = cutxn
4583 .credential_unix_set_password(&cust, ct, "password")
4584 .unwrap_err();
4585 assert!(matches!(err, OperationError::AccessDenied));
4586
4587 let sshkey = SshPublicKey::from_string(SSHKEY_VALID_1).expect("Invalid SSHKEY_VALID_1");
4588
4589 let err = cutxn
4590 .credential_sshkey_add(&cust, ct, "label".to_string(), sshkey)
4591 .unwrap_err();
4592 assert!(matches!(err, OperationError::AccessDenied));
4593
4594 let err = cutxn.credential_primary_init_totp(&cust, ct).unwrap_err();
4596 assert!(matches!(err, OperationError::AccessDenied));
4597
4598 let err = cutxn
4600 .credential_primary_check_totp(&cust, ct, 0, "totp")
4601 .unwrap_err();
4602 assert!(matches!(err, OperationError::AccessDenied));
4603
4604 let err = cutxn
4606 .credential_primary_accept_sha1_totp(&cust, ct)
4607 .unwrap_err();
4608 assert!(matches!(err, OperationError::AccessDenied));
4609
4610 let err = cutxn
4612 .credential_primary_remove_totp(&cust, ct, "totp")
4613 .unwrap_err();
4614 assert!(matches!(err, OperationError::AccessDenied));
4615
4616 let err = cutxn
4618 .credential_primary_init_backup_codes(&cust, ct)
4619 .unwrap_err();
4620 assert!(matches!(err, OperationError::AccessDenied));
4621
4622 let err = cutxn
4624 .credential_primary_remove_backup_codes(&cust, ct)
4625 .unwrap_err();
4626 assert!(matches!(err, OperationError::AccessDenied));
4627
4628 let err = cutxn.credential_primary_delete(&cust, ct).unwrap_err();
4630 assert!(matches!(err, OperationError::AccessDenied));
4631
4632 let err = cutxn.credential_passkey_init(&cust, ct).unwrap_err();
4634 assert!(matches!(err, OperationError::AccessDenied));
4635
4636 let err = cutxn
4641 .credential_passkey_remove(&cust, ct, Uuid::new_v4())
4642 .unwrap_err();
4643 assert!(matches!(err, OperationError::AccessDenied));
4644
4645 let c_status = cutxn
4646 .credential_update_status(&cust, ct)
4647 .expect("Failed to get the current session status.");
4648 trace!(?c_status);
4649 assert!(c_status.primary.is_none());
4650 assert!(c_status.passkeys.is_empty());
4651
4652 assert!(!c_status.can_commit);
4654 assert!(c_status
4655 .warnings
4656 .contains(&CredentialUpdateSessionStatusWarnings::NoValidCredentials));
4657 }
4658
4659 #[idm_test]
4661 async fn credential_update_account_policy_mfa_required(
4662 idms: &IdmServer,
4663 _idms_delayed: &mut IdmServerDelayed,
4664 ) {
4665 let test_pw = "fo3EitierohF9AelaNgiem0Ei6vup4equo1Oogeevaetehah8Tobeengae3Ci0ooh0uki";
4666 let ct = Duration::from_secs(TEST_CURRENT_TIME);
4667
4668 let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();
4669
4670 let modlist = ModifyList::new_purge_and_set(
4671 Attribute::CredentialTypeMinimum,
4672 CredentialType::Mfa.into(),
4673 );
4674 idms_prox_write
4675 .qs_write
4676 .internal_modify_uuid(UUID_IDM_ALL_ACCOUNTS, &modlist)
4677 .expect("Unable to change default session exp");
4678
4679 assert!(idms_prox_write.commit().is_ok());
4680 let (cust, _) = setup_test_session(idms, ct).await;
4683
4684 let cutxn = idms.cred_update_transaction().await.unwrap();
4685
4686 let c_status = cutxn
4690 .credential_update_status(&cust, ct)
4691 .expect("Failed to get the current session status.");
4692
4693 trace!(?c_status);
4694
4695 assert!(c_status.primary.is_none());
4696
4697 let c_status = cutxn
4700 .credential_primary_set_password(&cust, ct, test_pw)
4701 .expect("Failed to update the primary cred password");
4702
4703 assert!(!c_status.can_commit);
4704 assert!(c_status
4705 .warnings
4706 .contains(&CredentialUpdateSessionStatusWarnings::MfaRequired));
4707 let c_status = cutxn
4710 .credential_primary_init_totp(&cust, ct)
4711 .expect("Failed to update the primary cred password");
4712
4713 let totp_token: Totp = match c_status.mfaregstate {
4715 MfaRegStateStatus::TotpCheck(secret) => Some(secret.try_into().unwrap()),
4716
4717 _ => None,
4718 }
4719 .expect("Unable to retrieve totp token, invalid state.");
4720
4721 trace!(?totp_token);
4722 let chal = totp_token
4723 .do_totp_duration_from_epoch(&ct)
4724 .expect("Failed to perform totp step");
4725
4726 let c_status = cutxn
4727 .credential_primary_check_totp(&cust, ct, chal, "totp")
4728 .expect("Failed to update the primary cred totp");
4729
4730 assert!(matches!(c_status.mfaregstate, MfaRegStateStatus::None));
4731 assert!(match c_status.primary.as_ref().map(|c| &c.type_) {
4732 Some(CredentialDetailType::PasswordMfa(totp, _, 0)) => !totp.is_empty(),
4733 _ => false,
4734 });
4735
4736 assert!(c_status.can_commit);
4738 assert!(c_status.warnings.is_empty());
4739
4740 drop(cutxn);
4741 commit_session(idms, ct, cust).await;
4742
4743 let (cust, _) = renew_test_session(idms, ct).await;
4745 let cutxn = idms.cred_update_transaction().await.unwrap();
4746
4747 let c_status = cutxn
4748 .credential_primary_remove_totp(&cust, ct, "totp")
4749 .expect("Failed to update the primary cred totp");
4750
4751 assert!(matches!(c_status.mfaregstate, MfaRegStateStatus::None));
4752 assert!(matches!(
4753 c_status.primary.as_ref().map(|c| &c.type_),
4754 Some(CredentialDetailType::Password)
4755 ));
4756
4757 assert!(!c_status.can_commit);
4759 assert!(c_status
4760 .warnings
4761 .contains(&CredentialUpdateSessionStatusWarnings::MfaRequired));
4762
4763 let c_status = cutxn
4765 .credential_primary_delete(&cust, ct)
4766 .expect("Failed to delete the primary credential");
4767 assert!(c_status.primary.is_none());
4768
4769 let origin = cutxn.get_origin().clone();
4770 let mut wa = WebauthnAuthenticator::new(SoftPasskey::new(true));
4771
4772 let c_status = create_new_passkey(ct, &origin, &cutxn, &cust, &mut wa).await;
4773
4774 assert!(c_status.can_commit);
4775 assert!(c_status.warnings.is_empty());
4776 assert_eq!(c_status.passkeys.len(), 1);
4777
4778 drop(cutxn);
4779 commit_session(idms, ct, cust).await;
4780 }
4781
4782 #[idm_test]
4783 async fn credential_update_account_policy_passkey_required(
4784 idms: &IdmServer,
4785 _idms_delayed: &mut IdmServerDelayed,
4786 ) {
4787 let test_pw = "fo3EitierohF9AelaNgiem0Ei6vup4equo1Oogeevaetehah8Tobeengae3Ci0ooh0uki";
4788 let ct = Duration::from_secs(TEST_CURRENT_TIME);
4789
4790 let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();
4791
4792 let modlist = ModifyList::new_purge_and_set(
4793 Attribute::CredentialTypeMinimum,
4794 CredentialType::Passkey.into(),
4795 );
4796 idms_prox_write
4797 .qs_write
4798 .internal_modify_uuid(UUID_IDM_ALL_ACCOUNTS, &modlist)
4799 .expect("Unable to change default session exp");
4800
4801 assert!(idms_prox_write.commit().is_ok());
4802 let (cust, _) = setup_test_session(idms, ct).await;
4805
4806 let cutxn = idms.cred_update_transaction().await.unwrap();
4807
4808 let c_status = cutxn
4812 .credential_update_status(&cust, ct)
4813 .expect("Failed to get the current session status.");
4814
4815 trace!(?c_status);
4816 assert!(c_status.primary.is_none());
4817 assert!(matches!(
4818 c_status.primary_state,
4819 CredentialState::PolicyDeny
4820 ));
4821
4822 let err = cutxn
4823 .credential_primary_set_password(&cust, ct, test_pw)
4824 .unwrap_err();
4825 assert!(matches!(err, OperationError::AccessDenied));
4826
4827 let origin = cutxn.get_origin().clone();
4828 let mut wa = WebauthnAuthenticator::new(SoftPasskey::new(true));
4829
4830 let c_status = create_new_passkey(ct, &origin, &cutxn, &cust, &mut wa).await;
4831
4832 assert!(c_status.can_commit);
4833 assert!(c_status.warnings.is_empty());
4834 assert_eq!(c_status.passkeys.len(), 1);
4835
4836 drop(cutxn);
4837 commit_session(idms, ct, cust).await;
4838 }
4839
4840 #[idm_test]
4843 async fn credential_update_account_policy_attested_passkey_required(
4844 idms: &IdmServer,
4845 idms_delayed: &mut IdmServerDelayed,
4846 ) {
4847 let ct = Duration::from_secs(TEST_CURRENT_TIME);
4848
4849 let (soft_token_valid_a, ca_root_a) = SoftToken::new(true).unwrap();
4851 let mut wa_token_valid = WebauthnAuthenticator::new(soft_token_valid_a);
4852
4853 let (soft_token_valid_b, ca_root_b) = SoftToken::new(true).unwrap();
4855 let mut wa_token_valid_b = WebauthnAuthenticator::new(soft_token_valid_b);
4856
4857 let mut att_ca_builder = AttestationCaListBuilder::new();
4859 att_ca_builder
4860 .insert_device_x509(
4861 ca_root_a,
4862 softtoken::AAGUID,
4863 "softtoken_a".to_string(),
4864 Default::default(),
4865 )
4866 .unwrap();
4867 att_ca_builder
4868 .insert_device_x509(
4869 ca_root_b,
4870 softtoken::AAGUID,
4871 "softtoken_b".to_string(),
4872 Default::default(),
4873 )
4874 .unwrap();
4875 let att_ca_list = att_ca_builder.build();
4876
4877 let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();
4878
4879 let modlist = ModifyList::new_purge_and_set(
4880 Attribute::WebauthnAttestationCaList,
4881 Value::WebauthnAttestationCaList(att_ca_list),
4882 );
4883 idms_prox_write
4884 .qs_write
4885 .internal_modify_uuid(UUID_IDM_ALL_ACCOUNTS, &modlist)
4886 .expect("Unable to change webauthn attestation policy");
4887
4888 assert!(idms_prox_write.commit().is_ok());
4889
4890 let (soft_token_invalid, _) = SoftToken::new(true).unwrap();
4892 let mut wa_token_invalid = WebauthnAuthenticator::new(soft_token_invalid);
4893
4894 let mut wa_passkey_invalid = WebauthnAuthenticator::new(SoftPasskey::new(true));
4895
4896 let (cust, _) = setup_test_session(idms, ct).await;
4899 let cutxn = idms.cred_update_transaction().await.unwrap();
4900 let origin = cutxn.get_origin().clone();
4901
4902 let c_status = cutxn
4904 .credential_update_status(&cust, ct)
4905 .expect("Failed to get the current session status.");
4906
4907 trace!(?c_status);
4908 assert!(c_status.attested_passkeys.is_empty());
4909 assert!(c_status
4910 .attested_passkeys_allowed_devices
4911 .contains(&"softtoken_a".to_string()));
4912 assert!(c_status
4913 .attested_passkeys_allowed_devices
4914 .contains(&"softtoken_b".to_string()));
4915
4916 let err = cutxn.credential_passkey_init(&cust, ct).unwrap_err();
4919 assert!(matches!(err, OperationError::AccessDenied));
4920
4921 let c_status = cutxn
4924 .credential_attested_passkey_init(&cust, ct)
4925 .expect("Failed to initiate attested passkey registration");
4926
4927 let passkey_chal = match c_status.mfaregstate {
4928 MfaRegStateStatus::AttestedPasskey(c) => Some(c),
4929 _ => None,
4930 }
4931 .expect("Unable to access passkey challenge, invalid state");
4932
4933 let passkey_resp = wa_passkey_invalid
4934 .do_registration(origin.clone(), passkey_chal)
4935 .expect("Failed to create soft passkey");
4936
4937 let label = "softtoken".to_string();
4939 let err = cutxn
4940 .credential_attested_passkey_finish(&cust, ct, label, &passkey_resp)
4941 .unwrap_err();
4942
4943 assert!(matches!(
4944 err,
4945 OperationError::CU0001WebauthnAttestationNotTrusted
4946 ));
4947
4948 let c_status = cutxn
4951 .credential_attested_passkey_init(&cust, ct)
4952 .expect("Failed to initiate attested passkey registration");
4953
4954 let passkey_chal = match c_status.mfaregstate {
4955 MfaRegStateStatus::AttestedPasskey(c) => Some(c),
4956 _ => None,
4957 }
4958 .expect("Unable to access passkey challenge, invalid state");
4959
4960 let passkey_resp = wa_token_invalid
4961 .do_registration(origin.clone(), passkey_chal)
4962 .expect("Failed to create soft passkey");
4963
4964 let label = "softtoken".to_string();
4966 let err = cutxn
4967 .credential_attested_passkey_finish(&cust, ct, label, &passkey_resp)
4968 .unwrap_err();
4969
4970 assert!(matches!(
4971 err,
4972 OperationError::CU0001WebauthnAttestationNotTrusted
4973 ));
4974
4975 let c_status = cutxn
4978 .credential_attested_passkey_init(&cust, ct)
4979 .expect("Failed to initiate attested passkey registration");
4980
4981 let passkey_chal = match c_status.mfaregstate {
4982 MfaRegStateStatus::AttestedPasskey(c) => Some(c),
4983 _ => None,
4984 }
4985 .expect("Unable to access passkey challenge, invalid state");
4986
4987 let passkey_resp = wa_token_valid
4988 .do_registration(origin.clone(), passkey_chal)
4989 .expect("Failed to create soft passkey");
4990
4991 let label = "softtoken".to_string();
4993 let c_status = cutxn
4994 .credential_attested_passkey_finish(&cust, ct, label, &passkey_resp)
4995 .expect("Failed to initiate passkey registration");
4996
4997 assert!(matches!(c_status.mfaregstate, MfaRegStateStatus::None));
4998 trace!(?c_status);
4999 assert_eq!(c_status.attested_passkeys.len(), 1);
5000
5001 let pk_uuid = c_status
5002 .attested_passkeys
5003 .first()
5004 .map(|pkd| pkd.uuid)
5005 .unwrap();
5006
5007 drop(cutxn);
5008 commit_session(idms, ct, cust).await;
5009
5010 assert!(check_testperson_passkey(
5012 idms,
5013 idms_delayed,
5014 &mut wa_token_valid,
5015 origin.clone(),
5016 ct
5017 )
5018 .await
5019 .is_some());
5020
5021 let (cust, _) = renew_test_session(idms, ct).await;
5023 let cutxn = idms.cred_update_transaction().await.unwrap();
5024
5025 trace!(?c_status);
5026 assert!(c_status.primary.is_none());
5027 assert!(c_status.passkeys.is_empty());
5028 assert_eq!(c_status.attested_passkeys.len(), 1);
5029
5030 let c_status = cutxn
5031 .credential_attested_passkey_remove(&cust, ct, pk_uuid)
5032 .expect("Failed to delete the attested passkey");
5033
5034 trace!(?c_status);
5035 assert!(c_status.primary.is_none());
5036 assert!(c_status.passkeys.is_empty());
5037 assert!(c_status.attested_passkeys.is_empty());
5038
5039 assert!(!c_status.can_commit);
5041 assert!(c_status
5042 .warnings
5043 .contains(&CredentialUpdateSessionStatusWarnings::NoValidCredentials));
5044
5045 let c_status = cutxn
5047 .credential_attested_passkey_init(&cust, ct)
5048 .expect("Failed to initiate attested passkey registration");
5049
5050 let passkey_chal = match c_status.mfaregstate {
5051 MfaRegStateStatus::AttestedPasskey(c) => Some(c),
5052 _ => None,
5053 }
5054 .expect("Unable to access passkey challenge, invalid state");
5055
5056 let passkey_resp = wa_token_valid_b
5058 .do_registration(origin.clone(), passkey_chal)
5059 .expect("Failed to create soft passkey");
5060
5061 let label = "softtoken".to_string();
5063 let c_status = cutxn
5064 .credential_attested_passkey_finish(&cust, ct, label, &passkey_resp)
5065 .expect("Failed to initiate passkey registration");
5066
5067 assert!(matches!(c_status.mfaregstate, MfaRegStateStatus::None));
5068 trace!(?c_status);
5069 assert_eq!(c_status.attested_passkeys.len(), 1);
5070
5071 drop(cutxn);
5072 commit_session(idms, ct, cust).await;
5073
5074 assert!(
5077 check_testperson_passkey(idms, idms_delayed, &mut wa_token_valid, origin, ct)
5078 .await
5079 .is_none()
5080 );
5081 }
5082
5083 #[idm_test(audit = 1)]
5084 async fn credential_update_account_policy_attested_passkey_changed(
5085 idms: &IdmServer,
5086 idms_delayed: &mut IdmServerDelayed,
5087 idms_audit: &mut IdmServerAudit,
5088 ) {
5089 let ct = Duration::from_secs(TEST_CURRENT_TIME);
5090
5091 let (soft_token_1, ca_root_1) = SoftToken::new(true).unwrap();
5093 let mut wa_token_1 = WebauthnAuthenticator::new(soft_token_1);
5094
5095 let (soft_token_2, ca_root_2) = SoftToken::new(true).unwrap();
5096 let mut wa_token_2 = WebauthnAuthenticator::new(soft_token_2);
5097
5098 let mut att_ca_builder = AttestationCaListBuilder::new();
5100 att_ca_builder
5101 .insert_device_x509(
5102 ca_root_1.clone(),
5103 softtoken::AAGUID,
5104 "softtoken_1".to_string(),
5105 Default::default(),
5106 )
5107 .unwrap();
5108 let att_ca_list = att_ca_builder.build();
5109
5110 trace!(?att_ca_list);
5111
5112 let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();
5113
5114 let modlist = ModifyList::new_purge_and_set(
5115 Attribute::WebauthnAttestationCaList,
5116 Value::WebauthnAttestationCaList(att_ca_list),
5117 );
5118 idms_prox_write
5119 .qs_write
5120 .internal_modify_uuid(UUID_IDM_ALL_ACCOUNTS, &modlist)
5121 .expect("Unable to change webauthn attestation policy");
5122
5123 assert!(idms_prox_write.commit().is_ok());
5124
5125 let mut att_ca_builder = AttestationCaListBuilder::new();
5127 att_ca_builder
5128 .insert_device_x509(
5129 ca_root_2,
5130 softtoken::AAGUID,
5131 "softtoken_2".to_string(),
5132 Default::default(),
5133 )
5134 .unwrap();
5135 let att_ca_list_post = att_ca_builder.build();
5136
5137 let (cust, _) = setup_test_session(idms, ct).await;
5139 let cutxn = idms.cred_update_transaction().await.unwrap();
5140 let origin = cutxn.get_origin().clone();
5141
5142 let c_status = cutxn
5144 .credential_attested_passkey_init(&cust, ct)
5145 .expect("Failed to initiate attested passkey registration");
5146
5147 let passkey_chal = match c_status.mfaregstate {
5148 MfaRegStateStatus::AttestedPasskey(c) => Some(c),
5149 _ => None,
5150 }
5151 .expect("Unable to access passkey challenge, invalid state");
5152
5153 let passkey_resp = wa_token_1
5154 .do_registration(origin.clone(), passkey_chal)
5155 .expect("Failed to create soft passkey");
5156
5157 let label = "softtoken".to_string();
5159 let c_status = cutxn
5160 .credential_attested_passkey_finish(&cust, ct, label, &passkey_resp)
5161 .expect("Failed to initiate passkey registration");
5162
5163 assert!(matches!(c_status.mfaregstate, MfaRegStateStatus::None));
5164 trace!(?c_status);
5165 assert_eq!(c_status.attested_passkeys.len(), 1);
5166
5167 drop(cutxn);
5170 commit_session(idms, ct, cust).await;
5171
5172 assert!(
5174 check_testperson_passkey(idms, idms_delayed, &mut wa_token_1, origin.clone(), ct)
5175 .await
5176 .is_some()
5177 );
5178
5179 let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();
5181
5182 let modlist = ModifyList::new_purge_and_set(
5183 Attribute::WebauthnAttestationCaList,
5184 Value::WebauthnAttestationCaList(att_ca_list_post),
5185 );
5186 idms_prox_write
5187 .qs_write
5188 .internal_modify_uuid(UUID_IDM_ALL_ACCOUNTS, &modlist)
5189 .expect("Unable to change webauthn attestation policy");
5190
5191 assert!(idms_prox_write.commit().is_ok());
5192
5193 assert!(
5195 check_testperson_passkey(idms, idms_delayed, &mut wa_token_1, origin.clone(), ct)
5196 .await
5197 .is_none()
5198 );
5199
5200 match idms_audit.audit_rx().try_recv() {
5203 Ok(AuditEvent::AuthenticationDenied { .. }) => {}
5204 _ => panic!("Oh no"),
5205 }
5206
5207 let (cust, _) = renew_test_session(idms, ct).await;
5209 let cutxn = idms.cred_update_transaction().await.unwrap();
5210
5211 let c_status = cutxn
5213 .credential_update_status(&cust, ct)
5214 .expect("Failed to get the current session status.");
5215
5216 trace!(?c_status);
5217 assert!(c_status.attested_passkeys.is_empty());
5218
5219 assert!(!c_status.can_commit);
5221 assert!(c_status
5222 .warnings
5223 .contains(&CredentialUpdateSessionStatusWarnings::NoValidCredentials));
5224
5225 let c_status = cutxn
5228 .credential_attested_passkey_init(&cust, ct)
5229 .expect("Failed to initiate attested passkey registration");
5230
5231 let passkey_chal = match c_status.mfaregstate {
5232 MfaRegStateStatus::AttestedPasskey(c) => Some(c),
5233 _ => None,
5234 }
5235 .expect("Unable to access passkey challenge, invalid state");
5236
5237 let passkey_resp = wa_token_2
5238 .do_registration(origin.clone(), passkey_chal)
5239 .expect("Failed to create soft passkey");
5240
5241 let label = "softtoken".to_string();
5243 let c_status = cutxn
5244 .credential_attested_passkey_finish(&cust, ct, label, &passkey_resp)
5245 .expect("Failed to initiate passkey registration");
5246
5247 assert!(matches!(c_status.mfaregstate, MfaRegStateStatus::None));
5248 trace!(?c_status);
5249 assert_eq!(c_status.attested_passkeys.len(), 1);
5250
5251 drop(cutxn);
5252 commit_session(idms, ct, cust).await;
5253
5254 assert!(
5256 check_testperson_passkey(idms, idms_delayed, &mut wa_token_1, origin.clone(), ct)
5257 .await
5258 .is_none()
5259 );
5260
5261 assert!(
5263 check_testperson_passkey(idms, idms_delayed, &mut wa_token_2, origin.clone(), ct)
5264 .await
5265 .is_some()
5266 );
5267 }
5268
5269 #[idm_test]
5271 async fn credential_update_account_policy_attested_passkey_downgrade(
5272 idms: &IdmServer,
5273 idms_delayed: &mut IdmServerDelayed,
5274 ) {
5275 let ct = Duration::from_secs(TEST_CURRENT_TIME);
5276
5277 let (soft_token_1, ca_root_1) = SoftToken::new(true).unwrap();
5279 let mut wa_token_1 = WebauthnAuthenticator::new(soft_token_1);
5280
5281 let mut att_ca_builder = AttestationCaListBuilder::new();
5282 att_ca_builder
5283 .insert_device_x509(
5284 ca_root_1.clone(),
5285 softtoken::AAGUID,
5286 "softtoken_1".to_string(),
5287 Default::default(),
5288 )
5289 .unwrap();
5290 let att_ca_list = att_ca_builder.build();
5291
5292 trace!(?att_ca_list);
5293
5294 let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();
5295
5296 let modlist = ModifyList::new_purge_and_set(
5297 Attribute::WebauthnAttestationCaList,
5298 Value::WebauthnAttestationCaList(att_ca_list),
5299 );
5300 idms_prox_write
5301 .qs_write
5302 .internal_modify_uuid(UUID_IDM_ALL_ACCOUNTS, &modlist)
5303 .expect("Unable to change webauthn attestation policy");
5304
5305 assert!(idms_prox_write.commit().is_ok());
5306
5307 let (cust, _) = setup_test_session(idms, ct).await;
5309 let cutxn = idms.cred_update_transaction().await.unwrap();
5310 let origin = cutxn.get_origin().clone();
5311
5312 let c_status = cutxn
5314 .credential_attested_passkey_init(&cust, ct)
5315 .expect("Failed to initiate attested passkey registration");
5316
5317 let passkey_chal = match c_status.mfaregstate {
5318 MfaRegStateStatus::AttestedPasskey(c) => Some(c),
5319 _ => None,
5320 }
5321 .expect("Unable to access passkey challenge, invalid state");
5322
5323 let passkey_resp = wa_token_1
5324 .do_registration(origin.clone(), passkey_chal)
5325 .expect("Failed to create soft passkey");
5326
5327 let label = "softtoken".to_string();
5329 let c_status = cutxn
5330 .credential_attested_passkey_finish(&cust, ct, label, &passkey_resp)
5331 .expect("Failed to initiate passkey registration");
5332
5333 assert!(matches!(c_status.mfaregstate, MfaRegStateStatus::None));
5334 trace!(?c_status);
5335 assert_eq!(c_status.attested_passkeys.len(), 1);
5336
5337 drop(cutxn);
5340 commit_session(idms, ct, cust).await;
5341
5342 assert!(
5344 check_testperson_passkey(idms, idms_delayed, &mut wa_token_1, origin.clone(), ct)
5345 .await
5346 .is_some()
5347 );
5348
5349 let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();
5351
5352 let modlist = ModifyList::new_purge(Attribute::WebauthnAttestationCaList);
5353 idms_prox_write
5354 .qs_write
5355 .internal_modify_uuid(UUID_IDM_ALL_ACCOUNTS, &modlist)
5356 .expect("Unable to change webauthn attestation policy");
5357
5358 assert!(idms_prox_write.commit().is_ok());
5359
5360 assert!(
5362 check_testperson_passkey(idms, idms_delayed, &mut wa_token_1, origin.clone(), ct)
5363 .await
5364 .is_some()
5365 );
5366
5367 let (cust, _) = renew_test_session(idms, ct).await;
5369 let cutxn = idms.cred_update_transaction().await.unwrap();
5370
5371 let c_status = cutxn
5372 .credential_update_status(&cust, ct)
5373 .expect("Failed to get the current session status.");
5374
5375 trace!(?c_status);
5376 assert_eq!(c_status.attested_passkeys.len(), 1);
5377 assert!(matches!(
5378 c_status.attested_passkeys_state,
5379 CredentialState::DeleteOnly
5380 ));
5381
5382 drop(cutxn);
5383 commit_session(idms, ct, cust).await;
5384 }
5385
5386 #[idm_test]
5387 async fn credential_update_unix_password(
5388 idms: &IdmServer,
5389 _idms_delayed: &mut IdmServerDelayed,
5390 ) {
5391 let test_pw = "fo3EitierohF9AelaNgiem0Ei6vup4equo1Oogeevaetehah8Tobeengae3Ci0ooh0uki";
5392 let ct = Duration::from_secs(TEST_CURRENT_TIME);
5393
5394 let (cust, _) = setup_test_session(idms, ct).await;
5395
5396 let cutxn = idms.cred_update_transaction().await.unwrap();
5397
5398 let c_status = cutxn
5402 .credential_update_status(&cust, ct)
5403 .expect("Failed to get the current session status.");
5404
5405 trace!(?c_status);
5406 assert!(c_status.unixcred.is_none());
5407
5408 assert!(c_status
5410 .warnings
5411 .contains(&CredentialUpdateSessionStatusWarnings::NoValidCredentials));
5412 assert!(!c_status.can_commit);
5413 let c_status = cutxn
5415 .credential_primary_set_password(&cust, ct, test_pw)
5416 .expect("Failed to update the primary cred password");
5417 assert!(c_status.can_commit);
5418 assert!(!c_status
5419 .warnings
5420 .contains(&CredentialUpdateSessionStatusWarnings::NoValidCredentials));
5421
5422 let c_status = cutxn
5425 .credential_unix_set_password(&cust, ct, test_pw)
5426 .expect("Failed to update the unix cred password");
5427
5428 assert!(c_status.can_commit);
5429
5430 drop(cutxn);
5431 commit_session(idms, ct, cust).await;
5432
5433 assert!(check_testperson_unix_password(idms, test_pw, ct)
5435 .await
5436 .is_some());
5437
5438 let (cust, _) = renew_test_session(idms, ct).await;
5440 let cutxn = idms.cred_update_transaction().await.unwrap();
5441
5442 let c_status = cutxn
5443 .credential_update_status(&cust, ct)
5444 .expect("Failed to get the current session status.");
5445 trace!(?c_status);
5446 assert!(c_status.unixcred.is_some());
5447
5448 let c_status = cutxn
5449 .credential_unix_delete(&cust, ct)
5450 .expect("Failed to delete the unix cred");
5451 trace!(?c_status);
5452 assert!(c_status.unixcred.is_none());
5453
5454 drop(cutxn);
5455 commit_session(idms, ct, cust).await;
5456
5457 assert!(check_testperson_unix_password(idms, test_pw, ct)
5459 .await
5460 .is_none());
5461 }
5462
5463 #[idm_test]
5464 async fn credential_update_sshkeys(idms: &IdmServer, _idms_delayed: &mut IdmServerDelayed) {
5465 let test_pw = "fo3EitierohF9AelaNgiem0Ei6vup4equo1Oogeevaetehah8Tobeengae3Ci0ooh0uki";
5466 let sshkey_valid_1 =
5467 SshPublicKey::from_string(SSHKEY_VALID_1).expect("Invalid SSHKEY_VALID_1");
5468 let sshkey_valid_2 =
5469 SshPublicKey::from_string(SSHKEY_VALID_2).expect("Invalid SSHKEY_VALID_2");
5470
5471 assert!(SshPublicKey::from_string(SSHKEY_INVALID).is_err());
5472
5473 let ct = Duration::from_secs(TEST_CURRENT_TIME);
5474 let (cust, _) = setup_test_session(idms, ct).await;
5475 let cutxn = idms.cred_update_transaction().await.unwrap();
5476
5477 let c_status = cutxn
5478 .credential_update_status(&cust, ct)
5479 .expect("Failed to get the current session status.");
5480
5481 assert!(c_status
5483 .warnings
5484 .contains(&CredentialUpdateSessionStatusWarnings::NoValidCredentials));
5485 assert!(!c_status.can_commit);
5486 let c_status = cutxn
5488 .credential_primary_set_password(&cust, ct, test_pw)
5489 .expect("Failed to update the primary cred password");
5490
5491 trace!(?c_status);
5494
5495 assert!(c_status.sshkeys.is_empty());
5496
5497 let result = cutxn.credential_sshkey_add(&cust, ct, "".to_string(), sshkey_valid_1.clone());
5499 assert!(matches!(result, Err(OperationError::InvalidLabel)));
5500
5501 let result =
5503 cutxn.credential_sshkey_add(&cust, ct, "🚛".to_string(), sshkey_valid_1.clone());
5504 assert!(matches!(result, Err(OperationError::InvalidLabel)));
5505
5506 let result = cutxn.credential_sshkey_remove(&cust, ct, "key1");
5508 assert!(matches!(result, Err(OperationError::NoMatchingEntries)));
5509
5510 let c_status = cutxn
5512 .credential_sshkey_add(&cust, ct, "key1".to_string(), sshkey_valid_1.clone())
5513 .expect("Failed to add sshkey_valid_1");
5514
5515 trace!(?c_status);
5516 assert_eq!(c_status.sshkeys.len(), 1);
5517 assert!(c_status.sshkeys.contains_key("key1"));
5518
5519 let c_status = cutxn
5521 .credential_sshkey_add(&cust, ct, "key2".to_string(), sshkey_valid_2.clone())
5522 .expect("Failed to add sshkey_valid_2");
5523
5524 trace!(?c_status);
5525 assert_eq!(c_status.sshkeys.len(), 2);
5526 assert!(c_status.sshkeys.contains_key("key1"));
5527 assert!(c_status.sshkeys.contains_key("key2"));
5528
5529 let c_status = cutxn
5531 .credential_sshkey_remove(&cust, ct, "key2")
5532 .expect("Failed to remove sshkey_valid_2");
5533
5534 trace!(?c_status);
5535 assert_eq!(c_status.sshkeys.len(), 1);
5536 assert!(c_status.sshkeys.contains_key("key1"));
5537
5538 let result =
5540 cutxn.credential_sshkey_add(&cust, ct, "key1".to_string(), sshkey_valid_2.clone());
5541 assert!(matches!(result, Err(OperationError::DuplicateLabel)));
5542
5543 let result =
5545 cutxn.credential_sshkey_add(&cust, ct, "key2".to_string(), sshkey_valid_1.clone());
5546 assert!(matches!(result, Err(OperationError::DuplicateKey)));
5547
5548 drop(cutxn);
5549 commit_session(idms, ct, cust).await;
5550 }
5551
5552 #[idm_test]
5554 async fn credential_update_at_least_one_credential(
5555 idms: &IdmServer,
5556 _idms_delayed: &mut IdmServerDelayed,
5557 ) {
5558 let test_pw = "fo3EitierohF9AelaNgiem0Ei6vup4equo1Oogeevaetehah8Tobeengae3Ci0ooh0uki";
5559 let ct = Duration::from_secs(TEST_CURRENT_TIME);
5560
5561 let (cust, _) = setup_test_session(idms, ct).await;
5562
5563 let cutxn = idms.cred_update_transaction().await.unwrap();
5564
5565 let c_status = cutxn
5569 .credential_update_status(&cust, ct)
5570 .expect("Failed to get the current session status.");
5571
5572 trace!(?c_status);
5573
5574 assert!(c_status.primary.is_none());
5575 assert!(c_status
5577 .warnings
5578 .contains(&CredentialUpdateSessionStatusWarnings::NoValidCredentials));
5579 assert!(!c_status.can_commit);
5580
5581 let c_status = cutxn
5583 .credential_primary_set_password(&cust, ct, test_pw)
5584 .expect("Failed to update the primary cred password");
5585
5586 assert!(c_status.can_commit);
5588 assert!(!c_status
5589 .warnings
5590 .contains(&CredentialUpdateSessionStatusWarnings::NoValidCredentials));
5591
5592 let c_status = cutxn
5594 .credential_primary_delete(&cust, ct)
5595 .expect("Failed to remove the primary credential");
5596
5597 assert!(c_status
5599 .warnings
5600 .contains(&CredentialUpdateSessionStatusWarnings::NoValidCredentials));
5601 assert!(!c_status.can_commit);
5602 }
5603
5604 async fn get_testperson_password_changed_time(idms: &IdmServer) -> Option<OffsetDateTime> {
5605 let mut txn = idms.proxy_read().await.unwrap();
5606 let entry = txn
5607 .qs_read
5608 .internal_search_uuid(TESTPERSON_UUID)
5609 .expect("Failed to read testperson entry");
5610 entry.get_ava_single_datetime(Attribute::PasswordChangedTime)
5611 }
5612
5613 #[idm_test]
5614 async fn credential_update_password_changed_time_password_set(
5615 idms: &IdmServer,
5616 _idms_delayed: &mut IdmServerDelayed,
5617 ) {
5618 let ct = Duration::from_secs(TEST_CURRENT_TIME);
5619
5620 let (cust, _) = setup_test_session(idms, ct).await;
5621
5622 assert!(get_testperson_password_changed_time(idms).await.is_none());
5624
5625 let cutxn = idms.cred_update_transaction().await.unwrap();
5627 let c_status = cutxn
5628 .credential_primary_set_password(&cust, ct, TESTPERSON_PASSWORD)
5629 .expect("Failed to update the primary cred password");
5630 assert!(c_status.can_commit);
5631 drop(cutxn);
5632 commit_session(idms, ct, cust).await;
5633
5634 let pwd_changed = get_testperson_password_changed_time(idms)
5635 .await
5636 .expect("PasswordChangedTime should be set after setting primary password");
5637 assert_eq!(pwd_changed, OffsetDateTime::UNIX_EPOCH);
5638
5639 let (cust, _) = renew_test_session(idms, ct).await;
5641 let cutxn = idms.cred_update_transaction().await.unwrap();
5642 let c_status = cutxn
5643 .credential_unix_set_password(&cust, ct, TESTPERSON_PASSWORD)
5644 .expect("Failed to set unix password");
5645 assert!(c_status.can_commit);
5646 drop(cutxn);
5647 commit_session(idms, ct, cust).await;
5648
5649 let pwd_changed = get_testperson_password_changed_time(idms)
5650 .await
5651 .expect("PasswordChangedTime should be set after setting both passwords");
5652 assert_eq!(pwd_changed, OffsetDateTime::UNIX_EPOCH + ct);
5654
5655 let ct = Duration::from_secs(TEST_CURRENT_TIME + 1000);
5656
5657 let (cust, _) = renew_test_session(idms, ct).await;
5659 let cutxn = idms.cred_update_transaction().await.unwrap();
5660 let _ = cutxn
5661 .credential_unix_set_password(&cust, ct, "R290Y2hhIGFnYWlu")
5662 .expect("Failed to set unix password on second update");
5663 drop(cutxn);
5664 commit_session(idms, ct, cust).await;
5665
5666 let pwd_changed_2 = get_testperson_password_changed_time(idms)
5667 .await
5668 .expect("PasswordChangedTime should be updated on second update");
5669 assert_eq!(pwd_changed_2, OffsetDateTime::UNIX_EPOCH + ct);
5670 assert!(pwd_changed_2 > pwd_changed);
5671 }
5672
5673 #[idm_test]
5674 async fn credential_update_password_changed_time_unix_deleted(
5675 idms: &IdmServer,
5676 _idms_delayed: &mut IdmServerDelayed,
5677 ) {
5678 let ct = Duration::from_secs(TEST_CURRENT_TIME);
5679
5680 let (cust, _) = setup_test_session(idms, ct).await;
5682 let cutxn = idms.cred_update_transaction().await.unwrap();
5683 let _ = cutxn
5684 .credential_primary_set_password(&cust, ct, TESTPERSON_PASSWORD)
5685 .expect("Failed to set primary password");
5686 let _ = cutxn
5687 .credential_unix_set_password(&cust, ct, TESTPERSON_PASSWORD)
5688 .expect("Failed to set unix password");
5689 drop(cutxn);
5690 commit_session(idms, ct, cust).await;
5691
5692 let pwd_changed_1 = get_testperson_password_changed_time(idms)
5693 .await
5694 .expect("PasswordChangedTime should be set");
5695 assert_eq!(pwd_changed_1, OffsetDateTime::UNIX_EPOCH + ct);
5696
5697 let ct = Duration::from_secs(TEST_CURRENT_TIME + 1000);
5698
5699 let (cust, _) = renew_test_session(idms, ct).await;
5701 let cutxn = idms.cred_update_transaction().await.unwrap();
5702 let c_status = cutxn
5703 .credential_unix_delete(&cust, ct)
5704 .expect("Failed to delete unix credential");
5705 assert!(c_status.unixcred.is_none());
5706 assert!(c_status.can_commit);
5707 drop(cutxn);
5708 commit_session(idms, ct, cust).await;
5709
5710 let pwd_changed_2 = get_testperson_password_changed_time(idms)
5712 .await
5713 .expect("PasswordChangedTime should still be set after deleting unix password");
5714 assert_eq!(pwd_changed_2, OffsetDateTime::UNIX_EPOCH);
5715 }
5716
5717 #[idm_test]
5718 async fn credential_update_password_changed_time_non_posix(
5719 idms: &IdmServer,
5720 _idms_delayed: &mut IdmServerDelayed,
5721 ) {
5722 let ct = Duration::from_secs(TEST_CURRENT_TIME);
5723
5724 let (cust, _) = setup_test_session_no_posix(idms, ct).await;
5725
5726 assert!(get_testperson_password_changed_time(idms).await.is_none());
5728
5729 let cutxn = idms.cred_update_transaction().await.unwrap();
5731 let c_status = cutxn
5732 .credential_primary_set_password(&cust, ct, TESTPERSON_PASSWORD)
5733 .expect("Failed to set primary password");
5734 assert!(c_status.can_commit);
5735 drop(cutxn);
5736 commit_session(idms, ct, cust).await;
5737
5738 let pwd_changed = get_testperson_password_changed_time(idms)
5739 .await
5740 .expect("PasswordChangedTime should be set for non-posix person");
5741 assert_eq!(pwd_changed, OffsetDateTime::UNIX_EPOCH);
5742
5743 let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();
5745 idms_prox_write
5746 .qs_write
5747 .internal_modify_uuid(
5748 UUID_IDM_ALL_ACCOUNTS,
5749 &ModifyList::new_purge_and_set(
5750 Attribute::AllowPrimaryCredFallback,
5751 Value::new_bool(true),
5752 ),
5753 )
5754 .expect("Unable to set allow_primary_cred_fallback");
5755 idms_prox_write.commit().expect("Failed to commit txn");
5756
5757 let (cust, _) = renew_test_session(idms, ct).await;
5759 let cutxn = idms.cred_update_transaction().await.unwrap();
5760 let _ = cutxn
5761 .credential_primary_set_password(&cust, ct, TESTPERSON_PASSWORD)
5762 .expect("Failed to set primary password");
5763 drop(cutxn);
5764 commit_session(idms, ct, cust).await;
5765 let pwd_changed = get_testperson_password_changed_time(idms)
5766 .await
5767 .expect("PasswordChangedTime should be set with fallback enabled");
5768 assert_eq!(pwd_changed, OffsetDateTime::UNIX_EPOCH + ct);
5769 }
5770
5771 #[idm_test]
5772 async fn credential_update_password_changed_time_passkey_only(
5773 idms: &IdmServer,
5774 _idms_delayed: &mut IdmServerDelayed,
5775 ) {
5776 let ct = Duration::from_secs(TEST_CURRENT_TIME);
5777 let (cust, _) = setup_test_session_no_posix(idms, ct).await;
5778
5779 assert!(get_testperson_password_changed_time(idms).await.is_none());
5781
5782 let cutxn = idms.cred_update_transaction().await.unwrap();
5784 let origin = cutxn.get_origin().clone();
5785 let mut wa = WebauthnAuthenticator::new(SoftPasskey::new(true));
5786
5787 let c_status = create_new_passkey(ct, &origin, &cutxn, &cust, &mut wa).await;
5788 assert!(c_status.can_commit);
5789 assert_eq!(c_status.passkeys.len(), 1);
5790 drop(cutxn);
5791 commit_session(idms, ct, cust).await;
5792
5793 let pwd_changed = get_testperson_password_changed_time(idms)
5795 .await
5796 .expect("PasswordChangedTime should be set even for passkey-only");
5797 assert_eq!(pwd_changed, time::OffsetDateTime::UNIX_EPOCH);
5798 }
5799
5800 #[idm_test]
5801 async fn credential_update_password_changed_time_no_change_commit(
5802 idms: &IdmServer,
5803 _idms_delayed: &mut IdmServerDelayed,
5804 ) {
5805 let ct = Duration::from_secs(TEST_CURRENT_TIME);
5806 let (cust, _) = setup_test_session(idms, ct).await;
5807
5808 let cutxn = idms.cred_update_transaction().await.unwrap();
5809 let _ = cutxn
5810 .credential_primary_set_password(&cust, ct, TESTPERSON_PASSWORD)
5811 .expect("Failed to set primary password");
5812 let _ = cutxn
5813 .credential_unix_set_password(&cust, ct, TESTPERSON_PASSWORD)
5814 .expect("Failed to set unix password");
5815 drop(cutxn);
5816 commit_session(idms, ct, cust).await;
5817
5818 let pwd_changed_1 = get_testperson_password_changed_time(idms)
5819 .await
5820 .expect("PasswordChangedTime should be set after first update");
5821
5822 let ct2 = Duration::from_secs(TEST_CURRENT_TIME + 2000);
5823
5824 let (cust, c_status) = renew_test_session(idms, ct2).await;
5826 assert!(c_status.primary.is_some());
5827 assert!(c_status.can_commit);
5828 commit_session(idms, ct2, cust).await;
5829
5830 let pwd_changed_2 = get_testperson_password_changed_time(idms)
5831 .await
5832 .expect("PasswordChangedTime should still be present after no-change commit");
5833
5834 assert_eq!(pwd_changed_2, OffsetDateTime::UNIX_EPOCH + ct);
5835 assert_eq!(pwd_changed_2, pwd_changed_1);
5836 }
5837
5838 #[idm_test]
5839 async fn credential_update_unix_password_deleted_falls_back(
5840 idms: &IdmServer,
5841 _idms_delayed: &mut IdmServerDelayed,
5842 ) {
5843 let ct = Duration::from_secs(TEST_CURRENT_TIME);
5844 let ct2 = Duration::from_secs(TEST_CURRENT_TIME + 50);
5845
5846 let (cust, _) = setup_test_session(idms, ct).await;
5847
5848 let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();
5850 idms_prox_write
5851 .qs_write
5852 .internal_modify_uuid(
5853 UUID_IDM_ALL_ACCOUNTS,
5854 &ModifyList::new_purge_and_set(
5855 Attribute::AllowPrimaryCredFallback,
5856 Value::new_bool(true),
5857 ),
5858 )
5859 .expect("Unable to set allow_primary_cred_fallback");
5860 idms_prox_write.commit().expect("Failed to commit txn");
5861
5862 assert!(get_testperson_password_changed_time(idms).await.is_none());
5864
5865 let cutxn = idms.cred_update_transaction().await.unwrap();
5866 let _ = cutxn
5867 .credential_primary_set_password(&cust, ct, TESTPERSON_PASSWORD)
5868 .expect("Failed to set primary password");
5869
5870 let _ = cutxn
5871 .credential_unix_set_password(&cust, ct2, TESTPERSON_PASSWORD)
5872 .expect("Failed to set unix password");
5873
5874 let c_status = cutxn
5875 .credential_primary_init_totp(&cust, ct)
5876 .expect("Failed to init totp");
5877
5878 let totp_token: Totp = match c_status.mfaregstate {
5879 MfaRegStateStatus::TotpCheck(secret) => Some(secret.try_into().unwrap()),
5880 _ => None,
5881 }
5882 .expect("Unable to retrieve totp token");
5883
5884 let chal = totp_token
5885 .do_totp_duration_from_epoch(&ct)
5886 .expect("Failed to perform totp step");
5887
5888 let c_status = cutxn
5889 .credential_primary_check_totp(&cust, ct, chal, "totp")
5890 .expect("Failed to check totp");
5891
5892 assert!(matches!(c_status.mfaregstate, MfaRegStateStatus::None));
5893 assert!(c_status.can_commit);
5894
5895 drop(cutxn);
5896 commit_session(idms, ct, cust).await;
5897
5898 let pwd_changed = get_testperson_password_changed_time(idms)
5899 .await
5900 .expect("PasswordChangedTime should be set for password+TOTP");
5901 assert_eq!(pwd_changed, OffsetDateTime::UNIX_EPOCH + ct2);
5903
5904 let (cust, _) = renew_test_session(idms, ct2).await;
5906 let cutxn = idms.cred_update_transaction().await.unwrap();
5907
5908 let _ = cutxn
5909 .credential_unix_delete(&cust, ct2)
5910 .expect("Failed to delete unix credential");
5911
5912 assert!(c_status.can_commit);
5913 drop(cutxn);
5914 commit_session(idms, ct2, cust).await;
5915
5916 let pwd_changed_2 = get_testperson_password_changed_time(idms)
5917 .await
5918 .expect("PasswordChangedTime should be set after switching to passkey");
5919 assert_eq!(pwd_changed_2, OffsetDateTime::UNIX_EPOCH + ct);
5920 }
5921}