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