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