Skip to main content

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 CredentialUpdateAccountRecovery {
540    // Who is it targeting?
541    pub email: String,
542    // How long is it valid for?
543    pub max_ttl: Option<Duration>,
544}
545
546pub struct InitCredentialUpdateIntentSendEvent {
547    // Who initiated this?
548    pub ident: Identity,
549    // Who is it targeting?
550    pub target: Uuid,
551    // How long is it valid for?
552    pub max_ttl: Option<Duration>,
553    // Optionally, which email to use?
554    pub email: Option<String>,
555}
556
557pub struct InitCredentialUpdateEvent {
558    pub ident: Identity,
559    pub target: Uuid,
560}
561
562impl InitCredentialUpdateEvent {
563    pub fn new(ident: Identity, target: Uuid) -> Self {
564        InitCredentialUpdateEvent { ident, target }
565    }
566
567    #[cfg(test)]
568    pub fn new_impersonate_entry(e: std::sync::Arc<Entry<EntrySealed, EntryCommitted>>) -> Self {
569        let ident = Identity::from_impersonate_entry_readwrite(e);
570        let target = ident.get_uuid();
571        InitCredentialUpdateEvent { ident, target }
572    }
573}
574
575impl IdmServerProxyWriteTransaction<'_> {
576    fn validate_init_credential_update(
577        &mut self,
578        target: Uuid,
579        ident: &Identity,
580    ) -> Result<(Account, ResolvedAccountPolicy, CredUpdateSessionPerms), OperationError> {
581        let entry = self.qs_write.internal_search_uuid(target)?;
582
583        security_info!(
584            %target,
585            "Initiating Credential Update Session",
586        );
587
588        // The initiating identity must be in readwrite mode! Effective permission assumes you
589        // are in rw.
590        if ident.access_scope() != AccessScope::ReadWrite {
591            security_access!("identity access scope is not permitted to modify");
592            security_access!("denied ❌");
593            return Err(OperationError::AccessDenied);
594        }
595
596        // Is target an account? This checks for us.
597        let (account, resolved_account_policy) =
598            Account::try_from_entry_with_policy(entry.as_ref(), &mut self.qs_write)?;
599
600        let effective_perms = self
601            .qs_write
602            .get_accesscontrols()
603            .effective_permission_check(
604                ident,
605                Some(btreeset![
606                    Attribute::PrimaryCredential,
607                    Attribute::PassKeys,
608                    Attribute::AttestedPasskeys,
609                    Attribute::UnixPassword,
610                    Attribute::SshPublicKey
611                ]),
612                &[entry],
613            )?;
614
615        let eperm = effective_perms.first().ok_or_else(|| {
616            error!("Effective Permission check returned no results");
617            OperationError::InvalidState
618        })?;
619
620        // Does the ident have permission to modify AND search the user-credentials of the target, given
621        // the current status of it's authentication?
622
623        if eperm.target != account.uuid {
624            error!("Effective Permission check target differs from requested entry uuid");
625            return Err(OperationError::InvalidEntryState);
626        }
627
628        let eperm_search_primary_cred = match &eperm.search {
629            Access::Deny => false,
630            Access::Grant => true,
631            Access::Allow(attrs) => attrs.contains(&Attribute::PrimaryCredential),
632        };
633
634        let eperm_mod_primary_cred = match &eperm.modify_pres {
635            Access::Deny => false,
636            Access::Grant => true,
637            Access::Allow(attrs) => attrs.contains(&Attribute::PrimaryCredential),
638        };
639
640        let eperm_rem_primary_cred = match &eperm.modify_rem {
641            Access::Deny => false,
642            Access::Grant => true,
643            Access::Allow(attrs) => attrs.contains(&Attribute::PrimaryCredential),
644        };
645
646        let primary_can_edit =
647            eperm_search_primary_cred && eperm_mod_primary_cred && eperm_rem_primary_cred;
648
649        let eperm_search_passkeys = match &eperm.search {
650            Access::Deny => false,
651            Access::Grant => true,
652            Access::Allow(attrs) => attrs.contains(&Attribute::PassKeys),
653        };
654
655        let eperm_mod_passkeys = match &eperm.modify_pres {
656            Access::Deny => false,
657            Access::Grant => true,
658            Access::Allow(attrs) => attrs.contains(&Attribute::PassKeys),
659        };
660
661        let eperm_rem_passkeys = match &eperm.modify_rem {
662            Access::Deny => false,
663            Access::Grant => true,
664            Access::Allow(attrs) => attrs.contains(&Attribute::PassKeys),
665        };
666
667        let passkeys_can_edit = eperm_search_passkeys && eperm_mod_passkeys && eperm_rem_passkeys;
668
669        let eperm_search_attested_passkeys = match &eperm.search {
670            Access::Deny => false,
671            Access::Grant => true,
672            Access::Allow(attrs) => attrs.contains(&Attribute::AttestedPasskeys),
673        };
674
675        let eperm_mod_attested_passkeys = match &eperm.modify_pres {
676            Access::Deny => false,
677            Access::Grant => true,
678            Access::Allow(attrs) => attrs.contains(&Attribute::AttestedPasskeys),
679        };
680
681        let eperm_rem_attested_passkeys = match &eperm.modify_rem {
682            Access::Deny => false,
683            Access::Grant => true,
684            Access::Allow(attrs) => attrs.contains(&Attribute::AttestedPasskeys),
685        };
686
687        let attested_passkeys_can_edit = eperm_search_attested_passkeys
688            && eperm_mod_attested_passkeys
689            && eperm_rem_attested_passkeys;
690
691        let eperm_search_unixcred = match &eperm.search {
692            Access::Deny => false,
693            Access::Grant => true,
694            Access::Allow(attrs) => attrs.contains(&Attribute::UnixPassword),
695        };
696
697        let eperm_mod_unixcred = match &eperm.modify_pres {
698            Access::Deny => false,
699            Access::Grant => true,
700            Access::Allow(attrs) => attrs.contains(&Attribute::UnixPassword),
701        };
702
703        let eperm_rem_unixcred = match &eperm.modify_rem {
704            Access::Deny => false,
705            Access::Grant => true,
706            Access::Allow(attrs) => attrs.contains(&Attribute::UnixPassword),
707        };
708
709        let unixcred_can_edit = account.unix_extn().is_some()
710            && eperm_search_unixcred
711            && eperm_mod_unixcred
712            && eperm_rem_unixcred;
713
714        let eperm_search_sshpubkey = match &eperm.search {
715            Access::Deny => false,
716            Access::Grant => true,
717            Access::Allow(attrs) => attrs.contains(&Attribute::SshPublicKey),
718        };
719
720        let eperm_mod_sshpubkey = match &eperm.modify_pres {
721            Access::Deny => false,
722            Access::Grant => true,
723            Access::Allow(attrs) => attrs.contains(&Attribute::SshPublicKey),
724        };
725
726        let eperm_rem_sshpubkey = match &eperm.modify_rem {
727            Access::Deny => false,
728            Access::Grant => true,
729            Access::Allow(attrs) => attrs.contains(&Attribute::SshPublicKey),
730        };
731
732        let sshpubkey_can_edit = account.unix_extn().is_some()
733            && eperm_search_sshpubkey
734            && eperm_mod_sshpubkey
735            && eperm_rem_sshpubkey;
736
737        let ext_cred_portal_can_view = if let Some(sync_parent_uuid) = account.sync_parent_uuid {
738            // In theory this is always granted due to how access controls work, but we check anyway.
739            let entry = self.qs_write.internal_search_uuid(sync_parent_uuid)?;
740
741            let effective_perms = self
742                .qs_write
743                .get_accesscontrols()
744                .effective_permission_check(
745                    ident,
746                    Some(btreeset![Attribute::SyncCredentialPortal]),
747                    &[entry],
748                )?;
749
750            let eperm = effective_perms.first().ok_or_else(|| {
751                admin_error!("Effective Permission check returned no results");
752                OperationError::InvalidState
753            })?;
754
755            match &eperm.search {
756                Access::Deny => false,
757                Access::Grant => true,
758                Access::Allow(attrs) => attrs.contains(&Attribute::SyncCredentialPortal),
759            }
760        } else {
761            false
762        };
763
764        // At lease *one* must be modifiable OR visible.
765        if !(primary_can_edit
766            || passkeys_can_edit
767            || attested_passkeys_can_edit
768            || ext_cred_portal_can_view
769            || sshpubkey_can_edit
770            || unixcred_can_edit)
771        {
772            error!("Unable to proceed with credential update intent - at least one type of credential must be modifiable or visible.");
773            Err(OperationError::NotAuthorised)
774        } else {
775            security_info!(%primary_can_edit, %passkeys_can_edit, %unixcred_can_edit, %sshpubkey_can_edit, %ext_cred_portal_can_view, "Proceeding");
776            Ok((
777                account,
778                resolved_account_policy,
779                CredUpdateSessionPerms {
780                    ext_cred_portal_can_view,
781                    passkeys_can_edit,
782                    attested_passkeys_can_edit,
783                    primary_can_edit,
784                    unixcred_can_edit,
785                    sshpubkey_can_edit,
786                },
787            ))
788        }
789    }
790
791    fn create_credupdate_session(
792        &mut self,
793        sessionid: Uuid,
794        intent_token_id: Option<String>,
795        account: Account,
796        resolved_account_policy: ResolvedAccountPolicy,
797        perms: CredUpdateSessionPerms,
798        ct: Duration,
799    ) -> Result<(CredentialUpdateSessionToken, CredentialUpdateSessionStatus), OperationError> {
800        let ext_cred_portal_can_view = perms.ext_cred_portal_can_view;
801
802        let cred_type_min = resolved_account_policy.credential_policy();
803
804        // We can't decide this based on CredentialType alone since we may have CredentialType::Mfa
805        // and still need attestation. As a result, we have to decide this based on presence of
806        // the attestation policy.
807        let passkey_attestation_required = resolved_account_policy
808            .webauthn_attestation_ca_list()
809            .is_some();
810
811        let primary_state = if cred_type_min > CredentialType::Mfa {
812            CredentialState::PolicyDeny
813        } else if perms.primary_can_edit {
814            CredentialState::Modifiable
815        } else {
816            CredentialState::AccessDeny
817        };
818
819        let passkeys_state =
820            if cred_type_min > CredentialType::Passkey || passkey_attestation_required {
821                CredentialState::PolicyDeny
822            } else if perms.passkeys_can_edit {
823                CredentialState::Modifiable
824            } else {
825                CredentialState::AccessDeny
826            };
827
828        let attested_passkeys_state = if cred_type_min > CredentialType::AttestedPasskey {
829            CredentialState::PolicyDeny
830        } else if perms.attested_passkeys_can_edit {
831            if passkey_attestation_required {
832                CredentialState::Modifiable
833            } else {
834                // User can only delete, no police available to add more keys.
835                CredentialState::DeleteOnly
836            }
837        } else {
838            CredentialState::AccessDeny
839        };
840
841        let unixcred_state = if account.unix_extn().is_none() {
842            CredentialState::PolicyDeny
843        } else if perms.unixcred_can_edit {
844            CredentialState::Modifiable
845        } else {
846            CredentialState::AccessDeny
847        };
848
849        let sshkeys_state = if perms.sshpubkey_can_edit {
850            CredentialState::Modifiable
851        } else {
852            CredentialState::AccessDeny
853        };
854
855        // - stash the current state of all associated credentials
856        let primary = if matches!(primary_state, CredentialState::Modifiable) {
857            account.primary.clone()
858        } else {
859            None
860        };
861
862        let passkeys = if matches!(passkeys_state, CredentialState::Modifiable) {
863            account.passkeys.clone()
864        } else {
865            BTreeMap::default()
866        };
867
868        let unixcred: Option<Credential> = if matches!(unixcred_state, CredentialState::Modifiable)
869        {
870            account.unix_extn().and_then(|uext| uext.ucred()).cloned()
871        } else {
872            None
873        };
874
875        let sshkeys = if matches!(sshkeys_state, CredentialState::Modifiable) {
876            account.sshkeys().clone()
877        } else {
878            BTreeMap::default()
879        };
880
881        // Before we start, we pre-filter out anything that no longer conforms to policy.
882        // These would already be failing authentication, so they should have the appearance
883        // of "being removed".
884        let attested_passkeys = if matches!(attested_passkeys_state, CredentialState::Modifiable)
885            || matches!(attested_passkeys_state, CredentialState::DeleteOnly)
886        {
887            if let Some(att_ca_list) = resolved_account_policy.webauthn_attestation_ca_list() {
888                let mut attested_passkeys = BTreeMap::default();
889
890                for (uuid, (label, apk)) in account.attested_passkeys.iter() {
891                    match apk.verify_attestation(att_ca_list) {
892                        Ok(_) => {
893                            // Good to go
894                            attested_passkeys.insert(*uuid, (label.clone(), apk.clone()));
895                        }
896                        Err(e) => {
897                            warn!(eclass=?e, emsg=%e, "credential no longer meets attestation criteria");
898                        }
899                    }
900                }
901
902                attested_passkeys
903            } else {
904                // Seems weird here to be skipping filtering of the credentials. The reason is that
905                // if an account had registered attested passkeys in the past we can delete them, but
906                // not add new ones. Situation only occurs when policy isn't present on the account.
907                account.attested_passkeys.clone()
908            }
909        } else {
910            BTreeMap::default()
911        };
912
913        // Get the external credential portal, if any.
914        let ext_cred_portal = match (account.sync_parent_uuid, ext_cred_portal_can_view) {
915            (Some(sync_parent_uuid), true) => {
916                let sync_entry = self.qs_write.internal_search_uuid(sync_parent_uuid)?;
917                sync_entry
918                    .get_ava_single_url(Attribute::SyncCredentialPortal)
919                    .cloned()
920                    .map(CUExtPortal::Some)
921                    .unwrap_or(CUExtPortal::Hidden)
922            }
923            (Some(_), false) => CUExtPortal::Hidden,
924            (None, _) => CUExtPortal::None,
925        };
926
927        // Stash the issuer for some UI elements
928        let issuer = self.qs_write.get_domain_display_name().to_string();
929
930        // - store account policy (if present)
931        let session = CredentialUpdateSession {
932            account,
933            resolved_account_policy,
934            issuer,
935            intent_token_id,
936            ext_cred_portal,
937            primary,
938            primary_state,
939            unixcred,
940            unixcred_state,
941            sshkeys,
942            sshkeys_state,
943            passkeys,
944            passkeys_state,
945            attested_passkeys,
946            attested_passkeys_state,
947            mfaregstate: MfaRegState::None,
948        };
949
950        let max_ttl = ct + MAXIMUM_CRED_UPDATE_TTL;
951
952        let token = CredentialUpdateSessionTokenInner { sessionid, max_ttl };
953
954        let token_data = serde_json::to_vec(&token).map_err(|e| {
955            admin_error!(err = ?e, "Unable to encode token data");
956            OperationError::SerdeJsonError
957        })?;
958
959        let token_jwe = JweBuilder::from(token_data).build();
960
961        let token_enc = self
962            .qs_write
963            .get_domain_key_object_handle()?
964            .jwe_a128gcm_encrypt(&token_jwe, ct)?;
965
966        let status: CredentialUpdateSessionStatus = (&session).into();
967
968        let session = Arc::new(Mutex::new(session));
969
970        // Point of no return
971
972        // Sneaky! Now we know it will work, prune old sessions.
973        self.expire_credential_update_sessions(ct);
974
975        // Store the update session into the map.
976        self.cred_update_sessions.insert(sessionid, session);
977        trace!("cred_update_sessions.insert - {}", sessionid);
978
979        // - issue the CredentialUpdateToken (enc)
980        Ok((CredentialUpdateSessionToken { token_enc }, status))
981    }
982
983    pub fn credential_update_account_recovery(
984        &mut self,
985        event: CredentialUpdateAccountRecovery,
986        ct: Duration,
987    ) -> Result<(), OperationError> {
988        if !self.qs_write.domain_info().allow_account_recovery() {
989            error!("Account Recovery is Disabled, Rejecting Attempt");
990            return Err(OperationError::CU0010AccountRecoveryDisabled);
991        }
992
993        // This is an internal identity that can only process limited
994        // account requests.
995        let ident = Identity::account_request();
996
997        // Look up the account.
998        let filter = filter!(f_eq(
999            Attribute::Mail,
1000            PartialValue::EmailAddress(event.email.clone())
1001        ));
1002
1003        let mut entries = self
1004            .qs_write
1005            .impersonate_search(filter.clone(), filter, &ident)?;
1006
1007        let entry = entries
1008            .pop()
1009            .and_then(|entry| entries.is_empty().then_some(entry))
1010            .ok_or(OperationError::CU0009AccountEmailNotFound)?;
1011
1012        let target = entry.get_uuid();
1013
1014        // Verify our internal service account has access to modify the target.
1015        //
1016        // IMPORTANT: We use the permissions of the *target* as this is effectively a "self-request"
1017        // to change their credentials. If we were to use the AccountRequest identity, we MAY be allowing
1018        // someone to modify credentials they are not allowed to!
1019        //
1020        // This shows a limitation of the current security system in how we currently focus permissions
1021        // on attributes, not *actions*.
1022        let target_ident = Identity::from_impersonate_entry_readwrite(entry);
1023
1024        let (account, _resolved_account_policy, perms) =
1025            self.validate_init_credential_update(target, &target_ident)?;
1026
1027        // Setup and process the credential update transmission. We do this as the AccountRequest
1028        // identity which needs to be able to create/modify the email object.
1029        self.process_credential_update_send(&account, event.max_ttl, perms, event.email, ct)
1030    }
1031
1032    #[instrument(level = "debug", skip_all)]
1033    pub fn init_credential_update_intent_send(
1034        &mut self,
1035        event: InitCredentialUpdateIntentSendEvent,
1036        ct: Duration,
1037    ) -> Result<(), OperationError> {
1038        let (account, _resolved_account_policy, perms) =
1039            self.validate_init_credential_update(event.target, &event.ident)?;
1040
1041        // If the email is set, validate it.
1042
1043        let to_email = if let Some(to_email) = event.email {
1044            account.mail().contains(&to_email)
1045                .then_some(to_email)
1046                .ok_or_else(|| {
1047                    error!(spn = %account.spn(), "Requested email address is not present on account, unable to send credential reset.");
1048                    OperationError::CU0007AccountEmailNotFound
1049                })
1050        } else {
1051            let maybe_to_email = account.mail_primary().map(String::from);
1052
1053            maybe_to_email.ok_or_else(|| {
1054                error!(spn = %account.spn(), "account does not have a primary email address, unable to send credential reset.");
1055                OperationError::CU0008AccountMissingEmail
1056            })
1057        }?;
1058
1059        self.process_credential_update_send(&account, event.max_ttl, perms, to_email, ct)
1060    }
1061
1062    fn process_credential_update_send(
1063        &mut self,
1064        account: &Account,
1065        max_ttl: Option<Duration>,
1066        perms: CredUpdateSessionPerms,
1067        to_email: String,
1068        ct: Duration,
1069    ) -> Result<(), OperationError> {
1070        // ==== AUTHORISATION CHECKED ===
1071        let (intent_id, expiry_time) =
1072            self.build_credential_update_intent(max_ttl, account, perms, ct)?;
1073
1074        // Queue the message to be sent.
1075        let display_name = account.display_name().to_owned();
1076
1077        let message = OutboundMessage::CredentialResetV1 {
1078            display_name,
1079            intent_id,
1080            expiry_time,
1081        };
1082
1083        let ident = Identity::message_queue();
1084
1085        self.qs_write.queue_message(
1086            // Should we actually impersonate here? We probably need an account for internal sending
1087            // that is disconnected from the act of creating the reset.
1088            &ident, message, to_email,
1089        )
1090    }
1091
1092    #[instrument(level = "debug", skip_all)]
1093    pub fn init_credential_update_intent(
1094        &mut self,
1095        event: &InitCredentialUpdateIntentEvent,
1096        ct: Duration,
1097    ) -> Result<CredentialUpdateIntentToken, OperationError> {
1098        let (account, _resolved_account_policy, perms) =
1099            self.validate_init_credential_update(event.target, &event.ident)?;
1100
1101        // We should check in the acc-pol if we can proceed?
1102        // Is there a reason account policy might deny us from proceeding?
1103
1104        // ==== AUTHORISATION CHECKED ===
1105        let (intent_id, expiry_time) =
1106            self.build_credential_update_intent(event.max_ttl, &account, perms, ct)?;
1107
1108        Ok(CredentialUpdateIntentToken {
1109            intent_id,
1110            expiry_time,
1111        })
1112    }
1113
1114    fn build_credential_update_intent(
1115        &mut self,
1116        max_ttl: Option<Duration>,
1117        account: &Account,
1118        perms: CredUpdateSessionPerms,
1119        ct: Duration,
1120    ) -> Result<(String, OffsetDateTime), OperationError> {
1121        // Build the intent token. Previously this was using 0 and then
1122        // relying on clamp to raise this to 5 minutes, but that led to
1123        // rapid timeouts that affected some users.
1124        let mttl = max_ttl.unwrap_or(DEFAULT_INTENT_TTL);
1125        let clamped_mttl = mttl.clamp(MINIMUM_INTENT_TTL, MAXIMUM_INTENT_TTL);
1126        debug!(?clamped_mttl, "clamped update intent validity");
1127        // Absolute expiry of the intent token in epoch seconds
1128        let max_ttl = ct + clamped_mttl;
1129
1130        // Get the expiry of the intent token as an odt.
1131        let expiry_time = OffsetDateTime::UNIX_EPOCH + max_ttl;
1132
1133        let intent_id = readable_password_from_random();
1134
1135        // Mark that we have created an intent token on the user.
1136        // ⚠️   -- remember, there is a risk, very low, but still a risk of collision of the intent_id.
1137        //        instead of enforcing unique, which would divulge that the collision occurred, we
1138        //        write anyway, and instead on the intent access path we invalidate IF the collision
1139        //        occurs.
1140        let mut modlist = ModifyList::new_append(
1141            Attribute::CredentialUpdateIntentToken,
1142            Value::IntentToken(
1143                intent_id.clone(),
1144                IntentTokenState::Valid { max_ttl, perms },
1145            ),
1146        );
1147
1148        // Remove any old credential update intents
1149        account
1150            .credential_update_intent_tokens
1151            .iter()
1152            .for_each(|(existing_intent_id, state)| {
1153                let max_ttl = match state {
1154                    IntentTokenState::Valid { max_ttl, perms: _ }
1155                    | IntentTokenState::InProgress {
1156                        max_ttl,
1157                        perms: _,
1158                        session_id: _,
1159                        session_ttl: _,
1160                    }
1161                    | IntentTokenState::Consumed { max_ttl } => *max_ttl,
1162                };
1163
1164                if ct >= max_ttl {
1165                    modlist.push_mod(Modify::Removed(
1166                        Attribute::CredentialUpdateIntentToken,
1167                        PartialValue::IntentToken(existing_intent_id.clone()),
1168                    ));
1169                }
1170            });
1171
1172        self.qs_write
1173            .internal_modify(
1174                // Filter as executed
1175                &filter!(f_eq(Attribute::Uuid, PartialValue::Uuid(account.uuid))),
1176                &modlist,
1177            )
1178            .inspect_err(|err| {
1179                error!(?err);
1180            })
1181            .map(|_| (intent_id, expiry_time))
1182    }
1183
1184    #[instrument(level = "debug", skip_all)]
1185    pub fn revoke_credential_update_intent(
1186        &mut self,
1187        token: CredentialUpdateIntentTokenExchange,
1188        _current_time: Duration,
1189    ) -> Result<(), OperationError> {
1190        let CredentialUpdateIntentTokenExchange { intent_id } = token;
1191        // If given an intent_id, immediately transition it to consumed to prevent reuse.
1192        //
1193        // NOTE: We don't need ident/authentication here because posession of the intent_id is
1194        // sufficient to prove that you can revoke it - since if you have the intent_id, you could
1195        // just update the credentials anyway.
1196
1197        let entries = self.qs_write.internal_search(filter!(f_eq(
1198            Attribute::CredentialUpdateIntentToken,
1199            PartialValue::IntentToken(intent_id.clone())
1200        )))?;
1201
1202        // Given the low chance, if the intent_id conflicts we revoke them ALL.
1203        let batch_mod = entries
1204            .iter()
1205            .filter_map(|entry| {
1206                let intenttokens = entry
1207                    .get_ava_set(Attribute::CredentialUpdateIntentToken)
1208                    .and_then(|vs| vs.as_intenttoken_map());
1209
1210                // Shouldn't happen, but okay.
1211                let Some(intenttoken) = intenttokens.and_then(|m| m.get(&intent_id)) else {
1212                    debug_assert!(false);
1213                    return None;
1214                };
1215
1216                let max_ttl = match intenttoken {
1217                    // Already invalid, move on.
1218                    IntentTokenState::Consumed { max_ttl: _ } => return None,
1219                    // Still valid, remove it.
1220                    IntentTokenState::InProgress { max_ttl, .. }
1221                    | IntentTokenState::Valid { max_ttl, .. } => *max_ttl,
1222                };
1223
1224                let entry_uuid = entry.get_uuid();
1225
1226                let mut modlist = ModifyList::new();
1227
1228                modlist.push_mod(Modify::Removed(
1229                    Attribute::CredentialUpdateIntentToken,
1230                    PartialValue::IntentToken(intent_id.clone()),
1231                ));
1232
1233                modlist.push_mod(Modify::Present(
1234                    Attribute::CredentialUpdateIntentToken,
1235                    Value::IntentToken(intent_id.clone(), IntentTokenState::Consumed { max_ttl }),
1236                ));
1237
1238                Some((entry_uuid, modlist))
1239            })
1240            .collect::<Vec<_>>();
1241
1242        self.qs_write.internal_batch_modify(batch_mod.into_iter())
1243    }
1244
1245    pub fn exchange_intent_credential_update(
1246        &mut self,
1247        token: CredentialUpdateIntentTokenExchange,
1248        current_time: Duration,
1249    ) -> Result<(CredentialUpdateSessionToken, CredentialUpdateSessionStatus), OperationError> {
1250        let CredentialUpdateIntentTokenExchange { intent_id } = token;
1251
1252        /*
1253            let entry = self.qs_write.internal_search_uuid(&token.target)?;
1254        */
1255        // ⚠️  due to a low, but possible risk of intent_id collision, if there are multiple
1256        // entries, we will reject the intent.
1257        // DO we need to force both to "Consumed" in this step?
1258        //
1259        // ⚠️  If not present, it may be due to replication delay. We can report this.
1260
1261        let mut vs = self.qs_write.internal_search(filter!(f_eq(
1262            Attribute::CredentialUpdateIntentToken,
1263            PartialValue::IntentToken(intent_id.clone())
1264        )))?;
1265
1266        let entry = match vs.pop() {
1267            Some(entry) => {
1268                if vs.is_empty() {
1269                    // Happy Path!
1270                    entry
1271                } else {
1272                    // Multiple entries matched! This is bad!
1273                    let matched_uuids = std::iter::once(entry.get_uuid())
1274                        .chain(vs.iter().map(|e| e.get_uuid()))
1275                        .collect::<Vec<_>>();
1276
1277                    security_error!("Multiple entries had identical intent_id - for safety, rejecting the use of this intent_id! {:?}", matched_uuids);
1278
1279                    /*
1280                    let mut modlist = ModifyList::new();
1281
1282                    modlist.push_mod(Modify::Removed(
1283                        Attribute::CredentialUpdateIntentToken.into(),
1284                        PartialValue::IntentToken(intent_id.clone()),
1285                    ));
1286
1287                    let filter_or = matched_uuids.into_iter()
1288                        .map(|u| f_eq(Attribute::Uuid, PartialValue::new_uuid(u)))
1289                        .collect();
1290
1291                    self.qs_write
1292                        .internal_modify(
1293                            // Filter as executed
1294                            &filter!(f_or(filter_or)),
1295                            &modlist,
1296                        )
1297                        .map_err(|e| {
1298                            request_error!(error = ?e);
1299                            e
1300                        })?;
1301                    */
1302
1303                    return Err(OperationError::InvalidState);
1304                }
1305            }
1306            None => {
1307                security_info!(
1308                    "Rejecting Update Session - Intent Token does not exist (replication delay?)",
1309                );
1310                return Err(OperationError::Wait(
1311                    OffsetDateTime::UNIX_EPOCH + (current_time + Duration::from_secs(150)),
1312                ));
1313            }
1314        };
1315
1316        // Is target an account? This checks for us.
1317        let (account, resolved_account_policy) =
1318            Account::try_from_entry_with_policy(entry.as_ref(), &mut self.qs_write)?;
1319
1320        // Check there is not already a user session in progress with this intent token.
1321        // Is there a need to revoke intent tokens?
1322
1323        let (max_ttl, perms) = match account.credential_update_intent_tokens.get(&intent_id) {
1324            Some(IntentTokenState::Consumed { max_ttl: _ }) => {
1325                security_info!(
1326                    %entry,
1327                    %account.uuid,
1328                    "Rejecting Update Session - Intent Token has already been exchanged",
1329                );
1330                return Err(OperationError::SessionExpired);
1331            }
1332            Some(IntentTokenState::InProgress {
1333                max_ttl,
1334                perms,
1335                session_id,
1336                session_ttl,
1337            }) => {
1338                if current_time > *session_ttl {
1339                    // The former session has expired, continue.
1340                    security_info!(
1341                        %entry,
1342                        %account.uuid,
1343                        "Initiating Credential Update Session - Previous session {} has expired", session_id
1344                    );
1345                } else {
1346                    // The former session has been orphaned while in use. This can be from someone
1347                    // ctrl-c during their use of the session or refreshing the page without committing.
1348                    //
1349                    // we don't try to exclusive lock the token here with the current time as we previously
1350                    // did. This is because with async replication, there isn't a guarantee this will actually
1351                    // be sent to another server "soon enough" to prevent abuse on the separate server. So
1352                    // all this "lock" actually does is annoy legitimate users and not stop abuse. We
1353                    // STILL keep the InProgress state though since we check it on commit, so this
1354                    // forces the previous orphan session to be immediately invalidated!
1355                    security_info!(
1356                        %entry,
1357                        %account.uuid,
1358                        "Initiating Update Session - Intent Token was in use {} - this will be invalidated.", session_id
1359                    );
1360                };
1361                (*max_ttl, *perms)
1362            }
1363            Some(IntentTokenState::Valid { max_ttl, perms }) => (*max_ttl, *perms),
1364            None => {
1365                admin_error!("Corruption may have occurred - index yielded an entry for intent_id, but the entry does not contain that intent_id");
1366                return Err(OperationError::InvalidState);
1367            }
1368        };
1369
1370        if current_time >= max_ttl {
1371            security_info!(?current_time, ?max_ttl, %account.uuid, "intent has expired");
1372            return Err(OperationError::SessionExpired);
1373        }
1374
1375        security_info!(
1376            %entry,
1377            %account.uuid,
1378            "Initiating Credential Update Session",
1379        );
1380
1381        // To prevent issues with repl, we need to associate this cred update session id, with
1382        // this intent token id.
1383
1384        // Store the intent id in the session (if needed) so that we can check the state at the
1385        // end of the update.
1386
1387        // We need to pin the id from the intent token into the credential to ensure it's not reused
1388
1389        // Need to change this to the expiry time, so we can purge up to.
1390        let session_id = uuid_from_duration(current_time + MAXIMUM_CRED_UPDATE_TTL, self.sid);
1391
1392        let mut modlist = ModifyList::new();
1393
1394        modlist.push_mod(Modify::Removed(
1395            Attribute::CredentialUpdateIntentToken,
1396            PartialValue::IntentToken(intent_id.clone()),
1397        ));
1398        modlist.push_mod(Modify::Present(
1399            Attribute::CredentialUpdateIntentToken,
1400            Value::IntentToken(
1401                intent_id.clone(),
1402                IntentTokenState::InProgress {
1403                    max_ttl,
1404                    perms,
1405                    session_id,
1406                    session_ttl: current_time + MAXIMUM_CRED_UPDATE_TTL,
1407                },
1408            ),
1409        ));
1410
1411        self.qs_write
1412            .internal_modify(
1413                // Filter as executed
1414                &filter!(f_eq(Attribute::Uuid, PartialValue::Uuid(account.uuid))),
1415                &modlist,
1416            )
1417            .map_err(|e| {
1418                request_error!(error = ?e);
1419                e
1420            })?;
1421
1422        // ==========
1423        // Okay, good to exchange.
1424
1425        self.create_credupdate_session(
1426            session_id,
1427            Some(intent_id),
1428            account,
1429            resolved_account_policy,
1430            perms,
1431            current_time,
1432        )
1433    }
1434
1435    #[instrument(level = "debug", skip_all)]
1436    pub fn init_credential_update(
1437        &mut self,
1438        event: &InitCredentialUpdateEvent,
1439        current_time: Duration,
1440    ) -> Result<(CredentialUpdateSessionToken, CredentialUpdateSessionStatus), OperationError> {
1441        let (account, resolved_account_policy, perms) =
1442            self.validate_init_credential_update(event.target, &event.ident)?;
1443
1444        // ==== AUTHORISATION CHECKED ===
1445        // This is the expiry time, so that our cleanup task can "purge up to now" rather
1446        // than needing to do calculations.
1447        let sessionid = uuid_from_duration(current_time + MAXIMUM_CRED_UPDATE_TTL, self.sid);
1448
1449        // Build the cred update session.
1450        self.create_credupdate_session(
1451            sessionid,
1452            None,
1453            account,
1454            resolved_account_policy,
1455            perms,
1456            current_time,
1457        )
1458    }
1459
1460    #[instrument(level = "trace", skip(self))]
1461    pub fn expire_credential_update_sessions(&mut self, ct: Duration) {
1462        let before = self.cred_update_sessions.len();
1463        let split_at = uuid_from_duration(ct, self.sid);
1464        trace!(?split_at, "expiring less than");
1465        self.cred_update_sessions.split_off_lt(&split_at);
1466        let removed = before - self.cred_update_sessions.len();
1467        trace!(?removed);
1468    }
1469
1470    // This shares some common paths between commit and cancel.
1471    fn credential_update_commit_common(
1472        &mut self,
1473        cust: &CredentialUpdateSessionToken,
1474        ct: Duration,
1475    ) -> Result<
1476        (
1477            ModifyList<ModifyInvalid>,
1478            CredentialUpdateSession,
1479            CredentialUpdateSessionTokenInner,
1480        ),
1481        OperationError,
1482    > {
1483        let session_token: CredentialUpdateSessionTokenInner = self
1484            .qs_write
1485            .get_domain_key_object_handle()?
1486            .jwe_decrypt(&cust.token_enc)
1487            .map_err(|e| {
1488                admin_error!(?e, "Failed to decrypt credential update session request");
1489                OperationError::SessionExpired
1490            })
1491            .and_then(|data| {
1492                data.from_json().map_err(|e| {
1493                    admin_error!(err = ?e, "Failed to deserialise credential update session request");
1494                    OperationError::SerdeJsonError
1495                })
1496            })?;
1497
1498        if ct >= session_token.max_ttl {
1499            trace!(?ct, ?session_token.max_ttl);
1500            security_info!(%session_token.sessionid, "session expired");
1501            return Err(OperationError::SessionExpired);
1502        }
1503
1504        let session_handle = self.cred_update_sessions.remove(&session_token.sessionid)
1505            .ok_or_else(|| {
1506                admin_error!("No such sessionid exists on this server - may be due to a load balancer failover or replay? {:?}", session_token.sessionid);
1507                OperationError::InvalidState
1508            })?;
1509
1510        let session = session_handle
1511            .try_lock()
1512            .map(|guard| (*guard).clone())
1513            .map_err(|_| {
1514                admin_error!("Session already locked, unable to proceed.");
1515                OperationError::InvalidState
1516            })?;
1517
1518        trace!(?session);
1519
1520        let modlist = ModifyList::new();
1521
1522        Ok((modlist, session, session_token))
1523    }
1524
1525    pub fn commit_credential_update(
1526        &mut self,
1527        cust: &CredentialUpdateSessionToken,
1528        ct: Duration,
1529    ) -> Result<(), OperationError> {
1530        let (mut modlist, session, session_token) =
1531            self.credential_update_commit_common(cust, ct)?;
1532
1533        // Can we actually proceed?
1534        let can_commit = session.can_commit();
1535        if !can_commit.0 {
1536            let commit_failure_reasons = can_commit
1537                .1
1538                .iter()
1539                .map(|e| e.to_string())
1540                .collect::<Vec<String>>()
1541                .join(", ");
1542            admin_error!(
1543                "Session is unable to commit due to: {}",
1544                commit_failure_reasons
1545            );
1546            return Err(OperationError::CU0004SessionInconsistent);
1547        }
1548
1549        // Setup mods for the various bits. We always assert an *exact* state.
1550
1551        let entry = self.qs_write.internal_search_uuid(session.account.uuid)?;
1552        let account = Account::try_from_entry_rw(entry.as_ref(), &mut self.qs_write)?;
1553
1554        // IF an intent was used on this session, AND that intent is not in our
1555        // session state as an exact match, FAIL the commit. Move the intent to "Consumed".
1556        //
1557        // Should we mark the credential as suspect (lock the account?)
1558        //
1559        // If the credential has changed, reject? Do we need "asserts" in the modlist?
1560        // that would allow better expression of this, and will allow resolving via replication
1561
1562        // If an intent token was used, remove it's former value, and add it as consumed.
1563        if let Some(intent_token_id) = &session.intent_token_id {
1564            let max_ttl = match account.credential_update_intent_tokens.get(intent_token_id) {
1565                Some(IntentTokenState::InProgress {
1566                    max_ttl,
1567                    perms: _,
1568                    session_id,
1569                    session_ttl: _,
1570                }) => {
1571                    if *session_id != session_token.sessionid {
1572                        security_info!("Session originated from an intent token, but the intent token has initiated a conflicting second update session. Refusing to commit changes.");
1573                        return Err(OperationError::CU0005IntentTokenConflict);
1574                    } else {
1575                        *max_ttl
1576                    }
1577                }
1578                Some(IntentTokenState::Consumed { max_ttl: _ })
1579                | Some(IntentTokenState::Valid {
1580                    max_ttl: _,
1581                    perms: _,
1582                })
1583                | None => {
1584                    security_info!("Session originated from an intent token, but the intent token has transitioned to an invalid state. Refusing to commit changes.");
1585                    return Err(OperationError::CU0006IntentTokenInvalidated);
1586                }
1587            };
1588
1589            modlist.push_mod(Modify::Removed(
1590                Attribute::CredentialUpdateIntentToken,
1591                PartialValue::IntentToken(intent_token_id.clone()),
1592            ));
1593            modlist.push_mod(Modify::Present(
1594                Attribute::CredentialUpdateIntentToken,
1595                Value::IntentToken(
1596                    intent_token_id.clone(),
1597                    IntentTokenState::Consumed { max_ttl },
1598                ),
1599            ));
1600        };
1601
1602        let mut cred_changed: Option<OffsetDateTime> = None;
1603
1604        match session.unixcred_state {
1605            CredentialState::DeleteOnly | CredentialState::Modifiable => {
1606                modlist.push_mod(Modify::Purged(Attribute::UnixPassword));
1607
1608                if let Some(ncred) = &session.unixcred {
1609                    let vcred = Value::new_credential("unix", ncred.clone());
1610                    modlist.push_mod(Modify::Present(Attribute::UnixPassword, vcred));
1611                    cred_changed = Some(ncred.timestamp());
1612                }
1613            }
1614            CredentialState::PolicyDeny => {
1615                modlist.push_mod(Modify::Purged(Attribute::UnixPassword));
1616            }
1617            CredentialState::AccessDeny => {}
1618        };
1619
1620        // If we cannot fall back
1621        if cred_changed.is_none()
1622            && session
1623                .resolved_account_policy
1624                .allow_primary_cred_fallback()
1625                != Some(true)
1626        {
1627            // then we don't need to update the password changed time
1628            cred_changed = Some(OffsetDateTime::UNIX_EPOCH);
1629        }
1630
1631        match session.primary_state {
1632            CredentialState::Modifiable => {
1633                modlist.push_mod(Modify::Purged(Attribute::PrimaryCredential));
1634                if let Some(ncred) = &session.primary {
1635                    let vcred = Value::new_credential("primary", ncred.clone());
1636                    modlist.push_mod(Modify::Present(Attribute::PrimaryCredential, vcred));
1637
1638                    cred_changed.get_or_insert(ncred.timestamp());
1639                };
1640            }
1641            CredentialState::DeleteOnly | CredentialState::PolicyDeny => {
1642                modlist.push_mod(Modify::Purged(Attribute::PrimaryCredential));
1643            }
1644            CredentialState::AccessDeny => {}
1645        };
1646
1647        cred_changed.get_or_insert(OffsetDateTime::UNIX_EPOCH);
1648
1649        if let Some(timestamp) = cred_changed {
1650            modlist.push_mod(Modify::Purged(Attribute::PasswordChangedTime));
1651            modlist.push_mod(Modify::Present(
1652                Attribute::PasswordChangedTime,
1653                Value::DateTime(timestamp),
1654            ));
1655        }
1656
1657        match session.passkeys_state {
1658            CredentialState::DeleteOnly | CredentialState::Modifiable => {
1659                modlist.push_mod(Modify::Purged(Attribute::PassKeys));
1660                // Add all the passkeys. If none, nothing will be added! This handles
1661                // the delete case quite cleanly :)
1662                session.passkeys.iter().for_each(|(uuid, (tag, pk))| {
1663                    let v_pk = Value::Passkey(*uuid, tag.clone(), pk.clone());
1664                    modlist.push_mod(Modify::Present(Attribute::PassKeys, v_pk));
1665                });
1666            }
1667            CredentialState::PolicyDeny => {
1668                modlist.push_mod(Modify::Purged(Attribute::PassKeys));
1669            }
1670            CredentialState::AccessDeny => {}
1671        };
1672
1673        match session.attested_passkeys_state {
1674            CredentialState::DeleteOnly | CredentialState::Modifiable => {
1675                modlist.push_mod(Modify::Purged(Attribute::AttestedPasskeys));
1676                // Add all the passkeys. If none, nothing will be added! This handles
1677                // the delete case quite cleanly :)
1678                session
1679                    .attested_passkeys
1680                    .iter()
1681                    .for_each(|(uuid, (tag, pk))| {
1682                        let v_pk = Value::AttestedPasskey(*uuid, tag.clone(), pk.clone());
1683                        modlist.push_mod(Modify::Present(Attribute::AttestedPasskeys, v_pk));
1684                    });
1685            }
1686            CredentialState::PolicyDeny => {
1687                modlist.push_mod(Modify::Purged(Attribute::AttestedPasskeys));
1688            }
1689            // CredentialState::Disabled |
1690            CredentialState::AccessDeny => {}
1691        };
1692
1693        match session.sshkeys_state {
1694            CredentialState::DeleteOnly | CredentialState::Modifiable => {
1695                modlist.push_mod(Modify::Purged(Attribute::SshPublicKey));
1696                for (tag, pk) in &session.sshkeys {
1697                    let v_sk = Value::SshKey(tag.clone(), pk.clone());
1698                    modlist.push_mod(Modify::Present(Attribute::SshPublicKey, v_sk));
1699                }
1700            }
1701            CredentialState::PolicyDeny => {
1702                modlist.push_mod(Modify::Purged(Attribute::SshPublicKey));
1703            }
1704            CredentialState::AccessDeny => {}
1705        };
1706
1707        // Apply to the account!
1708        trace!(?modlist, "processing change");
1709
1710        if modlist.is_empty() {
1711            trace!("no changes to apply");
1712            Ok(())
1713        } else {
1714            self.qs_write
1715                .internal_modify(
1716                    // Filter as executed
1717                    &filter!(f_eq(
1718                        Attribute::Uuid,
1719                        PartialValue::Uuid(session.account.uuid)
1720                    )),
1721                    &modlist,
1722                )
1723                .map_err(|e| {
1724                    request_error!(error = ?e);
1725                    e
1726                })
1727        }
1728    }
1729
1730    pub fn cancel_credential_update(
1731        &mut self,
1732        cust: &CredentialUpdateSessionToken,
1733        ct: Duration,
1734    ) -> Result<(), OperationError> {
1735        let (mut modlist, session, session_token) =
1736            self.credential_update_commit_common(cust, ct)?;
1737
1738        // If an intent token was used, remove it's former value, and add it as VALID since we didn't commit.
1739        if let Some(intent_token_id) = &session.intent_token_id {
1740            let entry = self.qs_write.internal_search_uuid(session.account.uuid)?;
1741            let account = Account::try_from_entry_rw(entry.as_ref(), &mut self.qs_write)?;
1742
1743            let (max_ttl, perms) = match account
1744                .credential_update_intent_tokens
1745                .get(intent_token_id)
1746            {
1747                Some(IntentTokenState::InProgress {
1748                    max_ttl,
1749                    perms,
1750                    session_id,
1751                    session_ttl: _,
1752                }) => {
1753                    if *session_id != session_token.sessionid {
1754                        security_info!("Session originated from an intent token, but the intent token has initiated a conflicting second update session. Refusing to commit changes.");
1755                        return Err(OperationError::InvalidState);
1756                    } else {
1757                        (*max_ttl, *perms)
1758                    }
1759                }
1760                Some(IntentTokenState::Consumed { max_ttl: _ })
1761                | Some(IntentTokenState::Valid {
1762                    max_ttl: _,
1763                    perms: _,
1764                })
1765                | None => {
1766                    security_info!("Session originated from an intent token, but the intent token has transitioned to an invalid state. Refusing to commit changes.");
1767                    return Err(OperationError::InvalidState);
1768                }
1769            };
1770
1771            modlist.push_mod(Modify::Removed(
1772                Attribute::CredentialUpdateIntentToken,
1773                PartialValue::IntentToken(intent_token_id.clone()),
1774            ));
1775            modlist.push_mod(Modify::Present(
1776                Attribute::CredentialUpdateIntentToken,
1777                Value::IntentToken(
1778                    intent_token_id.clone(),
1779                    IntentTokenState::Valid { max_ttl, perms },
1780                ),
1781            ));
1782        };
1783
1784        // Apply to the account!
1785        if !modlist.is_empty() {
1786            trace!(?modlist, "processing change");
1787
1788            self.qs_write
1789                .internal_modify(
1790                    // Filter as executed
1791                    &filter!(f_eq(
1792                        Attribute::Uuid,
1793                        PartialValue::Uuid(session.account.uuid)
1794                    )),
1795                    &modlist,
1796                )
1797                .map_err(|e| {
1798                    request_error!(error = ?e);
1799                    e
1800                })
1801        } else {
1802            Ok(())
1803        }
1804    }
1805}
1806
1807impl IdmServerCredUpdateTransaction<'_> {
1808    #[cfg(test)]
1809    pub fn get_origin(&self) -> &Url {
1810        &self.webauthn.get_allowed_origins()[0]
1811    }
1812
1813    fn get_current_session(
1814        &self,
1815        cust: &CredentialUpdateSessionToken,
1816        ct: Duration,
1817    ) -> Result<CredentialUpdateSessionMutex, OperationError> {
1818        let session_token: CredentialUpdateSessionTokenInner = self
1819            .qs_read
1820            .get_domain_key_object_handle()?
1821            .jwe_decrypt(&cust.token_enc)
1822            .map_err(|e| {
1823                admin_error!(?e, "Failed to decrypt credential update session request");
1824                OperationError::SessionExpired
1825            })
1826            .and_then(|data| {
1827                data.from_json().map_err(|e| {
1828                    admin_error!(err = ?e, "Failed to deserialise credential update session request");
1829                    OperationError::SerdeJsonError
1830                })
1831            })?;
1832
1833        // Check the TTL
1834        if ct >= session_token.max_ttl {
1835            trace!(?ct, ?session_token.max_ttl);
1836            security_info!(%session_token.sessionid, "session expired");
1837            return Err(OperationError::SessionExpired);
1838        }
1839
1840        self.cred_update_sessions.get(&session_token.sessionid)
1841            .ok_or_else(|| {
1842                admin_error!("No such sessionid exists on this server - may be due to a load balancer failover or token replay? {}", session_token.sessionid);
1843                OperationError::InvalidState
1844            })
1845            .cloned()
1846    }
1847
1848    // I think I need this to be a try lock instead, and fail on error, because
1849    // of the nature of the async bits.
1850    pub fn credential_update_status(
1851        &self,
1852        cust: &CredentialUpdateSessionToken,
1853        ct: Duration,
1854    ) -> Result<CredentialUpdateSessionStatus, OperationError> {
1855        let session_handle = self.get_current_session(cust, ct)?;
1856        let session = session_handle.try_lock().map_err(|_| {
1857            admin_error!("Session already locked, unable to proceed.");
1858            OperationError::InvalidState
1859        })?;
1860        trace!(?session);
1861
1862        let status: CredentialUpdateSessionStatus = session.deref().into();
1863        Ok(status)
1864    }
1865
1866    #[instrument(level = "trace", skip(self))]
1867    fn check_password_quality(
1868        &self,
1869        cleartext: &str,
1870        resolved_account_policy: &ResolvedAccountPolicy,
1871        related_inputs: &[&str],
1872        radius_secret: Option<&str>,
1873    ) -> Result<(), PasswordQuality> {
1874        // password strength and badlisting is always global, rather than per-pw-policy.
1875        // pw-policy as check on the account is about requirements for mfa for example.
1876
1877        // is the password at least 10 char?
1878        let pw_min_length = resolved_account_policy.pw_min_length();
1879        if cleartext.len() < pw_min_length as usize {
1880            return Err(PasswordQuality::TooShort(pw_min_length));
1881        }
1882
1883        if let Some(some_radius_secret) = radius_secret {
1884            if cleartext.contains(some_radius_secret) {
1885                return Err(PasswordQuality::DontReusePasswords);
1886            }
1887        }
1888
1889        // zxcvbn doesn't appear to be picking up related inputs, so we check
1890        // these manually. This can sometimes trip up if the input is short,
1891        // such as a user with the name "a".
1892        for related in related_inputs {
1893            if cleartext.contains(related) {
1894                return Err(PasswordQuality::Feedback(vec![
1895                    PasswordFeedback::NamesAndSurnamesByThemselvesAreEasyToGuess,
1896                    PasswordFeedback::AvoidDatesAndYearsThatAreAssociatedWithYou,
1897                ]));
1898            }
1899        }
1900
1901        // does the password pass zxcvbn?
1902        let entropy = zxcvbn(cleartext, related_inputs);
1903
1904        // PW's should always be enforced as strong as possible.
1905        if entropy.score() < Score::Four {
1906            // The password is too week as per:
1907            // https://docs.rs/zxcvbn/2.0.0/zxcvbn/struct.Entropy.html
1908            let feedback: zxcvbn::feedback::Feedback = entropy
1909                .feedback()
1910                .ok_or(OperationError::InvalidState)
1911                .cloned()
1912                .map_err(|e| {
1913                    security_info!("zxcvbn returned no feedback when score < 3 -> {:?}", e);
1914                    // Return some generic feedback when the password is this bad.
1915                    PasswordQuality::Feedback(vec![
1916                        PasswordFeedback::UseAFewWordsAvoidCommonPhrases,
1917                        PasswordFeedback::AddAnotherWordOrTwo,
1918                        PasswordFeedback::NoNeedForSymbolsDigitsOrUppercaseLetters,
1919                    ])
1920                })?;
1921
1922            security_info!(?feedback, "pw quality feedback");
1923
1924            let feedback: Vec<_> = feedback
1925                .suggestions()
1926                .iter()
1927                .map(|s| {
1928                    match s {
1929                            zxcvbn::feedback::Suggestion::UseAFewWordsAvoidCommonPhrases => {
1930                                PasswordFeedback::UseAFewWordsAvoidCommonPhrases
1931                            }
1932                            zxcvbn::feedback::Suggestion::NoNeedForSymbolsDigitsOrUppercaseLetters => {
1933                                PasswordFeedback::NoNeedForSymbolsDigitsOrUppercaseLetters
1934                            }
1935                            zxcvbn::feedback::Suggestion::AddAnotherWordOrTwo => {
1936                                PasswordFeedback::AddAnotherWordOrTwo
1937                            }
1938                            zxcvbn::feedback::Suggestion::CapitalizationDoesntHelpVeryMuch => {
1939                                PasswordFeedback::CapitalizationDoesntHelpVeryMuch
1940                            }
1941                            zxcvbn::feedback::Suggestion::AllUppercaseIsAlmostAsEasyToGuessAsAllLowercase => {
1942                                PasswordFeedback::AllUppercaseIsAlmostAsEasyToGuessAsAllLowercase
1943                            }
1944                            zxcvbn::feedback::Suggestion::ReversedWordsArentMuchHarderToGuess => {
1945                                PasswordFeedback::ReversedWordsArentMuchHarderToGuess
1946                            }
1947                            zxcvbn::feedback::Suggestion::PredictableSubstitutionsDontHelpVeryMuch => {
1948                                PasswordFeedback::PredictableSubstitutionsDontHelpVeryMuch
1949                            }
1950                            zxcvbn::feedback::Suggestion::UseALongerKeyboardPatternWithMoreTurns => {
1951                                PasswordFeedback::UseALongerKeyboardPatternWithMoreTurns
1952                            }
1953                            zxcvbn::feedback::Suggestion::AvoidRepeatedWordsAndCharacters => {
1954                                PasswordFeedback::AvoidRepeatedWordsAndCharacters
1955                            }
1956                            zxcvbn::feedback::Suggestion::AvoidSequences => {
1957                                PasswordFeedback::AvoidSequences
1958                            }
1959                            zxcvbn::feedback::Suggestion::AvoidRecentYears => {
1960                                PasswordFeedback::AvoidRecentYears
1961                            }
1962                            zxcvbn::feedback::Suggestion::AvoidYearsThatAreAssociatedWithYou => {
1963                                PasswordFeedback::AvoidYearsThatAreAssociatedWithYou
1964                            }
1965                            zxcvbn::feedback::Suggestion::AvoidDatesAndYearsThatAreAssociatedWithYou => {
1966                                PasswordFeedback::AvoidDatesAndYearsThatAreAssociatedWithYou
1967                            }
1968                        }
1969                })
1970                .chain(feedback.warning().map(|w| match w {
1971                    zxcvbn::feedback::Warning::StraightRowsOfKeysAreEasyToGuess => {
1972                        PasswordFeedback::StraightRowsOfKeysAreEasyToGuess
1973                    }
1974                    zxcvbn::feedback::Warning::ShortKeyboardPatternsAreEasyToGuess => {
1975                        PasswordFeedback::ShortKeyboardPatternsAreEasyToGuess
1976                    }
1977                    zxcvbn::feedback::Warning::RepeatsLikeAaaAreEasyToGuess => {
1978                        PasswordFeedback::RepeatsLikeAaaAreEasyToGuess
1979                    }
1980                    zxcvbn::feedback::Warning::RepeatsLikeAbcAbcAreOnlySlightlyHarderToGuess => {
1981                        PasswordFeedback::RepeatsLikeAbcAbcAreOnlySlightlyHarderToGuess
1982                    }
1983                    zxcvbn::feedback::Warning::ThisIsATop10Password => {
1984                        PasswordFeedback::ThisIsATop10Password
1985                    }
1986                    zxcvbn::feedback::Warning::ThisIsATop100Password => {
1987                        PasswordFeedback::ThisIsATop100Password
1988                    }
1989                    zxcvbn::feedback::Warning::ThisIsACommonPassword => {
1990                        PasswordFeedback::ThisIsACommonPassword
1991                    }
1992                    zxcvbn::feedback::Warning::ThisIsSimilarToACommonlyUsedPassword => {
1993                        PasswordFeedback::ThisIsSimilarToACommonlyUsedPassword
1994                    }
1995                    zxcvbn::feedback::Warning::SequencesLikeAbcAreEasyToGuess => {
1996                        PasswordFeedback::SequencesLikeAbcAreEasyToGuess
1997                    }
1998                    zxcvbn::feedback::Warning::RecentYearsAreEasyToGuess => {
1999                        PasswordFeedback::RecentYearsAreEasyToGuess
2000                    }
2001                    zxcvbn::feedback::Warning::AWordByItselfIsEasyToGuess => {
2002                        PasswordFeedback::AWordByItselfIsEasyToGuess
2003                    }
2004                    zxcvbn::feedback::Warning::DatesAreOftenEasyToGuess => {
2005                        PasswordFeedback::DatesAreOftenEasyToGuess
2006                    }
2007                    zxcvbn::feedback::Warning::NamesAndSurnamesByThemselvesAreEasyToGuess => {
2008                        PasswordFeedback::NamesAndSurnamesByThemselvesAreEasyToGuess
2009                    }
2010                    zxcvbn::feedback::Warning::CommonNamesAndSurnamesAreEasyToGuess => {
2011                        PasswordFeedback::CommonNamesAndSurnamesAreEasyToGuess
2012                    }
2013                }))
2014                .collect();
2015
2016            return Err(PasswordQuality::Feedback(feedback));
2017        }
2018
2019        // check a password badlist to eliminate more content
2020        // we check the password as "lower case" to help eliminate possibilities
2021        // also, when pw_badlist_cache is read from DB, it is read as Value (iutf8 lowercase)
2022        if self
2023            .qs_read
2024            .pw_badlist()
2025            .contains(&cleartext.to_lowercase())
2026        {
2027            security_info!("Password found in badlist, rejecting");
2028            Err(PasswordQuality::BadListed)
2029        } else {
2030            Ok(())
2031        }
2032    }
2033
2034    #[instrument(level = "trace", skip(cust, self))]
2035    pub fn credential_check_password_quality(
2036        &self,
2037        cust: &CredentialUpdateSessionToken,
2038        ct: Duration,
2039        pw: &str,
2040    ) -> Result<CredentialUpdateSessionStatus, OperationError> {
2041        let session_handle = self.get_current_session(cust, ct)?;
2042        let session = session_handle.try_lock().map_err(|_| {
2043            admin_error!("Session already locked, unable to proceed.");
2044            OperationError::InvalidState
2045        })?;
2046        trace!(?session);
2047
2048        self.check_password_quality(
2049            pw,
2050            &session.resolved_account_policy,
2051            session.account.related_inputs().as_slice(),
2052            session.account.radius_secret.as_deref(),
2053        )
2054        .map_err(|e| match e {
2055            PasswordQuality::TooShort(sz) => {
2056                OperationError::PasswordQuality(vec![PasswordFeedback::TooShort(sz)])
2057            }
2058            PasswordQuality::BadListed => {
2059                OperationError::PasswordQuality(vec![PasswordFeedback::BadListed])
2060            }
2061            PasswordQuality::DontReusePasswords => {
2062                OperationError::PasswordQuality(vec![PasswordFeedback::DontReusePasswords])
2063            }
2064            PasswordQuality::Feedback(feedback) => OperationError::PasswordQuality(feedback),
2065        })?;
2066
2067        Ok(session.deref().into())
2068    }
2069
2070    #[instrument(level = "trace", skip(cust, self))]
2071    pub fn credential_primary_set_password(
2072        &self,
2073        cust: &CredentialUpdateSessionToken,
2074        ct: Duration,
2075        pw: &str,
2076    ) -> Result<CredentialUpdateSessionStatus, OperationError> {
2077        let session_handle = self.get_current_session(cust, ct)?;
2078        let mut session = session_handle.try_lock().map_err(|_| {
2079            admin_error!("Session already locked, unable to proceed.");
2080            OperationError::InvalidState
2081        })?;
2082        trace!(?session);
2083
2084        if !matches!(session.primary_state, CredentialState::Modifiable) {
2085            error!("Session does not have permission to modify primary credential");
2086            return Err(OperationError::AccessDenied);
2087        };
2088
2089        let timestamp = OffsetDateTime::UNIX_EPOCH + ct;
2090
2091        self.check_password_quality(
2092            pw,
2093            &session.resolved_account_policy,
2094            session.account.related_inputs().as_slice(),
2095            session.account.radius_secret.as_deref(),
2096        )
2097        .map_err(|e| match e {
2098            PasswordQuality::TooShort(sz) => {
2099                OperationError::PasswordQuality(vec![PasswordFeedback::TooShort(sz)])
2100            }
2101            PasswordQuality::BadListed => {
2102                OperationError::PasswordQuality(vec![PasswordFeedback::BadListed])
2103            }
2104            PasswordQuality::DontReusePasswords => {
2105                OperationError::PasswordQuality(vec![PasswordFeedback::DontReusePasswords])
2106            }
2107            PasswordQuality::Feedback(feedback) => OperationError::PasswordQuality(feedback),
2108        })?;
2109
2110        let ncred = match &session.primary {
2111            Some(primary) => {
2112                // Is there a need to update the uuid of the cred re softlocks?
2113                primary.set_password(self.crypto_policy, pw, timestamp)?
2114            }
2115            None => Credential::new_password_only(self.crypto_policy, pw, timestamp)?,
2116        };
2117
2118        session.primary = Some(ncred);
2119        Ok(session.deref().into())
2120    }
2121
2122    pub fn credential_primary_init_totp(
2123        &self,
2124        cust: &CredentialUpdateSessionToken,
2125        ct: Duration,
2126    ) -> Result<CredentialUpdateSessionStatus, OperationError> {
2127        let session_handle = self.get_current_session(cust, ct)?;
2128        let mut session = session_handle.try_lock().map_err(|_| {
2129            admin_error!("Session already locked, unable to proceed.");
2130            OperationError::InvalidState
2131        })?;
2132        trace!(?session);
2133
2134        if !matches!(session.primary_state, CredentialState::Modifiable) {
2135            error!("Session does not have permission to modify primary credential");
2136            return Err(OperationError::AccessDenied);
2137        };
2138
2139        // Is there something else in progress? Cancel it if so.
2140        if !matches!(session.mfaregstate, MfaRegState::None) {
2141            debug!("Clearing incomplete mfareg");
2142        }
2143
2144        // Generate the TOTP.
2145        let totp_token = Totp::generate_secure(TOTP_DEFAULT_STEP);
2146
2147        session.mfaregstate = MfaRegState::TotpInit(totp_token);
2148        // Now that it's in the state, it'll be in the status when returned.
2149        Ok(session.deref().into())
2150    }
2151
2152    pub fn credential_primary_check_totp(
2153        &self,
2154        cust: &CredentialUpdateSessionToken,
2155        ct: Duration,
2156        totp_chal: u32,
2157        label: &str,
2158    ) -> Result<CredentialUpdateSessionStatus, OperationError> {
2159        let session_handle = self.get_current_session(cust, ct)?;
2160        let mut session = session_handle.try_lock().map_err(|_| {
2161            admin_error!("Session already locked, unable to proceed.");
2162            OperationError::InvalidState
2163        })?;
2164        trace!(?session);
2165
2166        if !matches!(session.primary_state, CredentialState::Modifiable) {
2167            error!("Session does not have permission to modify primary credential");
2168            return Err(OperationError::AccessDenied);
2169        };
2170
2171        let timestamp = OffsetDateTime::UNIX_EPOCH + ct;
2172
2173        // Are we in a totp reg state?
2174        match &session.mfaregstate {
2175            MfaRegState::TotpInit(totp_token)
2176            | MfaRegState::TotpTryAgain(totp_token)
2177            | MfaRegState::TotpNameTryAgain(totp_token, _)
2178            | MfaRegState::TotpInvalidSha1(totp_token, _, _) => {
2179                if session
2180                    .primary
2181                    .as_ref()
2182                    .map(|cred| cred.has_totp_by_name(label))
2183                    .unwrap_or_default()
2184                    || label.trim().is_empty()
2185                    || !Value::validate_str_escapes(label)
2186                {
2187                    // The user is trying to add a second TOTP under the same name. Lets save them from themselves
2188                    session.mfaregstate =
2189                        MfaRegState::TotpNameTryAgain(totp_token.clone(), label.into());
2190                    return Ok(session.deref().into());
2191                }
2192
2193                if totp_token.verify(totp_chal, ct) {
2194                    // It was valid. Update the credential.
2195                    let ncred = session
2196                        .primary
2197                        .as_ref()
2198                        .map(|cred| {
2199                            cred.append_totp(label.to_string(), totp_token.clone(), timestamp)
2200                        })
2201                        .ok_or_else(|| {
2202                            admin_error!("A TOTP was added, but no primary credential stub exists");
2203                            OperationError::InvalidState
2204                        })?;
2205
2206                    session.primary = Some(ncred);
2207
2208                    // Set the state to None.
2209                    session.mfaregstate = MfaRegState::None;
2210                    Ok(session.deref().into())
2211                } else {
2212                    // What if it's a broken authenticator app? Google authenticator
2213                    // and Authy both force SHA1 and ignore the algo we send. So let's
2214                    // check that just in case.
2215                    let token_sha1 = totp_token.clone().downgrade_to_legacy();
2216
2217                    if token_sha1.verify(totp_chal, ct) {
2218                        // Greeeaaaaaatttt. It's a broken app. Let's check the user
2219                        // knows this is broken, before we proceed.
2220                        session.mfaregstate = MfaRegState::TotpInvalidSha1(
2221                            totp_token.clone(),
2222                            token_sha1,
2223                            label.to_string(),
2224                        );
2225                        Ok(session.deref().into())
2226                    } else {
2227                        // Let them check again, it's a typo.
2228                        session.mfaregstate = MfaRegState::TotpTryAgain(totp_token.clone());
2229                        Ok(session.deref().into())
2230                    }
2231                }
2232            }
2233            _ => Err(OperationError::InvalidRequestState),
2234        }
2235    }
2236
2237    pub fn credential_primary_accept_sha1_totp(
2238        &self,
2239        cust: &CredentialUpdateSessionToken,
2240        ct: Duration,
2241    ) -> Result<CredentialUpdateSessionStatus, OperationError> {
2242        let session_handle = self.get_current_session(cust, ct)?;
2243        let mut session = session_handle.try_lock().map_err(|_| {
2244            admin_error!("Session already locked, unable to proceed.");
2245            OperationError::InvalidState
2246        })?;
2247        trace!(?session);
2248
2249        if !matches!(session.primary_state, CredentialState::Modifiable) {
2250            error!("Session does not have permission to modify primary credential");
2251            return Err(OperationError::AccessDenied);
2252        };
2253
2254        let timestamp = OffsetDateTime::UNIX_EPOCH + ct;
2255
2256        // Are we in a totp reg state?
2257        match &session.mfaregstate {
2258            MfaRegState::TotpInvalidSha1(_, token_sha1, label) => {
2259                // They have accepted it as sha1
2260                let ncred = session
2261                    .primary
2262                    .as_ref()
2263                    .map(|cred| cred.append_totp(label.to_string(), token_sha1.clone(), timestamp))
2264                    .ok_or_else(|| {
2265                        admin_error!("A TOTP was added, but no primary credential stub exists");
2266                        OperationError::InvalidState
2267                    })?;
2268
2269                security_info!("A SHA1 TOTP credential was accepted");
2270
2271                session.primary = Some(ncred);
2272
2273                // Set the state to None.
2274                session.mfaregstate = MfaRegState::None;
2275                Ok(session.deref().into())
2276            }
2277            _ => Err(OperationError::InvalidRequestState),
2278        }
2279    }
2280
2281    pub fn credential_primary_remove_totp(
2282        &self,
2283        cust: &CredentialUpdateSessionToken,
2284        ct: Duration,
2285        label: &str,
2286    ) -> Result<CredentialUpdateSessionStatus, OperationError> {
2287        let session_handle = self.get_current_session(cust, ct)?;
2288        let mut session = session_handle.try_lock().map_err(|_| {
2289            admin_error!("Session already locked, unable to proceed.");
2290            OperationError::InvalidState
2291        })?;
2292        trace!(?session);
2293
2294        if !matches!(session.primary_state, CredentialState::Modifiable) {
2295            error!("Session does not have permission to modify primary credential");
2296            return Err(OperationError::AccessDenied);
2297        };
2298
2299        if !matches!(session.mfaregstate, MfaRegState::None) {
2300            admin_info!("Invalid TOTP state, another update is in progress");
2301            return Err(OperationError::InvalidState);
2302        }
2303
2304        let timestamp = OffsetDateTime::UNIX_EPOCH + ct;
2305
2306        let ncred = session
2307            .primary
2308            .as_ref()
2309            .map(|cred| cred.remove_totp(label, timestamp))
2310            .ok_or_else(|| {
2311                admin_error!("Try to remove TOTP, but no primary credential stub exists");
2312                OperationError::InvalidState
2313            })?;
2314
2315        session.primary = Some(ncred);
2316
2317        // Set the state to None.
2318        session.mfaregstate = MfaRegState::None;
2319        Ok(session.deref().into())
2320    }
2321
2322    pub fn credential_primary_init_backup_codes(
2323        &self,
2324        cust: &CredentialUpdateSessionToken,
2325        ct: Duration,
2326    ) -> Result<CredentialUpdateSessionStatus, OperationError> {
2327        let session_handle = self.get_current_session(cust, ct)?;
2328        let mut session = session_handle.try_lock().map_err(|_| {
2329            error!("Session already locked, unable to proceed.");
2330            OperationError::InvalidState
2331        })?;
2332        trace!(?session);
2333
2334        if !matches!(session.primary_state, CredentialState::Modifiable) {
2335            error!("Session does not have permission to modify primary credential");
2336            return Err(OperationError::AccessDenied);
2337        };
2338
2339        let timestamp = OffsetDateTime::UNIX_EPOCH + ct;
2340
2341        // I think we override/map the status to inject the codes as a once-off state message.
2342
2343        let codes = backup_code_from_random();
2344
2345        let ncred = session
2346            .primary
2347            .as_ref()
2348            .ok_or_else(|| {
2349                error!("Tried to add backup codes, but no primary credential stub exists");
2350                OperationError::InvalidState
2351            })
2352            .and_then(|cred|
2353                cred.update_backup_code(BackupCodes::new(codes.clone()), timestamp)
2354                    .map_err(|_| {
2355                        error!("Tried to add backup codes, but MFA is not enabled on this credential yet");
2356                        OperationError::InvalidState
2357                    })
2358            )
2359            ?;
2360
2361        session.primary = Some(ncred);
2362
2363        Ok(session.deref().into()).map(|mut status: CredentialUpdateSessionStatus| {
2364            status.mfaregstate = MfaRegStateStatus::BackupCodes(codes);
2365            status
2366        })
2367    }
2368
2369    pub fn credential_primary_remove_backup_codes(
2370        &self,
2371        cust: &CredentialUpdateSessionToken,
2372        ct: Duration,
2373    ) -> Result<CredentialUpdateSessionStatus, OperationError> {
2374        let session_handle = self.get_current_session(cust, ct)?;
2375        let mut session = session_handle.try_lock().map_err(|_| {
2376            admin_error!("Session already locked, unable to proceed.");
2377            OperationError::InvalidState
2378        })?;
2379        trace!(?session);
2380
2381        if !matches!(session.primary_state, CredentialState::Modifiable) {
2382            error!("Session does not have permission to modify primary credential");
2383            return Err(OperationError::AccessDenied);
2384        };
2385
2386        let timestamp = OffsetDateTime::UNIX_EPOCH + ct;
2387
2388        let ncred = session
2389            .primary
2390            .as_ref()
2391            .ok_or_else(|| {
2392                admin_error!("Tried to add backup codes, but no primary credential stub exists");
2393                OperationError::InvalidState
2394            })
2395            .and_then(|cred|
2396                cred.remove_backup_code(timestamp)
2397                    .map_err(|_| {
2398                        admin_error!("Tried to remove backup codes, but MFA is not enabled on this credential yet");
2399                        OperationError::InvalidState
2400                    })
2401            )
2402            ?;
2403
2404        session.primary = Some(ncred);
2405
2406        Ok(session.deref().into())
2407    }
2408
2409    pub fn credential_primary_delete(
2410        &self,
2411        cust: &CredentialUpdateSessionToken,
2412        ct: Duration,
2413    ) -> Result<CredentialUpdateSessionStatus, OperationError> {
2414        let session_handle = self.get_current_session(cust, ct)?;
2415        let mut session = session_handle.try_lock().map_err(|_| {
2416            admin_error!("Session already locked, unable to proceed.");
2417            OperationError::InvalidState
2418        })?;
2419        trace!(?session);
2420
2421        if !(matches!(session.primary_state, CredentialState::Modifiable)
2422            || matches!(session.primary_state, CredentialState::DeleteOnly))
2423        {
2424            error!("Session does not have permission to modify primary credential");
2425            return Err(OperationError::AccessDenied);
2426        };
2427
2428        session.primary = None;
2429        Ok(session.deref().into())
2430    }
2431
2432    pub fn credential_passkey_init(
2433        &self,
2434        cust: &CredentialUpdateSessionToken,
2435        ct: Duration,
2436    ) -> Result<CredentialUpdateSessionStatus, OperationError> {
2437        let session_handle = self.get_current_session(cust, ct)?;
2438        let mut session = session_handle.try_lock().map_err(|_| {
2439            admin_error!("Session already locked, unable to proceed.");
2440            OperationError::InvalidState
2441        })?;
2442        trace!(?session);
2443
2444        if !matches!(session.passkeys_state, CredentialState::Modifiable) {
2445            error!("Session does not have permission to modify passkeys");
2446            return Err(OperationError::AccessDenied);
2447        };
2448
2449        if !matches!(session.mfaregstate, MfaRegState::None) {
2450            debug!("Clearing incomplete mfareg");
2451        }
2452
2453        let (ccr, pk_reg) = self
2454            .webauthn
2455            .start_passkey_registration(
2456                session.account.uuid,
2457                session.account.spn(),
2458                &session.account.displayname,
2459                session.account.existing_credential_id_list(),
2460            )
2461            .map_err(|e| {
2462                error!(eclass=?e, emsg=%e, "Unable to start passkey registration");
2463                OperationError::Webauthn
2464            })?;
2465
2466        session.mfaregstate = MfaRegState::Passkey(Box::new(ccr), pk_reg);
2467        // Now that it's in the state, it'll be in the status when returned.
2468        Ok(session.deref().into())
2469    }
2470
2471    pub fn credential_passkey_finish(
2472        &self,
2473        cust: &CredentialUpdateSessionToken,
2474        ct: Duration,
2475        label: String,
2476        reg: &RegisterPublicKeyCredential,
2477    ) -> Result<CredentialUpdateSessionStatus, OperationError> {
2478        let session_handle = self.get_current_session(cust, ct)?;
2479        let mut session = session_handle.try_lock().map_err(|_| {
2480            admin_error!("Session already locked, unable to proceed.");
2481            OperationError::InvalidState
2482        })?;
2483        trace!(?session);
2484
2485        if !matches!(session.passkeys_state, CredentialState::Modifiable) {
2486            error!("Session does not have permission to modify passkeys");
2487            return Err(OperationError::AccessDenied);
2488        };
2489
2490        match &session.mfaregstate {
2491            MfaRegState::Passkey(_ccr, pk_reg) => {
2492                let reg_result = self.webauthn.finish_passkey_registration(reg, pk_reg);
2493
2494                // Clean up state before returning results.
2495                session.mfaregstate = MfaRegState::None;
2496
2497                match reg_result {
2498                    Ok(passkey) => {
2499                        let pk_id = Uuid::new_v4();
2500                        session.passkeys.insert(pk_id, (label, passkey));
2501
2502                        let cu_status: CredentialUpdateSessionStatus = session.deref().into();
2503                        Ok(cu_status)
2504                    }
2505                    Err(WebauthnError::UserNotVerified) => {
2506                        let mut cu_status: CredentialUpdateSessionStatus = session.deref().into();
2507                        cu_status.append_ephemeral_warning(
2508                            CredentialUpdateSessionStatusWarnings::WebauthnUserVerificationRequired,
2509                        );
2510                        Ok(cu_status)
2511                    }
2512                    Err(err) => {
2513                        error!(eclass=?err, emsg=%err, "Unable to complete passkey registration");
2514                        Err(OperationError::CU0002WebauthnRegistrationError)
2515                    }
2516                }
2517            }
2518            invalid_state => {
2519                warn!(?invalid_state);
2520                Err(OperationError::InvalidRequestState)
2521            }
2522        }
2523    }
2524
2525    pub fn credential_passkey_remove(
2526        &self,
2527        cust: &CredentialUpdateSessionToken,
2528        ct: Duration,
2529        uuid: Uuid,
2530    ) -> Result<CredentialUpdateSessionStatus, OperationError> {
2531        let session_handle = self.get_current_session(cust, ct)?;
2532        let mut session = session_handle.try_lock().map_err(|_| {
2533            admin_error!("Session already locked, unable to proceed.");
2534            OperationError::InvalidState
2535        })?;
2536        trace!(?session);
2537
2538        if !(matches!(session.passkeys_state, CredentialState::Modifiable)
2539            || matches!(session.passkeys_state, CredentialState::DeleteOnly))
2540        {
2541            error!("Session does not have permission to modify passkeys");
2542            return Err(OperationError::AccessDenied);
2543        };
2544
2545        // No-op if not present
2546        session.passkeys.remove(&uuid);
2547
2548        Ok(session.deref().into())
2549    }
2550
2551    pub fn credential_attested_passkey_init(
2552        &self,
2553        cust: &CredentialUpdateSessionToken,
2554        ct: Duration,
2555    ) -> Result<CredentialUpdateSessionStatus, OperationError> {
2556        let session_handle = self.get_current_session(cust, ct)?;
2557        let mut session = session_handle.try_lock().map_err(|_| {
2558            error!("Session already locked, unable to proceed.");
2559            OperationError::InvalidState
2560        })?;
2561        trace!(?session);
2562
2563        if !matches!(session.attested_passkeys_state, CredentialState::Modifiable) {
2564            error!("Session does not have permission to modify attested passkeys");
2565            return Err(OperationError::AccessDenied);
2566        };
2567
2568        if !matches!(session.mfaregstate, MfaRegState::None) {
2569            debug!("Cancelling abandoned mfareg");
2570        }
2571
2572        let att_ca_list = session
2573            .resolved_account_policy
2574            .webauthn_attestation_ca_list()
2575            .cloned()
2576            .ok_or_else(|| {
2577                error!(
2578                    "No attestation CA list is available, can not proceed with attested passkeys."
2579                );
2580                OperationError::AccessDenied
2581            })?;
2582
2583        let (ccr, pk_reg) = self
2584            .webauthn
2585            .start_attested_passkey_registration(
2586                session.account.uuid,
2587                session.account.spn(),
2588                &session.account.displayname,
2589                session.account.existing_credential_id_list(),
2590                att_ca_list,
2591                None,
2592            )
2593            .map_err(|e| {
2594                error!(eclass=?e, emsg=%e, "Unable to start passkey registration");
2595                OperationError::Webauthn
2596            })?;
2597
2598        session.mfaregstate = MfaRegState::AttestedPasskey(Box::new(ccr), pk_reg);
2599        // Now that it's in the state, it'll be in the status when returned.
2600        Ok(session.deref().into())
2601    }
2602
2603    pub fn credential_attested_passkey_finish(
2604        &self,
2605        cust: &CredentialUpdateSessionToken,
2606        ct: Duration,
2607        label: String,
2608        reg: &RegisterPublicKeyCredential,
2609    ) -> Result<CredentialUpdateSessionStatus, OperationError> {
2610        let session_handle = self.get_current_session(cust, ct)?;
2611        let mut session = session_handle.try_lock().map_err(|_| {
2612            admin_error!("Session already locked, unable to proceed.");
2613            OperationError::InvalidState
2614        })?;
2615        trace!(?session);
2616
2617        if !matches!(session.attested_passkeys_state, CredentialState::Modifiable) {
2618            error!("Session does not have permission to modify attested passkeys");
2619            return Err(OperationError::AccessDenied);
2620        };
2621
2622        match &session.mfaregstate {
2623            MfaRegState::AttestedPasskey(_ccr, pk_reg) => {
2624                let result = self
2625                    .webauthn
2626                    .finish_attested_passkey_registration(reg, pk_reg)
2627                    .map_err(|e| {
2628                        error!(eclass=?e, emsg=%e, "Unable to complete attested passkey registration");
2629
2630                        match e {
2631                            WebauthnError::AttestationChainNotTrusted(_)
2632                            | WebauthnError::AttestationNotVerifiable => {
2633                                OperationError::CU0001WebauthnAttestationNotTrusted
2634                            },
2635                            WebauthnError::UserNotVerified => {
2636                                OperationError::CU0003WebauthnUserNotVerified
2637                            },
2638                            _ => OperationError::CU0002WebauthnRegistrationError,
2639                        }
2640                    });
2641
2642                // The reg is done. Clean up state before returning errors.
2643                session.mfaregstate = MfaRegState::None;
2644
2645                let passkey = result?;
2646                trace!(?passkey);
2647
2648                let pk_id = Uuid::new_v4();
2649                session.attested_passkeys.insert(pk_id, (label, passkey));
2650
2651                trace!(?session.attested_passkeys);
2652
2653                Ok(session.deref().into())
2654            }
2655            _ => Err(OperationError::InvalidRequestState),
2656        }
2657    }
2658
2659    pub fn credential_attested_passkey_remove(
2660        &self,
2661        cust: &CredentialUpdateSessionToken,
2662        ct: Duration,
2663        uuid: Uuid,
2664    ) -> Result<CredentialUpdateSessionStatus, OperationError> {
2665        let session_handle = self.get_current_session(cust, ct)?;
2666        let mut session = session_handle.try_lock().map_err(|_| {
2667            admin_error!("Session already locked, unable to proceed.");
2668            OperationError::InvalidState
2669        })?;
2670        trace!(?session);
2671
2672        if !(matches!(session.attested_passkeys_state, CredentialState::Modifiable)
2673            || matches!(session.attested_passkeys_state, CredentialState::DeleteOnly))
2674        {
2675            error!("Session does not have permission to modify attested passkeys");
2676            return Err(OperationError::AccessDenied);
2677        };
2678
2679        // No-op if not present
2680        session.attested_passkeys.remove(&uuid);
2681
2682        Ok(session.deref().into())
2683    }
2684
2685    #[instrument(level = "trace", skip(cust, self))]
2686    pub fn credential_unix_set_password(
2687        &self,
2688        cust: &CredentialUpdateSessionToken,
2689        ct: Duration,
2690        pw: &str,
2691    ) -> Result<CredentialUpdateSessionStatus, OperationError> {
2692        let session_handle = self.get_current_session(cust, ct)?;
2693        let mut session = session_handle.try_lock().map_err(|_| {
2694            admin_error!("Session already locked, unable to proceed.");
2695            OperationError::InvalidState
2696        })?;
2697        trace!(?session);
2698
2699        if !matches!(session.unixcred_state, CredentialState::Modifiable) {
2700            error!("Session does not have permission to modify unix credential");
2701            return Err(OperationError::AccessDenied);
2702        };
2703
2704        let timestamp = OffsetDateTime::UNIX_EPOCH + ct;
2705
2706        self.check_password_quality(
2707            pw,
2708            &session.resolved_account_policy,
2709            session.account.related_inputs().as_slice(),
2710            session.account.radius_secret.as_deref(),
2711        )
2712        .map_err(|e| match e {
2713            PasswordQuality::TooShort(sz) => {
2714                OperationError::PasswordQuality(vec![PasswordFeedback::TooShort(sz)])
2715            }
2716            PasswordQuality::BadListed => {
2717                OperationError::PasswordQuality(vec![PasswordFeedback::BadListed])
2718            }
2719            PasswordQuality::DontReusePasswords => {
2720                OperationError::PasswordQuality(vec![PasswordFeedback::DontReusePasswords])
2721            }
2722            PasswordQuality::Feedback(feedback) => OperationError::PasswordQuality(feedback),
2723        })?;
2724
2725        let ncred = match &session.unixcred {
2726            Some(unixcred) => {
2727                // Is there a need to update the uuid of the cred re softlocks?
2728                unixcred.set_password(self.crypto_policy, pw, timestamp)?
2729            }
2730            None => Credential::new_password_only(self.crypto_policy, pw, timestamp)?,
2731        };
2732
2733        session.unixcred = Some(ncred);
2734        Ok(session.deref().into())
2735    }
2736
2737    pub fn credential_unix_delete(
2738        &self,
2739        cust: &CredentialUpdateSessionToken,
2740        ct: Duration,
2741    ) -> Result<CredentialUpdateSessionStatus, OperationError> {
2742        let session_handle = self.get_current_session(cust, ct)?;
2743        let mut session = session_handle.try_lock().map_err(|_| {
2744            admin_error!("Session already locked, unable to proceed.");
2745            OperationError::InvalidState
2746        })?;
2747        trace!(?session);
2748
2749        if !(matches!(session.unixcred_state, CredentialState::Modifiable)
2750            || matches!(session.unixcred_state, CredentialState::DeleteOnly))
2751        {
2752            error!("Session does not have permission to modify unix credential");
2753            return Err(OperationError::AccessDenied);
2754        };
2755
2756        session.unixcred = None;
2757        Ok(session.deref().into())
2758    }
2759
2760    #[instrument(level = "trace", skip(cust, self))]
2761    pub fn credential_sshkey_add(
2762        &self,
2763        cust: &CredentialUpdateSessionToken,
2764        ct: Duration,
2765        label: String,
2766        sshpubkey: SshPublicKey,
2767    ) -> Result<CredentialUpdateSessionStatus, OperationError> {
2768        let session_handle = self.get_current_session(cust, ct)?;
2769        let mut session = session_handle.try_lock().map_err(|_| {
2770            admin_error!("Session already locked, unable to proceed.");
2771            OperationError::InvalidState
2772        })?;
2773        trace!(?session);
2774
2775        if !matches!(session.unixcred_state, CredentialState::Modifiable) {
2776            error!("Session does not have permission to modify unix credential");
2777            return Err(OperationError::AccessDenied);
2778        };
2779
2780        // Check the label.
2781        if !LABEL_RE.is_match(&label) {
2782            error!("SSH Public Key label invalid");
2783            return Err(OperationError::InvalidLabel);
2784        }
2785
2786        if session.sshkeys.contains_key(&label) {
2787            error!("SSH Public Key label duplicate");
2788            return Err(OperationError::DuplicateLabel);
2789        }
2790
2791        if session.sshkeys.values().any(|sk| *sk == sshpubkey) {
2792            error!("SSH Public Key duplicate");
2793            return Err(OperationError::DuplicateKey);
2794        }
2795
2796        session.sshkeys.insert(label, sshpubkey);
2797
2798        Ok(session.deref().into())
2799    }
2800
2801    pub fn credential_sshkey_remove(
2802        &self,
2803        cust: &CredentialUpdateSessionToken,
2804        ct: Duration,
2805        label: &str,
2806    ) -> Result<CredentialUpdateSessionStatus, OperationError> {
2807        let session_handle = self.get_current_session(cust, ct)?;
2808        let mut session = session_handle.try_lock().map_err(|_| {
2809            admin_error!("Session already locked, unable to proceed.");
2810            OperationError::InvalidState
2811        })?;
2812        trace!(?session);
2813
2814        if !(matches!(session.sshkeys_state, CredentialState::Modifiable)
2815            || matches!(session.sshkeys_state, CredentialState::DeleteOnly))
2816        {
2817            error!("Session does not have permission to modify sshkeys");
2818            return Err(OperationError::AccessDenied);
2819        };
2820
2821        session.sshkeys.remove(label).ok_or_else(|| {
2822            error!("No such key for label");
2823            OperationError::NoMatchingEntries
2824        })?;
2825
2826        // session.unixcred = None;
2827
2828        Ok(session.deref().into())
2829    }
2830
2831    pub fn credential_update_cancel_mfareg(
2832        &self,
2833        cust: &CredentialUpdateSessionToken,
2834        ct: Duration,
2835    ) -> Result<CredentialUpdateSessionStatus, OperationError> {
2836        let session_handle = self.get_current_session(cust, ct)?;
2837        let mut session = session_handle.try_lock().map_err(|_| {
2838            admin_error!("Session already locked, unable to proceed.");
2839            OperationError::InvalidState
2840        })?;
2841        trace!(?session);
2842        session.mfaregstate = MfaRegState::None;
2843        Ok(session.deref().into())
2844    }
2845
2846    // Generate password?
2847}
2848
2849#[cfg(test)]
2850mod tests {
2851    use super::{
2852        CredentialState, CredentialUpdateAccountRecovery, CredentialUpdateSessionStatus,
2853        CredentialUpdateSessionStatusWarnings, CredentialUpdateSessionToken,
2854        InitCredentialUpdateEvent, InitCredentialUpdateIntentEvent,
2855        InitCredentialUpdateIntentSendEvent, MfaRegStateStatus, MAXIMUM_CRED_UPDATE_TTL,
2856        MAXIMUM_INTENT_TTL, MINIMUM_INTENT_TTL,
2857    };
2858    use crate::credential::totp::Totp;
2859    use crate::event::CreateEvent;
2860    use crate::idm::audit::AuditEvent;
2861    use crate::idm::authentication::AuthState;
2862    use crate::idm::delayed::DelayedAction;
2863    use crate::idm::event::{
2864        AuthEvent, AuthResult, RegenerateRadiusSecretEvent, UnixUserAuthEvent,
2865    };
2866    use crate::idm::server::{IdmServer, IdmServerCredUpdateTransaction, IdmServerDelayed};
2867    use crate::prelude::*;
2868    use crate::utils::password_from_random_len;
2869    use crate::value::CredentialType;
2870    use crate::valueset::ValueSetEmailAddress;
2871    use compact_jwt::JwsCompact;
2872    use kanidm_proto::internal::{CUExtPortal, CredentialDetailType, PasswordFeedback};
2873    use kanidm_proto::v1::OutboundMessage;
2874    use kanidm_proto::v1::{AuthAllowed, AuthIssueSession, AuthMech, UnixUserToken};
2875    use sshkey_attest::proto::PublicKey as SshPublicKey;
2876    use std::time::Duration;
2877    use time::OffsetDateTime;
2878    use uuid::uuid;
2879    use webauthn_authenticator_rs::softpasskey::SoftPasskey;
2880    use webauthn_authenticator_rs::softtoken::{self, SoftToken};
2881    use webauthn_authenticator_rs::{AuthenticatorBackend, WebauthnAuthenticator};
2882    use webauthn_rs::prelude::AttestationCaListBuilder;
2883
2884    const TEST_CURRENT_TIME: u64 = 6000;
2885    const TESTPERSON_UUID: Uuid = uuid!("cf231fea-1a8f-4410-a520-fd9b1a379c86");
2886    const TESTPERSON_NAME: &str = "testperson";
2887
2888    const TESTPERSON_PASSWORD: &str = "SSBndWVzcyB5b3UgZGlzY292ZXJlZCB0aGUgc2VjcmV0";
2889
2890    const SSHKEY_VALID_1: &str = "sk-ecdsa-sha2-nistp256@openssh.com AAAAInNrLWVjZHNhLXNoYTItbmlzdHAyNTZAb3BlbnNzaC5jb20AAAAIbmlzdHAyNTYAAABBBENubZikrb8hu+HeVRdZ0pp/VAk2qv4JDbuJhvD0yNdWDL2e3cBbERiDeNPkWx58Q4rVnxkbV1fa8E2waRtT91wAAAAEc3NoOg== testuser@fidokey";
2891    const SSHKEY_VALID_2: &str = "sk-ecdsa-sha2-nistp256@openssh.com AAAAInNrLWVjZHNhLXNoYTItbmlzdHAyNTZAb3BlbnNzaC5jb20AAAAIbmlzdHAyNTYAAABBBIbkSsdGCRoW6v0nO/3vNYPhG20YhWU0wQPY7x52EOb4dmYhC4IJfzVDpEPg313BxWRKQglb5RQ1PPkou7JFyCUAAAAEc3NoOg== testuser@fidokey";
2892    const SSHKEY_INVALID: &str = "sk-ecrsa-sha9000-nistp@openssh.com AAAAInNrLWVjZHNhLXNoYTItbmlzdHAyNTZAb3BlbnNzaC5jb20AAAAIbmlzdHAyNTYAAABBBIbkSsdGCRoW6v0nO/3vNYPhG20YhWU0wQPY7x52EOb4dmYhC4IJfzVDpEPg313BxWRKQglb5RQ1PPkou7JFyCUAAAAEc3NoOg== badkey@rejectme";
2893
2894    #[idm_test]
2895    async fn credential_update_session_init(
2896        idms: &IdmServer,
2897        _idms_delayed: &mut IdmServerDelayed,
2898    ) {
2899        let ct = Duration::from_secs(TEST_CURRENT_TIME);
2900        let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();
2901
2902        let testaccount_uuid = Uuid::new_v4();
2903
2904        let e1 = entry_init!(
2905            (Attribute::Class, EntryClass::Object.to_value()),
2906            (Attribute::Class, EntryClass::Account.to_value()),
2907            (Attribute::Class, EntryClass::ServiceAccount.to_value()),
2908            (Attribute::Name, Value::new_iname("user_account_only")),
2909            (Attribute::Uuid, Value::Uuid(testaccount_uuid)),
2910            (Attribute::Description, Value::new_utf8s("testaccount")),
2911            (Attribute::DisplayName, Value::new_utf8s("testaccount"))
2912        );
2913
2914        let e2 = entry_init!(
2915            (Attribute::Class, EntryClass::Object.to_value()),
2916            (Attribute::Class, EntryClass::Account.to_value()),
2917            (Attribute::Class, EntryClass::PosixAccount.to_value()),
2918            (Attribute::Class, EntryClass::Person.to_value()),
2919            (Attribute::Name, Value::new_iname(TESTPERSON_NAME)),
2920            (Attribute::Uuid, Value::Uuid(TESTPERSON_UUID)),
2921            (Attribute::Description, Value::new_utf8s(TESTPERSON_NAME)),
2922            (Attribute::DisplayName, Value::new_utf8s(TESTPERSON_NAME))
2923        );
2924
2925        let ce = CreateEvent::new_internal(vec![e1, e2]);
2926        let cr = idms_prox_write.qs_write.create(&ce);
2927        assert!(cr.is_ok());
2928
2929        let testaccount = idms_prox_write
2930            .qs_write
2931            .internal_search_uuid(testaccount_uuid)
2932            .expect("failed");
2933
2934        let testperson = idms_prox_write
2935            .qs_write
2936            .internal_search_uuid(TESTPERSON_UUID)
2937            .expect("failed");
2938
2939        let idm_admin = idms_prox_write
2940            .qs_write
2941            .internal_search_uuid(UUID_IDM_ADMIN)
2942            .expect("failed");
2943
2944        // user without permission - fail
2945        // - accounts don't have self-write permission.
2946
2947        let cur = idms_prox_write.init_credential_update(
2948            &InitCredentialUpdateEvent::new_impersonate_entry(testaccount),
2949            ct,
2950        );
2951
2952        assert!(matches!(cur, Err(OperationError::NotAuthorised)));
2953
2954        // user with permission - success
2955
2956        let cur = idms_prox_write.init_credential_update(
2957            &InitCredentialUpdateEvent::new_impersonate_entry(testperson),
2958            ct,
2959        );
2960
2961        assert!(cur.is_ok());
2962
2963        // create intent token without permission - fail
2964
2965        // create intent token with permission - success
2966
2967        let cur = idms_prox_write.init_credential_update_intent(
2968            &InitCredentialUpdateIntentEvent::new_impersonate_entry(
2969                idm_admin.clone(),
2970                TESTPERSON_UUID,
2971                MINIMUM_INTENT_TTL,
2972            ),
2973            ct,
2974        );
2975
2976        assert!(cur.is_ok());
2977        let intent_tok = cur.expect("Failed to create intent token!");
2978
2979        // exchange intent token - invalid - fail
2980        // Expired
2981        let cur = idms_prox_write
2982            .exchange_intent_credential_update(intent_tok.clone().into(), ct + MINIMUM_INTENT_TTL);
2983
2984        assert!(matches!(cur, Err(OperationError::SessionExpired)));
2985
2986        let cur = idms_prox_write
2987            .exchange_intent_credential_update(intent_tok.clone().into(), ct + MAXIMUM_INTENT_TTL);
2988
2989        assert!(matches!(cur, Err(OperationError::SessionExpired)));
2990
2991        // exchange intent token - success
2992        let (cust_a, _c_status) = idms_prox_write
2993            .exchange_intent_credential_update(intent_tok.clone().into(), ct)
2994            .unwrap();
2995
2996        // Session in progress - This will succeed and then block the former success from
2997        // committing.
2998        let (cust_b, _c_status) = idms_prox_write
2999            .exchange_intent_credential_update(intent_tok.into(), ct + Duration::from_secs(1))
3000            .unwrap();
3001
3002        let cur = idms_prox_write.commit_credential_update(&cust_a, ct);
3003
3004        // Fails as the txn was orphaned.
3005        trace!(?cur);
3006        assert!(cur.is_err());
3007
3008        // Success - this was the second use of the token and is valid.
3009        let _ = idms_prox_write.commit_credential_update(&cust_b, ct);
3010
3011        debug!("Start intent token revoke");
3012
3013        // Create an intent token and then revoke it.
3014        let intent_tok = idms_prox_write
3015            .init_credential_update_intent(
3016                &InitCredentialUpdateIntentEvent::new_impersonate_entry(
3017                    idm_admin,
3018                    TESTPERSON_UUID,
3019                    MINIMUM_INTENT_TTL,
3020                ),
3021                ct,
3022            )
3023            .expect("Failed to create intent token!");
3024
3025        idms_prox_write
3026            .revoke_credential_update_intent(intent_tok.clone().into(), ct)
3027            .expect("Failed to revoke intent");
3028
3029        // Can't exchange, token ded.
3030        let cur = idms_prox_write.exchange_intent_credential_update(
3031            intent_tok.clone().into(),
3032            ct + Duration::from_secs(1),
3033        );
3034        debug!(?cur);
3035        assert!(matches!(cur, Err(OperationError::SessionExpired)));
3036
3037        idms_prox_write.commit().expect("Failed to commit txn");
3038    }
3039
3040    async fn setup_test_session(
3041        idms: &IdmServer,
3042        ct: Duration,
3043    ) -> (CredentialUpdateSessionToken, CredentialUpdateSessionStatus) {
3044        setup_test_session_inner(idms, ct, true).await
3045    }
3046
3047    async fn setup_test_session_no_posix(
3048        idms: &IdmServer,
3049        ct: Duration,
3050    ) -> (CredentialUpdateSessionToken, CredentialUpdateSessionStatus) {
3051        setup_test_session_inner(idms, ct, false).await
3052    }
3053
3054    async fn setup_test_session_inner(
3055        idms: &IdmServer,
3056        ct: Duration,
3057        posix: bool,
3058    ) -> (CredentialUpdateSessionToken, CredentialUpdateSessionStatus) {
3059        let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();
3060
3061        // Remove the default all persons policy, it interferes with our test.
3062        let modlist = ModifyList::new_purge(Attribute::CredentialTypeMinimum);
3063        idms_prox_write
3064            .qs_write
3065            .internal_modify_uuid(UUID_IDM_ALL_PERSONS, &modlist)
3066            .expect("Unable to change default session exp");
3067
3068        let mut builder = entry_init!(
3069            (Attribute::Class, EntryClass::Object.to_value()),
3070            (Attribute::Class, EntryClass::Account.to_value()),
3071            (Attribute::Class, EntryClass::Person.to_value()),
3072            (Attribute::Name, Value::new_iname(TESTPERSON_NAME)),
3073            (Attribute::Uuid, Value::Uuid(TESTPERSON_UUID)),
3074            (Attribute::Description, Value::new_utf8s(TESTPERSON_NAME)),
3075            (Attribute::DisplayName, Value::new_utf8s(TESTPERSON_NAME))
3076        );
3077
3078        if posix {
3079            builder.add_ava(Attribute::Class, EntryClass::PosixAccount.to_value());
3080        }
3081
3082        let ce = CreateEvent::new_internal(vec![builder]);
3083        let cr = idms_prox_write.qs_write.create(&ce);
3084        assert!(cr.is_ok());
3085
3086        let testperson = idms_prox_write
3087            .qs_write
3088            .internal_search_uuid(TESTPERSON_UUID)
3089            .expect("failed");
3090
3091        if posix {
3092            // Setup the radius creds to ensure we don't use them anywhere else.
3093            let rrse = RegenerateRadiusSecretEvent::new_internal(TESTPERSON_UUID);
3094
3095            let _ = idms_prox_write
3096                .regenerate_radius_secret(&rrse)
3097                .expect("Failed to reset radius credential 1");
3098        }
3099
3100        let cur = idms_prox_write.init_credential_update(
3101            &InitCredentialUpdateEvent::new_impersonate_entry(testperson),
3102            ct,
3103        );
3104
3105        idms_prox_write.commit().expect("Failed to commit txn");
3106
3107        cur.expect("Failed to start update")
3108    }
3109
3110    async fn renew_test_session(
3111        idms: &IdmServer,
3112        ct: Duration,
3113    ) -> (CredentialUpdateSessionToken, CredentialUpdateSessionStatus) {
3114        let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();
3115
3116        let testperson = idms_prox_write
3117            .qs_write
3118            .internal_search_uuid(TESTPERSON_UUID)
3119            .expect("failed");
3120
3121        let cur = idms_prox_write.init_credential_update(
3122            &InitCredentialUpdateEvent::new_impersonate_entry(testperson),
3123            ct,
3124        );
3125
3126        trace!(renew_test_session_result = ?cur);
3127
3128        idms_prox_write.commit().expect("Failed to commit txn");
3129
3130        cur.expect("Failed to start update")
3131    }
3132
3133    async fn commit_session(idms: &IdmServer, ct: Duration, cust: CredentialUpdateSessionToken) {
3134        let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();
3135
3136        idms_prox_write
3137            .commit_credential_update(&cust, ct)
3138            .expect("Failed to commit credential update.");
3139
3140        idms_prox_write.commit().expect("Failed to commit txn");
3141    }
3142
3143    async fn check_testperson_password(
3144        idms: &IdmServer,
3145        idms_delayed: &mut IdmServerDelayed,
3146        pw: &str,
3147        ct: Duration,
3148    ) -> Option<JwsCompact> {
3149        let mut idms_auth = idms.auth().await.unwrap();
3150
3151        let auth_init = AuthEvent::named_init(TESTPERSON_NAME);
3152
3153        let r1 = idms_auth
3154            .auth(&auth_init, ct, Source::Internal.into())
3155            .await;
3156        let ar = r1.unwrap();
3157        let AuthResult { sessionid, state } = ar;
3158
3159        if !matches!(state, AuthState::Choose(_)) {
3160            debug!("Can't proceed - {:?}", state);
3161            return None;
3162        };
3163
3164        let auth_begin = AuthEvent::begin_mech(sessionid, AuthMech::Password);
3165
3166        let r2 = idms_auth
3167            .auth(&auth_begin, ct, Source::Internal.into())
3168            .await;
3169        let ar = r2.unwrap();
3170        let AuthResult { sessionid, state } = ar;
3171
3172        assert!(matches!(state, AuthState::Continue(_)));
3173
3174        let pw_step = AuthEvent::cred_step_password(sessionid, pw);
3175
3176        // Expect success
3177        let r2 = idms_auth.auth(&pw_step, ct, Source::Internal.into()).await;
3178        debug!("r2 ==> {:?}", r2);
3179        idms_auth.commit().expect("Must not fail");
3180
3181        match r2 {
3182            Ok(AuthResult {
3183                sessionid: _,
3184                state: AuthState::Success(token, AuthIssueSession::Token),
3185            }) => {
3186                // Process the auth session
3187                let da = idms_delayed.try_recv().expect("invalid");
3188                assert!(matches!(da, DelayedAction::AuthSessionRecord(_)));
3189
3190                Some(*token)
3191            }
3192            _ => None,
3193        }
3194    }
3195
3196    async fn check_testperson_unix_password(
3197        idms: &IdmServer,
3198        // idms_delayed: &mut IdmServerDelayed,
3199        pw: &str,
3200        ct: Duration,
3201    ) -> Option<UnixUserToken> {
3202        let mut idms_auth = idms.auth().await.unwrap();
3203
3204        let auth_event = UnixUserAuthEvent::new_internal(TESTPERSON_UUID, pw);
3205
3206        idms_auth
3207            .auth_unix(&auth_event, ct)
3208            .await
3209            .expect("Unable to perform unix authentication")
3210    }
3211
3212    async fn check_testperson_password_totp(
3213        idms: &IdmServer,
3214        idms_delayed: &mut IdmServerDelayed,
3215        pw: &str,
3216        token: &Totp,
3217        ct: Duration,
3218    ) -> Option<JwsCompact> {
3219        let mut idms_auth = idms.auth().await.unwrap();
3220
3221        let auth_init = AuthEvent::named_init(TESTPERSON_NAME);
3222
3223        let r1 = idms_auth
3224            .auth(&auth_init, ct, Source::Internal.into())
3225            .await;
3226        let ar = r1.unwrap();
3227        let AuthResult { sessionid, state } = ar;
3228
3229        if !matches!(state, AuthState::Choose(_)) {
3230            debug!("Can't proceed - {:?}", state);
3231            return None;
3232        };
3233
3234        let auth_begin = AuthEvent::begin_mech(sessionid, AuthMech::PasswordTotp);
3235
3236        let r2 = idms_auth
3237            .auth(&auth_begin, ct, Source::Internal.into())
3238            .await;
3239        let ar = r2.unwrap();
3240        let AuthResult { sessionid, state } = ar;
3241
3242        assert!(matches!(state, AuthState::Continue(_)));
3243
3244        let totp = token
3245            .do_totp_duration_from_epoch(&ct)
3246            .expect("Failed to perform totp step");
3247
3248        let totp_step = AuthEvent::cred_step_totp(sessionid, totp);
3249        let r2 = idms_auth
3250            .auth(&totp_step, ct, Source::Internal.into())
3251            .await;
3252        let ar = r2.unwrap();
3253        let AuthResult { sessionid, state } = ar;
3254
3255        assert!(matches!(state, AuthState::Continue(_)));
3256
3257        let pw_step = AuthEvent::cred_step_password(sessionid, pw);
3258
3259        // Expect success
3260        let r3 = idms_auth.auth(&pw_step, ct, Source::Internal.into()).await;
3261        debug!("r3 ==> {:?}", r3);
3262        idms_auth.commit().expect("Must not fail");
3263
3264        match r3 {
3265            Ok(AuthResult {
3266                sessionid: _,
3267                state: AuthState::Success(token, AuthIssueSession::Token),
3268            }) => {
3269                // Process the auth session
3270                let da = idms_delayed.try_recv().expect("invalid");
3271                assert!(matches!(da, DelayedAction::AuthSessionRecord(_)));
3272                Some(*token)
3273            }
3274            _ => None,
3275        }
3276    }
3277
3278    async fn check_testperson_password_backup_code(
3279        idms: &IdmServer,
3280        idms_delayed: &mut IdmServerDelayed,
3281        pw: &str,
3282        code: &str,
3283        ct: Duration,
3284    ) -> Option<JwsCompact> {
3285        let mut idms_auth = idms.auth().await.unwrap();
3286
3287        let auth_init = AuthEvent::named_init(TESTPERSON_NAME);
3288
3289        let r1 = idms_auth
3290            .auth(&auth_init, ct, Source::Internal.into())
3291            .await;
3292        let ar = r1.unwrap();
3293        let AuthResult { sessionid, state } = ar;
3294
3295        if !matches!(state, AuthState::Choose(_)) {
3296            debug!("Can't proceed - {:?}", state);
3297            return None;
3298        };
3299
3300        let auth_begin = AuthEvent::begin_mech(sessionid, AuthMech::PasswordBackupCode);
3301
3302        let r2 = idms_auth
3303            .auth(&auth_begin, ct, Source::Internal.into())
3304            .await;
3305        let ar = r2.unwrap();
3306        let AuthResult { sessionid, state } = ar;
3307
3308        assert!(matches!(state, AuthState::Continue(_)));
3309
3310        let code_step = AuthEvent::cred_step_backup_code(sessionid, code);
3311        let r2 = idms_auth
3312            .auth(&code_step, ct, Source::Internal.into())
3313            .await;
3314        let ar = r2.unwrap();
3315        let AuthResult { sessionid, state } = ar;
3316
3317        assert!(matches!(state, AuthState::Continue(_)));
3318
3319        let pw_step = AuthEvent::cred_step_password(sessionid, pw);
3320
3321        // Expect success
3322        let r3 = idms_auth.auth(&pw_step, ct, Source::Internal.into()).await;
3323        debug!("r3 ==> {:?}", r3);
3324        idms_auth.commit().expect("Must not fail");
3325
3326        match r3 {
3327            Ok(AuthResult {
3328                sessionid: _,
3329                state: AuthState::Success(token, AuthIssueSession::Token),
3330            }) => {
3331                // There now should be a backup code invalidation present
3332                let da = idms_delayed.try_recv().expect("invalid");
3333                assert!(matches!(da, DelayedAction::BackupCodeRemoval(_)));
3334                let r = idms.delayed_action(ct, da).await;
3335                assert!(r.is_ok());
3336
3337                // Process the auth session
3338                let da = idms_delayed.try_recv().expect("invalid");
3339                assert!(matches!(da, DelayedAction::AuthSessionRecord(_)));
3340                Some(*token)
3341            }
3342            _ => None,
3343        }
3344    }
3345
3346    async fn check_testperson_passkey<T: AuthenticatorBackend>(
3347        idms: &IdmServer,
3348        idms_delayed: &mut IdmServerDelayed,
3349        wa: &mut WebauthnAuthenticator<T>,
3350        origin: Url,
3351        ct: Duration,
3352    ) -> Option<JwsCompact> {
3353        let mut idms_auth = idms.auth().await.unwrap();
3354
3355        let auth_init = AuthEvent::named_init(TESTPERSON_NAME);
3356
3357        let r1 = idms_auth
3358            .auth(&auth_init, ct, Source::Internal.into())
3359            .await;
3360        let ar = r1.unwrap();
3361        let AuthResult { sessionid, state } = ar;
3362
3363        if !matches!(state, AuthState::Choose(_)) {
3364            debug!("Can't proceed - {:?}", state);
3365            return None;
3366        };
3367
3368        let auth_begin = AuthEvent::begin_mech(sessionid, AuthMech::Passkey);
3369
3370        let ar = idms_auth
3371            .auth(&auth_begin, ct, Source::Internal.into())
3372            .await
3373            .inspect_err(|err| error!(?err))
3374            .ok()?;
3375        let AuthResult { sessionid, state } = ar;
3376
3377        trace!(?state);
3378
3379        let rcr = match state {
3380            AuthState::Continue(mut allowed) => match allowed.pop() {
3381                Some(AuthAllowed::Passkey(rcr)) => rcr,
3382                _ => unreachable!(),
3383            },
3384            _ => unreachable!(),
3385        };
3386
3387        trace!(?rcr);
3388
3389        let resp = wa
3390            .do_authentication(origin, rcr)
3391            .inspect_err(|err| error!(?err))
3392            .ok()?;
3393
3394        let passkey_step = AuthEvent::cred_step_passkey(sessionid, resp);
3395
3396        let r3 = idms_auth
3397            .auth(&passkey_step, ct, Source::Internal.into())
3398            .await;
3399        debug!("r3 ==> {:?}", r3);
3400        idms_auth.commit().expect("Must not fail");
3401
3402        match r3 {
3403            Ok(AuthResult {
3404                sessionid: _,
3405                state: AuthState::Success(token, AuthIssueSession::Token),
3406            }) => {
3407                // Process the webauthn update
3408                let da = idms_delayed.try_recv().expect("invalid");
3409                assert!(matches!(da, DelayedAction::WebauthnCounterIncrement(_)));
3410                let r = idms.delayed_action(ct, da).await;
3411                assert!(r.is_ok());
3412
3413                // Process the auth session
3414                let da = idms_delayed.try_recv().expect("invalid");
3415                assert!(matches!(da, DelayedAction::AuthSessionRecord(_)));
3416
3417                Some(*token)
3418            }
3419            _ => None,
3420        }
3421    }
3422
3423    #[idm_test]
3424    async fn credential_update_session_cleanup(
3425        idms: &IdmServer,
3426        _idms_delayed: &mut IdmServerDelayed,
3427    ) {
3428        let ct = Duration::from_secs(TEST_CURRENT_TIME);
3429        let (cust, _) = setup_test_session(idms, ct).await;
3430
3431        let cutxn = idms.cred_update_transaction().await.unwrap();
3432        // The session exists
3433        let c_status = cutxn.credential_update_status(&cust, ct);
3434        assert!(c_status.is_ok());
3435        drop(cutxn);
3436
3437        // Making a new session is what triggers the clean of old sessions.
3438        let (_cust, _) =
3439            renew_test_session(idms, ct + MAXIMUM_CRED_UPDATE_TTL + Duration::from_secs(1)).await;
3440
3441        let cutxn = idms.cred_update_transaction().await.unwrap();
3442
3443        // Now fake going back in time .... allows the token to decrypt, but the session
3444        // is gone anyway!
3445        let c_status = cutxn
3446            .credential_update_status(&cust, ct)
3447            .expect_err("Session is still valid!");
3448        assert!(matches!(c_status, OperationError::InvalidState));
3449    }
3450
3451    #[idm_test]
3452    async fn credential_update_intent_send(idms: &IdmServer, _idms_delayed: &mut IdmServerDelayed) {
3453        let ct = Duration::from_secs(TEST_CURRENT_TIME);
3454
3455        let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();
3456
3457        let email_address = format!("{}@example.com", TESTPERSON_NAME);
3458
3459        let test_entry = EntryInitNew::from_iter([
3460            (
3461                Attribute::Class,
3462                ValueSetIutf8::from_iter([
3463                    EntryClass::Object.into(),
3464                    EntryClass::Account.into(),
3465                    EntryClass::PosixAccount.into(),
3466                    EntryClass::Person.into(),
3467                ])
3468                .unwrap() as ValueSet,
3469            ),
3470            (
3471                Attribute::Name,
3472                ValueSetIname::new(TESTPERSON_NAME) as ValueSet,
3473            ),
3474            (
3475                Attribute::Uuid,
3476                ValueSetUuid::new(TESTPERSON_UUID) as ValueSet,
3477            ),
3478            (
3479                Attribute::Description,
3480                ValueSetUtf8::new(TESTPERSON_NAME.into()) as ValueSet,
3481            ),
3482            (
3483                Attribute::DisplayName,
3484                ValueSetUtf8::new(TESTPERSON_NAME.into()) as ValueSet,
3485            ),
3486        ]);
3487
3488        let ce = CreateEvent::new_internal(vec![test_entry]);
3489        let cr = idms_prox_write.qs_write.create(&ce);
3490        assert!(cr.is_ok());
3491
3492        let idm_admin_identity = idms_prox_write
3493            .qs_write
3494            .impersonate_uuid_as_readwrite_identity(UUID_IDM_ADMIN)
3495            .expect("Failed to retrieve identity");
3496
3497        // Test account with no email.
3498        let event = InitCredentialUpdateIntentSendEvent {
3499            ident: idm_admin_identity.clone(),
3500            target: TESTPERSON_UUID,
3501            max_ttl: None,
3502            email: None,
3503        };
3504
3505        let err = idms_prox_write
3506            .init_credential_update_intent_send(event, ct)
3507            .expect_err("Should not succeed!");
3508        assert_eq!(err, OperationError::CU0008AccountMissingEmail);
3509
3510        // Set the account up with an email.
3511        idms_prox_write
3512            .qs_write
3513            .internal_modify_uuid(
3514                TESTPERSON_UUID,
3515                &ModifyList::new_set(
3516                    Attribute::Mail,
3517                    ValueSetEmailAddress::new(email_address.clone()) as ValueSet,
3518                ),
3519            )
3520            .expect("Failed to update test person account");
3521
3522        // Request a change, but the email is not present on the account.
3523
3524        let event = InitCredentialUpdateIntentSendEvent {
3525            ident: idm_admin_identity.clone(),
3526            target: TESTPERSON_UUID,
3527            max_ttl: None,
3528            email: Some("email-that-is-not-present@example.com".into()),
3529        };
3530
3531        let err = idms_prox_write
3532            .init_credential_update_intent_send(event, ct)
3533            .expect_err("Should not succeed!");
3534        assert_eq!(err, OperationError::CU0007AccountEmailNotFound);
3535
3536        // Create a credential update
3537        // Ensure a message was made.
3538
3539        let event = InitCredentialUpdateIntentSendEvent {
3540            ident: idm_admin_identity.clone(),
3541            target: TESTPERSON_UUID,
3542            max_ttl: None,
3543            email: Some(email_address.clone()),
3544        };
3545
3546        idms_prox_write
3547            .init_credential_update_intent_send(event, ct)
3548            .expect("Should succeed!");
3549
3550        // Find the message in the queue.
3551        let filter = filter!(f_and(vec![
3552            f_eq(Attribute::Class, EntryClass::OutboundMessage.into()),
3553            f_eq(
3554                Attribute::MailDestination,
3555                PartialValue::EmailAddress(email_address)
3556            )
3557        ]));
3558
3559        let mut entries = idms_prox_write
3560            .qs_write
3561            .impersonate_search(filter.clone(), filter, &idm_admin_identity)
3562            .expect("Unable to search message queue");
3563
3564        assert_eq!(entries.len(), 1);
3565        let message_entry = entries.pop().unwrap();
3566
3567        let message = message_entry
3568            .get_ava_set(Attribute::MessageTemplate)
3569            .and_then(|vs| vs.as_message())
3570            .unwrap();
3571
3572        match message {
3573            OutboundMessage::CredentialResetV1 { display_name, .. } => {
3574                assert_eq!(display_name, TESTPERSON_NAME);
3575            }
3576            _ => panic!("Wrong message type!"),
3577        }
3578        // Done!!! We quwued an email to send.
3579    }
3580
3581    #[idm_test]
3582    async fn account_recovery_basic(idms: &IdmServer, _idms_delayed: &mut IdmServerDelayed) {
3583        let ct = Duration::from_secs(TEST_CURRENT_TIME);
3584
3585        let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();
3586
3587        let idm_admin_identity = idms_prox_write
3588            .qs_write
3589            .impersonate_uuid_as_readwrite_identity(UUID_IDM_ADMIN)
3590            .expect("Failed to retrieve identity");
3591
3592        let email_address = format!("{}@example.com", TESTPERSON_NAME);
3593
3594        let test_entry = EntryInitNew::from_iter([
3595            (
3596                Attribute::Class,
3597                ValueSetIutf8::from_iter([
3598                    EntryClass::Object.into(),
3599                    EntryClass::Account.into(),
3600                    EntryClass::Person.into(),
3601                ])
3602                .unwrap() as ValueSet,
3603            ),
3604            (
3605                Attribute::Name,
3606                ValueSetIname::new(TESTPERSON_NAME) as ValueSet,
3607            ),
3608            (
3609                Attribute::Uuid,
3610                ValueSetUuid::new(TESTPERSON_UUID) as ValueSet,
3611            ),
3612            (
3613                Attribute::DisplayName,
3614                ValueSetUtf8::new(TESTPERSON_NAME.into()) as ValueSet,
3615            ),
3616            (
3617                Attribute::Mail,
3618                ValueSetEmailAddress::new(email_address.clone()) as ValueSet,
3619            ),
3620        ]);
3621
3622        let ce =
3623            CreateEvent::new_impersonate_identity(idm_admin_identity.clone(), vec![test_entry]);
3624        let cr = idms_prox_write.qs_write.create(&ce);
3625        assert!(cr.is_ok());
3626
3627        // Test with the feature DISABLED. Must be rejected!
3628        let event = CredentialUpdateAccountRecovery {
3629            email: "invalid@example.com".into(),
3630            max_ttl: None,
3631        };
3632
3633        let result = idms_prox_write
3634            .credential_update_account_recovery(event, ct)
3635            .expect_err("Must not succeed!");
3636
3637        assert_eq!(result, OperationError::CU0010AccountRecoveryDisabled);
3638
3639        // Enable the feature
3640        idms_prox_write
3641            .qs_write
3642            .internal_modify_uuid(
3643                UUID_DOMAIN_INFO,
3644                &ModifyList::new_set(
3645                    Attribute::DomainAllowAccountRecovery,
3646                    ValueSetBool::new(true),
3647                ),
3648            )
3649            .expect("Unable to activate credential reset feature.");
3650
3651        idms_prox_write
3652            .qs_write
3653            .reload()
3654            .expect("Unable to reload domain info.");
3655
3656        // Use a non-existant email.
3657        let event = CredentialUpdateAccountRecovery {
3658            email: "invalid@example.com".into(),
3659            max_ttl: None,
3660        };
3661
3662        let result = idms_prox_write
3663            .credential_update_account_recovery(event, ct)
3664            .expect_err("Must not succeed!");
3665
3666        assert_eq!(result, OperationError::CU0009AccountEmailNotFound);
3667
3668        // Use a real email.
3669        let event = CredentialUpdateAccountRecovery {
3670            email: email_address.clone(),
3671            max_ttl: None,
3672        };
3673
3674        idms_prox_write
3675            .credential_update_account_recovery(event, ct)
3676            .expect("Must succeed!");
3677
3678        // Find the message in the queue.
3679        let filter = filter!(f_and(vec![
3680            f_eq(Attribute::Class, EntryClass::OutboundMessage.into()),
3681            f_eq(
3682                Attribute::MailDestination,
3683                PartialValue::EmailAddress(email_address)
3684            )
3685        ]));
3686
3687        let mut entries = idms_prox_write
3688            .qs_write
3689            .impersonate_search(filter.clone(), filter, &idm_admin_identity)
3690            .expect("Unable to search message queue");
3691
3692        assert_eq!(entries.len(), 1);
3693        let message_entry = entries.pop().unwrap();
3694
3695        let message = message_entry
3696            .get_ava_set(Attribute::MessageTemplate)
3697            .and_then(|vs| vs.as_message())
3698            .unwrap();
3699
3700        match message {
3701            OutboundMessage::CredentialResetV1 { display_name, .. } => {
3702                assert_eq!(display_name, TESTPERSON_NAME);
3703            }
3704            _ => panic!("Wrong message type!"),
3705        }
3706        // Done!!! We quwued an email to send.
3707    }
3708
3709    #[idm_test]
3710    async fn credential_update_onboarding_create_new_pw(
3711        idms: &IdmServer,
3712        idms_delayed: &mut IdmServerDelayed,
3713    ) {
3714        let test_pw = "fo3EitierohF9AelaNgiem0Ei6vup4equo1Oogeevaetehah8Tobeengae3Ci0ooh0uki";
3715        let ct = Duration::from_secs(TEST_CURRENT_TIME);
3716
3717        let (cust, _) = setup_test_session(idms, ct).await;
3718
3719        let cutxn = idms.cred_update_transaction().await.unwrap();
3720
3721        // Get the credential status - this should tell
3722        // us the details of the credentials, as well as
3723        // if they are ready and valid to commit?
3724        let c_status = cutxn
3725            .credential_update_status(&cust, ct)
3726            .expect("Failed to get the current session status.");
3727
3728        trace!(?c_status);
3729        assert!(c_status.primary.is_none());
3730
3731        // Test initially creating a credential.
3732        //   - pw first
3733        let c_status = cutxn
3734            .credential_primary_set_password(&cust, ct, test_pw)
3735            .expect("Failed to update the primary cred password");
3736
3737        assert!(c_status.can_commit);
3738
3739        drop(cutxn);
3740        commit_session(idms, ct, cust).await;
3741
3742        // Check it works!
3743        assert!(check_testperson_password(idms, idms_delayed, test_pw, ct)
3744            .await
3745            .is_some());
3746
3747        // Test deleting the pw
3748        let (cust, _) = renew_test_session(idms, ct).await;
3749        let cutxn = idms.cred_update_transaction().await.unwrap();
3750
3751        let c_status = cutxn
3752            .credential_update_status(&cust, ct)
3753            .expect("Failed to get the current session status.");
3754        trace!(?c_status);
3755        assert!(c_status.primary.is_some());
3756
3757        let c_status = cutxn
3758            .credential_primary_delete(&cust, ct)
3759            .expect("Failed to delete the primary cred");
3760        trace!(?c_status);
3761        assert!(c_status.primary.is_none());
3762        assert!(c_status
3763            .warnings
3764            .contains(&CredentialUpdateSessionStatusWarnings::NoValidCredentials));
3765        // Can't delete, would be the last credential!
3766        assert!(!c_status.can_commit);
3767
3768        drop(cutxn);
3769    }
3770
3771    #[idm_test]
3772    async fn credential_update_password_quality_checks(
3773        idms: &IdmServer,
3774        _idms_delayed: &mut IdmServerDelayed,
3775    ) {
3776        let ct = Duration::from_secs(TEST_CURRENT_TIME);
3777        let (cust, _) = setup_test_session(idms, ct).await;
3778
3779        // Get the radius pw
3780
3781        let mut r_txn = idms.proxy_read().await.unwrap();
3782
3783        let radius_secret = r_txn
3784            .qs_read
3785            .internal_search_uuid(TESTPERSON_UUID)
3786            .expect("No such entry")
3787            .get_ava_single_secret(Attribute::RadiusSecret)
3788            .expect("No radius secret found")
3789            .to_string();
3790
3791        drop(r_txn);
3792
3793        let cutxn = idms.cred_update_transaction().await.unwrap();
3794
3795        // Get the credential status - this should tell
3796        // us the details of the credentials, as well as
3797        // if they are ready and valid to commit?
3798        let c_status = cutxn
3799            .credential_update_status(&cust, ct)
3800            .expect("Failed to get the current session status.");
3801
3802        trace!(?c_status);
3803
3804        assert!(c_status.primary.is_none());
3805
3806        // Test initially creating a credential.
3807        //   - pw first
3808
3809        let err = cutxn
3810            .credential_primary_set_password(&cust, ct, "password")
3811            .unwrap_err();
3812        trace!(?err);
3813        assert!(
3814            matches!(err, OperationError::PasswordQuality(details) if details == vec!(PasswordFeedback::TooShort(PW_MIN_LENGTH),))
3815        );
3816
3817        let err = cutxn
3818            .credential_primary_set_password(&cust, ct, "password1234")
3819            .unwrap_err();
3820        trace!(?err);
3821        assert!(
3822            matches!(err, OperationError::PasswordQuality(details) if details
3823            == vec!(
3824                PasswordFeedback::AddAnotherWordOrTwo,
3825                PasswordFeedback::ThisIsACommonPassword,
3826            ))
3827        );
3828
3829        let err = cutxn
3830            .credential_primary_set_password(&cust, ct, &radius_secret)
3831            .unwrap_err();
3832        trace!(?err);
3833        assert!(
3834            matches!(err, OperationError::PasswordQuality(details) if details == vec!(PasswordFeedback::DontReusePasswords,))
3835        );
3836
3837        let err = cutxn
3838            .credential_primary_set_password(&cust, ct, "testperson2023")
3839            .unwrap_err();
3840        trace!(?err);
3841        assert!(
3842            matches!(err, OperationError::PasswordQuality(details) if details == vec!(
3843            PasswordFeedback::NamesAndSurnamesByThemselvesAreEasyToGuess,
3844            PasswordFeedback::AvoidDatesAndYearsThatAreAssociatedWithYou,
3845                   ))
3846        );
3847
3848        let err = cutxn
3849            .credential_primary_set_password(
3850                &cust,
3851                ct,
3852                "demo_badlist_shohfie3aeci2oobur0aru9uushah6EiPi2woh4hohngoighaiRuepieN3ongoo1",
3853            )
3854            .unwrap_err();
3855        trace!(?err);
3856        assert!(
3857            matches!(err, OperationError::PasswordQuality(details) if details == vec!(PasswordFeedback::BadListed))
3858        );
3859
3860        // There are no credentials so we can't proceed.
3861        assert!(c_status
3862            .warnings
3863            .contains(&CredentialUpdateSessionStatusWarnings::NoValidCredentials));
3864        assert!(!c_status.can_commit);
3865
3866        drop(cutxn);
3867    }
3868
3869    #[idm_test]
3870    async fn credential_update_password_min_length_account_policy(
3871        idms: &IdmServer,
3872        _idms_delayed: &mut IdmServerDelayed,
3873    ) {
3874        let ct = Duration::from_secs(TEST_CURRENT_TIME);
3875
3876        // Set the account policy min pw length
3877        let test_pw_min_length = PW_MIN_LENGTH * 2;
3878
3879        let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();
3880
3881        let modlist = ModifyList::new_purge_and_set(
3882            Attribute::AuthPasswordMinimumLength,
3883            Value::Uint32(test_pw_min_length),
3884        );
3885        idms_prox_write
3886            .qs_write
3887            .internal_modify_uuid(UUID_IDM_ALL_ACCOUNTS, &modlist)
3888            .expect("Unable to change default session exp");
3889
3890        assert!(idms_prox_write.commit().is_ok());
3891        // This now will affect all accounts for the next cred update.
3892
3893        let (cust, _) = setup_test_session(idms, ct).await;
3894
3895        let cutxn = idms.cred_update_transaction().await.unwrap();
3896
3897        // Get the credential status - this should tell
3898        // us the details of the credentials, as well as
3899        // if they are ready and valid to commit?
3900        let c_status = cutxn
3901            .credential_update_status(&cust, ct)
3902            .expect("Failed to get the current session status.");
3903
3904        trace!(?c_status);
3905
3906        assert!(c_status.primary.is_none());
3907
3908        // Test initially creating a credential.
3909        //   - pw first
3910        let pw = password_from_random_len(8);
3911        let err = cutxn
3912            .credential_primary_set_password(&cust, ct, &pw)
3913            .unwrap_err();
3914        trace!(?err);
3915        assert!(
3916            matches!(err, OperationError::PasswordQuality(details) if details == vec!(PasswordFeedback::TooShort(test_pw_min_length),))
3917        );
3918
3919        // Test pw len of len minus 1
3920        let pw = password_from_random_len(test_pw_min_length - 1);
3921        let err = cutxn
3922            .credential_primary_set_password(&cust, ct, &pw)
3923            .unwrap_err();
3924        trace!(?err);
3925        assert!(matches!(err,OperationError::PasswordQuality(details)
3926                if details == vec!(PasswordFeedback::TooShort(test_pw_min_length),)));
3927
3928        // Test pw len of exact len
3929        let pw = password_from_random_len(test_pw_min_length);
3930        let c_status = cutxn
3931            .credential_primary_set_password(&cust, ct, &pw)
3932            .expect("Failed to update the primary cred password");
3933
3934        assert!(c_status.can_commit);
3935
3936        drop(cutxn);
3937        commit_session(idms, ct, cust).await;
3938    }
3939
3940    // Test set of primary account password
3941    //    - fail pw quality checks etc
3942    //    - set correctly.
3943
3944    // - setup TOTP
3945    #[idm_test]
3946    async fn credential_update_onboarding_create_new_mfa_totp_basic(
3947        idms: &IdmServer,
3948        idms_delayed: &mut IdmServerDelayed,
3949    ) {
3950        let test_pw = "fo3EitierohF9AelaNgiem0Ei6vup4equo1Oogeevaetehah8Tobeengae3Ci0ooh0uki";
3951        let ct = Duration::from_secs(TEST_CURRENT_TIME);
3952
3953        let (cust, _) = setup_test_session(idms, ct).await;
3954        let cutxn = idms.cred_update_transaction().await.unwrap();
3955
3956        // Setup the PW
3957        let c_status = cutxn
3958            .credential_primary_set_password(&cust, ct, test_pw)
3959            .expect("Failed to update the primary cred password");
3960
3961        // Since it's pw only.
3962        assert!(c_status.can_commit);
3963
3964        //
3965        let c_status = cutxn
3966            .credential_primary_init_totp(&cust, ct)
3967            .expect("Failed to update the primary cred password");
3968
3969        // Check the status has the token.
3970        let totp_token: Totp = match c_status.mfaregstate {
3971            MfaRegStateStatus::TotpCheck(secret) => Some(secret.try_into().unwrap()),
3972
3973            _ => None,
3974        }
3975        .expect("Unable to retrieve totp token, invalid state.");
3976
3977        trace!(?totp_token);
3978        let chal = totp_token
3979            .do_totp_duration_from_epoch(&ct)
3980            .expect("Failed to perform totp step");
3981
3982        // Intentionally get it wrong.
3983        let c_status = cutxn
3984            .credential_primary_check_totp(&cust, ct, chal + 1, "totp")
3985            .expect("Failed to update the primary cred totp");
3986
3987        assert!(
3988            matches!(c_status.mfaregstate, MfaRegStateStatus::TotpTryAgain),
3989            "{:?}",
3990            c_status.mfaregstate
3991        );
3992
3993        // Check that the user actually put something into the label
3994        let c_status = cutxn
3995            .credential_primary_check_totp(&cust, ct, chal, "")
3996            .expect("Failed to update the primary cred totp");
3997
3998        assert!(
3999            matches!(
4000                c_status.mfaregstate,
4001                MfaRegStateStatus::TotpNameTryAgain(ref val) if val.is_empty()
4002            ),
4003            "{:?}",
4004            c_status.mfaregstate
4005        );
4006
4007        // Okay, Now they are trying to be smart...
4008        let c_status = cutxn
4009            .credential_primary_check_totp(&cust, ct, chal, "           ")
4010            .expect("Failed to update the primary cred totp");
4011
4012        assert!(
4013            matches!(
4014                c_status.mfaregstate,
4015                MfaRegStateStatus::TotpNameTryAgain(ref val) if val == "           "
4016            ),
4017            "{:?}",
4018            c_status.mfaregstate
4019        );
4020
4021        let c_status = cutxn
4022            .credential_primary_check_totp(&cust, ct, chal, "totp")
4023            .expect("Failed to update the primary cred totp");
4024
4025        assert!(matches!(c_status.mfaregstate, MfaRegStateStatus::None));
4026        assert!(match c_status.primary.as_ref().map(|c| &c.type_) {
4027            Some(CredentialDetailType::PasswordMfa(totp, _, 0)) => !totp.is_empty(),
4028            _ => false,
4029        });
4030
4031        {
4032            let c_status = cutxn
4033                .credential_primary_init_totp(&cust, ct)
4034                .expect("Failed to update the primary cred password");
4035
4036            // Check the status has the token.
4037            let totp_token: Totp = match c_status.mfaregstate {
4038                MfaRegStateStatus::TotpCheck(secret) => Some(secret.try_into().unwrap()),
4039                _ => None,
4040            }
4041            .expect("Unable to retrieve totp token, invalid state.");
4042
4043            trace!(?totp_token);
4044            let chal = totp_token
4045                .do_totp_duration_from_epoch(&ct)
4046                .expect("Failed to perform totp step");
4047
4048            // They tried to add a second totp under the same name
4049            let c_status = cutxn
4050                .credential_primary_check_totp(&cust, ct, chal, "totp")
4051                .expect("Failed to update the primary cred totp");
4052
4053            assert!(
4054                matches!(
4055                    c_status.mfaregstate,
4056                    MfaRegStateStatus::TotpNameTryAgain(ref val) if val == "totp"
4057                ),
4058                "{:?}",
4059                c_status.mfaregstate
4060            );
4061
4062            assert!(cutxn.credential_update_cancel_mfareg(&cust, ct).is_ok())
4063        }
4064
4065        // Should be okay now!
4066
4067        drop(cutxn);
4068        commit_session(idms, ct, cust).await;
4069
4070        // Check it works!
4071        assert!(
4072            check_testperson_password_totp(idms, idms_delayed, test_pw, &totp_token, ct)
4073                .await
4074                .is_some()
4075        );
4076        // No need to test delete of the whole cred, we already did with pw above.
4077
4078        // If we remove TOTP, show it reverts back.
4079        let (cust, _) = renew_test_session(idms, ct).await;
4080        let cutxn = idms.cred_update_transaction().await.unwrap();
4081
4082        let c_status = cutxn
4083            .credential_primary_remove_totp(&cust, ct, "totp")
4084            .expect("Failed to update the primary cred password");
4085
4086        assert!(matches!(c_status.mfaregstate, MfaRegStateStatus::None));
4087        assert!(matches!(
4088            c_status.primary.as_ref().map(|c| &c.type_),
4089            Some(CredentialDetailType::Password)
4090        ));
4091
4092        drop(cutxn);
4093        commit_session(idms, ct, cust).await;
4094
4095        // Check it works with totp removed.
4096        assert!(check_testperson_password(idms, idms_delayed, test_pw, ct)
4097            .await
4098            .is_some());
4099    }
4100
4101    // Check sha1 totp.
4102    #[idm_test]
4103    async fn credential_update_onboarding_create_new_mfa_totp_sha1(
4104        idms: &IdmServer,
4105        idms_delayed: &mut IdmServerDelayed,
4106    ) {
4107        let test_pw = "fo3EitierohF9AelaNgiem0Ei6vup4equo1Oogeevaetehah8Tobeengae3Ci0ooh0uki";
4108        let ct = Duration::from_secs(TEST_CURRENT_TIME);
4109
4110        let (cust, _) = setup_test_session(idms, ct).await;
4111        let cutxn = idms.cred_update_transaction().await.unwrap();
4112
4113        // Setup the PW
4114        let c_status = cutxn
4115            .credential_primary_set_password(&cust, ct, test_pw)
4116            .expect("Failed to update the primary cred password");
4117
4118        // Since it's pw only.
4119        assert!(c_status.can_commit);
4120
4121        //
4122        let c_status = cutxn
4123            .credential_primary_init_totp(&cust, ct)
4124            .expect("Failed to update the primary cred password");
4125
4126        // Check the status has the token.
4127        let totp_token: Totp = match c_status.mfaregstate {
4128            MfaRegStateStatus::TotpCheck(secret) => Some(secret.try_into().unwrap()),
4129
4130            _ => None,
4131        }
4132        .expect("Unable to retrieve totp token, invalid state.");
4133
4134        let totp_token = totp_token.downgrade_to_legacy();
4135
4136        trace!(?totp_token);
4137        let chal = totp_token
4138            .do_totp_duration_from_epoch(&ct)
4139            .expect("Failed to perform totp step");
4140
4141        // Should getn the warn that it's sha1
4142        let c_status = cutxn
4143            .credential_primary_check_totp(&cust, ct, chal, "totp")
4144            .expect("Failed to update the primary cred password");
4145
4146        assert!(matches!(
4147            c_status.mfaregstate,
4148            MfaRegStateStatus::TotpInvalidSha1
4149        ));
4150
4151        // Accept it
4152        let c_status = cutxn
4153            .credential_primary_accept_sha1_totp(&cust, ct)
4154            .expect("Failed to update the primary cred password");
4155
4156        assert!(matches!(c_status.mfaregstate, MfaRegStateStatus::None));
4157        assert!(match c_status.primary.as_ref().map(|c| &c.type_) {
4158            Some(CredentialDetailType::PasswordMfa(totp, _, 0)) => !totp.is_empty(),
4159            _ => false,
4160        });
4161
4162        // Should be okay now!
4163
4164        drop(cutxn);
4165        commit_session(idms, ct, cust).await;
4166
4167        // Check it works!
4168        assert!(
4169            check_testperson_password_totp(idms, idms_delayed, test_pw, &totp_token, ct)
4170                .await
4171                .is_some()
4172        );
4173        // No need to test delete, we already did with pw above.
4174    }
4175
4176    #[idm_test]
4177    async fn credential_update_onboarding_create_new_mfa_totp_backup_codes(
4178        idms: &IdmServer,
4179        idms_delayed: &mut IdmServerDelayed,
4180    ) {
4181        let test_pw = "fo3EitierohF9AelaNgiem0Ei6vup4equo1Oogeevaetehah8Tobeengae3Ci0ooh0uki";
4182        let ct = Duration::from_secs(TEST_CURRENT_TIME);
4183
4184        let (cust, _) = setup_test_session(idms, ct).await;
4185        let cutxn = idms.cred_update_transaction().await.unwrap();
4186
4187        // Setup the PW
4188        let _c_status = cutxn
4189            .credential_primary_set_password(&cust, ct, test_pw)
4190            .expect("Failed to update the primary cred password");
4191
4192        // Backup codes are refused to be added because we don't have mfa yet.
4193        assert!(matches!(
4194            cutxn.credential_primary_init_backup_codes(&cust, ct),
4195            Err(OperationError::InvalidState)
4196        ));
4197
4198        let c_status = cutxn
4199            .credential_primary_init_totp(&cust, ct)
4200            .expect("Failed to update the primary cred password");
4201
4202        let totp_token: Totp = match c_status.mfaregstate {
4203            MfaRegStateStatus::TotpCheck(secret) => Some(secret.try_into().unwrap()),
4204            _ => None,
4205        }
4206        .expect("Unable to retrieve totp token, invalid state.");
4207
4208        trace!(?totp_token);
4209        let chal = totp_token
4210            .do_totp_duration_from_epoch(&ct)
4211            .expect("Failed to perform totp step");
4212
4213        let c_status = cutxn
4214            .credential_primary_check_totp(&cust, ct, chal, "totp")
4215            .expect("Failed to update the primary cred totp");
4216
4217        assert!(matches!(c_status.mfaregstate, MfaRegStateStatus::None));
4218        assert!(match c_status.primary.as_ref().map(|c| &c.type_) {
4219            Some(CredentialDetailType::PasswordMfa(totp, _, 0)) => !totp.is_empty(),
4220            _ => false,
4221        });
4222
4223        // Now good to go, we need to now add our backup codes.
4224        // What's the right way to get these back?
4225        let c_status = cutxn
4226            .credential_primary_init_backup_codes(&cust, ct)
4227            .expect("Failed to update the primary cred password");
4228
4229        let codes = match c_status.mfaregstate {
4230            MfaRegStateStatus::BackupCodes(codes) => Some(codes),
4231            _ => None,
4232        }
4233        .expect("Unable to retrieve backupcodes, invalid state.");
4234
4235        // Should error because the number is not 0
4236        debug!("{:?}", c_status.primary.as_ref().map(|c| &c.type_));
4237        assert!(match c_status.primary.as_ref().map(|c| &c.type_) {
4238            Some(CredentialDetailType::PasswordMfa(totp, _, 8)) => !totp.is_empty(),
4239            _ => false,
4240        });
4241
4242        // Should be okay now!
4243        drop(cutxn);
4244        commit_session(idms, ct, cust).await;
4245
4246        let backup_code = codes.iter().next().expect("No codes available");
4247
4248        // Check it works!
4249        assert!(check_testperson_password_backup_code(
4250            idms,
4251            idms_delayed,
4252            test_pw,
4253            backup_code,
4254            ct
4255        )
4256        .await
4257        .is_some());
4258
4259        // Renew to start the next steps
4260        let (cust, _) = renew_test_session(idms, ct).await;
4261        let cutxn = idms.cred_update_transaction().await.unwrap();
4262
4263        // Only 7 codes left.
4264        let c_status = cutxn
4265            .credential_update_status(&cust, ct)
4266            .expect("Failed to get the current session status.");
4267
4268        assert!(match c_status.primary.as_ref().map(|c| &c.type_) {
4269            Some(CredentialDetailType::PasswordMfa(totp, _, 7)) => !totp.is_empty(),
4270            _ => false,
4271        });
4272
4273        // If we remove codes, it leaves totp.
4274        let c_status = cutxn
4275            .credential_primary_remove_backup_codes(&cust, ct)
4276            .expect("Failed to update the primary cred password");
4277
4278        assert!(matches!(c_status.mfaregstate, MfaRegStateStatus::None));
4279        assert!(match c_status.primary.as_ref().map(|c| &c.type_) {
4280            Some(CredentialDetailType::PasswordMfa(totp, _, 0)) => !totp.is_empty(),
4281            _ => false,
4282        });
4283
4284        // Re-add the codes.
4285        let c_status = cutxn
4286            .credential_primary_init_backup_codes(&cust, ct)
4287            .expect("Failed to update the primary cred password");
4288
4289        assert!(matches!(
4290            c_status.mfaregstate,
4291            MfaRegStateStatus::BackupCodes(_)
4292        ));
4293        assert!(match c_status.primary.as_ref().map(|c| &c.type_) {
4294            Some(CredentialDetailType::PasswordMfa(totp, _, 8)) => !totp.is_empty(),
4295            _ => false,
4296        });
4297
4298        // If we remove totp, it removes codes.
4299        let c_status = cutxn
4300            .credential_primary_remove_totp(&cust, ct, "totp")
4301            .expect("Failed to update the primary cred password");
4302
4303        assert!(matches!(c_status.mfaregstate, MfaRegStateStatus::None));
4304        assert!(matches!(
4305            c_status.primary.as_ref().map(|c| &c.type_),
4306            Some(CredentialDetailType::Password)
4307        ));
4308
4309        drop(cutxn);
4310        commit_session(idms, ct, cust).await;
4311    }
4312
4313    #[idm_test]
4314    async fn credential_update_onboarding_cancel_inprogress_totp(
4315        idms: &IdmServer,
4316        idms_delayed: &mut IdmServerDelayed,
4317    ) {
4318        let test_pw = "fo3EitierohF9AelaNgiem0Ei6vup4equo1Oogeevaetehah8Tobeengae3Ci0ooh0uki";
4319        let ct = Duration::from_secs(TEST_CURRENT_TIME);
4320
4321        let (cust, _) = setup_test_session(idms, ct).await;
4322        let cutxn = idms.cred_update_transaction().await.unwrap();
4323
4324        // Setup the PW
4325        let c_status = cutxn
4326            .credential_primary_set_password(&cust, ct, test_pw)
4327            .expect("Failed to update the primary cred password");
4328
4329        // Since it's pw only.
4330        assert!(c_status.can_commit);
4331
4332        //
4333        let c_status = cutxn
4334            .credential_primary_init_totp(&cust, ct)
4335            .expect("Failed to update the primary cred totp");
4336
4337        // Check the status has the token.
4338        assert!(c_status.can_commit);
4339        assert!(matches!(
4340            c_status.mfaregstate,
4341            MfaRegStateStatus::TotpCheck(_)
4342        ));
4343
4344        let c_status = cutxn
4345            .credential_update_cancel_mfareg(&cust, ct)
4346            .expect("Failed to cancel in-flight totp change");
4347
4348        assert!(matches!(c_status.mfaregstate, MfaRegStateStatus::None));
4349        assert!(c_status.can_commit);
4350
4351        drop(cutxn);
4352        commit_session(idms, ct, cust).await;
4353
4354        // It's pw only, since we canceled TOTP
4355        assert!(check_testperson_password(idms, idms_delayed, test_pw, ct)
4356            .await
4357            .is_some());
4358    }
4359
4360    // Primary cred must be pw or pwmfa
4361
4362    // - setup webauthn
4363    // - remove webauthn
4364    // - test multiple webauthn token.
4365
4366    async fn create_new_passkey(
4367        ct: Duration,
4368        origin: &Url,
4369        cutxn: &IdmServerCredUpdateTransaction<'_>,
4370        cust: &CredentialUpdateSessionToken,
4371        wa: &mut WebauthnAuthenticator<SoftPasskey>,
4372    ) -> CredentialUpdateSessionStatus {
4373        // Start the registration
4374        let c_status = cutxn
4375            .credential_passkey_init(cust, ct)
4376            .expect("Failed to initiate passkey registration");
4377
4378        assert!(c_status.passkeys.is_empty());
4379
4380        let passkey_chal = match c_status.mfaregstate {
4381            MfaRegStateStatus::Passkey(c) => Some(c),
4382            _ => None,
4383        }
4384        .expect("Unable to access passkey challenge, invalid state");
4385
4386        let passkey_resp = wa
4387            .do_registration(origin.clone(), passkey_chal)
4388            .expect("Failed to create soft passkey");
4389
4390        // Finish the registration
4391        let label = "softtoken".to_string();
4392        let c_status = cutxn
4393            .credential_passkey_finish(cust, ct, label, &passkey_resp)
4394            .expect("Failed to initiate passkey registration");
4395
4396        assert!(matches!(c_status.mfaregstate, MfaRegStateStatus::None));
4397        assert!(c_status.primary.as_ref().is_none());
4398
4399        // Check we have the passkey
4400        trace!(?c_status);
4401        assert_eq!(c_status.passkeys.len(), 1);
4402
4403        c_status
4404    }
4405
4406    #[idm_test]
4407    async fn credential_update_onboarding_create_new_passkey(
4408        idms: &IdmServer,
4409        idms_delayed: &mut IdmServerDelayed,
4410    ) {
4411        let ct = Duration::from_secs(TEST_CURRENT_TIME);
4412        let test_pw = "fo3EitierohF9AelaNgiem0Ei6vup4equo1Oogeevaetehah8Tobeengae3Ci0ooh0uki";
4413
4414        let (cust, _) = setup_test_session(idms, ct).await;
4415        let cutxn = idms.cred_update_transaction().await.unwrap();
4416        let origin = cutxn.get_origin().clone();
4417
4418        // Create a soft passkey
4419        let mut wa = WebauthnAuthenticator::new(SoftPasskey::new(true));
4420
4421        let c_status = create_new_passkey(ct, &origin, &cutxn, &cust, &mut wa).await;
4422
4423        // Get the UUID of the passkey here.
4424        let pk_uuid = c_status.passkeys.first().map(|pkd| pkd.uuid).unwrap();
4425
4426        // Commit
4427        drop(cutxn);
4428        commit_session(idms, ct, cust).await;
4429
4430        // Do an auth test
4431        assert!(
4432            check_testperson_passkey(idms, idms_delayed, &mut wa, origin.clone(), ct)
4433                .await
4434                .is_some()
4435        );
4436
4437        // Now test removing the token
4438        let (cust, _) = renew_test_session(idms, ct).await;
4439        let cutxn = idms.cred_update_transaction().await.unwrap();
4440
4441        trace!(?c_status);
4442        assert!(c_status.primary.is_none());
4443        assert_eq!(c_status.passkeys.len(), 1);
4444
4445        let c_status = cutxn
4446            .credential_passkey_remove(&cust, ct, pk_uuid)
4447            .expect("Failed to delete the passkey");
4448
4449        trace!(?c_status);
4450        assert!(c_status.primary.is_none());
4451        assert!(c_status.passkeys.is_empty());
4452
4453        assert!(c_status
4454            .warnings
4455            .contains(&CredentialUpdateSessionStatusWarnings::NoValidCredentials));
4456        assert!(!c_status.can_commit);
4457
4458        // For now, set a password to allow saving.
4459        let c_status = cutxn
4460            .credential_primary_set_password(&cust, ct, test_pw)
4461            .expect("Failed to update the primary cred password");
4462
4463        // Could proceed now!
4464        assert!(c_status.can_commit);
4465        assert!(!c_status
4466            .warnings
4467            .contains(&CredentialUpdateSessionStatusWarnings::NoValidCredentials));
4468
4469        drop(cutxn);
4470        commit_session(idms, ct, cust).await;
4471
4472        // Must fail now as the passkeys were removed!!!
4473        assert!(
4474            check_testperson_passkey(idms, idms_delayed, &mut wa, origin, ct)
4475                .await
4476                .is_none()
4477        );
4478    }
4479
4480    #[idm_test]
4481    async fn credential_update_access_denied(
4482        idms: &IdmServer,
4483        _idms_delayed: &mut IdmServerDelayed,
4484    ) {
4485        // Test that if access is denied for a synced account, that the actual action to update
4486        // the credentials is always denied.
4487
4488        let ct = Duration::from_secs(TEST_CURRENT_TIME);
4489
4490        let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();
4491
4492        let sync_uuid = Uuid::new_v4();
4493
4494        let e1 = entry_init!(
4495            (Attribute::Class, EntryClass::Object.to_value()),
4496            (Attribute::Class, EntryClass::SyncAccount.to_value()),
4497            (Attribute::Name, Value::new_iname("test_scim_sync")),
4498            (Attribute::Uuid, Value::Uuid(sync_uuid)),
4499            (
4500                Attribute::Description,
4501                Value::new_utf8s("A test sync agreement")
4502            )
4503        );
4504
4505        let e2 = entry_init!(
4506            (Attribute::Class, EntryClass::Object.to_value()),
4507            (Attribute::Class, EntryClass::SyncObject.to_value()),
4508            (Attribute::Class, EntryClass::Account.to_value()),
4509            (Attribute::Class, EntryClass::PosixAccount.to_value()),
4510            (Attribute::Class, EntryClass::Person.to_value()),
4511            (Attribute::SyncParentUuid, Value::Refer(sync_uuid)),
4512            (Attribute::Name, Value::new_iname(TESTPERSON_NAME)),
4513            (Attribute::Uuid, Value::Uuid(TESTPERSON_UUID)),
4514            (Attribute::Description, Value::new_utf8s(TESTPERSON_NAME)),
4515            (Attribute::DisplayName, Value::new_utf8s(TESTPERSON_NAME))
4516        );
4517
4518        let ce = CreateEvent::new_internal(vec![e1, e2]);
4519        let cr = idms_prox_write.qs_write.create(&ce);
4520        assert!(cr.is_ok());
4521
4522        let testperson = idms_prox_write
4523            .qs_write
4524            .internal_search_uuid(TESTPERSON_UUID)
4525            .expect("failed");
4526
4527        let cur = idms_prox_write.init_credential_update(
4528            &InitCredentialUpdateEvent::new_impersonate_entry(testperson),
4529            ct,
4530        );
4531
4532        idms_prox_write.commit().expect("Failed to commit txn");
4533
4534        let (cust, custatus) = cur.expect("Failed to start update");
4535
4536        trace!(?custatus);
4537
4538        // Destructure to force us to update this test if we change this
4539        // structure at all.
4540        let CredentialUpdateSessionStatus {
4541            spn: _,
4542            displayname: _,
4543            ext_cred_portal,
4544            mfaregstate: _,
4545            can_commit: _,
4546            warnings: _,
4547            primary: _,
4548            primary_state,
4549            passkeys: _,
4550            passkeys_state,
4551            attested_passkeys: _,
4552            attested_passkeys_state,
4553            attested_passkeys_allowed_devices: _,
4554            unixcred_state,
4555            unixcred: _,
4556            sshkeys: _,
4557            sshkeys_state,
4558        } = custatus;
4559
4560        assert!(matches!(ext_cred_portal, CUExtPortal::Hidden));
4561        assert!(matches!(primary_state, CredentialState::AccessDeny));
4562        assert!(matches!(passkeys_state, CredentialState::AccessDeny));
4563        assert!(matches!(
4564            attested_passkeys_state,
4565            CredentialState::AccessDeny
4566        ));
4567        assert!(matches!(unixcred_state, CredentialState::AccessDeny));
4568        assert!(matches!(sshkeys_state, CredentialState::AccessDeny));
4569
4570        let cutxn = idms.cred_update_transaction().await.unwrap();
4571
4572        // let origin = cutxn.get_origin().clone();
4573
4574        // Test that any of the primary or passkey update methods fail with access denied.
4575
4576        // credential_primary_set_password
4577        let err = cutxn
4578            .credential_primary_set_password(&cust, ct, "password")
4579            .unwrap_err();
4580        assert!(matches!(err, OperationError::AccessDenied));
4581
4582        let err = cutxn
4583            .credential_unix_set_password(&cust, ct, "password")
4584            .unwrap_err();
4585        assert!(matches!(err, OperationError::AccessDenied));
4586
4587        let sshkey = SshPublicKey::from_string(SSHKEY_VALID_1).expect("Invalid SSHKEY_VALID_1");
4588
4589        let err = cutxn
4590            .credential_sshkey_add(&cust, ct, "label".to_string(), sshkey)
4591            .unwrap_err();
4592        assert!(matches!(err, OperationError::AccessDenied));
4593
4594        // credential_primary_init_totp
4595        let err = cutxn.credential_primary_init_totp(&cust, ct).unwrap_err();
4596        assert!(matches!(err, OperationError::AccessDenied));
4597
4598        // credential_primary_check_totp
4599        let err = cutxn
4600            .credential_primary_check_totp(&cust, ct, 0, "totp")
4601            .unwrap_err();
4602        assert!(matches!(err, OperationError::AccessDenied));
4603
4604        // credential_primary_accept_sha1_totp
4605        let err = cutxn
4606            .credential_primary_accept_sha1_totp(&cust, ct)
4607            .unwrap_err();
4608        assert!(matches!(err, OperationError::AccessDenied));
4609
4610        // credential_primary_remove_totp
4611        let err = cutxn
4612            .credential_primary_remove_totp(&cust, ct, "totp")
4613            .unwrap_err();
4614        assert!(matches!(err, OperationError::AccessDenied));
4615
4616        // credential_primary_init_backup_codes
4617        let err = cutxn
4618            .credential_primary_init_backup_codes(&cust, ct)
4619            .unwrap_err();
4620        assert!(matches!(err, OperationError::AccessDenied));
4621
4622        // credential_primary_remove_backup_codes
4623        let err = cutxn
4624            .credential_primary_remove_backup_codes(&cust, ct)
4625            .unwrap_err();
4626        assert!(matches!(err, OperationError::AccessDenied));
4627
4628        // credential_primary_delete
4629        let err = cutxn.credential_primary_delete(&cust, ct).unwrap_err();
4630        assert!(matches!(err, OperationError::AccessDenied));
4631
4632        // credential_passkey_init
4633        let err = cutxn.credential_passkey_init(&cust, ct).unwrap_err();
4634        assert!(matches!(err, OperationError::AccessDenied));
4635
4636        // credential_passkey_finish
4637        //   Can't test because we need a public key response.
4638
4639        // credential_passkey_remove
4640        let err = cutxn
4641            .credential_passkey_remove(&cust, ct, Uuid::new_v4())
4642            .unwrap_err();
4643        assert!(matches!(err, OperationError::AccessDenied));
4644
4645        let c_status = cutxn
4646            .credential_update_status(&cust, ct)
4647            .expect("Failed to get the current session status.");
4648        trace!(?c_status);
4649        assert!(c_status.primary.is_none());
4650        assert!(c_status.passkeys.is_empty());
4651
4652        // Since there are no credentials we can't proceed anyway.
4653        assert!(!c_status.can_commit);
4654        assert!(c_status
4655            .warnings
4656            .contains(&CredentialUpdateSessionStatusWarnings::NoValidCredentials));
4657    }
4658
4659    // Assert we can't create "just" a password when mfa is required.
4660    #[idm_test]
4661    async fn credential_update_account_policy_mfa_required(
4662        idms: &IdmServer,
4663        _idms_delayed: &mut IdmServerDelayed,
4664    ) {
4665        let test_pw = "fo3EitierohF9AelaNgiem0Ei6vup4equo1Oogeevaetehah8Tobeengae3Ci0ooh0uki";
4666        let ct = Duration::from_secs(TEST_CURRENT_TIME);
4667
4668        let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();
4669
4670        let modlist = ModifyList::new_purge_and_set(
4671            Attribute::CredentialTypeMinimum,
4672            CredentialType::Mfa.into(),
4673        );
4674        idms_prox_write
4675            .qs_write
4676            .internal_modify_uuid(UUID_IDM_ALL_ACCOUNTS, &modlist)
4677            .expect("Unable to change default session exp");
4678
4679        assert!(idms_prox_write.commit().is_ok());
4680        // This now will affect all accounts for the next cred update.
4681
4682        let (cust, _) = setup_test_session(idms, ct).await;
4683
4684        let cutxn = idms.cred_update_transaction().await.unwrap();
4685
4686        // Get the credential status - this should tell
4687        // us the details of the credentials, as well as
4688        // if they are ready and valid to commit?
4689        let c_status = cutxn
4690            .credential_update_status(&cust, ct)
4691            .expect("Failed to get the current session status.");
4692
4693        trace!(?c_status);
4694
4695        assert!(c_status.primary.is_none());
4696
4697        // Test initially creating a credential.
4698        //   - pw first
4699        let c_status = cutxn
4700            .credential_primary_set_password(&cust, ct, test_pw)
4701            .expect("Failed to update the primary cred password");
4702
4703        assert!(!c_status.can_commit);
4704        assert!(c_status
4705            .warnings
4706            .contains(&CredentialUpdateSessionStatusWarnings::MfaRequired));
4707        // Check reason! Must show "no mfa". We need totp to be added now.
4708
4709        let c_status = cutxn
4710            .credential_primary_init_totp(&cust, ct)
4711            .expect("Failed to update the primary cred password");
4712
4713        // Check the status has the token.
4714        let totp_token: Totp = match c_status.mfaregstate {
4715            MfaRegStateStatus::TotpCheck(secret) => Some(secret.try_into().unwrap()),
4716
4717            _ => None,
4718        }
4719        .expect("Unable to retrieve totp token, invalid state.");
4720
4721        trace!(?totp_token);
4722        let chal = totp_token
4723            .do_totp_duration_from_epoch(&ct)
4724            .expect("Failed to perform totp step");
4725
4726        let c_status = cutxn
4727            .credential_primary_check_totp(&cust, ct, chal, "totp")
4728            .expect("Failed to update the primary cred totp");
4729
4730        assert!(matches!(c_status.mfaregstate, MfaRegStateStatus::None));
4731        assert!(match c_status.primary.as_ref().map(|c| &c.type_) {
4732            Some(CredentialDetailType::PasswordMfa(totp, _, 0)) => !totp.is_empty(),
4733            _ => false,
4734        });
4735
4736        // Done, can now commit.
4737        assert!(c_status.can_commit);
4738        assert!(c_status.warnings.is_empty());
4739
4740        drop(cutxn);
4741        commit_session(idms, ct, cust).await;
4742
4743        // If we remove TOTP, it blocks commit.
4744        let (cust, _) = renew_test_session(idms, ct).await;
4745        let cutxn = idms.cred_update_transaction().await.unwrap();
4746
4747        let c_status = cutxn
4748            .credential_primary_remove_totp(&cust, ct, "totp")
4749            .expect("Failed to update the primary cred totp");
4750
4751        assert!(matches!(c_status.mfaregstate, MfaRegStateStatus::None));
4752        assert!(matches!(
4753            c_status.primary.as_ref().map(|c| &c.type_),
4754            Some(CredentialDetailType::Password)
4755        ));
4756
4757        // Delete of the totp forces us back here.
4758        assert!(!c_status.can_commit);
4759        assert!(c_status
4760            .warnings
4761            .contains(&CredentialUpdateSessionStatusWarnings::MfaRequired));
4762
4763        // Passkeys satisfy the policy though
4764        let c_status = cutxn
4765            .credential_primary_delete(&cust, ct)
4766            .expect("Failed to delete the primary credential");
4767        assert!(c_status.primary.is_none());
4768
4769        let origin = cutxn.get_origin().clone();
4770        let mut wa = WebauthnAuthenticator::new(SoftPasskey::new(true));
4771
4772        let c_status = create_new_passkey(ct, &origin, &cutxn, &cust, &mut wa).await;
4773
4774        assert!(c_status.can_commit);
4775        assert!(c_status.warnings.is_empty());
4776        assert_eq!(c_status.passkeys.len(), 1);
4777
4778        drop(cutxn);
4779        commit_session(idms, ct, cust).await;
4780    }
4781
4782    #[idm_test]
4783    async fn credential_update_account_policy_passkey_required(
4784        idms: &IdmServer,
4785        _idms_delayed: &mut IdmServerDelayed,
4786    ) {
4787        let test_pw = "fo3EitierohF9AelaNgiem0Ei6vup4equo1Oogeevaetehah8Tobeengae3Ci0ooh0uki";
4788        let ct = Duration::from_secs(TEST_CURRENT_TIME);
4789
4790        let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();
4791
4792        let modlist = ModifyList::new_purge_and_set(
4793            Attribute::CredentialTypeMinimum,
4794            CredentialType::Passkey.into(),
4795        );
4796        idms_prox_write
4797            .qs_write
4798            .internal_modify_uuid(UUID_IDM_ALL_ACCOUNTS, &modlist)
4799            .expect("Unable to change default session exp");
4800
4801        assert!(idms_prox_write.commit().is_ok());
4802        // This now will affect all accounts for the next cred update.
4803
4804        let (cust, _) = setup_test_session(idms, ct).await;
4805
4806        let cutxn = idms.cred_update_transaction().await.unwrap();
4807
4808        // Get the credential status - this should tell
4809        // us the details of the credentials, as well as
4810        // if they are ready and valid to commit?
4811        let c_status = cutxn
4812            .credential_update_status(&cust, ct)
4813            .expect("Failed to get the current session status.");
4814
4815        trace!(?c_status);
4816        assert!(c_status.primary.is_none());
4817        assert!(matches!(
4818            c_status.primary_state,
4819            CredentialState::PolicyDeny
4820        ));
4821
4822        let err = cutxn
4823            .credential_primary_set_password(&cust, ct, test_pw)
4824            .unwrap_err();
4825        assert!(matches!(err, OperationError::AccessDenied));
4826
4827        let origin = cutxn.get_origin().clone();
4828        let mut wa = WebauthnAuthenticator::new(SoftPasskey::new(true));
4829
4830        let c_status = create_new_passkey(ct, &origin, &cutxn, &cust, &mut wa).await;
4831
4832        assert!(c_status.can_commit);
4833        assert!(c_status.warnings.is_empty());
4834        assert_eq!(c_status.passkeys.len(), 1);
4835
4836        drop(cutxn);
4837        commit_session(idms, ct, cust).await;
4838    }
4839
4840    // Attested passkey types
4841
4842    #[idm_test]
4843    async fn credential_update_account_policy_attested_passkey_required(
4844        idms: &IdmServer,
4845        idms_delayed: &mut IdmServerDelayed,
4846    ) {
4847        let ct = Duration::from_secs(TEST_CURRENT_TIME);
4848
4849        // Create the attested soft token we will use in this test.
4850        let (soft_token_valid_a, ca_root_a) = SoftToken::new(true).unwrap();
4851        let mut wa_token_valid = WebauthnAuthenticator::new(soft_token_valid_a);
4852
4853        // We need a second for when we rotate the token.
4854        let (soft_token_valid_b, ca_root_b) = SoftToken::new(true).unwrap();
4855        let mut wa_token_valid_b = WebauthnAuthenticator::new(soft_token_valid_b);
4856
4857        // Create it's associated policy.
4858        let mut att_ca_builder = AttestationCaListBuilder::new();
4859        att_ca_builder
4860            .insert_device_x509(
4861                ca_root_a,
4862                softtoken::AAGUID,
4863                "softtoken_a".to_string(),
4864                Default::default(),
4865            )
4866            .unwrap();
4867        att_ca_builder
4868            .insert_device_x509(
4869                ca_root_b,
4870                softtoken::AAGUID,
4871                "softtoken_b".to_string(),
4872                Default::default(),
4873            )
4874            .unwrap();
4875        let att_ca_list = att_ca_builder.build();
4876
4877        let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();
4878
4879        let modlist = ModifyList::new_purge_and_set(
4880            Attribute::WebauthnAttestationCaList,
4881            Value::WebauthnAttestationCaList(att_ca_list),
4882        );
4883        idms_prox_write
4884            .qs_write
4885            .internal_modify_uuid(UUID_IDM_ALL_ACCOUNTS, &modlist)
4886            .expect("Unable to change webauthn attestation policy");
4887
4888        assert!(idms_prox_write.commit().is_ok());
4889
4890        // Create the invalid tokens
4891        let (soft_token_invalid, _) = SoftToken::new(true).unwrap();
4892        let mut wa_token_invalid = WebauthnAuthenticator::new(soft_token_invalid);
4893
4894        let mut wa_passkey_invalid = WebauthnAuthenticator::new(SoftPasskey::new(true));
4895
4896        // Setup the cred update session.
4897
4898        let (cust, _) = setup_test_session(idms, ct).await;
4899        let cutxn = idms.cred_update_transaction().await.unwrap();
4900        let origin = cutxn.get_origin().clone();
4901
4902        // Our status needs the correct device names for UI hinting.
4903        let c_status = cutxn
4904            .credential_update_status(&cust, ct)
4905            .expect("Failed to get the current session status.");
4906
4907        trace!(?c_status);
4908        assert!(c_status.attested_passkeys.is_empty());
4909        assert!(c_status
4910            .attested_passkeys_allowed_devices
4911            .contains(&"softtoken_a".to_string()));
4912        assert!(c_status
4913            .attested_passkeys_allowed_devices
4914            .contains(&"softtoken_b".to_string()));
4915
4916        // -------------------------------------------------------
4917        // Unable to add an passkey when attestation is requested.
4918        let err = cutxn.credential_passkey_init(&cust, ct).unwrap_err();
4919        assert!(matches!(err, OperationError::AccessDenied));
4920
4921        // -------------------------------------------------------
4922        // Reject a credential that lacks attestation
4923        let c_status = cutxn
4924            .credential_attested_passkey_init(&cust, ct)
4925            .expect("Failed to initiate attested passkey registration");
4926
4927        let passkey_chal = match c_status.mfaregstate {
4928            MfaRegStateStatus::AttestedPasskey(c) => Some(c),
4929            _ => None,
4930        }
4931        .expect("Unable to access passkey challenge, invalid state");
4932
4933        let passkey_resp = wa_passkey_invalid
4934            .do_registration(origin.clone(), passkey_chal)
4935            .expect("Failed to create soft passkey");
4936
4937        // Finish the registration
4938        let label = "softtoken".to_string();
4939        let err = cutxn
4940            .credential_attested_passkey_finish(&cust, ct, label, &passkey_resp)
4941            .unwrap_err();
4942
4943        assert!(matches!(
4944            err,
4945            OperationError::CU0001WebauthnAttestationNotTrusted
4946        ));
4947
4948        // -------------------------------------------------------
4949        // Reject a credential with wrong CA / correct aaguid
4950        let c_status = cutxn
4951            .credential_attested_passkey_init(&cust, ct)
4952            .expect("Failed to initiate attested passkey registration");
4953
4954        let passkey_chal = match c_status.mfaregstate {
4955            MfaRegStateStatus::AttestedPasskey(c) => Some(c),
4956            _ => None,
4957        }
4958        .expect("Unable to access passkey challenge, invalid state");
4959
4960        let passkey_resp = wa_token_invalid
4961            .do_registration(origin.clone(), passkey_chal)
4962            .expect("Failed to create soft passkey");
4963
4964        // Finish the registration
4965        let label = "softtoken".to_string();
4966        let err = cutxn
4967            .credential_attested_passkey_finish(&cust, ct, label, &passkey_resp)
4968            .unwrap_err();
4969
4970        assert!(matches!(
4971            err,
4972            OperationError::CU0001WebauthnAttestationNotTrusted
4973        ));
4974
4975        // -------------------------------------------------------
4976        // Accept credential with correct CA/aaguid
4977        let c_status = cutxn
4978            .credential_attested_passkey_init(&cust, ct)
4979            .expect("Failed to initiate attested passkey registration");
4980
4981        let passkey_chal = match c_status.mfaregstate {
4982            MfaRegStateStatus::AttestedPasskey(c) => Some(c),
4983            _ => None,
4984        }
4985        .expect("Unable to access passkey challenge, invalid state");
4986
4987        let passkey_resp = wa_token_valid
4988            .do_registration(origin.clone(), passkey_chal)
4989            .expect("Failed to create soft passkey");
4990
4991        // Finish the registration
4992        let label = "softtoken".to_string();
4993        let c_status = cutxn
4994            .credential_attested_passkey_finish(&cust, ct, label, &passkey_resp)
4995            .expect("Failed to initiate passkey registration");
4996
4997        assert!(matches!(c_status.mfaregstate, MfaRegStateStatus::None));
4998        trace!(?c_status);
4999        assert_eq!(c_status.attested_passkeys.len(), 1);
5000
5001        let pk_uuid = c_status
5002            .attested_passkeys
5003            .first()
5004            .map(|pkd| pkd.uuid)
5005            .unwrap();
5006
5007        drop(cutxn);
5008        commit_session(idms, ct, cust).await;
5009
5010        // Assert that auth works.
5011        assert!(check_testperson_passkey(
5012            idms,
5013            idms_delayed,
5014            &mut wa_token_valid,
5015            origin.clone(),
5016            ct
5017        )
5018        .await
5019        .is_some());
5020
5021        // Remove attested passkey works.
5022        let (cust, _) = renew_test_session(idms, ct).await;
5023        let cutxn = idms.cred_update_transaction().await.unwrap();
5024
5025        trace!(?c_status);
5026        assert!(c_status.primary.is_none());
5027        assert!(c_status.passkeys.is_empty());
5028        assert_eq!(c_status.attested_passkeys.len(), 1);
5029
5030        let c_status = cutxn
5031            .credential_attested_passkey_remove(&cust, ct, pk_uuid)
5032            .expect("Failed to delete the attested passkey");
5033
5034        trace!(?c_status);
5035        assert!(c_status.primary.is_none());
5036        assert!(c_status.passkeys.is_empty());
5037        assert!(c_status.attested_passkeys.is_empty());
5038
5039        // Removed every passkey, you can't proceed!!!!
5040        assert!(!c_status.can_commit);
5041        assert!(c_status
5042            .warnings
5043            .contains(&CredentialUpdateSessionStatusWarnings::NoValidCredentials));
5044
5045        // Add a new, but differenter passkey
5046        let c_status = cutxn
5047            .credential_attested_passkey_init(&cust, ct)
5048            .expect("Failed to initiate attested passkey registration");
5049
5050        let passkey_chal = match c_status.mfaregstate {
5051            MfaRegStateStatus::AttestedPasskey(c) => Some(c),
5052            _ => None,
5053        }
5054        .expect("Unable to access passkey challenge, invalid state");
5055
5056        // Note this is the second token, not the first.
5057        let passkey_resp = wa_token_valid_b
5058            .do_registration(origin.clone(), passkey_chal)
5059            .expect("Failed to create soft passkey");
5060
5061        // Finish the registration
5062        let label = "softtoken".to_string();
5063        let c_status = cutxn
5064            .credential_attested_passkey_finish(&cust, ct, label, &passkey_resp)
5065            .expect("Failed to initiate passkey registration");
5066
5067        assert!(matches!(c_status.mfaregstate, MfaRegStateStatus::None));
5068        trace!(?c_status);
5069        assert_eq!(c_status.attested_passkeys.len(), 1);
5070
5071        drop(cutxn);
5072        commit_session(idms, ct, cust).await;
5073
5074        // Must fail now, note we use the first token to auth here which we deleted from
5075        // the clients credentials.
5076        assert!(
5077            check_testperson_passkey(idms, idms_delayed, &mut wa_token_valid, origin, ct)
5078                .await
5079                .is_none()
5080        );
5081    }
5082
5083    #[idm_test(audit = 1)]
5084    async fn credential_update_account_policy_attested_passkey_changed(
5085        idms: &IdmServer,
5086        idms_delayed: &mut IdmServerDelayed,
5087        idms_audit: &mut IdmServerAudit,
5088    ) {
5089        let ct = Duration::from_secs(TEST_CURRENT_TIME);
5090
5091        // Setup the policy.
5092        let (soft_token_1, ca_root_1) = SoftToken::new(true).unwrap();
5093        let mut wa_token_1 = WebauthnAuthenticator::new(soft_token_1);
5094
5095        let (soft_token_2, ca_root_2) = SoftToken::new(true).unwrap();
5096        let mut wa_token_2 = WebauthnAuthenticator::new(soft_token_2);
5097
5098        // This is the original policy that we enroll.
5099        let mut att_ca_builder = AttestationCaListBuilder::new();
5100        att_ca_builder
5101            .insert_device_x509(
5102                ca_root_1.clone(),
5103                softtoken::AAGUID,
5104                "softtoken_1".to_string(),
5105                Default::default(),
5106            )
5107            .unwrap();
5108        let att_ca_list = att_ca_builder.build();
5109
5110        trace!(?att_ca_list);
5111
5112        let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();
5113
5114        let modlist = ModifyList::new_purge_and_set(
5115            Attribute::WebauthnAttestationCaList,
5116            Value::WebauthnAttestationCaList(att_ca_list),
5117        );
5118        idms_prox_write
5119            .qs_write
5120            .internal_modify_uuid(UUID_IDM_ALL_ACCOUNTS, &modlist)
5121            .expect("Unable to change webauthn attestation policy");
5122
5123        assert!(idms_prox_write.commit().is_ok());
5124
5125        // Setup the policy for later that lacks token 1.
5126        let mut att_ca_builder = AttestationCaListBuilder::new();
5127        att_ca_builder
5128            .insert_device_x509(
5129                ca_root_2,
5130                softtoken::AAGUID,
5131                "softtoken_2".to_string(),
5132                Default::default(),
5133            )
5134            .unwrap();
5135        let att_ca_list_post = att_ca_builder.build();
5136
5137        // Enroll the attested keys
5138        let (cust, _) = setup_test_session(idms, ct).await;
5139        let cutxn = idms.cred_update_transaction().await.unwrap();
5140        let origin = cutxn.get_origin().clone();
5141
5142        // -------------------------------------------------------
5143        let c_status = cutxn
5144            .credential_attested_passkey_init(&cust, ct)
5145            .expect("Failed to initiate attested passkey registration");
5146
5147        let passkey_chal = match c_status.mfaregstate {
5148            MfaRegStateStatus::AttestedPasskey(c) => Some(c),
5149            _ => None,
5150        }
5151        .expect("Unable to access passkey challenge, invalid state");
5152
5153        let passkey_resp = wa_token_1
5154            .do_registration(origin.clone(), passkey_chal)
5155            .expect("Failed to create soft passkey");
5156
5157        // Finish the registration of token 1
5158        let label = "softtoken".to_string();
5159        let c_status = cutxn
5160            .credential_attested_passkey_finish(&cust, ct, label, &passkey_resp)
5161            .expect("Failed to initiate passkey registration");
5162
5163        assert!(matches!(c_status.mfaregstate, MfaRegStateStatus::None));
5164        trace!(?c_status);
5165        assert_eq!(c_status.attested_passkeys.len(), 1);
5166
5167        // -------------------------------------------------------
5168        // Commit
5169        drop(cutxn);
5170        commit_session(idms, ct, cust).await;
5171
5172        // Check auth works
5173        assert!(
5174            check_testperson_passkey(idms, idms_delayed, &mut wa_token_1, origin.clone(), ct)
5175                .await
5176                .is_some()
5177        );
5178
5179        // Change policy
5180        let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();
5181
5182        let modlist = ModifyList::new_purge_and_set(
5183            Attribute::WebauthnAttestationCaList,
5184            Value::WebauthnAttestationCaList(att_ca_list_post),
5185        );
5186        idms_prox_write
5187            .qs_write
5188            .internal_modify_uuid(UUID_IDM_ALL_ACCOUNTS, &modlist)
5189            .expect("Unable to change webauthn attestation policy");
5190
5191        assert!(idms_prox_write.commit().is_ok());
5192
5193        // Auth fail, the CA is no longer valid.
5194        assert!(
5195            check_testperson_passkey(idms, idms_delayed, &mut wa_token_1, origin.clone(), ct)
5196                .await
5197                .is_none()
5198        );
5199
5200        // This gives an auth denied because the attested passkey still exists but it no longer
5201        // meets criteria.
5202        match idms_audit.audit_rx().try_recv() {
5203            Ok(AuditEvent::AuthenticationDenied { .. }) => {}
5204            _ => panic!("Oh no"),
5205        }
5206
5207        //  Update creds
5208        let (cust, _) = renew_test_session(idms, ct).await;
5209        let cutxn = idms.cred_update_transaction().await.unwrap();
5210
5211        // Invalid key removed
5212        let c_status = cutxn
5213            .credential_update_status(&cust, ct)
5214            .expect("Failed to get the current session status.");
5215
5216        trace!(?c_status);
5217        assert!(c_status.attested_passkeys.is_empty());
5218
5219        // But we can't commit:
5220        assert!(!c_status.can_commit);
5221        assert!(c_status
5222            .warnings
5223            .contains(&CredentialUpdateSessionStatusWarnings::NoValidCredentials));
5224
5225        // -------------------------------------------------------
5226        // Now enroll the new token.
5227        let c_status = cutxn
5228            .credential_attested_passkey_init(&cust, ct)
5229            .expect("Failed to initiate attested passkey registration");
5230
5231        let passkey_chal = match c_status.mfaregstate {
5232            MfaRegStateStatus::AttestedPasskey(c) => Some(c),
5233            _ => None,
5234        }
5235        .expect("Unable to access passkey challenge, invalid state");
5236
5237        let passkey_resp = wa_token_2
5238            .do_registration(origin.clone(), passkey_chal)
5239            .expect("Failed to create soft passkey");
5240
5241        // Finish the registration of token 1
5242        let label = "softtoken".to_string();
5243        let c_status = cutxn
5244            .credential_attested_passkey_finish(&cust, ct, label, &passkey_resp)
5245            .expect("Failed to initiate passkey registration");
5246
5247        assert!(matches!(c_status.mfaregstate, MfaRegStateStatus::None));
5248        trace!(?c_status);
5249        assert_eq!(c_status.attested_passkeys.len(), 1);
5250
5251        drop(cutxn);
5252        commit_session(idms, ct, cust).await;
5253
5254        // Auth fail with the first token still
5255        assert!(
5256            check_testperson_passkey(idms, idms_delayed, &mut wa_token_1, origin.clone(), ct)
5257                .await
5258                .is_none()
5259        );
5260
5261        // But the new token works.
5262        assert!(
5263            check_testperson_passkey(idms, idms_delayed, &mut wa_token_2, origin.clone(), ct)
5264                .await
5265                .is_some()
5266        );
5267    }
5268
5269    // Test that when attestation policy is removed, the apk downgrades to passkey and still works.
5270    #[idm_test]
5271    async fn credential_update_account_policy_attested_passkey_downgrade(
5272        idms: &IdmServer,
5273        idms_delayed: &mut IdmServerDelayed,
5274    ) {
5275        let ct = Duration::from_secs(TEST_CURRENT_TIME);
5276
5277        // Setup the policy.
5278        let (soft_token_1, ca_root_1) = SoftToken::new(true).unwrap();
5279        let mut wa_token_1 = WebauthnAuthenticator::new(soft_token_1);
5280
5281        let mut att_ca_builder = AttestationCaListBuilder::new();
5282        att_ca_builder
5283            .insert_device_x509(
5284                ca_root_1.clone(),
5285                softtoken::AAGUID,
5286                "softtoken_1".to_string(),
5287                Default::default(),
5288            )
5289            .unwrap();
5290        let att_ca_list = att_ca_builder.build();
5291
5292        trace!(?att_ca_list);
5293
5294        let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();
5295
5296        let modlist = ModifyList::new_purge_and_set(
5297            Attribute::WebauthnAttestationCaList,
5298            Value::WebauthnAttestationCaList(att_ca_list),
5299        );
5300        idms_prox_write
5301            .qs_write
5302            .internal_modify_uuid(UUID_IDM_ALL_ACCOUNTS, &modlist)
5303            .expect("Unable to change webauthn attestation policy");
5304
5305        assert!(idms_prox_write.commit().is_ok());
5306
5307        // Enroll the attested keys
5308        let (cust, _) = setup_test_session(idms, ct).await;
5309        let cutxn = idms.cred_update_transaction().await.unwrap();
5310        let origin = cutxn.get_origin().clone();
5311
5312        // -------------------------------------------------------
5313        let c_status = cutxn
5314            .credential_attested_passkey_init(&cust, ct)
5315            .expect("Failed to initiate attested passkey registration");
5316
5317        let passkey_chal = match c_status.mfaregstate {
5318            MfaRegStateStatus::AttestedPasskey(c) => Some(c),
5319            _ => None,
5320        }
5321        .expect("Unable to access passkey challenge, invalid state");
5322
5323        let passkey_resp = wa_token_1
5324            .do_registration(origin.clone(), passkey_chal)
5325            .expect("Failed to create soft passkey");
5326
5327        // Finish the registration
5328        let label = "softtoken".to_string();
5329        let c_status = cutxn
5330            .credential_attested_passkey_finish(&cust, ct, label, &passkey_resp)
5331            .expect("Failed to initiate passkey registration");
5332
5333        assert!(matches!(c_status.mfaregstate, MfaRegStateStatus::None));
5334        trace!(?c_status);
5335        assert_eq!(c_status.attested_passkeys.len(), 1);
5336
5337        // -------------------------------------------------------
5338        // Commit
5339        drop(cutxn);
5340        commit_session(idms, ct, cust).await;
5341
5342        // Check auth works
5343        assert!(
5344            check_testperson_passkey(idms, idms_delayed, &mut wa_token_1, origin.clone(), ct)
5345                .await
5346                .is_some()
5347        );
5348
5349        // Change policy
5350        let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();
5351
5352        let modlist = ModifyList::new_purge(Attribute::WebauthnAttestationCaList);
5353        idms_prox_write
5354            .qs_write
5355            .internal_modify_uuid(UUID_IDM_ALL_ACCOUNTS, &modlist)
5356            .expect("Unable to change webauthn attestation policy");
5357
5358        assert!(idms_prox_write.commit().is_ok());
5359
5360        // Auth still passes, key was downgraded.
5361        assert!(
5362            check_testperson_passkey(idms, idms_delayed, &mut wa_token_1, origin.clone(), ct)
5363                .await
5364                .is_some()
5365        );
5366
5367        // Show it still exists, but can only be deleted now.
5368        let (cust, _) = renew_test_session(idms, ct).await;
5369        let cutxn = idms.cred_update_transaction().await.unwrap();
5370
5371        let c_status = cutxn
5372            .credential_update_status(&cust, ct)
5373            .expect("Failed to get the current session status.");
5374
5375        trace!(?c_status);
5376        assert_eq!(c_status.attested_passkeys.len(), 1);
5377        assert!(matches!(
5378            c_status.attested_passkeys_state,
5379            CredentialState::DeleteOnly
5380        ));
5381
5382        drop(cutxn);
5383        commit_session(idms, ct, cust).await;
5384    }
5385
5386    #[idm_test]
5387    async fn credential_update_unix_password(
5388        idms: &IdmServer,
5389        _idms_delayed: &mut IdmServerDelayed,
5390    ) {
5391        let test_pw = "fo3EitierohF9AelaNgiem0Ei6vup4equo1Oogeevaetehah8Tobeengae3Ci0ooh0uki";
5392        let ct = Duration::from_secs(TEST_CURRENT_TIME);
5393
5394        let (cust, _) = setup_test_session(idms, ct).await;
5395
5396        let cutxn = idms.cred_update_transaction().await.unwrap();
5397
5398        // Get the credential status - this should tell
5399        // us the details of the credentials, as well as
5400        // if they are ready and valid to commit?
5401        let c_status = cutxn
5402            .credential_update_status(&cust, ct)
5403            .expect("Failed to get the current session status.");
5404
5405        trace!(?c_status);
5406        assert!(c_status.unixcred.is_none());
5407
5408        // There are no credentials so we can't proceed.
5409        assert!(c_status
5410            .warnings
5411            .contains(&CredentialUpdateSessionStatusWarnings::NoValidCredentials));
5412        assert!(!c_status.can_commit);
5413        // User needs at least one credential else they can't save.
5414        let c_status = cutxn
5415            .credential_primary_set_password(&cust, ct, test_pw)
5416            .expect("Failed to update the primary cred password");
5417        assert!(c_status.can_commit);
5418        assert!(!c_status
5419            .warnings
5420            .contains(&CredentialUpdateSessionStatusWarnings::NoValidCredentials));
5421
5422        // Test initially creating a credential.
5423        //   - pw first
5424        let c_status = cutxn
5425            .credential_unix_set_password(&cust, ct, test_pw)
5426            .expect("Failed to update the unix cred password");
5427
5428        assert!(c_status.can_commit);
5429
5430        drop(cutxn);
5431        commit_session(idms, ct, cust).await;
5432
5433        // Check it works!
5434        assert!(check_testperson_unix_password(idms, test_pw, ct)
5435            .await
5436            .is_some());
5437
5438        // Test deleting the pw
5439        let (cust, _) = renew_test_session(idms, ct).await;
5440        let cutxn = idms.cred_update_transaction().await.unwrap();
5441
5442        let c_status = cutxn
5443            .credential_update_status(&cust, ct)
5444            .expect("Failed to get the current session status.");
5445        trace!(?c_status);
5446        assert!(c_status.unixcred.is_some());
5447
5448        let c_status = cutxn
5449            .credential_unix_delete(&cust, ct)
5450            .expect("Failed to delete the unix cred");
5451        trace!(?c_status);
5452        assert!(c_status.unixcred.is_none());
5453
5454        drop(cutxn);
5455        commit_session(idms, ct, cust).await;
5456
5457        // Must fail now!
5458        assert!(check_testperson_unix_password(idms, test_pw, ct)
5459            .await
5460            .is_none());
5461    }
5462
5463    #[idm_test]
5464    async fn credential_update_sshkeys(idms: &IdmServer, _idms_delayed: &mut IdmServerDelayed) {
5465        let test_pw = "fo3EitierohF9AelaNgiem0Ei6vup4equo1Oogeevaetehah8Tobeengae3Ci0ooh0uki";
5466        let sshkey_valid_1 =
5467            SshPublicKey::from_string(SSHKEY_VALID_1).expect("Invalid SSHKEY_VALID_1");
5468        let sshkey_valid_2 =
5469            SshPublicKey::from_string(SSHKEY_VALID_2).expect("Invalid SSHKEY_VALID_2");
5470
5471        assert!(SshPublicKey::from_string(SSHKEY_INVALID).is_err());
5472
5473        let ct = Duration::from_secs(TEST_CURRENT_TIME);
5474        let (cust, _) = setup_test_session(idms, ct).await;
5475        let cutxn = idms.cred_update_transaction().await.unwrap();
5476
5477        let c_status = cutxn
5478            .credential_update_status(&cust, ct)
5479            .expect("Failed to get the current session status.");
5480
5481        // There are no credentials so we can't proceed.
5482        assert!(c_status
5483            .warnings
5484            .contains(&CredentialUpdateSessionStatusWarnings::NoValidCredentials));
5485        assert!(!c_status.can_commit);
5486        // User needs at least one credential else they can't save.
5487        let c_status = cutxn
5488            .credential_primary_set_password(&cust, ct, test_pw)
5489            .expect("Failed to update the primary cred password");
5490
5491        // Ready to proceed with ssh keys
5492
5493        trace!(?c_status);
5494
5495        assert!(c_status.sshkeys.is_empty());
5496
5497        // Reject empty str key label
5498        let result = cutxn.credential_sshkey_add(&cust, ct, "".to_string(), sshkey_valid_1.clone());
5499        assert!(matches!(result, Err(OperationError::InvalidLabel)));
5500
5501        // Reject invalid name label.
5502        let result =
5503            cutxn.credential_sshkey_add(&cust, ct, "🚛".to_string(), sshkey_valid_1.clone());
5504        assert!(matches!(result, Err(OperationError::InvalidLabel)));
5505
5506        // Remove non-existante
5507        let result = cutxn.credential_sshkey_remove(&cust, ct, "key1");
5508        assert!(matches!(result, Err(OperationError::NoMatchingEntries)));
5509
5510        // Add a valid key.
5511        let c_status = cutxn
5512            .credential_sshkey_add(&cust, ct, "key1".to_string(), sshkey_valid_1.clone())
5513            .expect("Failed to add sshkey_valid_1");
5514
5515        trace!(?c_status);
5516        assert_eq!(c_status.sshkeys.len(), 1);
5517        assert!(c_status.sshkeys.contains_key("key1"));
5518
5519        // Add a second valid key.
5520        let c_status = cutxn
5521            .credential_sshkey_add(&cust, ct, "key2".to_string(), sshkey_valid_2.clone())
5522            .expect("Failed to add sshkey_valid_2");
5523
5524        trace!(?c_status);
5525        assert_eq!(c_status.sshkeys.len(), 2);
5526        assert!(c_status.sshkeys.contains_key("key1"));
5527        assert!(c_status.sshkeys.contains_key("key2"));
5528
5529        // Remove a key (check second key untouched)
5530        let c_status = cutxn
5531            .credential_sshkey_remove(&cust, ct, "key2")
5532            .expect("Failed to remove sshkey_valid_2");
5533
5534        trace!(?c_status);
5535        assert_eq!(c_status.sshkeys.len(), 1);
5536        assert!(c_status.sshkeys.contains_key("key1"));
5537
5538        // Reject duplicate key label
5539        let result =
5540            cutxn.credential_sshkey_add(&cust, ct, "key1".to_string(), sshkey_valid_2.clone());
5541        assert!(matches!(result, Err(OperationError::DuplicateLabel)));
5542
5543        // Reject duplicate key
5544        let result =
5545            cutxn.credential_sshkey_add(&cust, ct, "key2".to_string(), sshkey_valid_1.clone());
5546        assert!(matches!(result, Err(OperationError::DuplicateKey)));
5547
5548        drop(cutxn);
5549        commit_session(idms, ct, cust).await;
5550    }
5551
5552    // Assert we need at least one credential on the accoutn to save.
5553    #[idm_test]
5554    async fn credential_update_at_least_one_credential(
5555        idms: &IdmServer,
5556        _idms_delayed: &mut IdmServerDelayed,
5557    ) {
5558        let test_pw = "fo3EitierohF9AelaNgiem0Ei6vup4equo1Oogeevaetehah8Tobeengae3Ci0ooh0uki";
5559        let ct = Duration::from_secs(TEST_CURRENT_TIME);
5560
5561        let (cust, _) = setup_test_session(idms, ct).await;
5562
5563        let cutxn = idms.cred_update_transaction().await.unwrap();
5564
5565        // Get the credential status - this should tell
5566        // us the details of the credentials, as well as
5567        // if they are ready and valid to commit?
5568        let c_status = cutxn
5569            .credential_update_status(&cust, ct)
5570            .expect("Failed to get the current session status.");
5571
5572        trace!(?c_status);
5573
5574        assert!(c_status.primary.is_none());
5575        // There are no credentials so we can't proceed.
5576        assert!(c_status
5577            .warnings
5578            .contains(&CredentialUpdateSessionStatusWarnings::NoValidCredentials));
5579        assert!(!c_status.can_commit);
5580
5581        // Test initially creating a credential.
5582        let c_status = cutxn
5583            .credential_primary_set_password(&cust, ct, test_pw)
5584            .expect("Failed to update the primary cred password");
5585
5586        // Could proceed now!
5587        assert!(c_status.can_commit);
5588        assert!(!c_status
5589            .warnings
5590            .contains(&CredentialUpdateSessionStatusWarnings::NoValidCredentials));
5591
5592        // But if we remove it, back to square 1.
5593        let c_status = cutxn
5594            .credential_primary_delete(&cust, ct)
5595            .expect("Failed to remove the primary credential");
5596
5597        // There are no credentials so we can't proceed.
5598        assert!(c_status
5599            .warnings
5600            .contains(&CredentialUpdateSessionStatusWarnings::NoValidCredentials));
5601        assert!(!c_status.can_commit);
5602    }
5603
5604    async fn get_testperson_password_changed_time(idms: &IdmServer) -> Option<OffsetDateTime> {
5605        let mut txn = idms.proxy_read().await.unwrap();
5606        let entry = txn
5607            .qs_read
5608            .internal_search_uuid(TESTPERSON_UUID)
5609            .expect("Failed to read testperson entry");
5610        entry.get_ava_single_datetime(Attribute::PasswordChangedTime)
5611    }
5612
5613    #[idm_test]
5614    async fn credential_update_password_changed_time_password_set(
5615        idms: &IdmServer,
5616        _idms_delayed: &mut IdmServerDelayed,
5617    ) {
5618        let ct = Duration::from_secs(TEST_CURRENT_TIME);
5619
5620        let (cust, _) = setup_test_session(idms, ct).await;
5621
5622        // Sanity check
5623        assert!(get_testperson_password_changed_time(idms).await.is_none());
5624
5625        // Primary Cred with No fallback => EPOCH
5626        let cutxn = idms.cred_update_transaction().await.unwrap();
5627        let c_status = cutxn
5628            .credential_primary_set_password(&cust, ct, TESTPERSON_PASSWORD)
5629            .expect("Failed to update the primary cred password");
5630        assert!(c_status.can_commit);
5631        drop(cutxn);
5632        commit_session(idms, ct, cust).await;
5633
5634        let pwd_changed = get_testperson_password_changed_time(idms)
5635            .await
5636            .expect("PasswordChangedTime should be set after setting primary password");
5637        assert_eq!(pwd_changed, OffsetDateTime::UNIX_EPOCH);
5638
5639        // UNIX Cred => Unix Cred Timestamp
5640        let (cust, _) = renew_test_session(idms, ct).await;
5641        let cutxn = idms.cred_update_transaction().await.unwrap();
5642        let c_status = cutxn
5643            .credential_unix_set_password(&cust, ct, TESTPERSON_PASSWORD)
5644            .expect("Failed to set unix password");
5645        assert!(c_status.can_commit);
5646        drop(cutxn);
5647        commit_session(idms, ct, cust).await;
5648
5649        let pwd_changed = get_testperson_password_changed_time(idms)
5650            .await
5651            .expect("PasswordChangedTime should be set after setting both passwords");
5652        // Unix credential is checked first, its timestamp is used.
5653        assert_eq!(pwd_changed, OffsetDateTime::UNIX_EPOCH + ct);
5654
5655        let ct = Duration::from_secs(TEST_CURRENT_TIME + 1000);
5656
5657        // Update UNIX Cred => New Unix Cred Timestamp
5658        let (cust, _) = renew_test_session(idms, ct).await;
5659        let cutxn = idms.cred_update_transaction().await.unwrap();
5660        let _ = cutxn
5661            .credential_unix_set_password(&cust, ct, "R290Y2hhIGFnYWlu")
5662            .expect("Failed to set unix password on second update");
5663        drop(cutxn);
5664        commit_session(idms, ct, cust).await;
5665
5666        let pwd_changed_2 = get_testperson_password_changed_time(idms)
5667            .await
5668            .expect("PasswordChangedTime should be updated on second update");
5669        assert_eq!(pwd_changed_2, OffsetDateTime::UNIX_EPOCH + ct);
5670        assert!(pwd_changed_2 > pwd_changed);
5671    }
5672
5673    #[idm_test]
5674    async fn credential_update_password_changed_time_unix_deleted(
5675        idms: &IdmServer,
5676        _idms_delayed: &mut IdmServerDelayed,
5677    ) {
5678        let ct = Duration::from_secs(TEST_CURRENT_TIME);
5679
5680        // Set up both credentials.
5681        let (cust, _) = setup_test_session(idms, ct).await;
5682        let cutxn = idms.cred_update_transaction().await.unwrap();
5683        let _ = cutxn
5684            .credential_primary_set_password(&cust, ct, TESTPERSON_PASSWORD)
5685            .expect("Failed to set primary password");
5686        let _ = cutxn
5687            .credential_unix_set_password(&cust, ct, TESTPERSON_PASSWORD)
5688            .expect("Failed to set unix password");
5689        drop(cutxn);
5690        commit_session(idms, ct, cust).await;
5691
5692        let pwd_changed_1 = get_testperson_password_changed_time(idms)
5693            .await
5694            .expect("PasswordChangedTime should be set");
5695        assert_eq!(pwd_changed_1, OffsetDateTime::UNIX_EPOCH + ct);
5696
5697        let ct = Duration::from_secs(TEST_CURRENT_TIME + 1000);
5698
5699        // Now delete unix password
5700        let (cust, _) = renew_test_session(idms, ct).await;
5701        let cutxn = idms.cred_update_transaction().await.unwrap();
5702        let c_status = cutxn
5703            .credential_unix_delete(&cust, ct)
5704            .expect("Failed to delete unix credential");
5705        assert!(c_status.unixcred.is_none());
5706        assert!(c_status.can_commit);
5707        drop(cutxn);
5708        commit_session(idms, ct, cust).await;
5709
5710        // Unix cred is gone and timestamp is EPOCH
5711        let pwd_changed_2 = get_testperson_password_changed_time(idms)
5712            .await
5713            .expect("PasswordChangedTime should still be set after deleting unix password");
5714        assert_eq!(pwd_changed_2, OffsetDateTime::UNIX_EPOCH);
5715    }
5716
5717    #[idm_test]
5718    async fn credential_update_password_changed_time_non_posix(
5719        idms: &IdmServer,
5720        _idms_delayed: &mut IdmServerDelayed,
5721    ) {
5722        let ct = Duration::from_secs(TEST_CURRENT_TIME);
5723
5724        let (cust, _) = setup_test_session_no_posix(idms, ct).await;
5725
5726        // Sanity check
5727        assert!(get_testperson_password_changed_time(idms).await.is_none());
5728
5729        // No POSIX still causes default to EPOCH
5730        let cutxn = idms.cred_update_transaction().await.unwrap();
5731        let c_status = cutxn
5732            .credential_primary_set_password(&cust, ct, TESTPERSON_PASSWORD)
5733            .expect("Failed to set primary password");
5734        assert!(c_status.can_commit);
5735        drop(cutxn);
5736        commit_session(idms, ct, cust).await;
5737
5738        let pwd_changed = get_testperson_password_changed_time(idms)
5739            .await
5740            .expect("PasswordChangedTime should be set for non-posix person");
5741        assert_eq!(pwd_changed, OffsetDateTime::UNIX_EPOCH);
5742
5743        // Enable allow_primary_cred_fallback on all accounts.
5744        let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();
5745        idms_prox_write
5746            .qs_write
5747            .internal_modify_uuid(
5748                UUID_IDM_ALL_ACCOUNTS,
5749                &ModifyList::new_purge_and_set(
5750                    Attribute::AllowPrimaryCredFallback,
5751                    Value::new_bool(true),
5752                ),
5753            )
5754            .expect("Unable to set allow_primary_cred_fallback");
5755        idms_prox_write.commit().expect("Failed to commit txn");
5756
5757        // With fallback enabled PrimaryCred timestamp is used despite not being POSIX account
5758        let (cust, _) = renew_test_session(idms, ct).await;
5759        let cutxn = idms.cred_update_transaction().await.unwrap();
5760        let _ = cutxn
5761            .credential_primary_set_password(&cust, ct, TESTPERSON_PASSWORD)
5762            .expect("Failed to set primary password");
5763        drop(cutxn);
5764        commit_session(idms, ct, cust).await;
5765        let pwd_changed = get_testperson_password_changed_time(idms)
5766            .await
5767            .expect("PasswordChangedTime should be set with fallback enabled");
5768        assert_eq!(pwd_changed, OffsetDateTime::UNIX_EPOCH + ct);
5769    }
5770
5771    #[idm_test]
5772    async fn credential_update_password_changed_time_passkey_only(
5773        idms: &IdmServer,
5774        _idms_delayed: &mut IdmServerDelayed,
5775    ) {
5776        let ct = Duration::from_secs(TEST_CURRENT_TIME);
5777        let (cust, _) = setup_test_session_no_posix(idms, ct).await;
5778
5779        // Sanity check
5780        assert!(get_testperson_password_changed_time(idms).await.is_none());
5781
5782        // Setting Passkey with no Password Credentials causes EPOCH to be set
5783        let cutxn = idms.cred_update_transaction().await.unwrap();
5784        let origin = cutxn.get_origin().clone();
5785        let mut wa = WebauthnAuthenticator::new(SoftPasskey::new(true));
5786
5787        let c_status = create_new_passkey(ct, &origin, &cutxn, &cust, &mut wa).await;
5788        assert!(c_status.can_commit);
5789        assert_eq!(c_status.passkeys.len(), 1);
5790        drop(cutxn);
5791        commit_session(idms, ct, cust).await;
5792
5793        // No password credential was set, so the sentinel UNIX_EPOCH is used.
5794        let pwd_changed = get_testperson_password_changed_time(idms)
5795            .await
5796            .expect("PasswordChangedTime should be set even for passkey-only");
5797        assert_eq!(pwd_changed, time::OffsetDateTime::UNIX_EPOCH);
5798    }
5799
5800    #[idm_test]
5801    async fn credential_update_password_changed_time_no_change_commit(
5802        idms: &IdmServer,
5803        _idms_delayed: &mut IdmServerDelayed,
5804    ) {
5805        let ct = Duration::from_secs(TEST_CURRENT_TIME);
5806        let (cust, _) = setup_test_session(idms, ct).await;
5807
5808        let cutxn = idms.cred_update_transaction().await.unwrap();
5809        let _ = cutxn
5810            .credential_primary_set_password(&cust, ct, TESTPERSON_PASSWORD)
5811            .expect("Failed to set primary password");
5812        let _ = cutxn
5813            .credential_unix_set_password(&cust, ct, TESTPERSON_PASSWORD)
5814            .expect("Failed to set unix password");
5815        drop(cutxn);
5816        commit_session(idms, ct, cust).await;
5817
5818        let pwd_changed_1 = get_testperson_password_changed_time(idms)
5819            .await
5820            .expect("PasswordChangedTime should be set after first update");
5821
5822        let ct2 = Duration::from_secs(TEST_CURRENT_TIME + 2000);
5823
5824        // Renew session and commit without making any changes.
5825        let (cust, c_status) = renew_test_session(idms, ct2).await;
5826        assert!(c_status.primary.is_some());
5827        assert!(c_status.can_commit);
5828        commit_session(idms, ct2, cust).await;
5829
5830        let pwd_changed_2 = get_testperson_password_changed_time(idms)
5831            .await
5832            .expect("PasswordChangedTime should still be present after no-change commit");
5833
5834        assert_eq!(pwd_changed_2, OffsetDateTime::UNIX_EPOCH + ct);
5835        assert_eq!(pwd_changed_2, pwd_changed_1);
5836    }
5837
5838    #[idm_test]
5839    async fn credential_update_unix_password_deleted_falls_back(
5840        idms: &IdmServer,
5841        _idms_delayed: &mut IdmServerDelayed,
5842    ) {
5843        let ct = Duration::from_secs(TEST_CURRENT_TIME);
5844        let ct2 = Duration::from_secs(TEST_CURRENT_TIME + 50);
5845
5846        let (cust, _) = setup_test_session(idms, ct).await;
5847
5848        // Enable allow_primary_cred_fallback on all accounts.
5849        let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();
5850        idms_prox_write
5851            .qs_write
5852            .internal_modify_uuid(
5853                UUID_IDM_ALL_ACCOUNTS,
5854                &ModifyList::new_purge_and_set(
5855                    Attribute::AllowPrimaryCredFallback,
5856                    Value::new_bool(true),
5857                ),
5858            )
5859            .expect("Unable to set allow_primary_cred_fallback");
5860        idms_prox_write.commit().expect("Failed to commit txn");
5861
5862        // Sanity check
5863        assert!(get_testperson_password_changed_time(idms).await.is_none());
5864
5865        let cutxn = idms.cred_update_transaction().await.unwrap();
5866        let _ = cutxn
5867            .credential_primary_set_password(&cust, ct, TESTPERSON_PASSWORD)
5868            .expect("Failed to set primary password");
5869
5870        let _ = cutxn
5871            .credential_unix_set_password(&cust, ct2, TESTPERSON_PASSWORD)
5872            .expect("Failed to set unix password");
5873
5874        let c_status = cutxn
5875            .credential_primary_init_totp(&cust, ct)
5876            .expect("Failed to init totp");
5877
5878        let totp_token: Totp = match c_status.mfaregstate {
5879            MfaRegStateStatus::TotpCheck(secret) => Some(secret.try_into().unwrap()),
5880            _ => None,
5881        }
5882        .expect("Unable to retrieve totp token");
5883
5884        let chal = totp_token
5885            .do_totp_duration_from_epoch(&ct)
5886            .expect("Failed to perform totp step");
5887
5888        let c_status = cutxn
5889            .credential_primary_check_totp(&cust, ct, chal, "totp")
5890            .expect("Failed to check totp");
5891
5892        assert!(matches!(c_status.mfaregstate, MfaRegStateStatus::None));
5893        assert!(c_status.can_commit);
5894
5895        drop(cutxn);
5896        commit_session(idms, ct, cust).await;
5897
5898        let pwd_changed = get_testperson_password_changed_time(idms)
5899            .await
5900            .expect("PasswordChangedTime should be set for password+TOTP");
5901        // No unix cred is set and fallback is disabled, so sentinel UNIX_EPOCH is used.
5902        assert_eq!(pwd_changed, OffsetDateTime::UNIX_EPOCH + ct2);
5903
5904        // Delete Unix, PasswordChangedTime should fall back to Primary
5905        let (cust, _) = renew_test_session(idms, ct2).await;
5906        let cutxn = idms.cred_update_transaction().await.unwrap();
5907
5908        let _ = cutxn
5909            .credential_unix_delete(&cust, ct2)
5910            .expect("Failed to delete unix credential");
5911
5912        assert!(c_status.can_commit);
5913        drop(cutxn);
5914        commit_session(idms, ct2, cust).await;
5915
5916        let pwd_changed_2 = get_testperson_password_changed_time(idms)
5917            .await
5918            .expect("PasswordChangedTime should be set after switching to passkey");
5919        assert_eq!(pwd_changed_2, OffsetDateTime::UNIX_EPOCH + ct);
5920    }
5921}