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