kanidmd_lib/idm/
credupdatesession.rs

1use core::ops::Deref;
2use std::collections::BTreeMap;
3use std::fmt::{self, Display};
4use std::sync::{Arc, Mutex};
5use std::time::Duration;
6
7use sshkey_attest::proto::PublicKey as SshPublicKey;
8
9use hashbrown::HashSet;
10use kanidm_proto::internal::{
11    CUCredState, CUExtPortal, CURegState, CURegWarning, CUStatus, CredentialDetail, PasskeyDetail,
12    PasswordFeedback, TotpSecret,
13};
14use serde::{Deserialize, Serialize};
15use time::OffsetDateTime;
16use webauthn_rs::prelude::{
17    AttestedPasskey as AttestedPasskeyV4, AttestedPasskeyRegistration, CreationChallengeResponse,
18    Passkey as PasskeyV4, PasskeyRegistration, RegisterPublicKeyCredential, WebauthnError,
19};
20use zxcvbn::{zxcvbn, Score};
21
22use crate::credential::totp::{Totp, TOTP_DEFAULT_STEP};
23use crate::credential::{BackupCodes, Credential};
24use crate::idm::account::Account;
25use crate::idm::server::{IdmServerCredUpdateTransaction, IdmServerProxyWriteTransaction};
26use crate::prelude::*;
27use crate::server::access::Access;
28use crate::utils::{backup_code_from_random, readable_password_from_random, uuid_from_duration};
29use crate::value::{CredUpdateSessionPerms, CredentialType, IntentTokenState, LABEL_RE};
30use compact_jwt::compact::JweCompact;
31use compact_jwt::jwe::JweBuilder;
32
33use super::accountpolicy::ResolvedAccountPolicy;
34
35// A user can take up to 15 minutes to update their credentials before we automatically
36// cancel on them.
37const MAXIMUM_CRED_UPDATE_TTL: Duration = Duration::from_secs(900);
38// Minimum 5 minutes.
39const MINIMUM_INTENT_TTL: Duration = Duration::from_secs(300);
40// Default 1 hour.
41const DEFAULT_INTENT_TTL: Duration = Duration::from_secs(3600);
42// Default 1 day.
43const MAXIMUM_INTENT_TTL: Duration = Duration::from_secs(86400);
44
45#[derive(Debug)]
46pub enum PasswordQuality {
47    TooShort(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    // The pw credential as they are being updated
147    primary_state: CredentialState,
148    primary: Option<Credential>,
149
150    // Unix / Sudo PW
151    unixcred: Option<Credential>,
152    unixcred_state: CredentialState,
153
154    // Ssh Keys
155    sshkeys: BTreeMap<String, SshPublicKey>,
156    sshkeys_state: CredentialState,
157
158    // Passkeys that have been configured.
159    passkeys: BTreeMap<Uuid, (String, PasskeyV4)>,
160    passkeys_state: CredentialState,
161
162    // Attested Passkeys
163    attested_passkeys: BTreeMap<Uuid, (String, AttestedPasskeyV4)>,
164    attested_passkeys_state: CredentialState,
165
166    // Internal reg state of any inprogress totp or webauthn credentials.
167    mfaregstate: MfaRegState,
168}
169
170impl fmt::Debug for CredentialUpdateSession {
171    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
172        let primary: Option<CredentialDetail> = self.primary.as_ref().map(|c| c.into());
173        let passkeys: Vec<PasskeyDetail> = self
174            .passkeys
175            .iter()
176            .map(|(uuid, (tag, _pk))| PasskeyDetail {
177                tag: tag.clone(),
178                uuid: *uuid,
179            })
180            .collect();
181        let attested_passkeys: Vec<PasskeyDetail> = self
182            .attested_passkeys
183            .iter()
184            .map(|(uuid, (tag, _pk))| PasskeyDetail {
185                tag: tag.clone(),
186                uuid: *uuid,
187            })
188            .collect();
189        f.debug_struct("CredentialUpdateSession")
190            .field("account.spn", &self.account.spn)
191            .field("account.unix", &self.account.unix_extn().is_some())
192            .field("resolved_account_policy", &self.resolved_account_policy)
193            .field("intent_token_id", &self.intent_token_id)
194            .field("primary.detail()", &primary)
195            .field("primary.state", &self.primary_state)
196            .field("passkeys.list()", &passkeys)
197            .field("passkeys.state", &self.passkeys_state)
198            .field("attested_passkeys.list()", &attested_passkeys)
199            .field("attested_passkeys.state", &self.attested_passkeys_state)
200            .field("mfaregstate", &self.mfaregstate)
201            .finish()
202    }
203}
204
205impl CredentialUpdateSession {
206    // Vec of the issues with the current session so that UI's can highlight properly how to proceed.
207    fn can_commit(&self) -> (bool, Vec<CredentialUpdateSessionStatusWarnings>) {
208        let mut warnings = Vec::with_capacity(0);
209        let mut can_proceed = true;
210
211        let cred_type_min = self.resolved_account_policy.credential_policy();
212
213        debug!(?cred_type_min);
214
215        match cred_type_min {
216            CredentialType::Any => {}
217            CredentialType::Mfa => {
218                if self
219                    .primary
220                    .as_ref()
221                    .map(|cred| !cred.is_mfa())
222                    // If it's none, then we can proceed because we satisfy mfa on other
223                    // parts.
224                    .unwrap_or(false)
225                {
226                    can_proceed = false;
227                    warnings.push(CredentialUpdateSessionStatusWarnings::MfaRequired);
228                }
229            }
230            CredentialType::Passkey => {
231                // NOTE: Technically this is unreachable, but we keep it for correctness.
232                // Primary can't be set at all.
233                if self.primary.is_some() {
234                    can_proceed = false;
235                    warnings.push(CredentialUpdateSessionStatusWarnings::PasskeyRequired);
236                }
237            }
238            CredentialType::AttestedPasskey => {
239                // Also unreachable - during these sessions, there will be no values present here.
240                if !self.passkeys.is_empty() || self.primary.is_some() {
241                    can_proceed = false;
242                    warnings.push(CredentialUpdateSessionStatusWarnings::AttestedPasskeyRequired);
243                }
244            }
245            CredentialType::AttestedResidentkey => {
246                // Also unreachable - during these sessions, there will be no values present here.
247                if !self.attested_passkeys.is_empty()
248                    || !self.passkeys.is_empty()
249                    || self.primary.is_some()
250                {
251                    can_proceed = false;
252                    warnings
253                        .push(CredentialUpdateSessionStatusWarnings::AttestedResidentKeyRequired);
254                }
255            }
256            CredentialType::Invalid => {
257                // special case, must always deny all changes.
258                can_proceed = false;
259                warnings.push(CredentialUpdateSessionStatusWarnings::Unsatisfiable)
260            }
261        }
262
263        if let Some(att_ca_list) = self.resolved_account_policy.webauthn_attestation_ca_list() {
264            if att_ca_list.is_empty() {
265                warnings
266                    .push(CredentialUpdateSessionStatusWarnings::WebauthnAttestationUnsatisfiable)
267            }
268        }
269
270        (can_proceed, warnings)
271    }
272}
273
274pub enum MfaRegStateStatus {
275    // Nothing in progress.
276    None,
277    TotpCheck(TotpSecret),
278    TotpTryAgain,
279    TotpNameTryAgain(String),
280    TotpInvalidSha1,
281    BackupCodes(HashSet<String>),
282    Passkey(CreationChallengeResponse),
283    AttestedPasskey(CreationChallengeResponse),
284}
285
286impl fmt::Debug for MfaRegStateStatus {
287    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
288        let t = match self {
289            MfaRegStateStatus::None => "MfaRegStateStatus::None",
290            MfaRegStateStatus::TotpCheck(_) => "MfaRegStateStatus::TotpCheck",
291            MfaRegStateStatus::TotpTryAgain => "MfaRegStateStatus::TotpTryAgain",
292            MfaRegStateStatus::TotpNameTryAgain(_) => "MfaRegStateStatus::TotpNameTryAgain",
293            MfaRegStateStatus::TotpInvalidSha1 => "MfaRegStateStatus::TotpInvalidSha1",
294            MfaRegStateStatus::BackupCodes(_) => "MfaRegStateStatus::BackupCodes",
295            MfaRegStateStatus::Passkey(_) => "MfaRegStateStatus::Passkey",
296            MfaRegStateStatus::AttestedPasskey(_) => "MfaRegStateStatus::AttestedPasskey",
297        };
298        write!(f, "{t}")
299    }
300}
301
302#[derive(Debug, PartialEq, Eq, Clone, Copy)]
303pub enum CredentialUpdateSessionStatusWarnings {
304    MfaRequired,
305    PasskeyRequired,
306    AttestedPasskeyRequired,
307    AttestedResidentKeyRequired,
308    Unsatisfiable,
309    WebauthnAttestationUnsatisfiable,
310    WebauthnUserVerificationRequired,
311}
312
313impl Display for CredentialUpdateSessionStatusWarnings {
314    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
315        write!(f, "{:?}", self)
316    }
317}
318
319impl From<CredentialUpdateSessionStatusWarnings> for CURegWarning {
320    fn from(val: CredentialUpdateSessionStatusWarnings) -> CURegWarning {
321        match val {
322            CredentialUpdateSessionStatusWarnings::MfaRequired => CURegWarning::MfaRequired,
323            CredentialUpdateSessionStatusWarnings::PasskeyRequired => CURegWarning::PasskeyRequired,
324            CredentialUpdateSessionStatusWarnings::AttestedPasskeyRequired => {
325                CURegWarning::AttestedPasskeyRequired
326            }
327            CredentialUpdateSessionStatusWarnings::AttestedResidentKeyRequired => {
328                CURegWarning::AttestedResidentKeyRequired
329            }
330            CredentialUpdateSessionStatusWarnings::Unsatisfiable => CURegWarning::Unsatisfiable,
331            CredentialUpdateSessionStatusWarnings::WebauthnAttestationUnsatisfiable => {
332                CURegWarning::WebauthnAttestationUnsatisfiable
333            }
334            CredentialUpdateSessionStatusWarnings::WebauthnUserVerificationRequired => {
335                CURegWarning::WebauthnUserVerificationRequired
336            }
337        }
338    }
339}
340
341#[derive(Debug)]
342pub struct CredentialUpdateSessionStatus {
343    spn: String,
344    // The target user's display name
345    displayname: String,
346    ext_cred_portal: CUExtPortal,
347    // Any info the client needs about mfareg state.
348    mfaregstate: MfaRegStateStatus,
349    can_commit: bool,
350    // If can_commit is false, this will have warnings populated.
351    warnings: Vec<CredentialUpdateSessionStatusWarnings>,
352    primary: Option<CredentialDetail>,
353    primary_state: CredentialState,
354    passkeys: Vec<PasskeyDetail>,
355    passkeys_state: CredentialState,
356    attested_passkeys: Vec<PasskeyDetail>,
357    attested_passkeys_state: CredentialState,
358    attested_passkeys_allowed_devices: Vec<String>,
359
360    unixcred: Option<CredentialDetail>,
361    unixcred_state: CredentialState,
362
363    sshkeys: BTreeMap<String, SshPublicKey>,
364    sshkeys_state: CredentialState,
365}
366
367impl CredentialUpdateSessionStatus {
368    /// Append a single warning to this session status, which will only be displayed to the
369    /// user once. This is different to other warnings that are derived from the state of the
370    /// session as a whole.
371    pub fn append_ephemeral_warning(&mut self, warning: CredentialUpdateSessionStatusWarnings) {
372        self.warnings.push(warning)
373    }
374
375    pub fn can_commit(&self) -> bool {
376        self.can_commit
377    }
378
379    pub fn mfaregstate(&self) -> &MfaRegStateStatus {
380        &self.mfaregstate
381    }
382}
383
384// We allow Into here because CUStatus is foreign so it's impossible for us to implement From
385// in a valid manner
386#[allow(clippy::from_over_into)]
387impl Into<CUStatus> for CredentialUpdateSessionStatus {
388    fn into(self) -> CUStatus {
389        CUStatus {
390            spn: self.spn,
391            displayname: self.displayname,
392            ext_cred_portal: self.ext_cred_portal,
393            mfaregstate: match self.mfaregstate {
394                MfaRegStateStatus::None => CURegState::None,
395                MfaRegStateStatus::TotpCheck(c) => CURegState::TotpCheck(c),
396                MfaRegStateStatus::TotpTryAgain => CURegState::TotpTryAgain,
397                MfaRegStateStatus::TotpNameTryAgain(label) => CURegState::TotpNameTryAgain(label),
398                MfaRegStateStatus::TotpInvalidSha1 => CURegState::TotpInvalidSha1,
399                MfaRegStateStatus::BackupCodes(s) => {
400                    CURegState::BackupCodes(s.into_iter().collect())
401                }
402                MfaRegStateStatus::Passkey(r) => CURegState::Passkey(r),
403                MfaRegStateStatus::AttestedPasskey(r) => CURegState::AttestedPasskey(r),
404            },
405            can_commit: self.can_commit,
406            warnings: self.warnings.into_iter().map(|w| w.into()).collect(),
407            primary: self.primary,
408            primary_state: self.primary_state.into(),
409            passkeys: self.passkeys,
410            passkeys_state: self.passkeys_state.into(),
411            attested_passkeys: self.attested_passkeys,
412            attested_passkeys_state: self.attested_passkeys_state.into(),
413            attested_passkeys_allowed_devices: self.attested_passkeys_allowed_devices,
414            unixcred: self.unixcred,
415            unixcred_state: self.unixcred_state.into(),
416            sshkeys: self.sshkeys,
417            sshkeys_state: self.sshkeys_state.into(),
418        }
419    }
420}
421
422impl From<&CredentialUpdateSession> for CredentialUpdateSessionStatus {
423    fn from(session: &CredentialUpdateSession) -> Self {
424        let (can_commit, warnings) = session.can_commit();
425
426        let attested_passkeys_allowed_devices: Vec<String> = session
427            .resolved_account_policy
428            .webauthn_attestation_ca_list()
429            .iter()
430            .flat_map(|att_ca_list: &&webauthn_rs::prelude::AttestationCaList| {
431                att_ca_list.cas().values().flat_map(|ca| {
432                    ca.aaguids()
433                        .values()
434                        .map(|device| device.description_en().to_string())
435                })
436            })
437            .collect();
438
439        CredentialUpdateSessionStatus {
440            spn: session.account.spn.clone(),
441            displayname: session.account.displayname.clone(),
442            ext_cred_portal: session.ext_cred_portal.clone(),
443            can_commit,
444            warnings,
445            primary: session.primary.as_ref().map(|c| c.into()),
446            primary_state: session.primary_state,
447            passkeys: session
448                .passkeys
449                .iter()
450                .map(|(uuid, (tag, _pk))| PasskeyDetail {
451                    tag: tag.clone(),
452                    uuid: *uuid,
453                })
454                .collect(),
455            passkeys_state: session.passkeys_state,
456            attested_passkeys: session
457                .attested_passkeys
458                .iter()
459                .map(|(uuid, (tag, _pk))| PasskeyDetail {
460                    tag: tag.clone(),
461                    uuid: *uuid,
462                })
463                .collect(),
464            attested_passkeys_state: session.attested_passkeys_state,
465            attested_passkeys_allowed_devices,
466
467            unixcred: session.unixcred.as_ref().map(|c| c.into()),
468            unixcred_state: session.unixcred_state,
469
470            sshkeys: session.sshkeys.clone(),
471            sshkeys_state: session.sshkeys_state,
472
473            mfaregstate: match &session.mfaregstate {
474                MfaRegState::None => MfaRegStateStatus::None,
475                MfaRegState::TotpInit(token) => MfaRegStateStatus::TotpCheck(
476                    token.to_proto(session.account.name.as_str(), session.issuer.as_str()),
477                ),
478                MfaRegState::TotpNameTryAgain(_, name) => {
479                    MfaRegStateStatus::TotpNameTryAgain(name.clone())
480                }
481                MfaRegState::TotpTryAgain(_) => MfaRegStateStatus::TotpTryAgain,
482                MfaRegState::TotpInvalidSha1(_, _, _) => MfaRegStateStatus::TotpInvalidSha1,
483                MfaRegState::Passkey(r, _) => MfaRegStateStatus::Passkey(r.as_ref().clone()),
484                MfaRegState::AttestedPasskey(r, _) => {
485                    MfaRegStateStatus::AttestedPasskey(r.as_ref().clone())
486                }
487            },
488        }
489    }
490}
491
492pub(crate) type CredentialUpdateSessionMutex = Arc<Mutex<CredentialUpdateSession>>;
493
494pub struct InitCredentialUpdateIntentEvent {
495    // Who initiated this?
496    pub ident: Identity,
497    // Who is it targeting?
498    pub target: Uuid,
499    // How long is it valid for?
500    pub max_ttl: Option<Duration>,
501}
502
503impl InitCredentialUpdateIntentEvent {
504    pub fn new(ident: Identity, target: Uuid, max_ttl: Option<Duration>) -> Self {
505        InitCredentialUpdateIntentEvent {
506            ident,
507            target,
508            max_ttl,
509        }
510    }
511
512    #[cfg(test)]
513    pub fn new_impersonate_entry(
514        e: std::sync::Arc<Entry<EntrySealed, EntryCommitted>>,
515        target: Uuid,
516        max_ttl: Duration,
517    ) -> Self {
518        let ident = Identity::from_impersonate_entry_readwrite(e);
519        InitCredentialUpdateIntentEvent {
520            ident,
521            target,
522            max_ttl: Some(max_ttl),
523        }
524    }
525}
526
527pub struct InitCredentialUpdateEvent {
528    pub ident: Identity,
529    pub target: Uuid,
530}
531
532impl InitCredentialUpdateEvent {
533    pub fn new(ident: Identity, target: Uuid) -> Self {
534        InitCredentialUpdateEvent { ident, target }
535    }
536
537    #[cfg(test)]
538    pub fn new_impersonate_entry(e: std::sync::Arc<Entry<EntrySealed, EntryCommitted>>) -> Self {
539        let ident = Identity::from_impersonate_entry_readwrite(e);
540
541        let target = ident
542            .get_uuid()
543            .ok_or(OperationError::InvalidState)
544            .expect("Identity has no uuid associated");
545        InitCredentialUpdateEvent { ident, target }
546    }
547}
548
549impl IdmServerProxyWriteTransaction<'_> {
550    fn validate_init_credential_update(
551        &mut self,
552        target: Uuid,
553        ident: &Identity,
554    ) -> Result<(Account, ResolvedAccountPolicy, CredUpdateSessionPerms), OperationError> {
555        let entry = self.qs_write.internal_search_uuid(target)?;
556
557        security_info!(
558            %target,
559            "Initiating Credential Update Session",
560        );
561
562        // The initiating identity must be in readwrite mode! Effective permission assumes you
563        // are in rw.
564        if ident.access_scope() != AccessScope::ReadWrite {
565            security_access!("identity access scope is not permitted to modify");
566            security_access!("denied ❌");
567            return Err(OperationError::AccessDenied);
568        }
569
570        // Is target an account? This checks for us.
571        let (account, resolved_account_policy) =
572            Account::try_from_entry_with_policy(entry.as_ref(), &mut self.qs_write)?;
573
574        let effective_perms = self
575            .qs_write
576            .get_accesscontrols()
577            .effective_permission_check(
578                ident,
579                Some(btreeset![
580                    Attribute::PrimaryCredential,
581                    Attribute::PassKeys,
582                    Attribute::AttestedPasskeys,
583                    Attribute::UnixPassword,
584                    Attribute::SshPublicKey
585                ]),
586                &[entry],
587            )?;
588
589        let eperm = effective_perms.first().ok_or_else(|| {
590            error!("Effective Permission check returned no results");
591            OperationError::InvalidState
592        })?;
593
594        // Does the ident have permission to modify AND search the user-credentials of the target, given
595        // the current status of it's authentication?
596
597        if eperm.target != account.uuid {
598            error!("Effective Permission check target differs from requested entry uuid");
599            return Err(OperationError::InvalidEntryState);
600        }
601
602        let eperm_search_primary_cred = match &eperm.search {
603            Access::Deny => false,
604            Access::Grant => true,
605            Access::Allow(attrs) => attrs.contains(&Attribute::PrimaryCredential),
606        };
607
608        let eperm_mod_primary_cred = match &eperm.modify_pres {
609            Access::Deny => false,
610            Access::Grant => true,
611            Access::Allow(attrs) => attrs.contains(&Attribute::PrimaryCredential),
612        };
613
614        let eperm_rem_primary_cred = match &eperm.modify_rem {
615            Access::Deny => false,
616            Access::Grant => true,
617            Access::Allow(attrs) => attrs.contains(&Attribute::PrimaryCredential),
618        };
619
620        let primary_can_edit =
621            eperm_search_primary_cred && eperm_mod_primary_cred && eperm_rem_primary_cred;
622
623        let eperm_search_passkeys = match &eperm.search {
624            Access::Deny => false,
625            Access::Grant => true,
626            Access::Allow(attrs) => attrs.contains(&Attribute::PassKeys),
627        };
628
629        let eperm_mod_passkeys = match &eperm.modify_pres {
630            Access::Deny => false,
631            Access::Grant => true,
632            Access::Allow(attrs) => attrs.contains(&Attribute::PassKeys),
633        };
634
635        let eperm_rem_passkeys = match &eperm.modify_rem {
636            Access::Deny => false,
637            Access::Grant => true,
638            Access::Allow(attrs) => attrs.contains(&Attribute::PassKeys),
639        };
640
641        let passkeys_can_edit = eperm_search_passkeys && eperm_mod_passkeys && eperm_rem_passkeys;
642
643        let eperm_search_attested_passkeys = match &eperm.search {
644            Access::Deny => false,
645            Access::Grant => true,
646            Access::Allow(attrs) => attrs.contains(&Attribute::AttestedPasskeys),
647        };
648
649        let eperm_mod_attested_passkeys = match &eperm.modify_pres {
650            Access::Deny => false,
651            Access::Grant => true,
652            Access::Allow(attrs) => attrs.contains(&Attribute::AttestedPasskeys),
653        };
654
655        let eperm_rem_attested_passkeys = match &eperm.modify_rem {
656            Access::Deny => false,
657            Access::Grant => true,
658            Access::Allow(attrs) => attrs.contains(&Attribute::AttestedPasskeys),
659        };
660
661        let attested_passkeys_can_edit = eperm_search_attested_passkeys
662            && eperm_mod_attested_passkeys
663            && eperm_rem_attested_passkeys;
664
665        let eperm_search_unixcred = match &eperm.search {
666            Access::Deny => false,
667            Access::Grant => true,
668            Access::Allow(attrs) => attrs.contains(&Attribute::UnixPassword),
669        };
670
671        let eperm_mod_unixcred = match &eperm.modify_pres {
672            Access::Deny => false,
673            Access::Grant => true,
674            Access::Allow(attrs) => attrs.contains(&Attribute::UnixPassword),
675        };
676
677        let eperm_rem_unixcred = match &eperm.modify_rem {
678            Access::Deny => false,
679            Access::Grant => true,
680            Access::Allow(attrs) => attrs.contains(&Attribute::UnixPassword),
681        };
682
683        let unixcred_can_edit = account.unix_extn().is_some()
684            && eperm_search_unixcred
685            && eperm_mod_unixcred
686            && eperm_rem_unixcred;
687
688        let eperm_search_sshpubkey = match &eperm.search {
689            Access::Deny => false,
690            Access::Grant => true,
691            Access::Allow(attrs) => attrs.contains(&Attribute::SshPublicKey),
692        };
693
694        let eperm_mod_sshpubkey = match &eperm.modify_pres {
695            Access::Deny => false,
696            Access::Grant => true,
697            Access::Allow(attrs) => attrs.contains(&Attribute::SshPublicKey),
698        };
699
700        let eperm_rem_sshpubkey = match &eperm.modify_rem {
701            Access::Deny => false,
702            Access::Grant => true,
703            Access::Allow(attrs) => attrs.contains(&Attribute::SshPublicKey),
704        };
705
706        let sshpubkey_can_edit = account.unix_extn().is_some()
707            && eperm_search_sshpubkey
708            && eperm_mod_sshpubkey
709            && eperm_rem_sshpubkey;
710
711        let ext_cred_portal_can_view = if let Some(sync_parent_uuid) = account.sync_parent_uuid {
712            // In theory this is always granted due to how access controls work, but we check anyway.
713            let entry = self.qs_write.internal_search_uuid(sync_parent_uuid)?;
714
715            let effective_perms = self
716                .qs_write
717                .get_accesscontrols()
718                .effective_permission_check(
719                    ident,
720                    Some(btreeset![Attribute::SyncCredentialPortal]),
721                    &[entry],
722                )?;
723
724            let eperm = effective_perms.first().ok_or_else(|| {
725                admin_error!("Effective Permission check returned no results");
726                OperationError::InvalidState
727            })?;
728
729            match &eperm.search {
730                Access::Deny => false,
731                Access::Grant => true,
732                Access::Allow(attrs) => attrs.contains(&Attribute::SyncCredentialPortal),
733            }
734        } else {
735            false
736        };
737
738        // At lease *one* must be modifiable OR visible.
739        if !(primary_can_edit
740            || passkeys_can_edit
741            || attested_passkeys_can_edit
742            || ext_cred_portal_can_view
743            || sshpubkey_can_edit
744            || unixcred_can_edit)
745        {
746            error!("Unable to proceed with credential update intent - at least one type of credential must be modifiable or visible.");
747            Err(OperationError::NotAuthorised)
748        } else {
749            security_info!(%primary_can_edit, %passkeys_can_edit, %unixcred_can_edit, %sshpubkey_can_edit, %ext_cred_portal_can_view, "Proceeding");
750            Ok((
751                account,
752                resolved_account_policy,
753                CredUpdateSessionPerms {
754                    ext_cred_portal_can_view,
755                    passkeys_can_edit,
756                    attested_passkeys_can_edit,
757                    primary_can_edit,
758                    unixcred_can_edit,
759                    sshpubkey_can_edit,
760                },
761            ))
762        }
763    }
764
765    fn create_credupdate_session(
766        &mut self,
767        sessionid: Uuid,
768        intent_token_id: Option<String>,
769        account: Account,
770        resolved_account_policy: ResolvedAccountPolicy,
771        perms: CredUpdateSessionPerms,
772        ct: Duration,
773    ) -> Result<(CredentialUpdateSessionToken, CredentialUpdateSessionStatus), OperationError> {
774        let ext_cred_portal_can_view = perms.ext_cred_portal_can_view;
775
776        let cred_type_min = resolved_account_policy.credential_policy();
777
778        // We can't decide this based on CredentialType alone since we may have CredentialType::Mfa
779        // and still need attestation. As a result, we have to decide this based on presence of
780        // the attestation policy.
781        let passkey_attestation_required = resolved_account_policy
782            .webauthn_attestation_ca_list()
783            .is_some();
784
785        let primary_state = if cred_type_min > CredentialType::Mfa {
786            CredentialState::PolicyDeny
787        } else if perms.primary_can_edit {
788            CredentialState::Modifiable
789        } else {
790            CredentialState::AccessDeny
791        };
792
793        let passkeys_state =
794            if cred_type_min > CredentialType::Passkey || passkey_attestation_required {
795                CredentialState::PolicyDeny
796            } else if perms.passkeys_can_edit {
797                CredentialState::Modifiable
798            } else {
799                CredentialState::AccessDeny
800            };
801
802        let attested_passkeys_state = if cred_type_min > CredentialType::AttestedPasskey {
803            CredentialState::PolicyDeny
804        } else if perms.attested_passkeys_can_edit {
805            if passkey_attestation_required {
806                CredentialState::Modifiable
807            } else {
808                // User can only delete, no police available to add more keys.
809                CredentialState::DeleteOnly
810            }
811        } else {
812            CredentialState::AccessDeny
813        };
814
815        let unixcred_state = if account.unix_extn().is_none() {
816            CredentialState::PolicyDeny
817        } else if perms.unixcred_can_edit {
818            CredentialState::Modifiable
819        } else {
820            CredentialState::AccessDeny
821        };
822
823        let sshkeys_state = if perms.sshpubkey_can_edit {
824            CredentialState::Modifiable
825        } else {
826            CredentialState::AccessDeny
827        };
828
829        // - stash the current state of all associated credentials
830        let primary = if matches!(primary_state, CredentialState::Modifiable) {
831            account.primary.clone()
832        } else {
833            None
834        };
835
836        let passkeys = if matches!(passkeys_state, CredentialState::Modifiable) {
837            account.passkeys.clone()
838        } else {
839            BTreeMap::default()
840        };
841
842        let unixcred: Option<Credential> = if matches!(unixcred_state, CredentialState::Modifiable)
843        {
844            account.unix_extn().and_then(|uext| uext.ucred()).cloned()
845        } else {
846            None
847        };
848
849        let sshkeys = if matches!(sshkeys_state, CredentialState::Modifiable) {
850            account.sshkeys().clone()
851        } else {
852            BTreeMap::default()
853        };
854
855        // Before we start, we pre-filter out anything that no longer conforms to policy.
856        // These would already be failing authentication, so they should have the appearance
857        // of "being removed".
858        let attested_passkeys = if matches!(attested_passkeys_state, CredentialState::Modifiable)
859            || matches!(attested_passkeys_state, CredentialState::DeleteOnly)
860        {
861            if let Some(att_ca_list) = resolved_account_policy.webauthn_attestation_ca_list() {
862                let mut attested_passkeys = BTreeMap::default();
863
864                for (uuid, (label, apk)) in account.attested_passkeys.iter() {
865                    match apk.verify_attestation(att_ca_list) {
866                        Ok(_) => {
867                            // Good to go
868                            attested_passkeys.insert(*uuid, (label.clone(), apk.clone()));
869                        }
870                        Err(e) => {
871                            warn!(eclass=?e, emsg=%e, "credential no longer meets attestation criteria");
872                        }
873                    }
874                }
875
876                attested_passkeys
877            } else {
878                // Seems weird here to be skipping filtering of the credentials. The reason is that
879                // if an account had registered attested passkeys in the past we can delete them, but
880                // not add new ones. Situation only occurs when policy isn't present on the account.
881                account.attested_passkeys.clone()
882            }
883        } else {
884            BTreeMap::default()
885        };
886
887        // Get the external credential portal, if any.
888        let ext_cred_portal = match (account.sync_parent_uuid, ext_cred_portal_can_view) {
889            (Some(sync_parent_uuid), true) => {
890                let sync_entry = self.qs_write.internal_search_uuid(sync_parent_uuid)?;
891                sync_entry
892                    .get_ava_single_url(Attribute::SyncCredentialPortal)
893                    .cloned()
894                    .map(CUExtPortal::Some)
895                    .unwrap_or(CUExtPortal::Hidden)
896            }
897            (Some(_), false) => CUExtPortal::Hidden,
898            (None, _) => CUExtPortal::None,
899        };
900
901        // Stash the issuer for some UI elements
902        let issuer = self.qs_write.get_domain_display_name().to_string();
903
904        // - store account policy (if present)
905        let session = CredentialUpdateSession {
906            account,
907            resolved_account_policy,
908            issuer,
909            intent_token_id,
910            ext_cred_portal,
911            primary,
912            primary_state,
913            unixcred,
914            unixcred_state,
915            sshkeys,
916            sshkeys_state,
917            passkeys,
918            passkeys_state,
919            attested_passkeys,
920            attested_passkeys_state,
921            mfaregstate: MfaRegState::None,
922        };
923
924        let max_ttl = ct + MAXIMUM_CRED_UPDATE_TTL;
925
926        let token = CredentialUpdateSessionTokenInner { sessionid, max_ttl };
927
928        let token_data = serde_json::to_vec(&token).map_err(|e| {
929            admin_error!(err = ?e, "Unable to encode token data");
930            OperationError::SerdeJsonError
931        })?;
932
933        let token_jwe = JweBuilder::from(token_data).build();
934
935        let token_enc = self
936            .qs_write
937            .get_domain_key_object_handle()?
938            .jwe_a128gcm_encrypt(&token_jwe, ct)?;
939
940        let status: CredentialUpdateSessionStatus = (&session).into();
941
942        let session = Arc::new(Mutex::new(session));
943
944        // Point of no return
945
946        // Sneaky! Now we know it will work, prune old sessions.
947        self.expire_credential_update_sessions(ct);
948
949        // Store the update session into the map.
950        self.cred_update_sessions.insert(sessionid, session);
951        trace!("cred_update_sessions.insert - {}", sessionid);
952
953        // - issue the CredentialUpdateToken (enc)
954        Ok((CredentialUpdateSessionToken { token_enc }, status))
955    }
956
957    #[instrument(level = "debug", skip_all)]
958    pub fn init_credential_update_intent(
959        &mut self,
960        event: &InitCredentialUpdateIntentEvent,
961        ct: Duration,
962    ) -> Result<CredentialUpdateIntentToken, OperationError> {
963        let (account, _resolved_account_policy, perms) =
964            self.validate_init_credential_update(event.target, &event.ident)?;
965
966        // We should check in the acc-pol if we can proceed?
967        // Is there a reason account policy might deny us from proceeding?
968
969        // ==== AUTHORISATION CHECKED ===
970
971        // Build the intent token. Previously this was using 0 and then
972        // relying on clamp to raise this to 5 minutes, but that led to
973        // rapid timeouts that affected some users.
974        let mttl = event.max_ttl.unwrap_or(DEFAULT_INTENT_TTL);
975        let clamped_mttl = mttl.clamp(MINIMUM_INTENT_TTL, MAXIMUM_INTENT_TTL);
976        debug!(?clamped_mttl, "clamped update intent validity");
977        // Absolute expiry of the intent token in epoch seconds
978        let max_ttl = ct + clamped_mttl;
979
980        // Get the expiry of the intent token as an odt.
981        let expiry_time = OffsetDateTime::UNIX_EPOCH + max_ttl;
982
983        let intent_id = readable_password_from_random();
984
985        // Mark that we have created an intent token on the user.
986        // ⚠️   -- remember, there is a risk, very low, but still a risk of collision of the intent_id.
987        //        instead of enforcing unique, which would divulge that the collision occurred, we
988        //        write anyway, and instead on the intent access path we invalidate IF the collision
989        //        occurs.
990        let mut modlist = ModifyList::new_append(
991            Attribute::CredentialUpdateIntentToken,
992            Value::IntentToken(
993                intent_id.clone(),
994                IntentTokenState::Valid { max_ttl, perms },
995            ),
996        );
997
998        // Remove any old credential update intents
999        account
1000            .credential_update_intent_tokens
1001            .iter()
1002            .for_each(|(existing_intent_id, state)| {
1003                let max_ttl = match state {
1004                    IntentTokenState::Valid { max_ttl, perms: _ }
1005                    | IntentTokenState::InProgress {
1006                        max_ttl,
1007                        perms: _,
1008                        session_id: _,
1009                        session_ttl: _,
1010                    }
1011                    | IntentTokenState::Consumed { max_ttl } => *max_ttl,
1012                };
1013
1014                if ct >= max_ttl {
1015                    modlist.push_mod(Modify::Removed(
1016                        Attribute::CredentialUpdateIntentToken,
1017                        PartialValue::IntentToken(existing_intent_id.clone()),
1018                    ));
1019                }
1020            });
1021
1022        self.qs_write
1023            .internal_modify(
1024                // Filter as executed
1025                &filter!(f_eq(Attribute::Uuid, PartialValue::Uuid(account.uuid))),
1026                &modlist,
1027            )
1028            .map_err(|e| {
1029                request_error!(error = ?e);
1030                e
1031            })?;
1032
1033        Ok(CredentialUpdateIntentToken {
1034            intent_id,
1035            expiry_time,
1036        })
1037    }
1038
1039    pub fn exchange_intent_credential_update(
1040        &mut self,
1041        token: CredentialUpdateIntentTokenExchange,
1042        current_time: Duration,
1043    ) -> Result<(CredentialUpdateSessionToken, CredentialUpdateSessionStatus), OperationError> {
1044        let CredentialUpdateIntentTokenExchange { intent_id } = token;
1045
1046        /*
1047            let entry = self.qs_write.internal_search_uuid(&token.target)?;
1048        */
1049        // ⚠️  due to a low, but possible risk of intent_id collision, if there are multiple
1050        // entries, we will reject the intent.
1051        // DO we need to force both to "Consumed" in this step?
1052        //
1053        // ⚠️  If not present, it may be due to replication delay. We can report this.
1054
1055        let mut vs = self.qs_write.internal_search(filter!(f_eq(
1056            Attribute::CredentialUpdateIntentToken,
1057            PartialValue::IntentToken(intent_id.clone())
1058        )))?;
1059
1060        let entry = match vs.pop() {
1061            Some(entry) => {
1062                if vs.is_empty() {
1063                    // Happy Path!
1064                    entry
1065                } else {
1066                    // Multiple entries matched! This is bad!
1067                    let matched_uuids = std::iter::once(entry.get_uuid())
1068                        .chain(vs.iter().map(|e| e.get_uuid()))
1069                        .collect::<Vec<_>>();
1070
1071                    security_error!("Multiple entries had identical intent_id - for safety, rejecting the use of this intent_id! {:?}", matched_uuids);
1072
1073                    /*
1074                    let mut modlist = ModifyList::new();
1075
1076                    modlist.push_mod(Modify::Removed(
1077                        Attribute::CredentialUpdateIntentToken.into(),
1078                        PartialValue::IntentToken(intent_id.clone()),
1079                    ));
1080
1081                    let filter_or = matched_uuids.into_iter()
1082                        .map(|u| f_eq(Attribute::Uuid, PartialValue::new_uuid(u)))
1083                        .collect();
1084
1085                    self.qs_write
1086                        .internal_modify(
1087                            // Filter as executed
1088                            &filter!(f_or(filter_or)),
1089                            &modlist,
1090                        )
1091                        .map_err(|e| {
1092                            request_error!(error = ?e);
1093                            e
1094                        })?;
1095                    */
1096
1097                    return Err(OperationError::InvalidState);
1098                }
1099            }
1100            None => {
1101                security_info!(
1102                    "Rejecting Update Session - Intent Token does not exist (replication delay?)",
1103                );
1104                return Err(OperationError::Wait(
1105                    OffsetDateTime::UNIX_EPOCH + (current_time + Duration::from_secs(150)),
1106                ));
1107            }
1108        };
1109
1110        // Is target an account? This checks for us.
1111        let (account, resolved_account_policy) =
1112            Account::try_from_entry_with_policy(entry.as_ref(), &mut self.qs_write)?;
1113
1114        // Check there is not already a user session in progress with this intent token.
1115        // Is there a need to revoke intent tokens?
1116
1117        let (max_ttl, perms) = match account.credential_update_intent_tokens.get(&intent_id) {
1118            Some(IntentTokenState::Consumed { max_ttl: _ }) => {
1119                security_info!(
1120                    %entry,
1121                    %account.uuid,
1122                    "Rejecting Update Session - Intent Token has already been exchanged",
1123                );
1124                return Err(OperationError::SessionExpired);
1125            }
1126            Some(IntentTokenState::InProgress {
1127                max_ttl,
1128                perms,
1129                session_id,
1130                session_ttl,
1131            }) => {
1132                if current_time > *session_ttl {
1133                    // The former session has expired, continue.
1134                    security_info!(
1135                        %entry,
1136                        %account.uuid,
1137                        "Initiating Credential Update Session - Previous session {} has expired", session_id
1138                    );
1139                } else {
1140                    // The former session has been orphaned while in use. This can be from someone
1141                    // ctrl-c during their use of the session or refreshing the page without committing.
1142                    //
1143                    // we don't try to exclusive lock the token here with the current time as we previously
1144                    // did. This is because with async replication, there isn't a guarantee this will actually
1145                    // be sent to another server "soon enough" to prevent abuse on the separate server. So
1146                    // all this "lock" actually does is annoy legitimate users and not stop abuse. We
1147                    // STILL keep the InProgress state though since we check it on commit, so this
1148                    // forces the previous orphan session to be immediately invalidated!
1149                    security_info!(
1150                        %entry,
1151                        %account.uuid,
1152                        "Initiating Update Session - Intent Token was in use {} - this will be invalidated.", session_id
1153                    );
1154                };
1155                (*max_ttl, *perms)
1156            }
1157            Some(IntentTokenState::Valid { max_ttl, perms }) => (*max_ttl, *perms),
1158            None => {
1159                admin_error!("Corruption may have occurred - index yielded an entry for intent_id, but the entry does not contain that intent_id");
1160                return Err(OperationError::InvalidState);
1161            }
1162        };
1163
1164        if current_time >= max_ttl {
1165            security_info!(?current_time, ?max_ttl, %account.uuid, "intent has expired");
1166            return Err(OperationError::SessionExpired);
1167        }
1168
1169        security_info!(
1170            %entry,
1171            %account.uuid,
1172            "Initiating Credential Update Session",
1173        );
1174
1175        // To prevent issues with repl, we need to associate this cred update session id, with
1176        // this intent token id.
1177
1178        // Store the intent id in the session (if needed) so that we can check the state at the
1179        // end of the update.
1180
1181        // We need to pin the id from the intent token into the credential to ensure it's not reused
1182
1183        // Need to change this to the expiry time, so we can purge up to.
1184        let session_id = uuid_from_duration(current_time + MAXIMUM_CRED_UPDATE_TTL, self.sid);
1185
1186        let mut modlist = ModifyList::new();
1187
1188        modlist.push_mod(Modify::Removed(
1189            Attribute::CredentialUpdateIntentToken,
1190            PartialValue::IntentToken(intent_id.clone()),
1191        ));
1192        modlist.push_mod(Modify::Present(
1193            Attribute::CredentialUpdateIntentToken,
1194            Value::IntentToken(
1195                intent_id.clone(),
1196                IntentTokenState::InProgress {
1197                    max_ttl,
1198                    perms,
1199                    session_id,
1200                    session_ttl: current_time + MAXIMUM_CRED_UPDATE_TTL,
1201                },
1202            ),
1203        ));
1204
1205        self.qs_write
1206            .internal_modify(
1207                // Filter as executed
1208                &filter!(f_eq(Attribute::Uuid, PartialValue::Uuid(account.uuid))),
1209                &modlist,
1210            )
1211            .map_err(|e| {
1212                request_error!(error = ?e);
1213                e
1214            })?;
1215
1216        // ==========
1217        // Okay, good to exchange.
1218
1219        self.create_credupdate_session(
1220            session_id,
1221            Some(intent_id),
1222            account,
1223            resolved_account_policy,
1224            perms,
1225            current_time,
1226        )
1227    }
1228
1229    #[instrument(level = "debug", skip_all)]
1230    pub fn init_credential_update(
1231        &mut self,
1232        event: &InitCredentialUpdateEvent,
1233        current_time: Duration,
1234    ) -> Result<(CredentialUpdateSessionToken, CredentialUpdateSessionStatus), OperationError> {
1235        let (account, resolved_account_policy, perms) =
1236            self.validate_init_credential_update(event.target, &event.ident)?;
1237
1238        // ==== AUTHORISATION CHECKED ===
1239        // This is the expiry time, so that our cleanup task can "purge up to now" rather
1240        // than needing to do calculations.
1241        let sessionid = uuid_from_duration(current_time + MAXIMUM_CRED_UPDATE_TTL, self.sid);
1242
1243        // Build the cred update session.
1244        self.create_credupdate_session(
1245            sessionid,
1246            None,
1247            account,
1248            resolved_account_policy,
1249            perms,
1250            current_time,
1251        )
1252    }
1253
1254    #[instrument(level = "trace", skip(self))]
1255    pub fn expire_credential_update_sessions(&mut self, ct: Duration) {
1256        let before = self.cred_update_sessions.len();
1257        let split_at = uuid_from_duration(ct, self.sid);
1258        trace!(?split_at, "expiring less than");
1259        self.cred_update_sessions.split_off_lt(&split_at);
1260        let removed = before - self.cred_update_sessions.len();
1261        trace!(?removed);
1262    }
1263
1264    // This shares some common paths between commit and cancel.
1265    fn credential_update_commit_common(
1266        &mut self,
1267        cust: &CredentialUpdateSessionToken,
1268        ct: Duration,
1269    ) -> Result<
1270        (
1271            ModifyList<ModifyInvalid>,
1272            CredentialUpdateSession,
1273            CredentialUpdateSessionTokenInner,
1274        ),
1275        OperationError,
1276    > {
1277        let session_token: CredentialUpdateSessionTokenInner = self
1278            .qs_write
1279            .get_domain_key_object_handle()?
1280            .jwe_decrypt(&cust.token_enc)
1281            .map_err(|e| {
1282                admin_error!(?e, "Failed to decrypt credential update session request");
1283                OperationError::SessionExpired
1284            })
1285            .and_then(|data| {
1286                data.from_json().map_err(|e| {
1287                    admin_error!(err = ?e, "Failed to deserialise credential update session request");
1288                    OperationError::SerdeJsonError
1289                })
1290            })?;
1291
1292        if ct >= session_token.max_ttl {
1293            trace!(?ct, ?session_token.max_ttl);
1294            security_info!(%session_token.sessionid, "session expired");
1295            return Err(OperationError::SessionExpired);
1296        }
1297
1298        let session_handle = self.cred_update_sessions.remove(&session_token.sessionid)
1299            .ok_or_else(|| {
1300                admin_error!("No such sessionid exists on this server - may be due to a load balancer failover or replay? {:?}", session_token.sessionid);
1301                OperationError::InvalidState
1302            })?;
1303
1304        let session = session_handle
1305            .try_lock()
1306            .map(|guard| (*guard).clone())
1307            .map_err(|_| {
1308                admin_error!("Session already locked, unable to proceed.");
1309                OperationError::InvalidState
1310            })?;
1311
1312        trace!(?session);
1313
1314        let modlist = ModifyList::new();
1315
1316        Ok((modlist, session, session_token))
1317    }
1318
1319    pub fn commit_credential_update(
1320        &mut self,
1321        cust: &CredentialUpdateSessionToken,
1322        ct: Duration,
1323    ) -> Result<(), OperationError> {
1324        let (mut modlist, session, session_token) =
1325            self.credential_update_commit_common(cust, ct)?;
1326
1327        // Can we actually proceed?
1328        let can_commit = session.can_commit();
1329        if !can_commit.0 {
1330            let commit_failure_reasons = can_commit
1331                .1
1332                .iter()
1333                .map(|e| e.to_string())
1334                .collect::<Vec<String>>()
1335                .join(", ");
1336            admin_error!(
1337                "Session is unable to commit due to: {}",
1338                commit_failure_reasons
1339            );
1340            return Err(OperationError::CU0004SessionInconsistent);
1341        }
1342
1343        // Setup mods for the various bits. We always assert an *exact* state.
1344
1345        // IF an intent was used on this session, AND that intent is not in our
1346        // session state as an exact match, FAIL the commit. Move the intent to "Consumed".
1347        //
1348        // Should we mark the credential as suspect (lock the account?)
1349        //
1350        // If the credential has changed, reject? Do we need "asserts" in the modlist?
1351        // that would allow better expression of this, and will allow resolving via replication
1352
1353        // If an intent token was used, remove it's former value, and add it as consumed.
1354        if let Some(intent_token_id) = &session.intent_token_id {
1355            let entry = self.qs_write.internal_search_uuid(session.account.uuid)?;
1356            let account = Account::try_from_entry_rw(entry.as_ref(), &mut self.qs_write)?;
1357
1358            let max_ttl = match account.credential_update_intent_tokens.get(intent_token_id) {
1359                Some(IntentTokenState::InProgress {
1360                    max_ttl,
1361                    perms: _,
1362                    session_id,
1363                    session_ttl: _,
1364                }) => {
1365                    if *session_id != session_token.sessionid {
1366                        security_info!("Session originated from an intent token, but the intent token has initiated a conflicting second update session. Refusing to commit changes.");
1367                        return Err(OperationError::CU0005IntentTokenConflict);
1368                    } else {
1369                        *max_ttl
1370                    }
1371                }
1372                Some(IntentTokenState::Consumed { max_ttl: _ })
1373                | Some(IntentTokenState::Valid {
1374                    max_ttl: _,
1375                    perms: _,
1376                })
1377                | None => {
1378                    security_info!("Session originated from an intent token, but the intent token has transitioned to an invalid state. Refusing to commit changes.");
1379                    return Err(OperationError::CU0006IntentTokenInvalidated);
1380                }
1381            };
1382
1383            modlist.push_mod(Modify::Removed(
1384                Attribute::CredentialUpdateIntentToken,
1385                PartialValue::IntentToken(intent_token_id.clone()),
1386            ));
1387            modlist.push_mod(Modify::Present(
1388                Attribute::CredentialUpdateIntentToken,
1389                Value::IntentToken(
1390                    intent_token_id.clone(),
1391                    IntentTokenState::Consumed { max_ttl },
1392                ),
1393            ));
1394        };
1395
1396        match session.primary_state {
1397            CredentialState::Modifiable => {
1398                modlist.push_mod(Modify::Purged(Attribute::PrimaryCredential));
1399                if let Some(ncred) = &session.primary {
1400                    let vcred = Value::new_credential("primary", ncred.clone());
1401                    modlist.push_mod(Modify::Present(Attribute::PrimaryCredential, vcred));
1402                };
1403            }
1404            CredentialState::DeleteOnly | CredentialState::PolicyDeny => {
1405                modlist.push_mod(Modify::Purged(Attribute::PrimaryCredential));
1406            }
1407            CredentialState::AccessDeny => {}
1408        };
1409
1410        match session.passkeys_state {
1411            CredentialState::DeleteOnly | CredentialState::Modifiable => {
1412                modlist.push_mod(Modify::Purged(Attribute::PassKeys));
1413                // Add all the passkeys. If none, nothing will be added! This handles
1414                // the delete case quite cleanly :)
1415                session.passkeys.iter().for_each(|(uuid, (tag, pk))| {
1416                    let v_pk = Value::Passkey(*uuid, tag.clone(), pk.clone());
1417                    modlist.push_mod(Modify::Present(Attribute::PassKeys, v_pk));
1418                });
1419            }
1420            CredentialState::PolicyDeny => {
1421                modlist.push_mod(Modify::Purged(Attribute::PassKeys));
1422            }
1423            CredentialState::AccessDeny => {}
1424        };
1425
1426        match session.attested_passkeys_state {
1427            CredentialState::DeleteOnly | CredentialState::Modifiable => {
1428                modlist.push_mod(Modify::Purged(Attribute::AttestedPasskeys));
1429                // Add all the passkeys. If none, nothing will be added! This handles
1430                // the delete case quite cleanly :)
1431                session
1432                    .attested_passkeys
1433                    .iter()
1434                    .for_each(|(uuid, (tag, pk))| {
1435                        let v_pk = Value::AttestedPasskey(*uuid, tag.clone(), pk.clone());
1436                        modlist.push_mod(Modify::Present(Attribute::AttestedPasskeys, v_pk));
1437                    });
1438            }
1439            CredentialState::PolicyDeny => {
1440                modlist.push_mod(Modify::Purged(Attribute::AttestedPasskeys));
1441            }
1442            // CredentialState::Disabled |
1443            CredentialState::AccessDeny => {}
1444        };
1445
1446        match session.unixcred_state {
1447            CredentialState::DeleteOnly | CredentialState::Modifiable => {
1448                modlist.push_mod(Modify::Purged(Attribute::UnixPassword));
1449                if let Some(ncred) = &session.unixcred {
1450                    let vcred = Value::new_credential("unix", ncred.clone());
1451                    modlist.push_mod(Modify::Present(Attribute::UnixPassword, vcred));
1452                }
1453            }
1454            CredentialState::PolicyDeny => {
1455                modlist.push_mod(Modify::Purged(Attribute::UnixPassword));
1456            }
1457            CredentialState::AccessDeny => {}
1458        };
1459
1460        match session.sshkeys_state {
1461            CredentialState::DeleteOnly | CredentialState::Modifiable => {
1462                modlist.push_mod(Modify::Purged(Attribute::SshPublicKey));
1463                for (tag, pk) in &session.sshkeys {
1464                    let v_sk = Value::SshKey(tag.clone(), pk.clone());
1465                    modlist.push_mod(Modify::Present(Attribute::SshPublicKey, v_sk));
1466                }
1467            }
1468            CredentialState::PolicyDeny => {
1469                modlist.push_mod(Modify::Purged(Attribute::SshPublicKey));
1470            }
1471            CredentialState::AccessDeny => {}
1472        };
1473
1474        // Apply to the account!
1475        trace!(?modlist, "processing change");
1476
1477        if modlist.is_empty() {
1478            trace!("no changes to apply");
1479            Ok(())
1480        } else {
1481            self.qs_write
1482                .internal_modify(
1483                    // Filter as executed
1484                    &filter!(f_eq(
1485                        Attribute::Uuid,
1486                        PartialValue::Uuid(session.account.uuid)
1487                    )),
1488                    &modlist,
1489                )
1490                .map_err(|e| {
1491                    request_error!(error = ?e);
1492                    e
1493                })
1494        }
1495    }
1496
1497    pub fn cancel_credential_update(
1498        &mut self,
1499        cust: &CredentialUpdateSessionToken,
1500        ct: Duration,
1501    ) -> Result<(), OperationError> {
1502        let (mut modlist, session, session_token) =
1503            self.credential_update_commit_common(cust, ct)?;
1504
1505        // If an intent token was used, remove it's former value, and add it as VALID since we didn't commit.
1506        if let Some(intent_token_id) = &session.intent_token_id {
1507            let entry = self.qs_write.internal_search_uuid(session.account.uuid)?;
1508            let account = Account::try_from_entry_rw(entry.as_ref(), &mut self.qs_write)?;
1509
1510            let (max_ttl, perms) = match account
1511                .credential_update_intent_tokens
1512                .get(intent_token_id)
1513            {
1514                Some(IntentTokenState::InProgress {
1515                    max_ttl,
1516                    perms,
1517                    session_id,
1518                    session_ttl: _,
1519                }) => {
1520                    if *session_id != session_token.sessionid {
1521                        security_info!("Session originated from an intent token, but the intent token has initiated a conflicting second update session. Refusing to commit changes.");
1522                        return Err(OperationError::InvalidState);
1523                    } else {
1524                        (*max_ttl, *perms)
1525                    }
1526                }
1527                Some(IntentTokenState::Consumed { max_ttl: _ })
1528                | Some(IntentTokenState::Valid {
1529                    max_ttl: _,
1530                    perms: _,
1531                })
1532                | None => {
1533                    security_info!("Session originated from an intent token, but the intent token has transitioned to an invalid state. Refusing to commit changes.");
1534                    return Err(OperationError::InvalidState);
1535                }
1536            };
1537
1538            modlist.push_mod(Modify::Removed(
1539                Attribute::CredentialUpdateIntentToken,
1540                PartialValue::IntentToken(intent_token_id.clone()),
1541            ));
1542            modlist.push_mod(Modify::Present(
1543                Attribute::CredentialUpdateIntentToken,
1544                Value::IntentToken(
1545                    intent_token_id.clone(),
1546                    IntentTokenState::Valid { max_ttl, perms },
1547                ),
1548            ));
1549        };
1550
1551        // Apply to the account!
1552        if !modlist.is_empty() {
1553            trace!(?modlist, "processing change");
1554
1555            self.qs_write
1556                .internal_modify(
1557                    // Filter as executed
1558                    &filter!(f_eq(
1559                        Attribute::Uuid,
1560                        PartialValue::Uuid(session.account.uuid)
1561                    )),
1562                    &modlist,
1563                )
1564                .map_err(|e| {
1565                    request_error!(error = ?e);
1566                    e
1567                })
1568        } else {
1569            Ok(())
1570        }
1571    }
1572}
1573
1574impl IdmServerCredUpdateTransaction<'_> {
1575    #[cfg(test)]
1576    pub fn get_origin(&self) -> &Url {
1577        &self.webauthn.get_allowed_origins()[0]
1578    }
1579
1580    fn get_current_session(
1581        &self,
1582        cust: &CredentialUpdateSessionToken,
1583        ct: Duration,
1584    ) -> Result<CredentialUpdateSessionMutex, OperationError> {
1585        let session_token: CredentialUpdateSessionTokenInner = self
1586            .qs_read
1587            .get_domain_key_object_handle()?
1588            .jwe_decrypt(&cust.token_enc)
1589            .map_err(|e| {
1590                admin_error!(?e, "Failed to decrypt credential update session request");
1591                OperationError::SessionExpired
1592            })
1593            .and_then(|data| {
1594                data.from_json().map_err(|e| {
1595                    admin_error!(err = ?e, "Failed to deserialise credential update session request");
1596                    OperationError::SerdeJsonError
1597                })
1598            })?;
1599
1600        // Check the TTL
1601        if ct >= session_token.max_ttl {
1602            trace!(?ct, ?session_token.max_ttl);
1603            security_info!(%session_token.sessionid, "session expired");
1604            return Err(OperationError::SessionExpired);
1605        }
1606
1607        self.cred_update_sessions.get(&session_token.sessionid)
1608            .ok_or_else(|| {
1609                admin_error!("No such sessionid exists on this server - may be due to a load balancer failover or token replay? {}", session_token.sessionid);
1610                OperationError::InvalidState
1611            })
1612            .cloned()
1613    }
1614
1615    // I think I need this to be a try lock instead, and fail on error, because
1616    // of the nature of the async bits.
1617    pub fn credential_update_status(
1618        &self,
1619        cust: &CredentialUpdateSessionToken,
1620        ct: Duration,
1621    ) -> Result<CredentialUpdateSessionStatus, OperationError> {
1622        let session_handle = self.get_current_session(cust, ct)?;
1623        let session = session_handle.try_lock().map_err(|_| {
1624            admin_error!("Session already locked, unable to proceed.");
1625            OperationError::InvalidState
1626        })?;
1627        trace!(?session);
1628
1629        let status: CredentialUpdateSessionStatus = session.deref().into();
1630        Ok(status)
1631    }
1632
1633    #[instrument(level = "trace", skip(self))]
1634    fn check_password_quality(
1635        &self,
1636        cleartext: &str,
1637        resolved_account_policy: &ResolvedAccountPolicy,
1638        related_inputs: &[&str],
1639        radius_secret: Option<&str>,
1640    ) -> Result<(), PasswordQuality> {
1641        // password strength and badlisting is always global, rather than per-pw-policy.
1642        // pw-policy as check on the account is about requirements for mfa for example.
1643
1644        // is the password at least 10 char?
1645        let pw_min_length = resolved_account_policy.pw_min_length();
1646        if cleartext.len() < pw_min_length as usize {
1647            return Err(PasswordQuality::TooShort(pw_min_length));
1648        }
1649
1650        if let Some(some_radius_secret) = radius_secret {
1651            if cleartext.contains(some_radius_secret) {
1652                return Err(PasswordQuality::DontReusePasswords);
1653            }
1654        }
1655
1656        // zxcvbn doesn't appear to be picking these up?
1657        for related in related_inputs {
1658            if cleartext.contains(related) {
1659                return Err(PasswordQuality::Feedback(vec![
1660                    PasswordFeedback::NamesAndSurnamesByThemselvesAreEasyToGuess,
1661                    PasswordFeedback::AvoidDatesAndYearsThatAreAssociatedWithYou,
1662                ]));
1663            }
1664        }
1665
1666        // does the password pass zxcvbn?
1667        let entropy = zxcvbn(cleartext, related_inputs);
1668
1669        // PW's should always be enforced as strong as possible.
1670        if entropy.score() < Score::Four {
1671            // The password is too week as per:
1672            // https://docs.rs/zxcvbn/2.0.0/zxcvbn/struct.Entropy.html
1673            let feedback: zxcvbn::feedback::Feedback = entropy
1674                .feedback()
1675                .ok_or(OperationError::InvalidState)
1676                .cloned()
1677                .map_err(|e| {
1678                    security_info!("zxcvbn returned no feedback when score < 3 -> {:?}", e);
1679                    // Return some generic feedback when the password is this bad.
1680                    PasswordQuality::Feedback(vec![
1681                        PasswordFeedback::UseAFewWordsAvoidCommonPhrases,
1682                        PasswordFeedback::AddAnotherWordOrTwo,
1683                        PasswordFeedback::NoNeedForSymbolsDigitsOrUppercaseLetters,
1684                    ])
1685                })?;
1686
1687            security_info!(?feedback, "pw quality feedback");
1688
1689            let feedback: Vec<_> = feedback
1690                .suggestions()
1691                .iter()
1692                .map(|s| {
1693                    match s {
1694                            zxcvbn::feedback::Suggestion::UseAFewWordsAvoidCommonPhrases => {
1695                                PasswordFeedback::UseAFewWordsAvoidCommonPhrases
1696                            }
1697                            zxcvbn::feedback::Suggestion::NoNeedForSymbolsDigitsOrUppercaseLetters => {
1698                                PasswordFeedback::NoNeedForSymbolsDigitsOrUppercaseLetters
1699                            }
1700                            zxcvbn::feedback::Suggestion::AddAnotherWordOrTwo => {
1701                                PasswordFeedback::AddAnotherWordOrTwo
1702                            }
1703                            zxcvbn::feedback::Suggestion::CapitalizationDoesntHelpVeryMuch => {
1704                                PasswordFeedback::CapitalizationDoesntHelpVeryMuch
1705                            }
1706                            zxcvbn::feedback::Suggestion::AllUppercaseIsAlmostAsEasyToGuessAsAllLowercase => {
1707                                PasswordFeedback::AllUppercaseIsAlmostAsEasyToGuessAsAllLowercase
1708                            }
1709                            zxcvbn::feedback::Suggestion::ReversedWordsArentMuchHarderToGuess => {
1710                                PasswordFeedback::ReversedWordsArentMuchHarderToGuess
1711                            }
1712                            zxcvbn::feedback::Suggestion::PredictableSubstitutionsDontHelpVeryMuch => {
1713                                PasswordFeedback::PredictableSubstitutionsDontHelpVeryMuch
1714                            }
1715                            zxcvbn::feedback::Suggestion::UseALongerKeyboardPatternWithMoreTurns => {
1716                                PasswordFeedback::UseALongerKeyboardPatternWithMoreTurns
1717                            }
1718                            zxcvbn::feedback::Suggestion::AvoidRepeatedWordsAndCharacters => {
1719                                PasswordFeedback::AvoidRepeatedWordsAndCharacters
1720                            }
1721                            zxcvbn::feedback::Suggestion::AvoidSequences => {
1722                                PasswordFeedback::AvoidSequences
1723                            }
1724                            zxcvbn::feedback::Suggestion::AvoidRecentYears => {
1725                                PasswordFeedback::AvoidRecentYears
1726                            }
1727                            zxcvbn::feedback::Suggestion::AvoidYearsThatAreAssociatedWithYou => {
1728                                PasswordFeedback::AvoidYearsThatAreAssociatedWithYou
1729                            }
1730                            zxcvbn::feedback::Suggestion::AvoidDatesAndYearsThatAreAssociatedWithYou => {
1731                                PasswordFeedback::AvoidDatesAndYearsThatAreAssociatedWithYou
1732                            }
1733                        }
1734                })
1735                .chain(feedback.warning().map(|w| match w {
1736                    zxcvbn::feedback::Warning::StraightRowsOfKeysAreEasyToGuess => {
1737                        PasswordFeedback::StraightRowsOfKeysAreEasyToGuess
1738                    }
1739                    zxcvbn::feedback::Warning::ShortKeyboardPatternsAreEasyToGuess => {
1740                        PasswordFeedback::ShortKeyboardPatternsAreEasyToGuess
1741                    }
1742                    zxcvbn::feedback::Warning::RepeatsLikeAaaAreEasyToGuess => {
1743                        PasswordFeedback::RepeatsLikeAaaAreEasyToGuess
1744                    }
1745                    zxcvbn::feedback::Warning::RepeatsLikeAbcAbcAreOnlySlightlyHarderToGuess => {
1746                        PasswordFeedback::RepeatsLikeAbcAbcAreOnlySlightlyHarderToGuess
1747                    }
1748                    zxcvbn::feedback::Warning::ThisIsATop10Password => {
1749                        PasswordFeedback::ThisIsATop10Password
1750                    }
1751                    zxcvbn::feedback::Warning::ThisIsATop100Password => {
1752                        PasswordFeedback::ThisIsATop100Password
1753                    }
1754                    zxcvbn::feedback::Warning::ThisIsACommonPassword => {
1755                        PasswordFeedback::ThisIsACommonPassword
1756                    }
1757                    zxcvbn::feedback::Warning::ThisIsSimilarToACommonlyUsedPassword => {
1758                        PasswordFeedback::ThisIsSimilarToACommonlyUsedPassword
1759                    }
1760                    zxcvbn::feedback::Warning::SequencesLikeAbcAreEasyToGuess => {
1761                        PasswordFeedback::SequencesLikeAbcAreEasyToGuess
1762                    }
1763                    zxcvbn::feedback::Warning::RecentYearsAreEasyToGuess => {
1764                        PasswordFeedback::RecentYearsAreEasyToGuess
1765                    }
1766                    zxcvbn::feedback::Warning::AWordByItselfIsEasyToGuess => {
1767                        PasswordFeedback::AWordByItselfIsEasyToGuess
1768                    }
1769                    zxcvbn::feedback::Warning::DatesAreOftenEasyToGuess => {
1770                        PasswordFeedback::DatesAreOftenEasyToGuess
1771                    }
1772                    zxcvbn::feedback::Warning::NamesAndSurnamesByThemselvesAreEasyToGuess => {
1773                        PasswordFeedback::NamesAndSurnamesByThemselvesAreEasyToGuess
1774                    }
1775                    zxcvbn::feedback::Warning::CommonNamesAndSurnamesAreEasyToGuess => {
1776                        PasswordFeedback::CommonNamesAndSurnamesAreEasyToGuess
1777                    }
1778                }))
1779                .collect();
1780
1781            return Err(PasswordQuality::Feedback(feedback));
1782        }
1783
1784        // check a password badlist to eliminate more content
1785        // we check the password as "lower case" to help eliminate possibilities
1786        // also, when pw_badlist_cache is read from DB, it is read as Value (iutf8 lowercase)
1787        if self
1788            .qs_read
1789            .pw_badlist()
1790            .contains(&cleartext.to_lowercase())
1791        {
1792            security_info!("Password found in badlist, rejecting");
1793            Err(PasswordQuality::BadListed)
1794        } else {
1795            Ok(())
1796        }
1797    }
1798
1799    #[instrument(level = "trace", skip(cust, self))]
1800    pub fn credential_primary_set_password(
1801        &self,
1802        cust: &CredentialUpdateSessionToken,
1803        ct: Duration,
1804        pw: &str,
1805    ) -> Result<CredentialUpdateSessionStatus, OperationError> {
1806        let session_handle = self.get_current_session(cust, ct)?;
1807        let mut session = session_handle.try_lock().map_err(|_| {
1808            admin_error!("Session already locked, unable to proceed.");
1809            OperationError::InvalidState
1810        })?;
1811        trace!(?session);
1812
1813        if !matches!(session.primary_state, CredentialState::Modifiable) {
1814            error!("Session does not have permission to modify primary credential");
1815            return Err(OperationError::AccessDenied);
1816        };
1817
1818        self.check_password_quality(
1819            pw,
1820            &session.resolved_account_policy,
1821            session.account.related_inputs().as_slice(),
1822            session.account.radius_secret.as_deref(),
1823        )
1824        .map_err(|e| match e {
1825            PasswordQuality::TooShort(sz) => {
1826                OperationError::PasswordQuality(vec![PasswordFeedback::TooShort(sz)])
1827            }
1828            PasswordQuality::BadListed => {
1829                OperationError::PasswordQuality(vec![PasswordFeedback::BadListed])
1830            }
1831            PasswordQuality::DontReusePasswords => {
1832                OperationError::PasswordQuality(vec![PasswordFeedback::DontReusePasswords])
1833            }
1834            PasswordQuality::Feedback(feedback) => OperationError::PasswordQuality(feedback),
1835        })?;
1836
1837        let ncred = match &session.primary {
1838            Some(primary) => {
1839                // Is there a need to update the uuid of the cred re softlocks?
1840                primary.set_password(self.crypto_policy, pw)?
1841            }
1842            None => Credential::new_password_only(self.crypto_policy, pw)?,
1843        };
1844
1845        session.primary = Some(ncred);
1846        Ok(session.deref().into())
1847    }
1848
1849    pub fn credential_primary_init_totp(
1850        &self,
1851        cust: &CredentialUpdateSessionToken,
1852        ct: Duration,
1853    ) -> Result<CredentialUpdateSessionStatus, OperationError> {
1854        let session_handle = self.get_current_session(cust, ct)?;
1855        let mut session = session_handle.try_lock().map_err(|_| {
1856            admin_error!("Session already locked, unable to proceed.");
1857            OperationError::InvalidState
1858        })?;
1859        trace!(?session);
1860
1861        if !matches!(session.primary_state, CredentialState::Modifiable) {
1862            error!("Session does not have permission to modify primary credential");
1863            return Err(OperationError::AccessDenied);
1864        };
1865
1866        // Is there something else in progress? Cancel it if so.
1867        if !matches!(session.mfaregstate, MfaRegState::None) {
1868            debug!("Clearing incomplete mfareg");
1869        }
1870
1871        // Generate the TOTP.
1872        let totp_token = Totp::generate_secure(TOTP_DEFAULT_STEP);
1873
1874        session.mfaregstate = MfaRegState::TotpInit(totp_token);
1875        // Now that it's in the state, it'll be in the status when returned.
1876        Ok(session.deref().into())
1877    }
1878
1879    pub fn credential_primary_check_totp(
1880        &self,
1881        cust: &CredentialUpdateSessionToken,
1882        ct: Duration,
1883        totp_chal: u32,
1884        label: &str,
1885    ) -> Result<CredentialUpdateSessionStatus, OperationError> {
1886        let session_handle = self.get_current_session(cust, ct)?;
1887        let mut session = session_handle.try_lock().map_err(|_| {
1888            admin_error!("Session already locked, unable to proceed.");
1889            OperationError::InvalidState
1890        })?;
1891        trace!(?session);
1892
1893        if !matches!(session.primary_state, CredentialState::Modifiable) {
1894            error!("Session does not have permission to modify primary credential");
1895            return Err(OperationError::AccessDenied);
1896        };
1897
1898        // Are we in a totp reg state?
1899        match &session.mfaregstate {
1900            MfaRegState::TotpInit(totp_token)
1901            | MfaRegState::TotpTryAgain(totp_token)
1902            | MfaRegState::TotpNameTryAgain(totp_token, _)
1903            | MfaRegState::TotpInvalidSha1(totp_token, _, _) => {
1904                if session
1905                    .primary
1906                    .as_ref()
1907                    .map(|cred| cred.has_totp_by_name(label))
1908                    .unwrap_or_default()
1909                    || label.trim().is_empty()
1910                    || !Value::validate_str_escapes(label)
1911                {
1912                    // The user is trying to add a second TOTP under the same name. Lets save them from themselves
1913                    session.mfaregstate =
1914                        MfaRegState::TotpNameTryAgain(totp_token.clone(), label.into());
1915                    return Ok(session.deref().into());
1916                }
1917
1918                if totp_token.verify(totp_chal, ct) {
1919                    // It was valid. Update the credential.
1920                    let ncred = session
1921                        .primary
1922                        .as_ref()
1923                        .map(|cred| cred.append_totp(label.to_string(), totp_token.clone()))
1924                        .ok_or_else(|| {
1925                            admin_error!("A TOTP was added, but no primary credential stub exists");
1926                            OperationError::InvalidState
1927                        })?;
1928
1929                    session.primary = Some(ncred);
1930
1931                    // Set the state to None.
1932                    session.mfaregstate = MfaRegState::None;
1933                    Ok(session.deref().into())
1934                } else {
1935                    // What if it's a broken authenticator app? Google authenticator
1936                    // and Authy both force SHA1 and ignore the algo we send. So let's
1937                    // check that just in case.
1938                    let token_sha1 = totp_token.clone().downgrade_to_legacy();
1939
1940                    if token_sha1.verify(totp_chal, ct) {
1941                        // Greeeaaaaaatttt. It's a broken app. Let's check the user
1942                        // knows this is broken, before we proceed.
1943                        session.mfaregstate = MfaRegState::TotpInvalidSha1(
1944                            totp_token.clone(),
1945                            token_sha1,
1946                            label.to_string(),
1947                        );
1948                        Ok(session.deref().into())
1949                    } else {
1950                        // Let them check again, it's a typo.
1951                        session.mfaregstate = MfaRegState::TotpTryAgain(totp_token.clone());
1952                        Ok(session.deref().into())
1953                    }
1954                }
1955            }
1956            _ => Err(OperationError::InvalidRequestState),
1957        }
1958    }
1959
1960    pub fn credential_primary_accept_sha1_totp(
1961        &self,
1962        cust: &CredentialUpdateSessionToken,
1963        ct: Duration,
1964    ) -> Result<CredentialUpdateSessionStatus, OperationError> {
1965        let session_handle = self.get_current_session(cust, ct)?;
1966        let mut session = session_handle.try_lock().map_err(|_| {
1967            admin_error!("Session already locked, unable to proceed.");
1968            OperationError::InvalidState
1969        })?;
1970        trace!(?session);
1971
1972        if !matches!(session.primary_state, CredentialState::Modifiable) {
1973            error!("Session does not have permission to modify primary credential");
1974            return Err(OperationError::AccessDenied);
1975        };
1976
1977        // Are we in a totp reg state?
1978        match &session.mfaregstate {
1979            MfaRegState::TotpInvalidSha1(_, token_sha1, label) => {
1980                // They have accepted it as sha1
1981                let ncred = session
1982                    .primary
1983                    .as_ref()
1984                    .map(|cred| cred.append_totp(label.to_string(), token_sha1.clone()))
1985                    .ok_or_else(|| {
1986                        admin_error!("A TOTP was added, but no primary credential stub exists");
1987                        OperationError::InvalidState
1988                    })?;
1989
1990                security_info!("A SHA1 TOTP credential was accepted");
1991
1992                session.primary = Some(ncred);
1993
1994                // Set the state to None.
1995                session.mfaregstate = MfaRegState::None;
1996                Ok(session.deref().into())
1997            }
1998            _ => Err(OperationError::InvalidRequestState),
1999        }
2000    }
2001
2002    pub fn credential_primary_remove_totp(
2003        &self,
2004        cust: &CredentialUpdateSessionToken,
2005        ct: Duration,
2006        label: &str,
2007    ) -> Result<CredentialUpdateSessionStatus, OperationError> {
2008        let session_handle = self.get_current_session(cust, ct)?;
2009        let mut session = session_handle.try_lock().map_err(|_| {
2010            admin_error!("Session already locked, unable to proceed.");
2011            OperationError::InvalidState
2012        })?;
2013        trace!(?session);
2014
2015        if !matches!(session.primary_state, CredentialState::Modifiable) {
2016            error!("Session does not have permission to modify primary credential");
2017            return Err(OperationError::AccessDenied);
2018        };
2019
2020        if !matches!(session.mfaregstate, MfaRegState::None) {
2021            admin_info!("Invalid TOTP state, another update is in progress");
2022            return Err(OperationError::InvalidState);
2023        }
2024
2025        let ncred = session
2026            .primary
2027            .as_ref()
2028            .map(|cred| cred.remove_totp(label))
2029            .ok_or_else(|| {
2030                admin_error!("Try to remove TOTP, but no primary credential stub exists");
2031                OperationError::InvalidState
2032            })?;
2033
2034        session.primary = Some(ncred);
2035
2036        // Set the state to None.
2037        session.mfaregstate = MfaRegState::None;
2038        Ok(session.deref().into())
2039    }
2040
2041    pub fn credential_primary_init_backup_codes(
2042        &self,
2043        cust: &CredentialUpdateSessionToken,
2044        ct: Duration,
2045    ) -> Result<CredentialUpdateSessionStatus, OperationError> {
2046        let session_handle = self.get_current_session(cust, ct)?;
2047        let mut session = session_handle.try_lock().map_err(|_| {
2048            error!("Session already locked, unable to proceed.");
2049            OperationError::InvalidState
2050        })?;
2051        trace!(?session);
2052
2053        if !matches!(session.primary_state, CredentialState::Modifiable) {
2054            error!("Session does not have permission to modify primary credential");
2055            return Err(OperationError::AccessDenied);
2056        };
2057
2058        // I think we override/map the status to inject the codes as a once-off state message.
2059
2060        let codes = backup_code_from_random();
2061
2062        let ncred = session
2063            .primary
2064            .as_ref()
2065            .ok_or_else(|| {
2066                error!("Tried to add backup codes, but no primary credential stub exists");
2067                OperationError::InvalidState
2068            })
2069            .and_then(|cred|
2070                cred.update_backup_code(BackupCodes::new(codes.clone()))
2071                    .map_err(|_| {
2072                        error!("Tried to add backup codes, but MFA is not enabled on this credential yet");
2073                        OperationError::InvalidState
2074                    })
2075            )
2076            ?;
2077
2078        session.primary = Some(ncred);
2079
2080        Ok(session.deref().into()).map(|mut status: CredentialUpdateSessionStatus| {
2081            status.mfaregstate = MfaRegStateStatus::BackupCodes(codes);
2082            status
2083        })
2084    }
2085
2086    pub fn credential_primary_remove_backup_codes(
2087        &self,
2088        cust: &CredentialUpdateSessionToken,
2089        ct: Duration,
2090    ) -> Result<CredentialUpdateSessionStatus, OperationError> {
2091        let session_handle = self.get_current_session(cust, ct)?;
2092        let mut session = session_handle.try_lock().map_err(|_| {
2093            admin_error!("Session already locked, unable to proceed.");
2094            OperationError::InvalidState
2095        })?;
2096        trace!(?session);
2097
2098        if !matches!(session.primary_state, CredentialState::Modifiable) {
2099            error!("Session does not have permission to modify primary credential");
2100            return Err(OperationError::AccessDenied);
2101        };
2102
2103        let ncred = session
2104            .primary
2105            .as_ref()
2106            .ok_or_else(|| {
2107                admin_error!("Tried to add backup codes, but no primary credential stub exists");
2108                OperationError::InvalidState
2109            })
2110            .and_then(|cred|
2111                cred.remove_backup_code()
2112                    .map_err(|_| {
2113                        admin_error!("Tried to remove backup codes, but MFA is not enabled on this credential yet");
2114                        OperationError::InvalidState
2115                    })
2116            )
2117            ?;
2118
2119        session.primary = Some(ncred);
2120
2121        Ok(session.deref().into())
2122    }
2123
2124    pub fn credential_primary_delete(
2125        &self,
2126        cust: &CredentialUpdateSessionToken,
2127        ct: Duration,
2128    ) -> Result<CredentialUpdateSessionStatus, OperationError> {
2129        let session_handle = self.get_current_session(cust, ct)?;
2130        let mut session = session_handle.try_lock().map_err(|_| {
2131            admin_error!("Session already locked, unable to proceed.");
2132            OperationError::InvalidState
2133        })?;
2134        trace!(?session);
2135
2136        if !(matches!(session.primary_state, CredentialState::Modifiable)
2137            || matches!(session.primary_state, CredentialState::DeleteOnly))
2138        {
2139            error!("Session does not have permission to modify primary credential");
2140            return Err(OperationError::AccessDenied);
2141        };
2142
2143        session.primary = None;
2144        Ok(session.deref().into())
2145    }
2146
2147    pub fn credential_passkey_init(
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.passkeys_state, CredentialState::Modifiable) {
2160            error!("Session does not have permission to modify passkeys");
2161            return Err(OperationError::AccessDenied);
2162        };
2163
2164        if !matches!(session.mfaregstate, MfaRegState::None) {
2165            debug!("Clearing incomplete mfareg");
2166        }
2167
2168        let (ccr, pk_reg) = self
2169            .webauthn
2170            .start_passkey_registration(
2171                session.account.uuid,
2172                &session.account.spn,
2173                &session.account.displayname,
2174                session.account.existing_credential_id_list(),
2175            )
2176            .map_err(|e| {
2177                error!(eclass=?e, emsg=%e, "Unable to start passkey registration");
2178                OperationError::Webauthn
2179            })?;
2180
2181        session.mfaregstate = MfaRegState::Passkey(Box::new(ccr), pk_reg);
2182        // Now that it's in the state, it'll be in the status when returned.
2183        Ok(session.deref().into())
2184    }
2185
2186    pub fn credential_passkey_finish(
2187        &self,
2188        cust: &CredentialUpdateSessionToken,
2189        ct: Duration,
2190        label: String,
2191        reg: &RegisterPublicKeyCredential,
2192    ) -> Result<CredentialUpdateSessionStatus, OperationError> {
2193        let session_handle = self.get_current_session(cust, ct)?;
2194        let mut session = session_handle.try_lock().map_err(|_| {
2195            admin_error!("Session already locked, unable to proceed.");
2196            OperationError::InvalidState
2197        })?;
2198        trace!(?session);
2199
2200        if !matches!(session.passkeys_state, CredentialState::Modifiable) {
2201            error!("Session does not have permission to modify passkeys");
2202            return Err(OperationError::AccessDenied);
2203        };
2204
2205        match &session.mfaregstate {
2206            MfaRegState::Passkey(_ccr, pk_reg) => {
2207                let reg_result = self.webauthn.finish_passkey_registration(reg, pk_reg);
2208
2209                // Clean up state before returning results.
2210                session.mfaregstate = MfaRegState::None;
2211
2212                match reg_result {
2213                    Ok(passkey) => {
2214                        let pk_id = Uuid::new_v4();
2215                        session.passkeys.insert(pk_id, (label, passkey));
2216
2217                        let cu_status: CredentialUpdateSessionStatus = session.deref().into();
2218                        Ok(cu_status)
2219                    }
2220                    Err(WebauthnError::UserNotVerified) => {
2221                        let mut cu_status: CredentialUpdateSessionStatus = session.deref().into();
2222                        cu_status.append_ephemeral_warning(
2223                            CredentialUpdateSessionStatusWarnings::WebauthnUserVerificationRequired,
2224                        );
2225                        Ok(cu_status)
2226                    }
2227                    Err(err) => {
2228                        error!(eclass=?err, emsg=%err, "Unable to complete passkey registration");
2229                        Err(OperationError::CU0002WebauthnRegistrationError)
2230                    }
2231                }
2232            }
2233            invalid_state => {
2234                warn!(?invalid_state);
2235                Err(OperationError::InvalidRequestState)
2236            }
2237        }
2238    }
2239
2240    pub fn credential_passkey_remove(
2241        &self,
2242        cust: &CredentialUpdateSessionToken,
2243        ct: Duration,
2244        uuid: Uuid,
2245    ) -> Result<CredentialUpdateSessionStatus, OperationError> {
2246        let session_handle = self.get_current_session(cust, ct)?;
2247        let mut session = session_handle.try_lock().map_err(|_| {
2248            admin_error!("Session already locked, unable to proceed.");
2249            OperationError::InvalidState
2250        })?;
2251        trace!(?session);
2252
2253        if !(matches!(session.passkeys_state, CredentialState::Modifiable)
2254            || matches!(session.passkeys_state, CredentialState::DeleteOnly))
2255        {
2256            error!("Session does not have permission to modify passkeys");
2257            return Err(OperationError::AccessDenied);
2258        };
2259
2260        // No-op if not present
2261        session.passkeys.remove(&uuid);
2262
2263        Ok(session.deref().into())
2264    }
2265
2266    pub fn credential_attested_passkey_init(
2267        &self,
2268        cust: &CredentialUpdateSessionToken,
2269        ct: Duration,
2270    ) -> Result<CredentialUpdateSessionStatus, OperationError> {
2271        let session_handle = self.get_current_session(cust, ct)?;
2272        let mut session = session_handle.try_lock().map_err(|_| {
2273            error!("Session already locked, unable to proceed.");
2274            OperationError::InvalidState
2275        })?;
2276        trace!(?session);
2277
2278        if !matches!(session.attested_passkeys_state, CredentialState::Modifiable) {
2279            error!("Session does not have permission to modify attested passkeys");
2280            return Err(OperationError::AccessDenied);
2281        };
2282
2283        if !matches!(session.mfaregstate, MfaRegState::None) {
2284            debug!("Cancelling abandoned mfareg");
2285        }
2286
2287        let att_ca_list = session
2288            .resolved_account_policy
2289            .webauthn_attestation_ca_list()
2290            .cloned()
2291            .ok_or_else(|| {
2292                error!(
2293                    "No attestation CA list is available, can not proceed with attested passkeys."
2294                );
2295                OperationError::AccessDenied
2296            })?;
2297
2298        let (ccr, pk_reg) = self
2299            .webauthn
2300            .start_attested_passkey_registration(
2301                session.account.uuid,
2302                &session.account.spn,
2303                &session.account.displayname,
2304                session.account.existing_credential_id_list(),
2305                att_ca_list,
2306                None,
2307            )
2308            .map_err(|e| {
2309                error!(eclass=?e, emsg=%e, "Unable to start passkey registration");
2310                OperationError::Webauthn
2311            })?;
2312
2313        session.mfaregstate = MfaRegState::AttestedPasskey(Box::new(ccr), pk_reg);
2314        // Now that it's in the state, it'll be in the status when returned.
2315        Ok(session.deref().into())
2316    }
2317
2318    pub fn credential_attested_passkey_finish(
2319        &self,
2320        cust: &CredentialUpdateSessionToken,
2321        ct: Duration,
2322        label: String,
2323        reg: &RegisterPublicKeyCredential,
2324    ) -> Result<CredentialUpdateSessionStatus, OperationError> {
2325        let session_handle = self.get_current_session(cust, ct)?;
2326        let mut session = session_handle.try_lock().map_err(|_| {
2327            admin_error!("Session already locked, unable to proceed.");
2328            OperationError::InvalidState
2329        })?;
2330        trace!(?session);
2331
2332        if !matches!(session.attested_passkeys_state, CredentialState::Modifiable) {
2333            error!("Session does not have permission to modify attested passkeys");
2334            return Err(OperationError::AccessDenied);
2335        };
2336
2337        match &session.mfaregstate {
2338            MfaRegState::AttestedPasskey(_ccr, pk_reg) => {
2339                let result = self
2340                    .webauthn
2341                    .finish_attested_passkey_registration(reg, pk_reg)
2342                    .map_err(|e| {
2343                        error!(eclass=?e, emsg=%e, "Unable to complete attested passkey registration");
2344
2345                        match e {
2346                            WebauthnError::AttestationChainNotTrusted(_)
2347                            | WebauthnError::AttestationNotVerifiable => {
2348                                OperationError::CU0001WebauthnAttestationNotTrusted
2349                            },
2350                            WebauthnError::UserNotVerified => {
2351                                OperationError::CU0003WebauthnUserNotVerified
2352                            },
2353                            _ => OperationError::CU0002WebauthnRegistrationError,
2354                        }
2355                    });
2356
2357                // The reg is done. Clean up state before returning errors.
2358                session.mfaregstate = MfaRegState::None;
2359
2360                let passkey = result?;
2361                trace!(?passkey);
2362
2363                let pk_id = Uuid::new_v4();
2364                session.attested_passkeys.insert(pk_id, (label, passkey));
2365
2366                trace!(?session.attested_passkeys);
2367
2368                Ok(session.deref().into())
2369            }
2370            _ => Err(OperationError::InvalidRequestState),
2371        }
2372    }
2373
2374    pub fn credential_attested_passkey_remove(
2375        &self,
2376        cust: &CredentialUpdateSessionToken,
2377        ct: Duration,
2378        uuid: Uuid,
2379    ) -> Result<CredentialUpdateSessionStatus, OperationError> {
2380        let session_handle = self.get_current_session(cust, ct)?;
2381        let mut session = session_handle.try_lock().map_err(|_| {
2382            admin_error!("Session already locked, unable to proceed.");
2383            OperationError::InvalidState
2384        })?;
2385        trace!(?session);
2386
2387        if !(matches!(session.attested_passkeys_state, CredentialState::Modifiable)
2388            || matches!(session.attested_passkeys_state, CredentialState::DeleteOnly))
2389        {
2390            error!("Session does not have permission to modify attested passkeys");
2391            return Err(OperationError::AccessDenied);
2392        };
2393
2394        // No-op if not present
2395        session.attested_passkeys.remove(&uuid);
2396
2397        Ok(session.deref().into())
2398    }
2399
2400    #[instrument(level = "trace", skip(cust, self))]
2401    pub fn credential_unix_set_password(
2402        &self,
2403        cust: &CredentialUpdateSessionToken,
2404        ct: Duration,
2405        pw: &str,
2406    ) -> Result<CredentialUpdateSessionStatus, OperationError> {
2407        let session_handle = self.get_current_session(cust, ct)?;
2408        let mut session = session_handle.try_lock().map_err(|_| {
2409            admin_error!("Session already locked, unable to proceed.");
2410            OperationError::InvalidState
2411        })?;
2412        trace!(?session);
2413
2414        if !matches!(session.unixcred_state, CredentialState::Modifiable) {
2415            error!("Session does not have permission to modify unix credential");
2416            return Err(OperationError::AccessDenied);
2417        };
2418
2419        self.check_password_quality(
2420            pw,
2421            &session.resolved_account_policy,
2422            session.account.related_inputs().as_slice(),
2423            session.account.radius_secret.as_deref(),
2424        )
2425        .map_err(|e| match e {
2426            PasswordQuality::TooShort(sz) => {
2427                OperationError::PasswordQuality(vec![PasswordFeedback::TooShort(sz)])
2428            }
2429            PasswordQuality::BadListed => {
2430                OperationError::PasswordQuality(vec![PasswordFeedback::BadListed])
2431            }
2432            PasswordQuality::DontReusePasswords => {
2433                OperationError::PasswordQuality(vec![PasswordFeedback::DontReusePasswords])
2434            }
2435            PasswordQuality::Feedback(feedback) => OperationError::PasswordQuality(feedback),
2436        })?;
2437
2438        let ncred = match &session.unixcred {
2439            Some(unixcred) => {
2440                // Is there a need to update the uuid of the cred re softlocks?
2441                unixcred.set_password(self.crypto_policy, pw)?
2442            }
2443            None => Credential::new_password_only(self.crypto_policy, pw)?,
2444        };
2445
2446        session.unixcred = Some(ncred);
2447        Ok(session.deref().into())
2448    }
2449
2450    pub fn credential_unix_delete(
2451        &self,
2452        cust: &CredentialUpdateSessionToken,
2453        ct: Duration,
2454    ) -> Result<CredentialUpdateSessionStatus, OperationError> {
2455        let session_handle = self.get_current_session(cust, ct)?;
2456        let mut session = session_handle.try_lock().map_err(|_| {
2457            admin_error!("Session already locked, unable to proceed.");
2458            OperationError::InvalidState
2459        })?;
2460        trace!(?session);
2461
2462        if !(matches!(session.unixcred_state, CredentialState::Modifiable)
2463            || matches!(session.unixcred_state, CredentialState::DeleteOnly))
2464        {
2465            error!("Session does not have permission to modify unix credential");
2466            return Err(OperationError::AccessDenied);
2467        };
2468
2469        session.unixcred = None;
2470        Ok(session.deref().into())
2471    }
2472
2473    #[instrument(level = "trace", skip(cust, self))]
2474    pub fn credential_sshkey_add(
2475        &self,
2476        cust: &CredentialUpdateSessionToken,
2477        ct: Duration,
2478        label: String,
2479        sshpubkey: SshPublicKey,
2480    ) -> Result<CredentialUpdateSessionStatus, OperationError> {
2481        let session_handle = self.get_current_session(cust, ct)?;
2482        let mut session = session_handle.try_lock().map_err(|_| {
2483            admin_error!("Session already locked, unable to proceed.");
2484            OperationError::InvalidState
2485        })?;
2486        trace!(?session);
2487
2488        if !matches!(session.unixcred_state, CredentialState::Modifiable) {
2489            error!("Session does not have permission to modify unix credential");
2490            return Err(OperationError::AccessDenied);
2491        };
2492
2493        // Check the label.
2494        if !LABEL_RE.is_match(&label) {
2495            error!("SSH Public Key label invalid");
2496            return Err(OperationError::InvalidLabel);
2497        }
2498
2499        if session.sshkeys.contains_key(&label) {
2500            error!("SSH Public Key label duplicate");
2501            return Err(OperationError::DuplicateLabel);
2502        }
2503
2504        if session.sshkeys.values().any(|sk| *sk == sshpubkey) {
2505            error!("SSH Public Key duplicate");
2506            return Err(OperationError::DuplicateKey);
2507        }
2508
2509        session.sshkeys.insert(label, sshpubkey);
2510
2511        Ok(session.deref().into())
2512    }
2513
2514    pub fn credential_sshkey_remove(
2515        &self,
2516        cust: &CredentialUpdateSessionToken,
2517        ct: Duration,
2518        label: &str,
2519    ) -> Result<CredentialUpdateSessionStatus, OperationError> {
2520        let session_handle = self.get_current_session(cust, ct)?;
2521        let mut session = session_handle.try_lock().map_err(|_| {
2522            admin_error!("Session already locked, unable to proceed.");
2523            OperationError::InvalidState
2524        })?;
2525        trace!(?session);
2526
2527        if !(matches!(session.sshkeys_state, CredentialState::Modifiable)
2528            || matches!(session.sshkeys_state, CredentialState::DeleteOnly))
2529        {
2530            error!("Session does not have permission to modify sshkeys");
2531            return Err(OperationError::AccessDenied);
2532        };
2533
2534        session.sshkeys.remove(label).ok_or_else(|| {
2535            error!("No such key for label");
2536            OperationError::NoMatchingEntries
2537        })?;
2538
2539        // session.unixcred = None;
2540
2541        Ok(session.deref().into())
2542    }
2543
2544    pub fn credential_update_cancel_mfareg(
2545        &self,
2546        cust: &CredentialUpdateSessionToken,
2547        ct: Duration,
2548    ) -> Result<CredentialUpdateSessionStatus, OperationError> {
2549        let session_handle = self.get_current_session(cust, ct)?;
2550        let mut session = session_handle.try_lock().map_err(|_| {
2551            admin_error!("Session already locked, unable to proceed.");
2552            OperationError::InvalidState
2553        })?;
2554        trace!(?session);
2555        session.mfaregstate = MfaRegState::None;
2556        Ok(session.deref().into())
2557    }
2558
2559    // Generate password?
2560}
2561
2562#[cfg(test)]
2563mod tests {
2564    use compact_jwt::JwsCompact;
2565    use std::time::Duration;
2566
2567    use kanidm_proto::internal::{CUExtPortal, CredentialDetailType, PasswordFeedback};
2568    use kanidm_proto::v1::{AuthAllowed, AuthIssueSession, AuthMech, UnixUserToken};
2569    use uuid::uuid;
2570    use webauthn_authenticator_rs::softpasskey::SoftPasskey;
2571    use webauthn_authenticator_rs::softtoken::{self, SoftToken};
2572    use webauthn_authenticator_rs::{AuthenticatorBackend, WebauthnAuthenticator};
2573    use webauthn_rs::prelude::AttestationCaListBuilder;
2574
2575    use super::{
2576        CredentialState, CredentialUpdateSessionStatus, CredentialUpdateSessionStatusWarnings,
2577        CredentialUpdateSessionToken, InitCredentialUpdateEvent, InitCredentialUpdateIntentEvent,
2578        MfaRegStateStatus, MAXIMUM_CRED_UPDATE_TTL, MAXIMUM_INTENT_TTL, MINIMUM_INTENT_TTL,
2579    };
2580    use crate::credential::totp::Totp;
2581    use crate::event::CreateEvent;
2582    use crate::idm::audit::AuditEvent;
2583    use crate::idm::delayed::DelayedAction;
2584    use crate::idm::event::{
2585        AuthEvent, AuthResult, RegenerateRadiusSecretEvent, UnixUserAuthEvent,
2586    };
2587    use crate::idm::server::{IdmServer, IdmServerCredUpdateTransaction, IdmServerDelayed};
2588    use crate::idm::AuthState;
2589    use crate::prelude::*;
2590    use crate::utils::password_from_random_len;
2591    use crate::value::CredentialType;
2592    use sshkey_attest::proto::PublicKey as SshPublicKey;
2593
2594    const TEST_CURRENT_TIME: u64 = 6000;
2595    const TESTPERSON_UUID: Uuid = uuid!("cf231fea-1a8f-4410-a520-fd9b1a379c86");
2596
2597    const SSHKEY_VALID_1: &str = "sk-ecdsa-sha2-nistp256@openssh.com AAAAInNrLWVjZHNhLXNoYTItbmlzdHAyNTZAb3BlbnNzaC5jb20AAAAIbmlzdHAyNTYAAABBBENubZikrb8hu+HeVRdZ0pp/VAk2qv4JDbuJhvD0yNdWDL2e3cBbERiDeNPkWx58Q4rVnxkbV1fa8E2waRtT91wAAAAEc3NoOg== testuser@fidokey";
2598    const SSHKEY_VALID_2: &str = "sk-ecdsa-sha2-nistp256@openssh.com AAAAInNrLWVjZHNhLXNoYTItbmlzdHAyNTZAb3BlbnNzaC5jb20AAAAIbmlzdHAyNTYAAABBBIbkSsdGCRoW6v0nO/3vNYPhG20YhWU0wQPY7x52EOb4dmYhC4IJfzVDpEPg313BxWRKQglb5RQ1PPkou7JFyCUAAAAEc3NoOg== testuser@fidokey";
2599    const SSHKEY_INVALID: &str = "sk-ecrsa-sha9000-nistp@openssh.com AAAAInNrLWVjZHNhLXNoYTItbmlzdHAyNTZAb3BlbnNzaC5jb20AAAAIbmlzdHAyNTYAAABBBIbkSsdGCRoW6v0nO/3vNYPhG20YhWU0wQPY7x52EOb4dmYhC4IJfzVDpEPg313BxWRKQglb5RQ1PPkou7JFyCUAAAAEc3NoOg== badkey@rejectme";
2600
2601    #[idm_test]
2602    async fn credential_update_session_init(
2603        idms: &IdmServer,
2604        _idms_delayed: &mut IdmServerDelayed,
2605    ) {
2606        let ct = Duration::from_secs(TEST_CURRENT_TIME);
2607        let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();
2608
2609        let testaccount_uuid = Uuid::new_v4();
2610
2611        let e1 = entry_init!(
2612            (Attribute::Class, EntryClass::Object.to_value()),
2613            (Attribute::Class, EntryClass::Account.to_value()),
2614            (Attribute::Class, EntryClass::ServiceAccount.to_value()),
2615            (Attribute::Name, Value::new_iname("user_account_only")),
2616            (Attribute::Uuid, Value::Uuid(testaccount_uuid)),
2617            (Attribute::Description, Value::new_utf8s("testaccount")),
2618            (Attribute::DisplayName, Value::new_utf8s("testaccount"))
2619        );
2620
2621        let e2 = entry_init!(
2622            (Attribute::Class, EntryClass::Object.to_value()),
2623            (Attribute::Class, EntryClass::Account.to_value()),
2624            (Attribute::Class, EntryClass::PosixAccount.to_value()),
2625            (Attribute::Class, EntryClass::Person.to_value()),
2626            (Attribute::Name, Value::new_iname("testperson")),
2627            (Attribute::Uuid, Value::Uuid(TESTPERSON_UUID)),
2628            (Attribute::Description, Value::new_utf8s("testperson")),
2629            (Attribute::DisplayName, Value::new_utf8s("testperson"))
2630        );
2631
2632        let ce = CreateEvent::new_internal(vec![e1, e2]);
2633        let cr = idms_prox_write.qs_write.create(&ce);
2634        assert!(cr.is_ok());
2635
2636        let testaccount = idms_prox_write
2637            .qs_write
2638            .internal_search_uuid(testaccount_uuid)
2639            .expect("failed");
2640
2641        let testperson = idms_prox_write
2642            .qs_write
2643            .internal_search_uuid(TESTPERSON_UUID)
2644            .expect("failed");
2645
2646        let idm_admin = idms_prox_write
2647            .qs_write
2648            .internal_search_uuid(UUID_IDM_ADMIN)
2649            .expect("failed");
2650
2651        // user without permission - fail
2652        // - accounts don't have self-write permission.
2653
2654        let cur = idms_prox_write.init_credential_update(
2655            &InitCredentialUpdateEvent::new_impersonate_entry(testaccount),
2656            ct,
2657        );
2658
2659        assert!(matches!(cur, Err(OperationError::NotAuthorised)));
2660
2661        // user with permission - success
2662
2663        let cur = idms_prox_write.init_credential_update(
2664            &InitCredentialUpdateEvent::new_impersonate_entry(testperson),
2665            ct,
2666        );
2667
2668        assert!(cur.is_ok());
2669
2670        // create intent token without permission - fail
2671
2672        // create intent token with permission - success
2673
2674        let cur = idms_prox_write.init_credential_update_intent(
2675            &InitCredentialUpdateIntentEvent::new_impersonate_entry(
2676                idm_admin,
2677                TESTPERSON_UUID,
2678                MINIMUM_INTENT_TTL,
2679            ),
2680            ct,
2681        );
2682
2683        assert!(cur.is_ok());
2684        let intent_tok = cur.expect("Failed to create intent token!");
2685
2686        // exchange intent token - invalid - fail
2687        // Expired
2688        let cur = idms_prox_write
2689            .exchange_intent_credential_update(intent_tok.clone().into(), ct + MINIMUM_INTENT_TTL);
2690
2691        assert!(matches!(cur, Err(OperationError::SessionExpired)));
2692
2693        let cur = idms_prox_write
2694            .exchange_intent_credential_update(intent_tok.clone().into(), ct + MAXIMUM_INTENT_TTL);
2695
2696        assert!(matches!(cur, Err(OperationError::SessionExpired)));
2697
2698        // exchange intent token - success
2699        let (cust_a, _c_status) = idms_prox_write
2700            .exchange_intent_credential_update(intent_tok.clone().into(), ct)
2701            .unwrap();
2702
2703        // Session in progress - This will succeed and then block the former success from
2704        // committing.
2705        let (cust_b, _c_status) = idms_prox_write
2706            .exchange_intent_credential_update(intent_tok.into(), ct + Duration::from_secs(1))
2707            .unwrap();
2708
2709        let cur = idms_prox_write.commit_credential_update(&cust_a, ct);
2710
2711        // Fails as the txn was orphaned.
2712        trace!(?cur);
2713        assert!(cur.is_err());
2714
2715        // Success - this was the second use of the token and is valid.
2716        let _ = idms_prox_write.commit_credential_update(&cust_b, ct);
2717
2718        idms_prox_write.commit().expect("Failed to commit txn");
2719    }
2720
2721    async fn setup_test_session(
2722        idms: &IdmServer,
2723        ct: Duration,
2724    ) -> (CredentialUpdateSessionToken, CredentialUpdateSessionStatus) {
2725        let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();
2726
2727        // Remove the default all persons policy, it interferes with our test.
2728        let modlist = ModifyList::new_purge(Attribute::CredentialTypeMinimum);
2729        idms_prox_write
2730            .qs_write
2731            .internal_modify_uuid(UUID_IDM_ALL_PERSONS, &modlist)
2732            .expect("Unable to change default session exp");
2733
2734        let e2 = entry_init!(
2735            (Attribute::Class, EntryClass::Object.to_value()),
2736            (Attribute::Class, EntryClass::Account.to_value()),
2737            (Attribute::Class, EntryClass::PosixAccount.to_value()),
2738            (Attribute::Class, EntryClass::Person.to_value()),
2739            (Attribute::Name, Value::new_iname("testperson")),
2740            (Attribute::Uuid, Value::Uuid(TESTPERSON_UUID)),
2741            (Attribute::Description, Value::new_utf8s("testperson")),
2742            (Attribute::DisplayName, Value::new_utf8s("testperson"))
2743        );
2744
2745        let ce = CreateEvent::new_internal(vec![e2]);
2746        let cr = idms_prox_write.qs_write.create(&ce);
2747        assert!(cr.is_ok());
2748
2749        let testperson = idms_prox_write
2750            .qs_write
2751            .internal_search_uuid(TESTPERSON_UUID)
2752            .expect("failed");
2753
2754        // Setup the radius creds to ensure we don't use them anywhere else.
2755        let rrse = RegenerateRadiusSecretEvent::new_internal(TESTPERSON_UUID);
2756
2757        let _ = idms_prox_write
2758            .regenerate_radius_secret(&rrse)
2759            .expect("Failed to reset radius credential 1");
2760
2761        let cur = idms_prox_write.init_credential_update(
2762            &InitCredentialUpdateEvent::new_impersonate_entry(testperson),
2763            ct,
2764        );
2765
2766        idms_prox_write.commit().expect("Failed to commit txn");
2767
2768        cur.expect("Failed to start update")
2769    }
2770
2771    async fn renew_test_session(
2772        idms: &IdmServer,
2773        ct: Duration,
2774    ) -> (CredentialUpdateSessionToken, CredentialUpdateSessionStatus) {
2775        let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();
2776
2777        let testperson = idms_prox_write
2778            .qs_write
2779            .internal_search_uuid(TESTPERSON_UUID)
2780            .expect("failed");
2781
2782        let cur = idms_prox_write.init_credential_update(
2783            &InitCredentialUpdateEvent::new_impersonate_entry(testperson),
2784            ct,
2785        );
2786
2787        trace!(renew_test_session_result = ?cur);
2788
2789        idms_prox_write.commit().expect("Failed to commit txn");
2790
2791        cur.expect("Failed to start update")
2792    }
2793
2794    async fn commit_session(idms: &IdmServer, ct: Duration, cust: CredentialUpdateSessionToken) {
2795        let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();
2796
2797        idms_prox_write
2798            .commit_credential_update(&cust, ct)
2799            .expect("Failed to commit credential update.");
2800
2801        idms_prox_write.commit().expect("Failed to commit txn");
2802    }
2803
2804    async fn check_testperson_password(
2805        idms: &IdmServer,
2806        idms_delayed: &mut IdmServerDelayed,
2807        pw: &str,
2808        ct: Duration,
2809    ) -> Option<JwsCompact> {
2810        let mut idms_auth = idms.auth().await.unwrap();
2811
2812        let auth_init = AuthEvent::named_init("testperson");
2813
2814        let r1 = idms_auth
2815            .auth(&auth_init, ct, Source::Internal.into())
2816            .await;
2817        let ar = r1.unwrap();
2818        let AuthResult { sessionid, state } = ar;
2819
2820        if !matches!(state, AuthState::Choose(_)) {
2821            debug!("Can't proceed - {:?}", state);
2822            return None;
2823        };
2824
2825        let auth_begin = AuthEvent::begin_mech(sessionid, AuthMech::Password);
2826
2827        let r2 = idms_auth
2828            .auth(&auth_begin, ct, Source::Internal.into())
2829            .await;
2830        let ar = r2.unwrap();
2831        let AuthResult { sessionid, state } = ar;
2832
2833        assert!(matches!(state, AuthState::Continue(_)));
2834
2835        let pw_step = AuthEvent::cred_step_password(sessionid, pw);
2836
2837        // Expect success
2838        let r2 = idms_auth.auth(&pw_step, ct, Source::Internal.into()).await;
2839        debug!("r2 ==> {:?}", r2);
2840        idms_auth.commit().expect("Must not fail");
2841
2842        match r2 {
2843            Ok(AuthResult {
2844                sessionid: _,
2845                state: AuthState::Success(token, AuthIssueSession::Token),
2846            }) => {
2847                // Process the auth session
2848                let da = idms_delayed.try_recv().expect("invalid");
2849                assert!(matches!(da, DelayedAction::AuthSessionRecord(_)));
2850
2851                Some(*token)
2852            }
2853            _ => None,
2854        }
2855    }
2856
2857    async fn check_testperson_unix_password(
2858        idms: &IdmServer,
2859        // idms_delayed: &mut IdmServerDelayed,
2860        pw: &str,
2861        ct: Duration,
2862    ) -> Option<UnixUserToken> {
2863        let mut idms_auth = idms.auth().await.unwrap();
2864
2865        let auth_event = UnixUserAuthEvent::new_internal(TESTPERSON_UUID, pw);
2866
2867        idms_auth
2868            .auth_unix(&auth_event, ct)
2869            .await
2870            .expect("Unable to perform unix authentication")
2871    }
2872
2873    async fn check_testperson_password_totp(
2874        idms: &IdmServer,
2875        idms_delayed: &mut IdmServerDelayed,
2876        pw: &str,
2877        token: &Totp,
2878        ct: Duration,
2879    ) -> Option<JwsCompact> {
2880        let mut idms_auth = idms.auth().await.unwrap();
2881
2882        let auth_init = AuthEvent::named_init("testperson");
2883
2884        let r1 = idms_auth
2885            .auth(&auth_init, ct, Source::Internal.into())
2886            .await;
2887        let ar = r1.unwrap();
2888        let AuthResult { sessionid, state } = ar;
2889
2890        if !matches!(state, AuthState::Choose(_)) {
2891            debug!("Can't proceed - {:?}", state);
2892            return None;
2893        };
2894
2895        let auth_begin = AuthEvent::begin_mech(sessionid, AuthMech::PasswordTotp);
2896
2897        let r2 = idms_auth
2898            .auth(&auth_begin, ct, Source::Internal.into())
2899            .await;
2900        let ar = r2.unwrap();
2901        let AuthResult { sessionid, state } = ar;
2902
2903        assert!(matches!(state, AuthState::Continue(_)));
2904
2905        let totp = token
2906            .do_totp_duration_from_epoch(&ct)
2907            .expect("Failed to perform totp step");
2908
2909        let totp_step = AuthEvent::cred_step_totp(sessionid, totp);
2910        let r2 = idms_auth
2911            .auth(&totp_step, ct, Source::Internal.into())
2912            .await;
2913        let ar = r2.unwrap();
2914        let AuthResult { sessionid, state } = ar;
2915
2916        assert!(matches!(state, AuthState::Continue(_)));
2917
2918        let pw_step = AuthEvent::cred_step_password(sessionid, pw);
2919
2920        // Expect success
2921        let r3 = idms_auth.auth(&pw_step, ct, Source::Internal.into()).await;
2922        debug!("r3 ==> {:?}", r3);
2923        idms_auth.commit().expect("Must not fail");
2924
2925        match r3 {
2926            Ok(AuthResult {
2927                sessionid: _,
2928                state: AuthState::Success(token, AuthIssueSession::Token),
2929            }) => {
2930                // Process the auth session
2931                let da = idms_delayed.try_recv().expect("invalid");
2932                assert!(matches!(da, DelayedAction::AuthSessionRecord(_)));
2933                Some(*token)
2934            }
2935            _ => None,
2936        }
2937    }
2938
2939    async fn check_testperson_password_backup_code(
2940        idms: &IdmServer,
2941        idms_delayed: &mut IdmServerDelayed,
2942        pw: &str,
2943        code: &str,
2944        ct: Duration,
2945    ) -> Option<JwsCompact> {
2946        let mut idms_auth = idms.auth().await.unwrap();
2947
2948        let auth_init = AuthEvent::named_init("testperson");
2949
2950        let r1 = idms_auth
2951            .auth(&auth_init, ct, Source::Internal.into())
2952            .await;
2953        let ar = r1.unwrap();
2954        let AuthResult { sessionid, state } = ar;
2955
2956        if !matches!(state, AuthState::Choose(_)) {
2957            debug!("Can't proceed - {:?}", state);
2958            return None;
2959        };
2960
2961        let auth_begin = AuthEvent::begin_mech(sessionid, AuthMech::PasswordBackupCode);
2962
2963        let r2 = idms_auth
2964            .auth(&auth_begin, ct, Source::Internal.into())
2965            .await;
2966        let ar = r2.unwrap();
2967        let AuthResult { sessionid, state } = ar;
2968
2969        assert!(matches!(state, AuthState::Continue(_)));
2970
2971        let code_step = AuthEvent::cred_step_backup_code(sessionid, code);
2972        let r2 = idms_auth
2973            .auth(&code_step, ct, Source::Internal.into())
2974            .await;
2975        let ar = r2.unwrap();
2976        let AuthResult { sessionid, state } = ar;
2977
2978        assert!(matches!(state, AuthState::Continue(_)));
2979
2980        let pw_step = AuthEvent::cred_step_password(sessionid, pw);
2981
2982        // Expect success
2983        let r3 = idms_auth.auth(&pw_step, ct, Source::Internal.into()).await;
2984        debug!("r3 ==> {:?}", r3);
2985        idms_auth.commit().expect("Must not fail");
2986
2987        match r3 {
2988            Ok(AuthResult {
2989                sessionid: _,
2990                state: AuthState::Success(token, AuthIssueSession::Token),
2991            }) => {
2992                // There now should be a backup code invalidation present
2993                let da = idms_delayed.try_recv().expect("invalid");
2994                assert!(matches!(da, DelayedAction::BackupCodeRemoval(_)));
2995                let r = idms.delayed_action(ct, da).await;
2996                assert!(r.is_ok());
2997
2998                // Process the auth session
2999                let da = idms_delayed.try_recv().expect("invalid");
3000                assert!(matches!(da, DelayedAction::AuthSessionRecord(_)));
3001                Some(*token)
3002            }
3003            _ => None,
3004        }
3005    }
3006
3007    async fn check_testperson_passkey<T: AuthenticatorBackend>(
3008        idms: &IdmServer,
3009        idms_delayed: &mut IdmServerDelayed,
3010        wa: &mut WebauthnAuthenticator<T>,
3011        origin: Url,
3012        ct: Duration,
3013    ) -> Option<JwsCompact> {
3014        let mut idms_auth = idms.auth().await.unwrap();
3015
3016        let auth_init = AuthEvent::named_init("testperson");
3017
3018        let r1 = idms_auth
3019            .auth(&auth_init, ct, Source::Internal.into())
3020            .await;
3021        let ar = r1.unwrap();
3022        let AuthResult { sessionid, state } = ar;
3023
3024        if !matches!(state, AuthState::Choose(_)) {
3025            debug!("Can't proceed - {:?}", state);
3026            return None;
3027        };
3028
3029        let auth_begin = AuthEvent::begin_mech(sessionid, AuthMech::Passkey);
3030
3031        let r2 = idms_auth
3032            .auth(&auth_begin, ct, Source::Internal.into())
3033            .await;
3034        let ar = r2.unwrap();
3035        let AuthResult { sessionid, state } = ar;
3036
3037        trace!(?state);
3038
3039        let rcr = match state {
3040            AuthState::Continue(mut allowed) => match allowed.pop() {
3041                Some(AuthAllowed::Passkey(rcr)) => rcr,
3042                _ => unreachable!(),
3043            },
3044            _ => unreachable!(),
3045        };
3046
3047        trace!(?rcr);
3048
3049        let resp = wa
3050            .do_authentication(origin, rcr)
3051            .expect("failed to use softtoken to authenticate");
3052
3053        let passkey_step = AuthEvent::cred_step_passkey(sessionid, resp);
3054
3055        let r3 = idms_auth
3056            .auth(&passkey_step, ct, Source::Internal.into())
3057            .await;
3058        debug!("r3 ==> {:?}", r3);
3059        idms_auth.commit().expect("Must not fail");
3060
3061        match r3 {
3062            Ok(AuthResult {
3063                sessionid: _,
3064                state: AuthState::Success(token, AuthIssueSession::Token),
3065            }) => {
3066                // Process the webauthn update
3067                let da = idms_delayed.try_recv().expect("invalid");
3068                assert!(matches!(da, DelayedAction::WebauthnCounterIncrement(_)));
3069                let r = idms.delayed_action(ct, da).await;
3070                assert!(r.is_ok());
3071
3072                // Process the auth session
3073                let da = idms_delayed.try_recv().expect("invalid");
3074                assert!(matches!(da, DelayedAction::AuthSessionRecord(_)));
3075
3076                Some(*token)
3077            }
3078            _ => None,
3079        }
3080    }
3081
3082    #[idm_test]
3083    async fn credential_update_session_cleanup(
3084        idms: &IdmServer,
3085        _idms_delayed: &mut IdmServerDelayed,
3086    ) {
3087        let ct = Duration::from_secs(TEST_CURRENT_TIME);
3088        let (cust, _) = setup_test_session(idms, ct).await;
3089
3090        let cutxn = idms.cred_update_transaction().await.unwrap();
3091        // The session exists
3092        let c_status = cutxn.credential_update_status(&cust, ct);
3093        assert!(c_status.is_ok());
3094        drop(cutxn);
3095
3096        // Making a new session is what triggers the clean of old sessions.
3097        let (_cust, _) =
3098            renew_test_session(idms, ct + MAXIMUM_CRED_UPDATE_TTL + Duration::from_secs(1)).await;
3099
3100        let cutxn = idms.cred_update_transaction().await.unwrap();
3101
3102        // Now fake going back in time .... allows the token to decrypt, but the session
3103        // is gone anyway!
3104        let c_status = cutxn
3105            .credential_update_status(&cust, ct)
3106            .expect_err("Session is still valid!");
3107        assert!(matches!(c_status, OperationError::InvalidState));
3108    }
3109
3110    #[idm_test]
3111    async fn credential_update_onboarding_create_new_pw(
3112        idms: &IdmServer,
3113        idms_delayed: &mut IdmServerDelayed,
3114    ) {
3115        let test_pw = "fo3EitierohF9AelaNgiem0Ei6vup4equo1Oogeevaetehah8Tobeengae3Ci0ooh0uki";
3116        let ct = Duration::from_secs(TEST_CURRENT_TIME);
3117
3118        let (cust, _) = setup_test_session(idms, ct).await;
3119
3120        let cutxn = idms.cred_update_transaction().await.unwrap();
3121
3122        // Get the credential status - this should tell
3123        // us the details of the credentials, as well as
3124        // if they are ready and valid to commit?
3125        let c_status = cutxn
3126            .credential_update_status(&cust, ct)
3127            .expect("Failed to get the current session status.");
3128
3129        trace!(?c_status);
3130
3131        assert!(c_status.primary.is_none());
3132
3133        // Test initially creating a credential.
3134        //   - pw first
3135        let c_status = cutxn
3136            .credential_primary_set_password(&cust, ct, test_pw)
3137            .expect("Failed to update the primary cred password");
3138
3139        assert!(c_status.can_commit);
3140
3141        drop(cutxn);
3142        commit_session(idms, ct, cust).await;
3143
3144        // Check it works!
3145        assert!(check_testperson_password(idms, idms_delayed, test_pw, ct)
3146            .await
3147            .is_some());
3148
3149        // Test deleting the pw
3150        let (cust, _) = renew_test_session(idms, ct).await;
3151        let cutxn = idms.cred_update_transaction().await.unwrap();
3152
3153        let c_status = cutxn
3154            .credential_update_status(&cust, ct)
3155            .expect("Failed to get the current session status.");
3156        trace!(?c_status);
3157        assert!(c_status.primary.is_some());
3158
3159        let c_status = cutxn
3160            .credential_primary_delete(&cust, ct)
3161            .expect("Failed to delete the primary cred");
3162        trace!(?c_status);
3163        assert!(c_status.primary.is_none());
3164
3165        drop(cutxn);
3166        commit_session(idms, ct, cust).await;
3167
3168        // Must fail now!
3169        assert!(check_testperson_password(idms, idms_delayed, test_pw, ct)
3170            .await
3171            .is_none());
3172    }
3173
3174    #[idm_test]
3175    async fn credential_update_password_quality_checks(
3176        idms: &IdmServer,
3177        _idms_delayed: &mut IdmServerDelayed,
3178    ) {
3179        let ct = Duration::from_secs(TEST_CURRENT_TIME);
3180        let (cust, _) = setup_test_session(idms, ct).await;
3181
3182        // Get the radius pw
3183
3184        let mut r_txn = idms.proxy_read().await.unwrap();
3185
3186        let radius_secret = r_txn
3187            .qs_read
3188            .internal_search_uuid(TESTPERSON_UUID)
3189            .expect("No such entry")
3190            .get_ava_single_secret(Attribute::RadiusSecret)
3191            .expect("No radius secret found")
3192            .to_string();
3193
3194        drop(r_txn);
3195
3196        let cutxn = idms.cred_update_transaction().await.unwrap();
3197
3198        // Get the credential status - this should tell
3199        // us the details of the credentials, as well as
3200        // if they are ready and valid to commit?
3201        let c_status = cutxn
3202            .credential_update_status(&cust, ct)
3203            .expect("Failed to get the current session status.");
3204
3205        trace!(?c_status);
3206
3207        assert!(c_status.primary.is_none());
3208
3209        // Test initially creating a credential.
3210        //   - pw first
3211
3212        let err = cutxn
3213            .credential_primary_set_password(&cust, ct, "password")
3214            .unwrap_err();
3215        trace!(?err);
3216        assert!(
3217            matches!(err, OperationError::PasswordQuality(details) if details == vec!(PasswordFeedback::TooShort(PW_MIN_LENGTH),))
3218        );
3219
3220        let err = cutxn
3221            .credential_primary_set_password(&cust, ct, "password1234")
3222            .unwrap_err();
3223        trace!(?err);
3224        assert!(
3225            matches!(err, OperationError::PasswordQuality(details) if details
3226            == vec!(
3227                PasswordFeedback::AddAnotherWordOrTwo,
3228                PasswordFeedback::ThisIsACommonPassword,
3229            ))
3230        );
3231
3232        let err = cutxn
3233            .credential_primary_set_password(&cust, ct, &radius_secret)
3234            .unwrap_err();
3235        trace!(?err);
3236        assert!(
3237            matches!(err, OperationError::PasswordQuality(details) if details == vec!(PasswordFeedback::DontReusePasswords,))
3238        );
3239
3240        let err = cutxn
3241            .credential_primary_set_password(&cust, ct, "testperson2023")
3242            .unwrap_err();
3243        trace!(?err);
3244        assert!(
3245            matches!(err, OperationError::PasswordQuality(details) if details == vec!(
3246            PasswordFeedback::NamesAndSurnamesByThemselvesAreEasyToGuess,
3247            PasswordFeedback::AvoidDatesAndYearsThatAreAssociatedWithYou,
3248                   ))
3249        );
3250
3251        let err = cutxn
3252            .credential_primary_set_password(
3253                &cust,
3254                ct,
3255                "demo_badlist_shohfie3aeci2oobur0aru9uushah6EiPi2woh4hohngoighaiRuepieN3ongoo1",
3256            )
3257            .unwrap_err();
3258        trace!(?err);
3259        assert!(
3260            matches!(err, OperationError::PasswordQuality(details) if details == vec!(PasswordFeedback::BadListed))
3261        );
3262
3263        assert!(c_status.can_commit);
3264
3265        drop(cutxn);
3266    }
3267
3268    #[idm_test]
3269    async fn credential_update_password_min_length_account_policy(
3270        idms: &IdmServer,
3271        _idms_delayed: &mut IdmServerDelayed,
3272    ) {
3273        let ct = Duration::from_secs(TEST_CURRENT_TIME);
3274
3275        // Set the account policy min pw length
3276        let test_pw_min_length = PW_MIN_LENGTH * 2;
3277
3278        let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();
3279
3280        let modlist = ModifyList::new_purge_and_set(
3281            Attribute::AuthPasswordMinimumLength,
3282            Value::Uint32(test_pw_min_length),
3283        );
3284        idms_prox_write
3285            .qs_write
3286            .internal_modify_uuid(UUID_IDM_ALL_ACCOUNTS, &modlist)
3287            .expect("Unable to change default session exp");
3288
3289        assert!(idms_prox_write.commit().is_ok());
3290        // This now will affect all accounts for the next cred update.
3291
3292        let (cust, _) = setup_test_session(idms, ct).await;
3293
3294        let cutxn = idms.cred_update_transaction().await.unwrap();
3295
3296        // Get the credential status - this should tell
3297        // us the details of the credentials, as well as
3298        // if they are ready and valid to commit?
3299        let c_status = cutxn
3300            .credential_update_status(&cust, ct)
3301            .expect("Failed to get the current session status.");
3302
3303        trace!(?c_status);
3304
3305        assert!(c_status.primary.is_none());
3306
3307        // Test initially creating a credential.
3308        //   - pw first
3309        let pw = password_from_random_len(8);
3310        let err = cutxn
3311            .credential_primary_set_password(&cust, ct, &pw)
3312            .unwrap_err();
3313        trace!(?err);
3314        assert!(
3315            matches!(err, OperationError::PasswordQuality(details) if details == vec!(PasswordFeedback::TooShort(test_pw_min_length),))
3316        );
3317
3318        // Test pw len of len minus 1
3319        let pw = password_from_random_len(test_pw_min_length - 1);
3320        let err = cutxn
3321            .credential_primary_set_password(&cust, ct, &pw)
3322            .unwrap_err();
3323        trace!(?err);
3324        assert!(matches!(err,OperationError::PasswordQuality(details)
3325                if details == vec!(PasswordFeedback::TooShort(test_pw_min_length),)));
3326
3327        // Test pw len of exact len
3328        let pw = password_from_random_len(test_pw_min_length);
3329        let c_status = cutxn
3330            .credential_primary_set_password(&cust, ct, &pw)
3331            .expect("Failed to update the primary cred password");
3332
3333        assert!(c_status.can_commit);
3334
3335        drop(cutxn);
3336        commit_session(idms, ct, cust).await;
3337    }
3338
3339    // Test set of primary account password
3340    //    - fail pw quality checks etc
3341    //    - set correctly.
3342
3343    // - setup TOTP
3344    #[idm_test]
3345    async fn credential_update_onboarding_create_new_mfa_totp_basic(
3346        idms: &IdmServer,
3347        idms_delayed: &mut IdmServerDelayed,
3348    ) {
3349        let test_pw = "fo3EitierohF9AelaNgiem0Ei6vup4equo1Oogeevaetehah8Tobeengae3Ci0ooh0uki";
3350        let ct = Duration::from_secs(TEST_CURRENT_TIME);
3351
3352        let (cust, _) = setup_test_session(idms, ct).await;
3353        let cutxn = idms.cred_update_transaction().await.unwrap();
3354
3355        // Setup the PW
3356        let c_status = cutxn
3357            .credential_primary_set_password(&cust, ct, test_pw)
3358            .expect("Failed to update the primary cred password");
3359
3360        // Since it's pw only.
3361        assert!(c_status.can_commit);
3362
3363        //
3364        let c_status = cutxn
3365            .credential_primary_init_totp(&cust, ct)
3366            .expect("Failed to update the primary cred password");
3367
3368        // Check the status has the token.
3369        let totp_token: Totp = match c_status.mfaregstate {
3370            MfaRegStateStatus::TotpCheck(secret) => Some(secret.try_into().unwrap()),
3371
3372            _ => None,
3373        }
3374        .expect("Unable to retrieve totp token, invalid state.");
3375
3376        trace!(?totp_token);
3377        let chal = totp_token
3378            .do_totp_duration_from_epoch(&ct)
3379            .expect("Failed to perform totp step");
3380
3381        // Intentionally get it wrong.
3382        let c_status = cutxn
3383            .credential_primary_check_totp(&cust, ct, chal + 1, "totp")
3384            .expect("Failed to update the primary cred totp");
3385
3386        assert!(
3387            matches!(c_status.mfaregstate, MfaRegStateStatus::TotpTryAgain),
3388            "{:?}",
3389            c_status.mfaregstate
3390        );
3391
3392        // Check that the user actually put something into the label
3393        let c_status = cutxn
3394            .credential_primary_check_totp(&cust, ct, chal, "")
3395            .expect("Failed to update the primary cred totp");
3396
3397        assert!(
3398            matches!(
3399                c_status.mfaregstate,
3400                MfaRegStateStatus::TotpNameTryAgain(ref val) if val.is_empty()
3401            ),
3402            "{:?}",
3403            c_status.mfaregstate
3404        );
3405
3406        // Okay, Now they are trying to be smart...
3407        let c_status = cutxn
3408            .credential_primary_check_totp(&cust, ct, chal, "           ")
3409            .expect("Failed to update the primary cred totp");
3410
3411        assert!(
3412            matches!(
3413                c_status.mfaregstate,
3414                MfaRegStateStatus::TotpNameTryAgain(ref val) if val == "           "
3415            ),
3416            "{:?}",
3417            c_status.mfaregstate
3418        );
3419
3420        let c_status = cutxn
3421            .credential_primary_check_totp(&cust, ct, chal, "totp")
3422            .expect("Failed to update the primary cred totp");
3423
3424        assert!(matches!(c_status.mfaregstate, MfaRegStateStatus::None));
3425        assert!(match c_status.primary.as_ref().map(|c| &c.type_) {
3426            Some(CredentialDetailType::PasswordMfa(totp, _, 0)) => !totp.is_empty(),
3427            _ => false,
3428        });
3429
3430        {
3431            let c_status = cutxn
3432                .credential_primary_init_totp(&cust, ct)
3433                .expect("Failed to update the primary cred password");
3434
3435            // Check the status has the token.
3436            let totp_token: Totp = match c_status.mfaregstate {
3437                MfaRegStateStatus::TotpCheck(secret) => Some(secret.try_into().unwrap()),
3438                _ => None,
3439            }
3440            .expect("Unable to retrieve totp token, invalid state.");
3441
3442            trace!(?totp_token);
3443            let chal = totp_token
3444                .do_totp_duration_from_epoch(&ct)
3445                .expect("Failed to perform totp step");
3446
3447            // They tried to add a second totp under the same name
3448            let c_status = cutxn
3449                .credential_primary_check_totp(&cust, ct, chal, "totp")
3450                .expect("Failed to update the primary cred totp");
3451
3452            assert!(
3453                matches!(
3454                    c_status.mfaregstate,
3455                    MfaRegStateStatus::TotpNameTryAgain(ref val) if val == "totp"
3456                ),
3457                "{:?}",
3458                c_status.mfaregstate
3459            );
3460
3461            assert!(cutxn.credential_update_cancel_mfareg(&cust, ct).is_ok())
3462        }
3463
3464        // Should be okay now!
3465
3466        drop(cutxn);
3467        commit_session(idms, ct, cust).await;
3468
3469        // Check it works!
3470        assert!(
3471            check_testperson_password_totp(idms, idms_delayed, test_pw, &totp_token, ct)
3472                .await
3473                .is_some()
3474        );
3475        // No need to test delete of the whole cred, we already did with pw above.
3476
3477        // If we remove TOTP, show it reverts back.
3478        let (cust, _) = renew_test_session(idms, ct).await;
3479        let cutxn = idms.cred_update_transaction().await.unwrap();
3480
3481        let c_status = cutxn
3482            .credential_primary_remove_totp(&cust, ct, "totp")
3483            .expect("Failed to update the primary cred password");
3484
3485        assert!(matches!(c_status.mfaregstate, MfaRegStateStatus::None));
3486        assert!(matches!(
3487            c_status.primary.as_ref().map(|c| &c.type_),
3488            Some(CredentialDetailType::Password)
3489        ));
3490
3491        drop(cutxn);
3492        commit_session(idms, ct, cust).await;
3493
3494        // Check it works with totp removed.
3495        assert!(check_testperson_password(idms, idms_delayed, test_pw, ct)
3496            .await
3497            .is_some());
3498    }
3499
3500    // Check sha1 totp.
3501    #[idm_test]
3502    async fn credential_update_onboarding_create_new_mfa_totp_sha1(
3503        idms: &IdmServer,
3504        idms_delayed: &mut IdmServerDelayed,
3505    ) {
3506        let test_pw = "fo3EitierohF9AelaNgiem0Ei6vup4equo1Oogeevaetehah8Tobeengae3Ci0ooh0uki";
3507        let ct = Duration::from_secs(TEST_CURRENT_TIME);
3508
3509        let (cust, _) = setup_test_session(idms, ct).await;
3510        let cutxn = idms.cred_update_transaction().await.unwrap();
3511
3512        // Setup the PW
3513        let c_status = cutxn
3514            .credential_primary_set_password(&cust, ct, test_pw)
3515            .expect("Failed to update the primary cred password");
3516
3517        // Since it's pw only.
3518        assert!(c_status.can_commit);
3519
3520        //
3521        let c_status = cutxn
3522            .credential_primary_init_totp(&cust, ct)
3523            .expect("Failed to update the primary cred password");
3524
3525        // Check the status has the token.
3526        let totp_token: Totp = match c_status.mfaregstate {
3527            MfaRegStateStatus::TotpCheck(secret) => Some(secret.try_into().unwrap()),
3528
3529            _ => None,
3530        }
3531        .expect("Unable to retrieve totp token, invalid state.");
3532
3533        let totp_token = totp_token.downgrade_to_legacy();
3534
3535        trace!(?totp_token);
3536        let chal = totp_token
3537            .do_totp_duration_from_epoch(&ct)
3538            .expect("Failed to perform totp step");
3539
3540        // Should getn the warn that it's sha1
3541        let c_status = cutxn
3542            .credential_primary_check_totp(&cust, ct, chal, "totp")
3543            .expect("Failed to update the primary cred password");
3544
3545        assert!(matches!(
3546            c_status.mfaregstate,
3547            MfaRegStateStatus::TotpInvalidSha1
3548        ));
3549
3550        // Accept it
3551        let c_status = cutxn
3552            .credential_primary_accept_sha1_totp(&cust, ct)
3553            .expect("Failed to update the primary cred password");
3554
3555        assert!(matches!(c_status.mfaregstate, MfaRegStateStatus::None));
3556        assert!(match c_status.primary.as_ref().map(|c| &c.type_) {
3557            Some(CredentialDetailType::PasswordMfa(totp, _, 0)) => !totp.is_empty(),
3558            _ => false,
3559        });
3560
3561        // Should be okay now!
3562
3563        drop(cutxn);
3564        commit_session(idms, ct, cust).await;
3565
3566        // Check it works!
3567        assert!(
3568            check_testperson_password_totp(idms, idms_delayed, test_pw, &totp_token, ct)
3569                .await
3570                .is_some()
3571        );
3572        // No need to test delete, we already did with pw above.
3573    }
3574
3575    #[idm_test]
3576    async fn credential_update_onboarding_create_new_mfa_totp_backup_codes(
3577        idms: &IdmServer,
3578        idms_delayed: &mut IdmServerDelayed,
3579    ) {
3580        let test_pw = "fo3EitierohF9AelaNgiem0Ei6vup4equo1Oogeevaetehah8Tobeengae3Ci0ooh0uki";
3581        let ct = Duration::from_secs(TEST_CURRENT_TIME);
3582
3583        let (cust, _) = setup_test_session(idms, ct).await;
3584        let cutxn = idms.cred_update_transaction().await.unwrap();
3585
3586        // Setup the PW
3587        let _c_status = cutxn
3588            .credential_primary_set_password(&cust, ct, test_pw)
3589            .expect("Failed to update the primary cred password");
3590
3591        // Backup codes are refused to be added because we don't have mfa yet.
3592        assert!(matches!(
3593            cutxn.credential_primary_init_backup_codes(&cust, ct),
3594            Err(OperationError::InvalidState)
3595        ));
3596
3597        let c_status = cutxn
3598            .credential_primary_init_totp(&cust, ct)
3599            .expect("Failed to update the primary cred password");
3600
3601        let totp_token: Totp = match c_status.mfaregstate {
3602            MfaRegStateStatus::TotpCheck(secret) => Some(secret.try_into().unwrap()),
3603            _ => None,
3604        }
3605        .expect("Unable to retrieve totp token, invalid state.");
3606
3607        trace!(?totp_token);
3608        let chal = totp_token
3609            .do_totp_duration_from_epoch(&ct)
3610            .expect("Failed to perform totp step");
3611
3612        let c_status = cutxn
3613            .credential_primary_check_totp(&cust, ct, chal, "totp")
3614            .expect("Failed to update the primary cred totp");
3615
3616        assert!(matches!(c_status.mfaregstate, MfaRegStateStatus::None));
3617        assert!(match c_status.primary.as_ref().map(|c| &c.type_) {
3618            Some(CredentialDetailType::PasswordMfa(totp, _, 0)) => !totp.is_empty(),
3619            _ => false,
3620        });
3621
3622        // Now good to go, we need to now add our backup codes.
3623        // What's the right way to get these back?
3624        let c_status = cutxn
3625            .credential_primary_init_backup_codes(&cust, ct)
3626            .expect("Failed to update the primary cred password");
3627
3628        let codes = match c_status.mfaregstate {
3629            MfaRegStateStatus::BackupCodes(codes) => Some(codes),
3630            _ => None,
3631        }
3632        .expect("Unable to retrieve backupcodes, invalid state.");
3633
3634        // Should error because the number is not 0
3635        debug!("{:?}", c_status.primary.as_ref().map(|c| &c.type_));
3636        assert!(match c_status.primary.as_ref().map(|c| &c.type_) {
3637            Some(CredentialDetailType::PasswordMfa(totp, _, 8)) => !totp.is_empty(),
3638            _ => false,
3639        });
3640
3641        // Should be okay now!
3642        drop(cutxn);
3643        commit_session(idms, ct, cust).await;
3644
3645        let backup_code = codes.iter().next().expect("No codes available");
3646
3647        // Check it works!
3648        assert!(check_testperson_password_backup_code(
3649            idms,
3650            idms_delayed,
3651            test_pw,
3652            backup_code,
3653            ct
3654        )
3655        .await
3656        .is_some());
3657
3658        // Renew to start the next steps
3659        let (cust, _) = renew_test_session(idms, ct).await;
3660        let cutxn = idms.cred_update_transaction().await.unwrap();
3661
3662        // Only 7 codes left.
3663        let c_status = cutxn
3664            .credential_update_status(&cust, ct)
3665            .expect("Failed to get the current session status.");
3666
3667        assert!(match c_status.primary.as_ref().map(|c| &c.type_) {
3668            Some(CredentialDetailType::PasswordMfa(totp, _, 7)) => !totp.is_empty(),
3669            _ => false,
3670        });
3671
3672        // If we remove codes, it leaves totp.
3673        let c_status = cutxn
3674            .credential_primary_remove_backup_codes(&cust, ct)
3675            .expect("Failed to update the primary cred password");
3676
3677        assert!(matches!(c_status.mfaregstate, MfaRegStateStatus::None));
3678        assert!(match c_status.primary.as_ref().map(|c| &c.type_) {
3679            Some(CredentialDetailType::PasswordMfa(totp, _, 0)) => !totp.is_empty(),
3680            _ => false,
3681        });
3682
3683        // Re-add the codes.
3684        let c_status = cutxn
3685            .credential_primary_init_backup_codes(&cust, ct)
3686            .expect("Failed to update the primary cred password");
3687
3688        assert!(matches!(
3689            c_status.mfaregstate,
3690            MfaRegStateStatus::BackupCodes(_)
3691        ));
3692        assert!(match c_status.primary.as_ref().map(|c| &c.type_) {
3693            Some(CredentialDetailType::PasswordMfa(totp, _, 8)) => !totp.is_empty(),
3694            _ => false,
3695        });
3696
3697        // If we remove totp, it removes codes.
3698        let c_status = cutxn
3699            .credential_primary_remove_totp(&cust, ct, "totp")
3700            .expect("Failed to update the primary cred password");
3701
3702        assert!(matches!(c_status.mfaregstate, MfaRegStateStatus::None));
3703        assert!(matches!(
3704            c_status.primary.as_ref().map(|c| &c.type_),
3705            Some(CredentialDetailType::Password)
3706        ));
3707
3708        drop(cutxn);
3709        commit_session(idms, ct, cust).await;
3710    }
3711
3712    #[idm_test]
3713    async fn credential_update_onboarding_cancel_inprogress_totp(
3714        idms: &IdmServer,
3715        idms_delayed: &mut IdmServerDelayed,
3716    ) {
3717        let test_pw = "fo3EitierohF9AelaNgiem0Ei6vup4equo1Oogeevaetehah8Tobeengae3Ci0ooh0uki";
3718        let ct = Duration::from_secs(TEST_CURRENT_TIME);
3719
3720        let (cust, _) = setup_test_session(idms, ct).await;
3721        let cutxn = idms.cred_update_transaction().await.unwrap();
3722
3723        // Setup the PW
3724        let c_status = cutxn
3725            .credential_primary_set_password(&cust, ct, test_pw)
3726            .expect("Failed to update the primary cred password");
3727
3728        // Since it's pw only.
3729        assert!(c_status.can_commit);
3730
3731        //
3732        let c_status = cutxn
3733            .credential_primary_init_totp(&cust, ct)
3734            .expect("Failed to update the primary cred totp");
3735
3736        // Check the status has the token.
3737        assert!(c_status.can_commit);
3738        assert!(matches!(
3739            c_status.mfaregstate,
3740            MfaRegStateStatus::TotpCheck(_)
3741        ));
3742
3743        let c_status = cutxn
3744            .credential_update_cancel_mfareg(&cust, ct)
3745            .expect("Failed to cancel in-flight totp change");
3746
3747        assert!(matches!(c_status.mfaregstate, MfaRegStateStatus::None));
3748        assert!(c_status.can_commit);
3749
3750        drop(cutxn);
3751        commit_session(idms, ct, cust).await;
3752
3753        // It's pw only, since we canceled TOTP
3754        assert!(check_testperson_password(idms, idms_delayed, test_pw, ct)
3755            .await
3756            .is_some());
3757    }
3758
3759    // Primary cred must be pw or pwmfa
3760
3761    // - setup webauthn
3762    // - remove webauthn
3763    // - test multiple webauthn token.
3764
3765    async fn create_new_passkey(
3766        ct: Duration,
3767        origin: &Url,
3768        cutxn: &IdmServerCredUpdateTransaction<'_>,
3769        cust: &CredentialUpdateSessionToken,
3770        wa: &mut WebauthnAuthenticator<SoftPasskey>,
3771    ) -> CredentialUpdateSessionStatus {
3772        // Start the registration
3773        let c_status = cutxn
3774            .credential_passkey_init(cust, ct)
3775            .expect("Failed to initiate passkey registration");
3776
3777        assert!(c_status.passkeys.is_empty());
3778
3779        let passkey_chal = match c_status.mfaregstate {
3780            MfaRegStateStatus::Passkey(c) => Some(c),
3781            _ => None,
3782        }
3783        .expect("Unable to access passkey challenge, invalid state");
3784
3785        let passkey_resp = wa
3786            .do_registration(origin.clone(), passkey_chal)
3787            .expect("Failed to create soft passkey");
3788
3789        // Finish the registration
3790        let label = "softtoken".to_string();
3791        let c_status = cutxn
3792            .credential_passkey_finish(cust, ct, label, &passkey_resp)
3793            .expect("Failed to initiate passkey registration");
3794
3795        assert!(matches!(c_status.mfaregstate, MfaRegStateStatus::None));
3796        assert!(c_status.primary.as_ref().is_none());
3797
3798        // Check we have the passkey
3799        trace!(?c_status);
3800        assert_eq!(c_status.passkeys.len(), 1);
3801
3802        c_status
3803    }
3804
3805    #[idm_test]
3806    async fn credential_update_onboarding_create_new_passkey(
3807        idms: &IdmServer,
3808        idms_delayed: &mut IdmServerDelayed,
3809    ) {
3810        let ct = Duration::from_secs(TEST_CURRENT_TIME);
3811
3812        let (cust, _) = setup_test_session(idms, ct).await;
3813        let cutxn = idms.cred_update_transaction().await.unwrap();
3814        let origin = cutxn.get_origin().clone();
3815
3816        // Create a soft passkey
3817        let mut wa = WebauthnAuthenticator::new(SoftPasskey::new(true));
3818
3819        let c_status = create_new_passkey(ct, &origin, &cutxn, &cust, &mut wa).await;
3820
3821        // Get the UUID of the passkey here.
3822        let pk_uuid = c_status.passkeys.first().map(|pkd| pkd.uuid).unwrap();
3823
3824        // Commit
3825        drop(cutxn);
3826        commit_session(idms, ct, cust).await;
3827
3828        // Do an auth test
3829        assert!(
3830            check_testperson_passkey(idms, idms_delayed, &mut wa, origin.clone(), ct)
3831                .await
3832                .is_some()
3833        );
3834
3835        // Now test removing the token
3836        let (cust, _) = renew_test_session(idms, ct).await;
3837        let cutxn = idms.cred_update_transaction().await.unwrap();
3838
3839        trace!(?c_status);
3840        assert!(c_status.primary.is_none());
3841        assert_eq!(c_status.passkeys.len(), 1);
3842
3843        let c_status = cutxn
3844            .credential_passkey_remove(&cust, ct, pk_uuid)
3845            .expect("Failed to delete the passkey");
3846
3847        trace!(?c_status);
3848        assert!(c_status.primary.is_none());
3849        assert!(c_status.passkeys.is_empty());
3850
3851        drop(cutxn);
3852        commit_session(idms, ct, cust).await;
3853
3854        // Must fail now!
3855        assert!(
3856            check_testperson_passkey(idms, idms_delayed, &mut wa, origin, ct)
3857                .await
3858                .is_none()
3859        );
3860    }
3861
3862    #[idm_test]
3863    async fn credential_update_access_denied(
3864        idms: &IdmServer,
3865        _idms_delayed: &mut IdmServerDelayed,
3866    ) {
3867        // Test that if access is denied for a synced account, that the actual action to update
3868        // the credentials is always denied.
3869
3870        let ct = Duration::from_secs(TEST_CURRENT_TIME);
3871
3872        let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();
3873
3874        let sync_uuid = Uuid::new_v4();
3875
3876        let e1 = entry_init!(
3877            (Attribute::Class, EntryClass::Object.to_value()),
3878            (Attribute::Class, EntryClass::SyncAccount.to_value()),
3879            (Attribute::Name, Value::new_iname("test_scim_sync")),
3880            (Attribute::Uuid, Value::Uuid(sync_uuid)),
3881            (
3882                Attribute::Description,
3883                Value::new_utf8s("A test sync agreement")
3884            )
3885        );
3886
3887        let e2 = entry_init!(
3888            (Attribute::Class, EntryClass::Object.to_value()),
3889            (Attribute::Class, EntryClass::SyncObject.to_value()),
3890            (Attribute::Class, EntryClass::Account.to_value()),
3891            (Attribute::Class, EntryClass::PosixAccount.to_value()),
3892            (Attribute::Class, EntryClass::Person.to_value()),
3893            (Attribute::SyncParentUuid, Value::Refer(sync_uuid)),
3894            (Attribute::Name, Value::new_iname("testperson")),
3895            (Attribute::Uuid, Value::Uuid(TESTPERSON_UUID)),
3896            (Attribute::Description, Value::new_utf8s("testperson")),
3897            (Attribute::DisplayName, Value::new_utf8s("testperson"))
3898        );
3899
3900        let ce = CreateEvent::new_internal(vec![e1, e2]);
3901        let cr = idms_prox_write.qs_write.create(&ce);
3902        assert!(cr.is_ok());
3903
3904        let testperson = idms_prox_write
3905            .qs_write
3906            .internal_search_uuid(TESTPERSON_UUID)
3907            .expect("failed");
3908
3909        let cur = idms_prox_write.init_credential_update(
3910            &InitCredentialUpdateEvent::new_impersonate_entry(testperson),
3911            ct,
3912        );
3913
3914        idms_prox_write.commit().expect("Failed to commit txn");
3915
3916        let (cust, custatus) = cur.expect("Failed to start update");
3917
3918        trace!(?custatus);
3919
3920        // Destructure to force us to update this test if we change this
3921        // structure at all.
3922        let CredentialUpdateSessionStatus {
3923            spn: _,
3924            displayname: _,
3925            ext_cred_portal,
3926            mfaregstate: _,
3927            can_commit: _,
3928            warnings: _,
3929            primary: _,
3930            primary_state,
3931            passkeys: _,
3932            passkeys_state,
3933            attested_passkeys: _,
3934            attested_passkeys_state,
3935            attested_passkeys_allowed_devices: _,
3936            unixcred_state,
3937            unixcred: _,
3938            sshkeys: _,
3939            sshkeys_state,
3940        } = custatus;
3941
3942        assert!(matches!(ext_cred_portal, CUExtPortal::Hidden));
3943        assert!(matches!(primary_state, CredentialState::AccessDeny));
3944        assert!(matches!(passkeys_state, CredentialState::AccessDeny));
3945        assert!(matches!(
3946            attested_passkeys_state,
3947            CredentialState::AccessDeny
3948        ));
3949        assert!(matches!(unixcred_state, CredentialState::AccessDeny));
3950        assert!(matches!(sshkeys_state, CredentialState::AccessDeny));
3951
3952        let cutxn = idms.cred_update_transaction().await.unwrap();
3953
3954        // let origin = cutxn.get_origin().clone();
3955
3956        // Test that any of the primary or passkey update methods fail with access denied.
3957
3958        // credential_primary_set_password
3959        let err = cutxn
3960            .credential_primary_set_password(&cust, ct, "password")
3961            .unwrap_err();
3962        assert!(matches!(err, OperationError::AccessDenied));
3963
3964        let err = cutxn
3965            .credential_unix_set_password(&cust, ct, "password")
3966            .unwrap_err();
3967        assert!(matches!(err, OperationError::AccessDenied));
3968
3969        let sshkey = SshPublicKey::from_string(SSHKEY_VALID_1).expect("Invalid SSHKEY_VALID_1");
3970
3971        let err = cutxn
3972            .credential_sshkey_add(&cust, ct, "label".to_string(), sshkey)
3973            .unwrap_err();
3974        assert!(matches!(err, OperationError::AccessDenied));
3975
3976        // credential_primary_init_totp
3977        let err = cutxn.credential_primary_init_totp(&cust, ct).unwrap_err();
3978        assert!(matches!(err, OperationError::AccessDenied));
3979
3980        // credential_primary_check_totp
3981        let err = cutxn
3982            .credential_primary_check_totp(&cust, ct, 0, "totp")
3983            .unwrap_err();
3984        assert!(matches!(err, OperationError::AccessDenied));
3985
3986        // credential_primary_accept_sha1_totp
3987        let err = cutxn
3988            .credential_primary_accept_sha1_totp(&cust, ct)
3989            .unwrap_err();
3990        assert!(matches!(err, OperationError::AccessDenied));
3991
3992        // credential_primary_remove_totp
3993        let err = cutxn
3994            .credential_primary_remove_totp(&cust, ct, "totp")
3995            .unwrap_err();
3996        assert!(matches!(err, OperationError::AccessDenied));
3997
3998        // credential_primary_init_backup_codes
3999        let err = cutxn
4000            .credential_primary_init_backup_codes(&cust, ct)
4001            .unwrap_err();
4002        assert!(matches!(err, OperationError::AccessDenied));
4003
4004        // credential_primary_remove_backup_codes
4005        let err = cutxn
4006            .credential_primary_remove_backup_codes(&cust, ct)
4007            .unwrap_err();
4008        assert!(matches!(err, OperationError::AccessDenied));
4009
4010        // credential_primary_delete
4011        let err = cutxn.credential_primary_delete(&cust, ct).unwrap_err();
4012        assert!(matches!(err, OperationError::AccessDenied));
4013
4014        // credential_passkey_init
4015        let err = cutxn.credential_passkey_init(&cust, ct).unwrap_err();
4016        assert!(matches!(err, OperationError::AccessDenied));
4017
4018        // credential_passkey_finish
4019        //   Can't test because we need a public key response.
4020
4021        // credential_passkey_remove
4022        let err = cutxn
4023            .credential_passkey_remove(&cust, ct, Uuid::new_v4())
4024            .unwrap_err();
4025        assert!(matches!(err, OperationError::AccessDenied));
4026
4027        let c_status = cutxn
4028            .credential_update_status(&cust, ct)
4029            .expect("Failed to get the current session status.");
4030        trace!(?c_status);
4031        assert!(c_status.primary.is_none());
4032        assert!(c_status.passkeys.is_empty());
4033
4034        drop(cutxn);
4035        commit_session(idms, ct, cust).await;
4036    }
4037
4038    // Assert we can't create "just" a password when mfa is required.
4039    #[idm_test]
4040    async fn credential_update_account_policy_mfa_required(
4041        idms: &IdmServer,
4042        _idms_delayed: &mut IdmServerDelayed,
4043    ) {
4044        let test_pw = "fo3EitierohF9AelaNgiem0Ei6vup4equo1Oogeevaetehah8Tobeengae3Ci0ooh0uki";
4045        let ct = Duration::from_secs(TEST_CURRENT_TIME);
4046
4047        let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();
4048
4049        let modlist = ModifyList::new_purge_and_set(
4050            Attribute::CredentialTypeMinimum,
4051            CredentialType::Mfa.into(),
4052        );
4053        idms_prox_write
4054            .qs_write
4055            .internal_modify_uuid(UUID_IDM_ALL_ACCOUNTS, &modlist)
4056            .expect("Unable to change default session exp");
4057
4058        assert!(idms_prox_write.commit().is_ok());
4059        // This now will affect all accounts for the next cred update.
4060
4061        let (cust, _) = setup_test_session(idms, ct).await;
4062
4063        let cutxn = idms.cred_update_transaction().await.unwrap();
4064
4065        // Get the credential status - this should tell
4066        // us the details of the credentials, as well as
4067        // if they are ready and valid to commit?
4068        let c_status = cutxn
4069            .credential_update_status(&cust, ct)
4070            .expect("Failed to get the current session status.");
4071
4072        trace!(?c_status);
4073
4074        assert!(c_status.primary.is_none());
4075
4076        // Test initially creating a credential.
4077        //   - pw first
4078        let c_status = cutxn
4079            .credential_primary_set_password(&cust, ct, test_pw)
4080            .expect("Failed to update the primary cred password");
4081
4082        assert!(!c_status.can_commit);
4083        assert!(c_status
4084            .warnings
4085            .contains(&CredentialUpdateSessionStatusWarnings::MfaRequired));
4086        // Check reason! Must show "no mfa". We need totp to be added now.
4087
4088        let c_status = cutxn
4089            .credential_primary_init_totp(&cust, ct)
4090            .expect("Failed to update the primary cred password");
4091
4092        // Check the status has the token.
4093        let totp_token: Totp = match c_status.mfaregstate {
4094            MfaRegStateStatus::TotpCheck(secret) => Some(secret.try_into().unwrap()),
4095
4096            _ => None,
4097        }
4098        .expect("Unable to retrieve totp token, invalid state.");
4099
4100        trace!(?totp_token);
4101        let chal = totp_token
4102            .do_totp_duration_from_epoch(&ct)
4103            .expect("Failed to perform totp step");
4104
4105        let c_status = cutxn
4106            .credential_primary_check_totp(&cust, ct, chal, "totp")
4107            .expect("Failed to update the primary cred totp");
4108
4109        assert!(matches!(c_status.mfaregstate, MfaRegStateStatus::None));
4110        assert!(match c_status.primary.as_ref().map(|c| &c.type_) {
4111            Some(CredentialDetailType::PasswordMfa(totp, _, 0)) => !totp.is_empty(),
4112            _ => false,
4113        });
4114
4115        // Done, can now commit.
4116        assert!(c_status.can_commit);
4117        assert!(c_status.warnings.is_empty());
4118
4119        drop(cutxn);
4120        commit_session(idms, ct, cust).await;
4121
4122        // If we remove TOTP, it blocks commit.
4123        let (cust, _) = renew_test_session(idms, ct).await;
4124        let cutxn = idms.cred_update_transaction().await.unwrap();
4125
4126        let c_status = cutxn
4127            .credential_primary_remove_totp(&cust, ct, "totp")
4128            .expect("Failed to update the primary cred totp");
4129
4130        assert!(matches!(c_status.mfaregstate, MfaRegStateStatus::None));
4131        assert!(matches!(
4132            c_status.primary.as_ref().map(|c| &c.type_),
4133            Some(CredentialDetailType::Password)
4134        ));
4135
4136        // Delete of the totp forces us back here.
4137        assert!(!c_status.can_commit);
4138        assert!(c_status
4139            .warnings
4140            .contains(&CredentialUpdateSessionStatusWarnings::MfaRequired));
4141
4142        // Passkeys satisfy the policy though
4143        let c_status = cutxn
4144            .credential_primary_delete(&cust, ct)
4145            .expect("Failed to delete the primary credential");
4146        assert!(c_status.primary.is_none());
4147
4148        let origin = cutxn.get_origin().clone();
4149        let mut wa = WebauthnAuthenticator::new(SoftPasskey::new(true));
4150
4151        let c_status = create_new_passkey(ct, &origin, &cutxn, &cust, &mut wa).await;
4152
4153        assert!(c_status.can_commit);
4154        assert!(c_status.warnings.is_empty());
4155        assert_eq!(c_status.passkeys.len(), 1);
4156
4157        drop(cutxn);
4158        commit_session(idms, ct, cust).await;
4159    }
4160
4161    #[idm_test]
4162    async fn credential_update_account_policy_passkey_required(
4163        idms: &IdmServer,
4164        _idms_delayed: &mut IdmServerDelayed,
4165    ) {
4166        let test_pw = "fo3EitierohF9AelaNgiem0Ei6vup4equo1Oogeevaetehah8Tobeengae3Ci0ooh0uki";
4167        let ct = Duration::from_secs(TEST_CURRENT_TIME);
4168
4169        let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();
4170
4171        let modlist = ModifyList::new_purge_and_set(
4172            Attribute::CredentialTypeMinimum,
4173            CredentialType::Passkey.into(),
4174        );
4175        idms_prox_write
4176            .qs_write
4177            .internal_modify_uuid(UUID_IDM_ALL_ACCOUNTS, &modlist)
4178            .expect("Unable to change default session exp");
4179
4180        assert!(idms_prox_write.commit().is_ok());
4181        // This now will affect all accounts for the next cred update.
4182
4183        let (cust, _) = setup_test_session(idms, ct).await;
4184
4185        let cutxn = idms.cred_update_transaction().await.unwrap();
4186
4187        // Get the credential status - this should tell
4188        // us the details of the credentials, as well as
4189        // if they are ready and valid to commit?
4190        let c_status = cutxn
4191            .credential_update_status(&cust, ct)
4192            .expect("Failed to get the current session status.");
4193
4194        trace!(?c_status);
4195        assert!(c_status.primary.is_none());
4196        assert!(matches!(
4197            c_status.primary_state,
4198            CredentialState::PolicyDeny
4199        ));
4200
4201        let err = cutxn
4202            .credential_primary_set_password(&cust, ct, test_pw)
4203            .unwrap_err();
4204        assert!(matches!(err, OperationError::AccessDenied));
4205
4206        let origin = cutxn.get_origin().clone();
4207        let mut wa = WebauthnAuthenticator::new(SoftPasskey::new(true));
4208
4209        let c_status = create_new_passkey(ct, &origin, &cutxn, &cust, &mut wa).await;
4210
4211        assert!(c_status.can_commit);
4212        assert!(c_status.warnings.is_empty());
4213        assert_eq!(c_status.passkeys.len(), 1);
4214
4215        drop(cutxn);
4216        commit_session(idms, ct, cust).await;
4217    }
4218
4219    // Attested passkey types
4220
4221    #[idm_test]
4222    async fn credential_update_account_policy_attested_passkey_required(
4223        idms: &IdmServer,
4224        idms_delayed: &mut IdmServerDelayed,
4225    ) {
4226        let ct = Duration::from_secs(TEST_CURRENT_TIME);
4227
4228        // Create the attested soft token we will use in this test.
4229        let (soft_token_valid, ca_root) = SoftToken::new(true).unwrap();
4230        let mut wa_token_valid = WebauthnAuthenticator::new(soft_token_valid);
4231
4232        // Create it's associated policy.
4233        let mut att_ca_builder = AttestationCaListBuilder::new();
4234        att_ca_builder
4235            .insert_device_x509(
4236                ca_root,
4237                softtoken::AAGUID,
4238                "softtoken".to_string(),
4239                Default::default(),
4240            )
4241            .unwrap();
4242        let att_ca_list = att_ca_builder.build();
4243
4244        let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();
4245
4246        let modlist = ModifyList::new_purge_and_set(
4247            Attribute::WebauthnAttestationCaList,
4248            Value::WebauthnAttestationCaList(att_ca_list),
4249        );
4250        idms_prox_write
4251            .qs_write
4252            .internal_modify_uuid(UUID_IDM_ALL_ACCOUNTS, &modlist)
4253            .expect("Unable to change webauthn attestation policy");
4254
4255        assert!(idms_prox_write.commit().is_ok());
4256
4257        // Create the invalid tokens
4258        let (soft_token_invalid, _) = SoftToken::new(true).unwrap();
4259        let mut wa_token_invalid = WebauthnAuthenticator::new(soft_token_invalid);
4260
4261        let mut wa_passkey_invalid = WebauthnAuthenticator::new(SoftPasskey::new(true));
4262
4263        // Setup the cred update session.
4264
4265        let (cust, _) = setup_test_session(idms, ct).await;
4266        let cutxn = idms.cred_update_transaction().await.unwrap();
4267        let origin = cutxn.get_origin().clone();
4268
4269        // Our status needs the correct device names for UI hinting.
4270        let c_status = cutxn
4271            .credential_update_status(&cust, ct)
4272            .expect("Failed to get the current session status.");
4273
4274        trace!(?c_status);
4275        assert!(c_status.attested_passkeys.is_empty());
4276        assert_eq!(
4277            c_status.attested_passkeys_allowed_devices,
4278            vec!["softtoken".to_string()]
4279        );
4280
4281        // -------------------------------------------------------
4282        // Unable to add an passkey when attestation is requested.
4283        let err = cutxn.credential_passkey_init(&cust, ct).unwrap_err();
4284        assert!(matches!(err, OperationError::AccessDenied));
4285
4286        // -------------------------------------------------------
4287        // Reject a credential that lacks attestation
4288        let c_status = cutxn
4289            .credential_attested_passkey_init(&cust, ct)
4290            .expect("Failed to initiate attested passkey registration");
4291
4292        let passkey_chal = match c_status.mfaregstate {
4293            MfaRegStateStatus::AttestedPasskey(c) => Some(c),
4294            _ => None,
4295        }
4296        .expect("Unable to access passkey challenge, invalid state");
4297
4298        let passkey_resp = wa_passkey_invalid
4299            .do_registration(origin.clone(), passkey_chal)
4300            .expect("Failed to create soft passkey");
4301
4302        // Finish the registration
4303        let label = "softtoken".to_string();
4304        let err = cutxn
4305            .credential_attested_passkey_finish(&cust, ct, label, &passkey_resp)
4306            .unwrap_err();
4307
4308        assert!(matches!(
4309            err,
4310            OperationError::CU0001WebauthnAttestationNotTrusted
4311        ));
4312
4313        // -------------------------------------------------------
4314        // Reject a credential with wrong CA / correct aaguid
4315        let c_status = cutxn
4316            .credential_attested_passkey_init(&cust, ct)
4317            .expect("Failed to initiate attested passkey registration");
4318
4319        let passkey_chal = match c_status.mfaregstate {
4320            MfaRegStateStatus::AttestedPasskey(c) => Some(c),
4321            _ => None,
4322        }
4323        .expect("Unable to access passkey challenge, invalid state");
4324
4325        let passkey_resp = wa_token_invalid
4326            .do_registration(origin.clone(), passkey_chal)
4327            .expect("Failed to create soft passkey");
4328
4329        // Finish the registration
4330        let label = "softtoken".to_string();
4331        let err = cutxn
4332            .credential_attested_passkey_finish(&cust, ct, label, &passkey_resp)
4333            .unwrap_err();
4334
4335        assert!(matches!(
4336            err,
4337            OperationError::CU0001WebauthnAttestationNotTrusted
4338        ));
4339
4340        // -------------------------------------------------------
4341        // Accept credential with correct CA/aaguid
4342        let c_status = cutxn
4343            .credential_attested_passkey_init(&cust, ct)
4344            .expect("Failed to initiate attested passkey registration");
4345
4346        let passkey_chal = match c_status.mfaregstate {
4347            MfaRegStateStatus::AttestedPasskey(c) => Some(c),
4348            _ => None,
4349        }
4350        .expect("Unable to access passkey challenge, invalid state");
4351
4352        let passkey_resp = wa_token_valid
4353            .do_registration(origin.clone(), passkey_chal)
4354            .expect("Failed to create soft passkey");
4355
4356        // Finish the registration
4357        let label = "softtoken".to_string();
4358        let c_status = cutxn
4359            .credential_attested_passkey_finish(&cust, ct, label, &passkey_resp)
4360            .expect("Failed to initiate passkey registration");
4361
4362        assert!(matches!(c_status.mfaregstate, MfaRegStateStatus::None));
4363        trace!(?c_status);
4364        assert_eq!(c_status.attested_passkeys.len(), 1);
4365
4366        let pk_uuid = c_status
4367            .attested_passkeys
4368            .first()
4369            .map(|pkd| pkd.uuid)
4370            .unwrap();
4371
4372        drop(cutxn);
4373        commit_session(idms, ct, cust).await;
4374
4375        // Assert that auth works.
4376        assert!(check_testperson_passkey(
4377            idms,
4378            idms_delayed,
4379            &mut wa_token_valid,
4380            origin.clone(),
4381            ct
4382        )
4383        .await
4384        .is_some());
4385
4386        // Remove attested passkey works.
4387        let (cust, _) = renew_test_session(idms, ct).await;
4388        let cutxn = idms.cred_update_transaction().await.unwrap();
4389
4390        trace!(?c_status);
4391        assert!(c_status.primary.is_none());
4392        assert!(c_status.passkeys.is_empty());
4393        assert_eq!(c_status.attested_passkeys.len(), 1);
4394
4395        let c_status = cutxn
4396            .credential_attested_passkey_remove(&cust, ct, pk_uuid)
4397            .expect("Failed to delete the attested passkey");
4398
4399        trace!(?c_status);
4400        assert!(c_status.primary.is_none());
4401        assert!(c_status.passkeys.is_empty());
4402        assert!(c_status.attested_passkeys.is_empty());
4403
4404        drop(cutxn);
4405        commit_session(idms, ct, cust).await;
4406
4407        // Must fail now!
4408        assert!(
4409            check_testperson_passkey(idms, idms_delayed, &mut wa_token_valid, origin, ct)
4410                .await
4411                .is_none()
4412        );
4413    }
4414
4415    #[idm_test(audit = 1)]
4416    async fn credential_update_account_policy_attested_passkey_changed(
4417        idms: &IdmServer,
4418        idms_delayed: &mut IdmServerDelayed,
4419        idms_audit: &mut IdmServerAudit,
4420    ) {
4421        let ct = Duration::from_secs(TEST_CURRENT_TIME);
4422
4423        // Setup the policy.
4424        let (soft_token_1, ca_root_1) = SoftToken::new(true).unwrap();
4425        let mut wa_token_1 = WebauthnAuthenticator::new(soft_token_1);
4426
4427        let (_soft_token_2, ca_root_2) = SoftToken::new(true).unwrap();
4428
4429        let mut att_ca_builder = AttestationCaListBuilder::new();
4430        att_ca_builder
4431            .insert_device_x509(
4432                ca_root_1.clone(),
4433                softtoken::AAGUID,
4434                "softtoken_1".to_string(),
4435                Default::default(),
4436            )
4437            .unwrap();
4438        let att_ca_list = att_ca_builder.build();
4439
4440        trace!(?att_ca_list);
4441
4442        let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();
4443
4444        let modlist = ModifyList::new_purge_and_set(
4445            Attribute::WebauthnAttestationCaList,
4446            Value::WebauthnAttestationCaList(att_ca_list),
4447        );
4448        idms_prox_write
4449            .qs_write
4450            .internal_modify_uuid(UUID_IDM_ALL_ACCOUNTS, &modlist)
4451            .expect("Unable to change webauthn attestation policy");
4452
4453        assert!(idms_prox_write.commit().is_ok());
4454
4455        // Setup the policy for later that lacks token 2.
4456        let mut att_ca_builder = AttestationCaListBuilder::new();
4457        att_ca_builder
4458            .insert_device_x509(
4459                ca_root_2,
4460                softtoken::AAGUID,
4461                "softtoken_2".to_string(),
4462                Default::default(),
4463            )
4464            .unwrap();
4465        let att_ca_list_post = att_ca_builder.build();
4466
4467        // Enroll the attested keys
4468        let (cust, _) = setup_test_session(idms, ct).await;
4469        let cutxn = idms.cred_update_transaction().await.unwrap();
4470        let origin = cutxn.get_origin().clone();
4471
4472        // -------------------------------------------------------
4473        let c_status = cutxn
4474            .credential_attested_passkey_init(&cust, ct)
4475            .expect("Failed to initiate attested passkey registration");
4476
4477        let passkey_chal = match c_status.mfaregstate {
4478            MfaRegStateStatus::AttestedPasskey(c) => Some(c),
4479            _ => None,
4480        }
4481        .expect("Unable to access passkey challenge, invalid state");
4482
4483        let passkey_resp = wa_token_1
4484            .do_registration(origin.clone(), passkey_chal)
4485            .expect("Failed to create soft passkey");
4486
4487        // Finish the registration
4488        let label = "softtoken".to_string();
4489        let c_status = cutxn
4490            .credential_attested_passkey_finish(&cust, ct, label, &passkey_resp)
4491            .expect("Failed to initiate passkey registration");
4492
4493        assert!(matches!(c_status.mfaregstate, MfaRegStateStatus::None));
4494        trace!(?c_status);
4495        assert_eq!(c_status.attested_passkeys.len(), 1);
4496
4497        // -------------------------------------------------------
4498        // Commit
4499        drop(cutxn);
4500        commit_session(idms, ct, cust).await;
4501
4502        // Check auth works
4503        assert!(
4504            check_testperson_passkey(idms, idms_delayed, &mut wa_token_1, origin.clone(), ct)
4505                .await
4506                .is_some()
4507        );
4508
4509        // Change policy
4510        let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();
4511
4512        let modlist = ModifyList::new_purge_and_set(
4513            Attribute::WebauthnAttestationCaList,
4514            Value::WebauthnAttestationCaList(att_ca_list_post),
4515        );
4516        idms_prox_write
4517            .qs_write
4518            .internal_modify_uuid(UUID_IDM_ALL_ACCOUNTS, &modlist)
4519            .expect("Unable to change webauthn attestation policy");
4520
4521        assert!(idms_prox_write.commit().is_ok());
4522
4523        // Auth fail
4524        assert!(
4525            check_testperson_passkey(idms, idms_delayed, &mut wa_token_1, origin.clone(), ct)
4526                .await
4527                .is_none()
4528        );
4529
4530        // This gives an auth denied because the attested passkey still exists but it no longer
4531        // meets criteria.
4532        match idms_audit.audit_rx().try_recv() {
4533            Ok(AuditEvent::AuthenticationDenied { .. }) => {}
4534            _ => panic!("Oh no"),
4535        }
4536
4537        //  Update creds
4538        let (cust, _) = renew_test_session(idms, ct).await;
4539        let cutxn = idms.cred_update_transaction().await.unwrap();
4540
4541        // Invalid key removed
4542        let c_status = cutxn
4543            .credential_update_status(&cust, ct)
4544            .expect("Failed to get the current session status.");
4545
4546        trace!(?c_status);
4547        assert!(c_status.attested_passkeys.is_empty());
4548
4549        drop(cutxn);
4550        commit_session(idms, ct, cust).await;
4551
4552        // Auth fail
4553        assert!(
4554            check_testperson_passkey(idms, idms_delayed, &mut wa_token_1, origin.clone(), ct)
4555                .await
4556                .is_none()
4557        );
4558    }
4559
4560    // Test that when attestation policy is removed, the apk downgrades to passkey and still works.
4561    #[idm_test]
4562    async fn credential_update_account_policy_attested_passkey_downgrade(
4563        idms: &IdmServer,
4564        idms_delayed: &mut IdmServerDelayed,
4565    ) {
4566        let ct = Duration::from_secs(TEST_CURRENT_TIME);
4567
4568        // Setup the policy.
4569        let (soft_token_1, ca_root_1) = SoftToken::new(true).unwrap();
4570        let mut wa_token_1 = WebauthnAuthenticator::new(soft_token_1);
4571
4572        let mut att_ca_builder = AttestationCaListBuilder::new();
4573        att_ca_builder
4574            .insert_device_x509(
4575                ca_root_1.clone(),
4576                softtoken::AAGUID,
4577                "softtoken_1".to_string(),
4578                Default::default(),
4579            )
4580            .unwrap();
4581        let att_ca_list = att_ca_builder.build();
4582
4583        trace!(?att_ca_list);
4584
4585        let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();
4586
4587        let modlist = ModifyList::new_purge_and_set(
4588            Attribute::WebauthnAttestationCaList,
4589            Value::WebauthnAttestationCaList(att_ca_list),
4590        );
4591        idms_prox_write
4592            .qs_write
4593            .internal_modify_uuid(UUID_IDM_ALL_ACCOUNTS, &modlist)
4594            .expect("Unable to change webauthn attestation policy");
4595
4596        assert!(idms_prox_write.commit().is_ok());
4597
4598        // Enroll the attested keys
4599        let (cust, _) = setup_test_session(idms, ct).await;
4600        let cutxn = idms.cred_update_transaction().await.unwrap();
4601        let origin = cutxn.get_origin().clone();
4602
4603        // -------------------------------------------------------
4604        let c_status = cutxn
4605            .credential_attested_passkey_init(&cust, ct)
4606            .expect("Failed to initiate attested passkey registration");
4607
4608        let passkey_chal = match c_status.mfaregstate {
4609            MfaRegStateStatus::AttestedPasskey(c) => Some(c),
4610            _ => None,
4611        }
4612        .expect("Unable to access passkey challenge, invalid state");
4613
4614        let passkey_resp = wa_token_1
4615            .do_registration(origin.clone(), passkey_chal)
4616            .expect("Failed to create soft passkey");
4617
4618        // Finish the registration
4619        let label = "softtoken".to_string();
4620        let c_status = cutxn
4621            .credential_attested_passkey_finish(&cust, ct, label, &passkey_resp)
4622            .expect("Failed to initiate passkey registration");
4623
4624        assert!(matches!(c_status.mfaregstate, MfaRegStateStatus::None));
4625        trace!(?c_status);
4626        assert_eq!(c_status.attested_passkeys.len(), 1);
4627
4628        // -------------------------------------------------------
4629        // Commit
4630        drop(cutxn);
4631        commit_session(idms, ct, cust).await;
4632
4633        // Check auth works
4634        assert!(
4635            check_testperson_passkey(idms, idms_delayed, &mut wa_token_1, origin.clone(), ct)
4636                .await
4637                .is_some()
4638        );
4639
4640        // Change policy
4641        let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();
4642
4643        let modlist = ModifyList::new_purge(Attribute::WebauthnAttestationCaList);
4644        idms_prox_write
4645            .qs_write
4646            .internal_modify_uuid(UUID_IDM_ALL_ACCOUNTS, &modlist)
4647            .expect("Unable to change webauthn attestation policy");
4648
4649        assert!(idms_prox_write.commit().is_ok());
4650
4651        // Auth still passes, key was downgraded.
4652        assert!(
4653            check_testperson_passkey(idms, idms_delayed, &mut wa_token_1, origin.clone(), ct)
4654                .await
4655                .is_some()
4656        );
4657
4658        // Show it still exists, but can only be deleted now.
4659        let (cust, _) = renew_test_session(idms, ct).await;
4660        let cutxn = idms.cred_update_transaction().await.unwrap();
4661
4662        let c_status = cutxn
4663            .credential_update_status(&cust, ct)
4664            .expect("Failed to get the current session status.");
4665
4666        trace!(?c_status);
4667        assert_eq!(c_status.attested_passkeys.len(), 1);
4668        assert!(matches!(
4669            c_status.attested_passkeys_state,
4670            CredentialState::DeleteOnly
4671        ));
4672
4673        drop(cutxn);
4674        commit_session(idms, ct, cust).await;
4675    }
4676
4677    #[idm_test]
4678    async fn credential_update_unix_password(
4679        idms: &IdmServer,
4680        _idms_delayed: &mut IdmServerDelayed,
4681    ) {
4682        let test_pw = "fo3EitierohF9AelaNgiem0Ei6vup4equo1Oogeevaetehah8Tobeengae3Ci0ooh0uki";
4683        let ct = Duration::from_secs(TEST_CURRENT_TIME);
4684
4685        let (cust, _) = setup_test_session(idms, ct).await;
4686
4687        let cutxn = idms.cred_update_transaction().await.unwrap();
4688
4689        // Get the credential status - this should tell
4690        // us the details of the credentials, as well as
4691        // if they are ready and valid to commit?
4692        let c_status = cutxn
4693            .credential_update_status(&cust, ct)
4694            .expect("Failed to get the current session status.");
4695
4696        trace!(?c_status);
4697
4698        assert!(c_status.unixcred.is_none());
4699
4700        // Test initially creating a credential.
4701        //   - pw first
4702        let c_status = cutxn
4703            .credential_unix_set_password(&cust, ct, test_pw)
4704            .expect("Failed to update the unix cred password");
4705
4706        assert!(c_status.can_commit);
4707
4708        drop(cutxn);
4709        commit_session(idms, ct, cust).await;
4710
4711        // Check it works!
4712        assert!(check_testperson_unix_password(idms, test_pw, ct)
4713            .await
4714            .is_some());
4715
4716        // Test deleting the pw
4717        let (cust, _) = renew_test_session(idms, ct).await;
4718        let cutxn = idms.cred_update_transaction().await.unwrap();
4719
4720        let c_status = cutxn
4721            .credential_update_status(&cust, ct)
4722            .expect("Failed to get the current session status.");
4723        trace!(?c_status);
4724        assert!(c_status.unixcred.is_some());
4725
4726        let c_status = cutxn
4727            .credential_unix_delete(&cust, ct)
4728            .expect("Failed to delete the unix cred");
4729        trace!(?c_status);
4730        assert!(c_status.unixcred.is_none());
4731
4732        drop(cutxn);
4733        commit_session(idms, ct, cust).await;
4734
4735        // Must fail now!
4736        assert!(check_testperson_unix_password(idms, test_pw, ct)
4737            .await
4738            .is_none());
4739    }
4740
4741    #[idm_test]
4742    async fn credential_update_sshkeys(idms: &IdmServer, _idms_delayed: &mut IdmServerDelayed) {
4743        let sshkey_valid_1 =
4744            SshPublicKey::from_string(SSHKEY_VALID_1).expect("Invalid SSHKEY_VALID_1");
4745        let sshkey_valid_2 =
4746            SshPublicKey::from_string(SSHKEY_VALID_2).expect("Invalid SSHKEY_VALID_2");
4747
4748        assert!(SshPublicKey::from_string(SSHKEY_INVALID).is_err());
4749
4750        let ct = Duration::from_secs(TEST_CURRENT_TIME);
4751        let (cust, _) = setup_test_session(idms, ct).await;
4752        let cutxn = idms.cred_update_transaction().await.unwrap();
4753
4754        let c_status = cutxn
4755            .credential_update_status(&cust, ct)
4756            .expect("Failed to get the current session status.");
4757
4758        trace!(?c_status);
4759
4760        assert!(c_status.sshkeys.is_empty());
4761
4762        // Reject empty str key label
4763        let result = cutxn.credential_sshkey_add(&cust, ct, "".to_string(), sshkey_valid_1.clone());
4764        assert!(matches!(result, Err(OperationError::InvalidLabel)));
4765
4766        // Reject invalid name label.
4767        let result =
4768            cutxn.credential_sshkey_add(&cust, ct, "🚛".to_string(), sshkey_valid_1.clone());
4769        assert!(matches!(result, Err(OperationError::InvalidLabel)));
4770
4771        // Remove non-existante
4772        let result = cutxn.credential_sshkey_remove(&cust, ct, "key1");
4773        assert!(matches!(result, Err(OperationError::NoMatchingEntries)));
4774
4775        // Add a valid key.
4776        let c_status = cutxn
4777            .credential_sshkey_add(&cust, ct, "key1".to_string(), sshkey_valid_1.clone())
4778            .expect("Failed to add sshkey_valid_1");
4779
4780        trace!(?c_status);
4781        assert_eq!(c_status.sshkeys.len(), 1);
4782        assert!(c_status.sshkeys.contains_key("key1"));
4783
4784        // Add a second valid key.
4785        let c_status = cutxn
4786            .credential_sshkey_add(&cust, ct, "key2".to_string(), sshkey_valid_2.clone())
4787            .expect("Failed to add sshkey_valid_2");
4788
4789        trace!(?c_status);
4790        assert_eq!(c_status.sshkeys.len(), 2);
4791        assert!(c_status.sshkeys.contains_key("key1"));
4792        assert!(c_status.sshkeys.contains_key("key2"));
4793
4794        // Remove a key (check second key untouched)
4795        let c_status = cutxn
4796            .credential_sshkey_remove(&cust, ct, "key2")
4797            .expect("Failed to remove sshkey_valid_2");
4798
4799        trace!(?c_status);
4800        assert_eq!(c_status.sshkeys.len(), 1);
4801        assert!(c_status.sshkeys.contains_key("key1"));
4802
4803        // Reject duplicate key label
4804        let result =
4805            cutxn.credential_sshkey_add(&cust, ct, "key1".to_string(), sshkey_valid_2.clone());
4806        assert!(matches!(result, Err(OperationError::DuplicateLabel)));
4807
4808        // Reject duplicate key
4809        let result =
4810            cutxn.credential_sshkey_add(&cust, ct, "key2".to_string(), sshkey_valid_1.clone());
4811        assert!(matches!(result, Err(OperationError::DuplicateKey)));
4812
4813        drop(cutxn);
4814        commit_session(idms, ct, cust).await;
4815    }
4816}