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