kanidmd_lib/idm/
credupdatesession.rs

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
32// A user can take up to 15 minutes to update their credentials before we automatically
33// cancel on them.
34const MAXIMUM_CRED_UPDATE_TTL: Duration = Duration::from_secs(900);
35// Minimum 5 minutes.
36const MINIMUM_INTENT_TTL: Duration = Duration::from_secs(300);
37// Default 1 hour.
38const DEFAULT_INTENT_TTL: Duration = Duration::from_secs(3600);
39// Default 1 day.
40const 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    // How long is it valid for?
73    pub max_ttl: Duration,
74}
75
76#[derive(Debug)]
77pub struct CredentialUpdateSessionToken {
78    pub token_enc: JweCompact,
79}
80
81/// The current state of MFA registration
82#[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    // Disabled,
116}
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            // CredentialState::Disabled => CUCredState::Disabled ,
126        }
127    }
128}
129
130#[derive(Clone)]
131pub(crate) struct CredentialUpdateSession {
132    issuer: String,
133    // Current credentials - these are on the Account!
134    account: Account,
135    // The account policy applied to this account
136    resolved_account_policy: ResolvedAccountPolicy,
137    // What intent was used to initiate this session.
138    intent_token_id: Option<String>,
139
140    // Is there an extertal credential portal?
141    ext_cred_portal: CUExtPortal,
142
143    // The pw credential as they are being updated
144    primary_state: CredentialState,
145    primary: Option<Credential>,
146
147    // Unix / Sudo PW
148    unixcred: Option<Credential>,
149    unixcred_state: CredentialState,
150
151    // Ssh Keys
152    sshkeys: BTreeMap<String, SshPublicKey>,
153    sshkeys_state: CredentialState,
154
155    // Passkeys that have been configured.
156    passkeys: BTreeMap<Uuid, (String, PasskeyV4)>,
157    passkeys_state: CredentialState,
158
159    // Attested Passkeys
160    attested_passkeys: BTreeMap<Uuid, (String, AttestedPasskeyV4)>,
161    attested_passkeys_state: CredentialState,
162
163    // Internal reg state of any inprogress totp or webauthn credentials.
164    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    // Vec of the issues with the current session so that UI's can highlight properly how to proceed.
204    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                    // If it's none, then we can proceed because we satisfy mfa on other
220                    // parts.
221                    .unwrap_or(false)
222                {
223                    can_commit = false;
224                    warnings.push(CredentialUpdateSessionStatusWarnings::MfaRequired);
225                }
226            }
227            CredentialType::Passkey => {
228                // NOTE: Technically this is unreachable, but we keep it for correctness.
229                // Primary can't be set at all.
230                if self.primary.is_some() {
231                    can_commit = false;
232                    warnings.push(CredentialUpdateSessionStatusWarnings::PasskeyRequired);
233                }
234            }
235            CredentialType::AttestedPasskey => {
236                // Also unreachable - during these sessions, there will be no values present here.
237                if !self.passkeys.is_empty() || self.primary.is_some() {
238                    can_commit = false;
239                    warnings.push(CredentialUpdateSessionStatusWarnings::AttestedPasskeyRequired);
240                }
241            }
242            CredentialType::AttestedResidentkey => {
243                // Also unreachable - during these sessions, there will be no values present here.
244                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                // special case, must always deny all changes.
255                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        // We only check this if we were able to proceed to a commit state. That way we don't warn needlessly.
268        if can_commit
269            && self.attested_passkeys.is_empty()
270            && self.passkeys.is_empty()
271            && self.primary.is_none()
272        {
273            // The user has no credentials to login to their account with, we can not proceed!
274            can_commit = false;
275            warnings.push(CredentialUpdateSessionStatusWarnings::NoValidCredentials)
276        }
277
278        (can_commit, warnings)
279    }
280}
281
282pub enum MfaRegStateStatus {
283    // Nothing in progress.
284    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    // The target user's display name
357    displayname: String,
358    ext_cred_portal: CUExtPortal,
359    // Any info the client needs about mfareg state.
360    mfaregstate: MfaRegStateStatus,
361    can_commit: bool,
362    // If can_commit is false, this will have warnings populated.
363    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    /// Append a single warning to this session status, which will only be displayed to the
381    /// user once. This is different to other warnings that are derived from the state of the
382    /// session as a whole.
383    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// We allow Into here because CUStatus is foreign so it's impossible for us to implement From
397// in a valid manner
398#[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    // Who initiated this?
508    pub ident: Identity,
509    // Who is it targeting?
510    pub target: Uuid,
511    // How long is it valid for?
512    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    // Who initiated this?
541    pub ident: Identity,
542    // Who is it targeting?
543    pub target: Uuid,
544    // How long is it valid for?
545    pub max_ttl: Option<Duration>,
546    // Optionally, which email to use?
547    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        // The initiating identity must be in readwrite mode! Effective permission assumes you
586        // are in rw.
587        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        // Is target an account? This checks for us.
594        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        // Does the ident have permission to modify AND search the user-credentials of the target, given
618        // the current status of it's authentication?
619
620        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            // In theory this is always granted due to how access controls work, but we check anyway.
736            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        // At lease *one* must be modifiable OR visible.
762        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        // We can't decide this based on CredentialType alone since we may have CredentialType::Mfa
802        // and still need attestation. As a result, we have to decide this based on presence of
803        // the attestation policy.
804        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                // User can only delete, no police available to add more keys.
832                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        // - stash the current state of all associated credentials
853        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        // Before we start, we pre-filter out anything that no longer conforms to policy.
879        // These would already be failing authentication, so they should have the appearance
880        // of "being removed".
881        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                            // Good to go
891                            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                // Seems weird here to be skipping filtering of the credentials. The reason is that
902                // if an account had registered attested passkeys in the past we can delete them, but
903                // not add new ones. Situation only occurs when policy isn't present on the account.
904                account.attested_passkeys.clone()
905            }
906        } else {
907            BTreeMap::default()
908        };
909
910        // Get the external credential portal, if any.
911        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        // Stash the issuer for some UI elements
925        let issuer = self.qs_write.get_domain_display_name().to_string();
926
927        // - store account policy (if present)
928        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        // Point of no return
968
969        // Sneaky! Now we know it will work, prune old sessions.
970        self.expire_credential_update_sessions(ct);
971
972        // Store the update session into the map.
973        self.cred_update_sessions.insert(sessionid, session);
974        trace!("cred_update_sessions.insert - {}", sessionid);
975
976        // - issue the CredentialUpdateToken (enc)
977        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        // If the email is set, validate it.
990
991        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        // ==== AUTHORISATION CHECKED ===
1008        let (intent_id, expiry_time) =
1009            self.build_credential_update_intent(event.max_ttl, &account, perms, ct)?;
1010
1011        // Queue the message to be sent.
1012        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            // Should we actually impersonate here? We probably need an account for internal sending
1022            // that is disconnected from the act of creating the reset.
1023            &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        // We should check in the acc-pol if we can proceed?
1039        // Is there a reason account policy might deny us from proceeding?
1040
1041        // ==== AUTHORISATION CHECKED ===
1042        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        // Build the intent token. Previously this was using 0 and then
1059        // relying on clamp to raise this to 5 minutes, but that led to
1060        // rapid timeouts that affected some users.
1061        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        // Absolute expiry of the intent token in epoch seconds
1065        let max_ttl = ct + clamped_mttl;
1066
1067        // Get the expiry of the intent token as an odt.
1068        let expiry_time = OffsetDateTime::UNIX_EPOCH + max_ttl;
1069
1070        let intent_id = readable_password_from_random();
1071
1072        // Mark that we have created an intent token on the user.
1073        // ⚠️   -- remember, there is a risk, very low, but still a risk of collision of the intent_id.
1074        //        instead of enforcing unique, which would divulge that the collision occurred, we
1075        //        write anyway, and instead on the intent access path we invalidate IF the collision
1076        //        occurs.
1077        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        // Remove any old credential update intents
1086        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 as executed
1112                &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        // If given an intent_id, immediately transition it to consumed to prevent reuse.
1129        //
1130        // NOTE: We don't need ident/authentication here because posession of the intent_id is
1131        // sufficient to prove that you can revoke it - since if you have the intent_id, you could
1132        // just update the credentials anyway.
1133
1134        let entries = self.qs_write.internal_search(filter!(f_eq(
1135            Attribute::CredentialUpdateIntentToken,
1136            PartialValue::IntentToken(intent_id.clone())
1137        )))?;
1138
1139        // Given the low chance, if the intent_id conflicts we revoke them ALL.
1140        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                // Shouldn't happen, but okay.
1148                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                    // Already invalid, move on.
1155                    IntentTokenState::Consumed { max_ttl: _ } => return None,
1156                    // Still valid, remove it.
1157                    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        /*
1190            let entry = self.qs_write.internal_search_uuid(&token.target)?;
1191        */
1192        // ⚠️  due to a low, but possible risk of intent_id collision, if there are multiple
1193        // entries, we will reject the intent.
1194        // DO we need to force both to "Consumed" in this step?
1195        //
1196        // ⚠️  If not present, it may be due to replication delay. We can report this.
1197
1198        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                    // Happy Path!
1207                    entry
1208                } else {
1209                    // Multiple entries matched! This is bad!
1210                    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                    /*
1217                    let mut modlist = ModifyList::new();
1218
1219                    modlist.push_mod(Modify::Removed(
1220                        Attribute::CredentialUpdateIntentToken.into(),
1221                        PartialValue::IntentToken(intent_id.clone()),
1222                    ));
1223
1224                    let filter_or = matched_uuids.into_iter()
1225                        .map(|u| f_eq(Attribute::Uuid, PartialValue::new_uuid(u)))
1226                        .collect();
1227
1228                    self.qs_write
1229                        .internal_modify(
1230                            // Filter as executed
1231                            &filter!(f_or(filter_or)),
1232                            &modlist,
1233                        )
1234                        .map_err(|e| {
1235                            request_error!(error = ?e);
1236                            e
1237                        })?;
1238                    */
1239
1240                    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        // Is target an account? This checks for us.
1254        let (account, resolved_account_policy) =
1255            Account::try_from_entry_with_policy(entry.as_ref(), &mut self.qs_write)?;
1256
1257        // Check there is not already a user session in progress with this intent token.
1258        // Is there a need to revoke intent tokens?
1259
1260        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                    // The former session has expired, continue.
1277                    security_info!(
1278                        %entry,
1279                        %account.uuid,
1280                        "Initiating Credential Update Session - Previous session {} has expired", session_id
1281                    );
1282                } else {
1283                    // The former session has been orphaned while in use. This can be from someone
1284                    // ctrl-c during their use of the session or refreshing the page without committing.
1285                    //
1286                    // we don't try to exclusive lock the token here with the current time as we previously
1287                    // did. This is because with async replication, there isn't a guarantee this will actually
1288                    // be sent to another server "soon enough" to prevent abuse on the separate server. So
1289                    // all this "lock" actually does is annoy legitimate users and not stop abuse. We
1290                    // STILL keep the InProgress state though since we check it on commit, so this
1291                    // forces the previous orphan session to be immediately invalidated!
1292                    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        // To prevent issues with repl, we need to associate this cred update session id, with
1319        // this intent token id.
1320
1321        // Store the intent id in the session (if needed) so that we can check the state at the
1322        // end of the update.
1323
1324        // We need to pin the id from the intent token into the credential to ensure it's not reused
1325
1326        // Need to change this to the expiry time, so we can purge up to.
1327        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 as executed
1351                &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        // ==========
1360        // Okay, good to exchange.
1361
1362        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        // ==== AUTHORISATION CHECKED ===
1382        // This is the expiry time, so that our cleanup task can "purge up to now" rather
1383        // than needing to do calculations.
1384        let sessionid = uuid_from_duration(current_time + MAXIMUM_CRED_UPDATE_TTL, self.sid);
1385
1386        // Build the cred update session.
1387        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    // This shares some common paths between commit and cancel.
1408    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        // Can we actually proceed?
1471        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        // Setup mods for the various bits. We always assert an *exact* state.
1487
1488        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 an intent was used on this session, AND that intent is not in our
1492        // session state as an exact match, FAIL the commit. Move the intent to "Consumed".
1493        //
1494        // Should we mark the credential as suspect (lock the account?)
1495        //
1496        // If the credential has changed, reject? Do we need "asserts" in the modlist?
1497        // that would allow better expression of this, and will allow resolving via replication
1498
1499        // If an intent token was used, remove it's former value, and add it as consumed.
1500        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 we cannot fall back
1558        if cred_changed.is_none()
1559            && session
1560                .resolved_account_policy
1561                .allow_primary_cred_fallback()
1562                != Some(true)
1563        {
1564            // then we don't need to update the password changed time
1565            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                // Add all the passkeys. If none, nothing will be added! This handles
1598                // the delete case quite cleanly :)
1599                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                // Add all the passkeys. If none, nothing will be added! This handles
1614                // the delete case quite cleanly :)
1615                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::Disabled |
1627            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        // Apply to the account!
1645        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 as executed
1654                    &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 an intent token was used, remove it's former value, and add it as VALID since we didn't commit.
1676        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        // Apply to the account!
1722        if !modlist.is_empty() {
1723            trace!(?modlist, "processing change");
1724
1725            self.qs_write
1726                .internal_modify(
1727                    // Filter as executed
1728                    &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        // Check the TTL
1771        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    // I think I need this to be a try lock instead, and fail on error, because
1786    // of the nature of the async bits.
1787    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        // password strength and badlisting is always global, rather than per-pw-policy.
1812        // pw-policy as check on the account is about requirements for mfa for example.
1813
1814        // is the password at least 10 char?
1815        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        // zxcvbn doesn't appear to be picking up related inputs, so we check
1827        // these manually. This can sometimes trip up if the input is short,
1828        // such as a user with the name "a".
1829        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        // does the password pass zxcvbn?
1839        let entropy = zxcvbn(cleartext, related_inputs);
1840
1841        // PW's should always be enforced as strong as possible.
1842        if entropy.score() < Score::Four {
1843            // The password is too week as per:
1844            // https://docs.rs/zxcvbn/2.0.0/zxcvbn/struct.Entropy.html
1845            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                    // Return some generic feedback when the password is this bad.
1852                    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        // check a password badlist to eliminate more content
1957        // we check the password as "lower case" to help eliminate possibilities
1958        // also, when pw_badlist_cache is read from DB, it is read as Value (iutf8 lowercase)
1959        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                // Is there a need to update the uuid of the cred re softlocks?
2050                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        // Is there something else in progress? Cancel it if so.
2077        if !matches!(session.mfaregstate, MfaRegState::None) {
2078            debug!("Clearing incomplete mfareg");
2079        }
2080
2081        // Generate the TOTP.
2082        let totp_token = Totp::generate_secure(TOTP_DEFAULT_STEP);
2083
2084        session.mfaregstate = MfaRegState::TotpInit(totp_token);
2085        // Now that it's in the state, it'll be in the status when returned.
2086        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        // Are we in a totp reg state?
2111        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                    // The user is trying to add a second TOTP under the same name. Lets save them from themselves
2125                    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                    // It was valid. Update the credential.
2132                    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                    // Set the state to None.
2146                    session.mfaregstate = MfaRegState::None;
2147                    Ok(session.deref().into())
2148                } else {
2149                    // What if it's a broken authenticator app? Google authenticator
2150                    // and Authy both force SHA1 and ignore the algo we send. So let's
2151                    // check that just in case.
2152                    let token_sha1 = totp_token.clone().downgrade_to_legacy();
2153
2154                    if token_sha1.verify(totp_chal, ct) {
2155                        // Greeeaaaaaatttt. It's a broken app. Let's check the user
2156                        // knows this is broken, before we proceed.
2157                        session.mfaregstate = MfaRegState::TotpInvalidSha1(
2158                            totp_token.clone(),
2159                            token_sha1,
2160                            label.to_string(),
2161                        );
2162                        Ok(session.deref().into())
2163                    } else {
2164                        // Let them check again, it's a typo.
2165                        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        // Are we in a totp reg state?
2194        match &session.mfaregstate {
2195            MfaRegState::TotpInvalidSha1(_, token_sha1, label) => {
2196                // They have accepted it as sha1
2197                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                // Set the state to None.
2211                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        // Set the state to None.
2255        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        // I think we override/map the status to inject the codes as a once-off state message.
2279
2280        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        // Now that it's in the state, it'll be in the status when returned.
2405        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                // Clean up state before returning results.
2432                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        // No-op if not present
2483        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        // Now that it's in the state, it'll be in the status when returned.
2537        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                // The reg is done. Clean up state before returning errors.
2580                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        // No-op if not present
2617        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                // Is there a need to update the uuid of the cred re softlocks?
2665                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        // Check the label.
2718        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        // session.unixcred = None;
2764
2765        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    // Generate password?
2784}
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        // user without permission - fail
2881        // - accounts don't have self-write permission.
2882
2883        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        // user with permission - success
2891
2892        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        // create intent token without permission - fail
2900
2901        // create intent token with permission - success
2902
2903        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        // exchange intent token - invalid - fail
2916        // Expired
2917        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        // exchange intent token - success
2928        let (cust_a, _c_status) = idms_prox_write
2929            .exchange_intent_credential_update(intent_tok.clone().into(), ct)
2930            .unwrap();
2931
2932        // Session in progress - This will succeed and then block the former success from
2933        // committing.
2934        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        // Fails as the txn was orphaned.
2941        trace!(?cur);
2942        assert!(cur.is_err());
2943
2944        // Success - this was the second use of the token and is valid.
2945        let _ = idms_prox_write.commit_credential_update(&cust_b, ct);
2946
2947        debug!("Start intent token revoke");
2948
2949        // Create an intent token and then revoke it.
2950        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        // Can't exchange, token ded.
2966        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        // Remove the default all persons policy, it interferes with our test.
2998        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            // Setup the radius creds to ensure we don't use them anywhere else.
3029            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        // Expect success
3113        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                // Process the auth session
3123                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        // idms_delayed: &mut IdmServerDelayed,
3135        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        // Expect success
3196        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                // Process the auth session
3206                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        // Expect success
3258        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                // There now should be a backup code invalidation present
3268                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                // Process the auth session
3274                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                // Process the webauthn update
3344                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                // Process the auth session
3350                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        // The session exists
3369        let c_status = cutxn.credential_update_status(&cust, ct);
3370        assert!(c_status.is_ok());
3371        drop(cutxn);
3372
3373        // Making a new session is what triggers the clean of old sessions.
3374        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        // Now fake going back in time .... allows the token to decrypt, but the session
3380        // is gone anyway!
3381        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        // Test account with no email.
3434        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        // Set the account up with an email.
3447        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        // Request a change, but the email is not present on the account.
3459
3460        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        // Create a credential update
3473        // Ensure a message was made.
3474
3475        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        // Find the message in the queue.
3487        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        // Done!!! We quwued an email to send.
3515    }
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        // Get the credential status - this should tell
3530        // us the details of the credentials, as well as
3531        // if they are ready and valid to commit?
3532        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        // Test initially creating a credential.
3540        //   - pw first
3541        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        // Check it works!
3551        assert!(check_testperson_password(idms, idms_delayed, test_pw, ct)
3552            .await
3553            .is_some());
3554
3555        // Test deleting the pw
3556        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        // Can't delete, would be the last credential!
3574        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        // Get the radius pw
3588
3589        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        // Get the credential status - this should tell
3604        // us the details of the credentials, as well as
3605        // if they are ready and valid to commit?
3606        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        // Test initially creating a credential.
3615        //   - pw first
3616
3617        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        // There are no credentials so we can't proceed.
3669        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        // Set the account policy min pw length
3685        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        // This now will affect all accounts for the next cred update.
3700
3701        let (cust, _) = setup_test_session(idms, ct).await;
3702
3703        let cutxn = idms.cred_update_transaction().await.unwrap();
3704
3705        // Get the credential status - this should tell
3706        // us the details of the credentials, as well as
3707        // if they are ready and valid to commit?
3708        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        // Test initially creating a credential.
3717        //   - pw first
3718        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        // Test pw len of len minus 1
3728        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        // Test pw len of exact len
3737        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    // Test set of primary account password
3749    //    - fail pw quality checks etc
3750    //    - set correctly.
3751
3752    // - setup TOTP
3753    #[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        // Setup the PW
3765        let c_status = cutxn
3766            .credential_primary_set_password(&cust, ct, test_pw)
3767            .expect("Failed to update the primary cred password");
3768
3769        // Since it's pw only.
3770        assert!(c_status.can_commit);
3771
3772        //
3773        let c_status = cutxn
3774            .credential_primary_init_totp(&cust, ct)
3775            .expect("Failed to update the primary cred password");
3776
3777        // Check the status has the token.
3778        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        // Intentionally get it wrong.
3791        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        // Check that the user actually put something into the label
3802        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        // Okay, Now they are trying to be smart...
3816        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            // Check the status has the token.
3845            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            // They tried to add a second totp under the same name
3857            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        // Should be okay now!
3874
3875        drop(cutxn);
3876        commit_session(idms, ct, cust).await;
3877
3878        // Check it works!
3879        assert!(
3880            check_testperson_password_totp(idms, idms_delayed, test_pw, &totp_token, ct)
3881                .await
3882                .is_some()
3883        );
3884        // No need to test delete of the whole cred, we already did with pw above.
3885
3886        // If we remove TOTP, show it reverts back.
3887        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        // Check it works with totp removed.
3904        assert!(check_testperson_password(idms, idms_delayed, test_pw, ct)
3905            .await
3906            .is_some());
3907    }
3908
3909    // Check sha1 totp.
3910    #[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        // Setup the PW
3922        let c_status = cutxn
3923            .credential_primary_set_password(&cust, ct, test_pw)
3924            .expect("Failed to update the primary cred password");
3925
3926        // Since it's pw only.
3927        assert!(c_status.can_commit);
3928
3929        //
3930        let c_status = cutxn
3931            .credential_primary_init_totp(&cust, ct)
3932            .expect("Failed to update the primary cred password");
3933
3934        // Check the status has the token.
3935        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        // Should getn the warn that it's sha1
3950        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        // Accept it
3960        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        // Should be okay now!
3971
3972        drop(cutxn);
3973        commit_session(idms, ct, cust).await;
3974
3975        // Check it works!
3976        assert!(
3977            check_testperson_password_totp(idms, idms_delayed, test_pw, &totp_token, ct)
3978                .await
3979                .is_some()
3980        );
3981        // No need to test delete, we already did with pw above.
3982    }
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        // Setup the PW
3996        let _c_status = cutxn
3997            .credential_primary_set_password(&cust, ct, test_pw)
3998            .expect("Failed to update the primary cred password");
3999
4000        // Backup codes are refused to be added because we don't have mfa yet.
4001        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        // Now good to go, we need to now add our backup codes.
4032        // What's the right way to get these back?
4033        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        // Should error because the number is not 0
4044        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        // Should be okay now!
4051        drop(cutxn);
4052        commit_session(idms, ct, cust).await;
4053
4054        let backup_code = codes.iter().next().expect("No codes available");
4055
4056        // Check it works!
4057        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        // Renew to start the next steps
4068        let (cust, _) = renew_test_session(idms, ct).await;
4069        let cutxn = idms.cred_update_transaction().await.unwrap();
4070
4071        // Only 7 codes left.
4072        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        // If we remove codes, it leaves totp.
4082        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        // Re-add the codes.
4093        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        // If we remove totp, it removes codes.
4107        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        // Setup the PW
4133        let c_status = cutxn
4134            .credential_primary_set_password(&cust, ct, test_pw)
4135            .expect("Failed to update the primary cred password");
4136
4137        // Since it's pw only.
4138        assert!(c_status.can_commit);
4139
4140        //
4141        let c_status = cutxn
4142            .credential_primary_init_totp(&cust, ct)
4143            .expect("Failed to update the primary cred totp");
4144
4145        // Check the status has the token.
4146        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        // It's pw only, since we canceled TOTP
4163        assert!(check_testperson_password(idms, idms_delayed, test_pw, ct)
4164            .await
4165            .is_some());
4166    }
4167
4168    // Primary cred must be pw or pwmfa
4169
4170    // - setup webauthn
4171    // - remove webauthn
4172    // - test multiple webauthn token.
4173
4174    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        // Start the registration
4182        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        // Finish the registration
4199        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        // Check we have the passkey
4208        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        // Create a soft passkey
4227        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        // Get the UUID of the passkey here.
4232        let pk_uuid = c_status.passkeys.first().map(|pkd| pkd.uuid).unwrap();
4233
4234        // Commit
4235        drop(cutxn);
4236        commit_session(idms, ct, cust).await;
4237
4238        // Do an auth test
4239        assert!(
4240            check_testperson_passkey(idms, idms_delayed, &mut wa, origin.clone(), ct)
4241                .await
4242                .is_some()
4243        );
4244
4245        // Now test removing the token
4246        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        // For now, set a password to allow saving.
4267        let c_status = cutxn
4268            .credential_primary_set_password(&cust, ct, test_pw)
4269            .expect("Failed to update the primary cred password");
4270
4271        // Could proceed now!
4272        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        // Must fail now as the passkeys were removed!!!
4281        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        // Test that if access is denied for a synced account, that the actual action to update
4294        // the credentials is always denied.
4295
4296        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        // Destructure to force us to update this test if we change this
4347        // structure at all.
4348        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 origin = cutxn.get_origin().clone();
4381
4382        // Test that any of the primary or passkey update methods fail with access denied.
4383
4384        // credential_primary_set_password
4385        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        // credential_primary_init_totp
4403        let err = cutxn.credential_primary_init_totp(&cust, ct).unwrap_err();
4404        assert!(matches!(err, OperationError::AccessDenied));
4405
4406        // credential_primary_check_totp
4407        let err = cutxn
4408            .credential_primary_check_totp(&cust, ct, 0, "totp")
4409            .unwrap_err();
4410        assert!(matches!(err, OperationError::AccessDenied));
4411
4412        // credential_primary_accept_sha1_totp
4413        let err = cutxn
4414            .credential_primary_accept_sha1_totp(&cust, ct)
4415            .unwrap_err();
4416        assert!(matches!(err, OperationError::AccessDenied));
4417
4418        // credential_primary_remove_totp
4419        let err = cutxn
4420            .credential_primary_remove_totp(&cust, ct, "totp")
4421            .unwrap_err();
4422        assert!(matches!(err, OperationError::AccessDenied));
4423
4424        // credential_primary_init_backup_codes
4425        let err = cutxn
4426            .credential_primary_init_backup_codes(&cust, ct)
4427            .unwrap_err();
4428        assert!(matches!(err, OperationError::AccessDenied));
4429
4430        // credential_primary_remove_backup_codes
4431        let err = cutxn
4432            .credential_primary_remove_backup_codes(&cust, ct)
4433            .unwrap_err();
4434        assert!(matches!(err, OperationError::AccessDenied));
4435
4436        // credential_primary_delete
4437        let err = cutxn.credential_primary_delete(&cust, ct).unwrap_err();
4438        assert!(matches!(err, OperationError::AccessDenied));
4439
4440        // credential_passkey_init
4441        let err = cutxn.credential_passkey_init(&cust, ct).unwrap_err();
4442        assert!(matches!(err, OperationError::AccessDenied));
4443
4444        // credential_passkey_finish
4445        //   Can't test because we need a public key response.
4446
4447        // credential_passkey_remove
4448        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        // Since there are no credentials we can't proceed anyway.
4461        assert!(!c_status.can_commit);
4462        assert!(c_status
4463            .warnings
4464            .contains(&CredentialUpdateSessionStatusWarnings::NoValidCredentials));
4465    }
4466
4467    // Assert we can't create "just" a password when mfa is required.
4468    #[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        // This now will affect all accounts for the next cred update.
4489
4490        let (cust, _) = setup_test_session(idms, ct).await;
4491
4492        let cutxn = idms.cred_update_transaction().await.unwrap();
4493
4494        // Get the credential status - this should tell
4495        // us the details of the credentials, as well as
4496        // if they are ready and valid to commit?
4497        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        // Test initially creating a credential.
4506        //   - pw first
4507        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        // Check reason! Must show "no mfa". We need totp to be added now.
4516
4517        let c_status = cutxn
4518            .credential_primary_init_totp(&cust, ct)
4519            .expect("Failed to update the primary cred password");
4520
4521        // Check the status has the token.
4522        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        // Done, can now commit.
4545        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        // If we remove TOTP, it blocks commit.
4552        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        // Delete of the totp forces us back here.
4566        assert!(!c_status.can_commit);
4567        assert!(c_status
4568            .warnings
4569            .contains(&CredentialUpdateSessionStatusWarnings::MfaRequired));
4570
4571        // Passkeys satisfy the policy though
4572        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        // This now will affect all accounts for the next cred update.
4611
4612        let (cust, _) = setup_test_session(idms, ct).await;
4613
4614        let cutxn = idms.cred_update_transaction().await.unwrap();
4615
4616        // Get the credential status - this should tell
4617        // us the details of the credentials, as well as
4618        // if they are ready and valid to commit?
4619        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    // Attested passkey types
4649
4650    #[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        // Create the attested soft token we will use in this test.
4658        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        // We need a second for when we rotate the token.
4662        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        // Create it's associated policy.
4666        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        // Create the invalid tokens
4699        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        // Setup the cred update session.
4705
4706        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        // Our status needs the correct device names for UI hinting.
4711        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        // -------------------------------------------------------
4725        // Unable to add an passkey when attestation is requested.
4726        let err = cutxn.credential_passkey_init(&cust, ct).unwrap_err();
4727        assert!(matches!(err, OperationError::AccessDenied));
4728
4729        // -------------------------------------------------------
4730        // Reject a credential that lacks attestation
4731        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        // Finish the registration
4746        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        // -------------------------------------------------------
4757        // Reject a credential with wrong CA / correct aaguid
4758        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        // Finish the registration
4773        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        // -------------------------------------------------------
4784        // Accept credential with correct CA/aaguid
4785        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        // Finish the registration
4800        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 that auth works.
4819        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        // Remove attested passkey works.
4830        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        // Removed every passkey, you can't proceed!!!!
4848        assert!(!c_status.can_commit);
4849        assert!(c_status
4850            .warnings
4851            .contains(&CredentialUpdateSessionStatusWarnings::NoValidCredentials));
4852
4853        // Add a new, but differenter passkey
4854        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        // Note this is the second token, not the first.
4865        let passkey_resp = wa_token_valid_b
4866            .do_registration(origin.clone(), passkey_chal)
4867            .expect("Failed to create soft passkey");
4868
4869        // Finish the registration
4870        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        // Must fail now, note we use the first token to auth here which we deleted from
4883        // the clients credentials.
4884        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        // Setup the policy.
4900        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        // This is the original policy that we enroll.
4907        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        // Setup the policy for later that lacks token 1.
4934        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        // Enroll the attested keys
4946        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        // -------------------------------------------------------
4951        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        // Finish the registration of token 1
4966        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        // -------------------------------------------------------
4976        // Commit
4977        drop(cutxn);
4978        commit_session(idms, ct, cust).await;
4979
4980        // Check auth works
4981        assert!(
4982            check_testperson_passkey(idms, idms_delayed, &mut wa_token_1, origin.clone(), ct)
4983                .await
4984                .is_some()
4985        );
4986
4987        // Change policy
4988        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        // Auth fail, the CA is no longer valid.
5002        assert!(
5003            check_testperson_passkey(idms, idms_delayed, &mut wa_token_1, origin.clone(), ct)
5004                .await
5005                .is_none()
5006        );
5007
5008        // This gives an auth denied because the attested passkey still exists but it no longer
5009        // meets criteria.
5010        match idms_audit.audit_rx().try_recv() {
5011            Ok(AuditEvent::AuthenticationDenied { .. }) => {}
5012            _ => panic!("Oh no"),
5013        }
5014
5015        //  Update creds
5016        let (cust, _) = renew_test_session(idms, ct).await;
5017        let cutxn = idms.cred_update_transaction().await.unwrap();
5018
5019        // Invalid key removed
5020        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        // But we can't commit:
5028        assert!(!c_status.can_commit);
5029        assert!(c_status
5030            .warnings
5031            .contains(&CredentialUpdateSessionStatusWarnings::NoValidCredentials));
5032
5033        // -------------------------------------------------------
5034        // Now enroll the new token.
5035        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        // Finish the registration of token 1
5050        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        // Auth fail with the first token still
5063        assert!(
5064            check_testperson_passkey(idms, idms_delayed, &mut wa_token_1, origin.clone(), ct)
5065                .await
5066                .is_none()
5067        );
5068
5069        // But the new token works.
5070        assert!(
5071            check_testperson_passkey(idms, idms_delayed, &mut wa_token_2, origin.clone(), ct)
5072                .await
5073                .is_some()
5074        );
5075    }
5076
5077    // Test that when attestation policy is removed, the apk downgrades to passkey and still works.
5078    #[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        // Setup the policy.
5086        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        // Enroll the attested keys
5116        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        // -------------------------------------------------------
5121        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        // Finish the registration
5136        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        // -------------------------------------------------------
5146        // Commit
5147        drop(cutxn);
5148        commit_session(idms, ct, cust).await;
5149
5150        // Check auth works
5151        assert!(
5152            check_testperson_passkey(idms, idms_delayed, &mut wa_token_1, origin.clone(), ct)
5153                .await
5154                .is_some()
5155        );
5156
5157        // Change policy
5158        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        // Auth still passes, key was downgraded.
5169        assert!(
5170            check_testperson_passkey(idms, idms_delayed, &mut wa_token_1, origin.clone(), ct)
5171                .await
5172                .is_some()
5173        );
5174
5175        // Show it still exists, but can only be deleted now.
5176        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        // Get the credential status - this should tell
5207        // us the details of the credentials, as well as
5208        // if they are ready and valid to commit?
5209        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        // There are no credentials so we can't proceed.
5217        assert!(c_status
5218            .warnings
5219            .contains(&CredentialUpdateSessionStatusWarnings::NoValidCredentials));
5220        assert!(!c_status.can_commit);
5221        // User needs at least one credential else they can't save.
5222        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        // Test initially creating a credential.
5231        //   - pw first
5232        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        // Check it works!
5242        assert!(check_testperson_unix_password(idms, test_pw, ct)
5243            .await
5244            .is_some());
5245
5246        // Test deleting the pw
5247        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        // Must fail now!
5266        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        // There are no credentials so we can't proceed.
5290        assert!(c_status
5291            .warnings
5292            .contains(&CredentialUpdateSessionStatusWarnings::NoValidCredentials));
5293        assert!(!c_status.can_commit);
5294        // User needs at least one credential else they can't save.
5295        let c_status = cutxn
5296            .credential_primary_set_password(&cust, ct, test_pw)
5297            .expect("Failed to update the primary cred password");
5298
5299        // Ready to proceed with ssh keys
5300
5301        trace!(?c_status);
5302
5303        assert!(c_status.sshkeys.is_empty());
5304
5305        // Reject empty str key label
5306        let result = cutxn.credential_sshkey_add(&cust, ct, "".to_string(), sshkey_valid_1.clone());
5307        assert!(matches!(result, Err(OperationError::InvalidLabel)));
5308
5309        // Reject invalid name label.
5310        let result =
5311            cutxn.credential_sshkey_add(&cust, ct, "🚛".to_string(), sshkey_valid_1.clone());
5312        assert!(matches!(result, Err(OperationError::InvalidLabel)));
5313
5314        // Remove non-existante
5315        let result = cutxn.credential_sshkey_remove(&cust, ct, "key1");
5316        assert!(matches!(result, Err(OperationError::NoMatchingEntries)));
5317
5318        // Add a valid key.
5319        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        // Add a second valid key.
5328        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        // Remove a key (check second key untouched)
5338        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        // Reject duplicate key label
5347        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        // Reject duplicate key
5352        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    // Assert we need at least one credential on the accoutn to save.
5361    #[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        // Get the credential status - this should tell
5374        // us the details of the credentials, as well as
5375        // if they are ready and valid to commit?
5376        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        // There are no credentials so we can't proceed.
5384        assert!(c_status
5385            .warnings
5386            .contains(&CredentialUpdateSessionStatusWarnings::NoValidCredentials));
5387        assert!(!c_status.can_commit);
5388
5389        // Test initially creating a credential.
5390        let c_status = cutxn
5391            .credential_primary_set_password(&cust, ct, test_pw)
5392            .expect("Failed to update the primary cred password");
5393
5394        // Could proceed now!
5395        assert!(c_status.can_commit);
5396        assert!(!c_status
5397            .warnings
5398            .contains(&CredentialUpdateSessionStatusWarnings::NoValidCredentials));
5399
5400        // But if we remove it, back to square 1.
5401        let c_status = cutxn
5402            .credential_primary_delete(&cust, ct)
5403            .expect("Failed to remove the primary credential");
5404
5405        // There are no credentials so we can't proceed.
5406        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        // Sanity check
5431        assert!(get_testperson_password_changed_time(idms).await.is_none());
5432
5433        // Primary Cred with No fallback => EPOCH
5434        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        // UNIX Cred => Unix Cred Timestamp
5448        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        // Unix credential is checked first, its timestamp is used.
5461        assert_eq!(pwd_changed, OffsetDateTime::UNIX_EPOCH + ct);
5462
5463        let ct = Duration::from_secs(TEST_CURRENT_TIME + 1000);
5464
5465        // Update UNIX Cred => New Unix Cred Timestamp
5466        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        // Set up both credentials.
5489        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        // Now delete unix password
5508        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        // Unix cred is gone and timestamp is EPOCH
5519        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        // Sanity check
5535        assert!(get_testperson_password_changed_time(idms).await.is_none());
5536
5537        // No POSIX still causes default to EPOCH
5538        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        // Enable allow_primary_cred_fallback on all accounts.
5552        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        // With fallback enabled PrimaryCred timestamp is used despite not being POSIX account
5566        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        // Sanity check
5588        assert!(get_testperson_password_changed_time(idms).await.is_none());
5589
5590        // Setting Passkey with no Password Credentials causes EPOCH to be set
5591        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        // No password credential was set, so the sentinel UNIX_EPOCH is used.
5602        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        // Renew session and commit without making any changes.
5633        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        // Enable allow_primary_cred_fallback on all accounts.
5657        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        // Sanity check
5671        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        // No unix cred is set and fallback is disabled, so sentinel UNIX_EPOCH is used.
5710        assert_eq!(pwd_changed, OffsetDateTime::UNIX_EPOCH + ct2);
5711
5712        // Delete Unix, PasswordChangedTime should fall back to Primary
5713        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}