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