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