kanidmd_lib/idm/
credupdatesession.rs

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