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_primary_set_password(
1801 &self,
1802 cust: &CredentialUpdateSessionToken,
1803 ct: Duration,
1804 pw: &str,
1805 ) -> Result<CredentialUpdateSessionStatus, OperationError> {
1806 let session_handle = self.get_current_session(cust, ct)?;
1807 let mut session = session_handle.try_lock().map_err(|_| {
1808 admin_error!("Session already locked, unable to proceed.");
1809 OperationError::InvalidState
1810 })?;
1811 trace!(?session);
1812
1813 if !matches!(session.primary_state, CredentialState::Modifiable) {
1814 error!("Session does not have permission to modify primary credential");
1815 return Err(OperationError::AccessDenied);
1816 };
1817
1818 self.check_password_quality(
1819 pw,
1820 &session.resolved_account_policy,
1821 session.account.related_inputs().as_slice(),
1822 session.account.radius_secret.as_deref(),
1823 )
1824 .map_err(|e| match e {
1825 PasswordQuality::TooShort(sz) => {
1826 OperationError::PasswordQuality(vec![PasswordFeedback::TooShort(sz)])
1827 }
1828 PasswordQuality::BadListed => {
1829 OperationError::PasswordQuality(vec![PasswordFeedback::BadListed])
1830 }
1831 PasswordQuality::DontReusePasswords => {
1832 OperationError::PasswordQuality(vec![PasswordFeedback::DontReusePasswords])
1833 }
1834 PasswordQuality::Feedback(feedback) => OperationError::PasswordQuality(feedback),
1835 })?;
1836
1837 let ncred = match &session.primary {
1838 Some(primary) => {
1839 primary.set_password(self.crypto_policy, pw)?
1841 }
1842 None => Credential::new_password_only(self.crypto_policy, pw)?,
1843 };
1844
1845 session.primary = Some(ncred);
1846 Ok(session.deref().into())
1847 }
1848
1849 pub fn credential_primary_init_totp(
1850 &self,
1851 cust: &CredentialUpdateSessionToken,
1852 ct: Duration,
1853 ) -> Result<CredentialUpdateSessionStatus, OperationError> {
1854 let session_handle = self.get_current_session(cust, ct)?;
1855 let mut session = session_handle.try_lock().map_err(|_| {
1856 admin_error!("Session already locked, unable to proceed.");
1857 OperationError::InvalidState
1858 })?;
1859 trace!(?session);
1860
1861 if !matches!(session.primary_state, CredentialState::Modifiable) {
1862 error!("Session does not have permission to modify primary credential");
1863 return Err(OperationError::AccessDenied);
1864 };
1865
1866 if !matches!(session.mfaregstate, MfaRegState::None) {
1868 debug!("Clearing incomplete mfareg");
1869 }
1870
1871 let totp_token = Totp::generate_secure(TOTP_DEFAULT_STEP);
1873
1874 session.mfaregstate = MfaRegState::TotpInit(totp_token);
1875 Ok(session.deref().into())
1877 }
1878
1879 pub fn credential_primary_check_totp(
1880 &self,
1881 cust: &CredentialUpdateSessionToken,
1882 ct: Duration,
1883 totp_chal: u32,
1884 label: &str,
1885 ) -> Result<CredentialUpdateSessionStatus, OperationError> {
1886 let session_handle = self.get_current_session(cust, ct)?;
1887 let mut session = session_handle.try_lock().map_err(|_| {
1888 admin_error!("Session already locked, unable to proceed.");
1889 OperationError::InvalidState
1890 })?;
1891 trace!(?session);
1892
1893 if !matches!(session.primary_state, CredentialState::Modifiable) {
1894 error!("Session does not have permission to modify primary credential");
1895 return Err(OperationError::AccessDenied);
1896 };
1897
1898 match &session.mfaregstate {
1900 MfaRegState::TotpInit(totp_token)
1901 | MfaRegState::TotpTryAgain(totp_token)
1902 | MfaRegState::TotpNameTryAgain(totp_token, _)
1903 | MfaRegState::TotpInvalidSha1(totp_token, _, _) => {
1904 if session
1905 .primary
1906 .as_ref()
1907 .map(|cred| cred.has_totp_by_name(label))
1908 .unwrap_or_default()
1909 || label.trim().is_empty()
1910 || !Value::validate_str_escapes(label)
1911 {
1912 session.mfaregstate =
1914 MfaRegState::TotpNameTryAgain(totp_token.clone(), label.into());
1915 return Ok(session.deref().into());
1916 }
1917
1918 if totp_token.verify(totp_chal, ct) {
1919 let ncred = session
1921 .primary
1922 .as_ref()
1923 .map(|cred| cred.append_totp(label.to_string(), totp_token.clone()))
1924 .ok_or_else(|| {
1925 admin_error!("A TOTP was added, but no primary credential stub exists");
1926 OperationError::InvalidState
1927 })?;
1928
1929 session.primary = Some(ncred);
1930
1931 session.mfaregstate = MfaRegState::None;
1933 Ok(session.deref().into())
1934 } else {
1935 let token_sha1 = totp_token.clone().downgrade_to_legacy();
1939
1940 if token_sha1.verify(totp_chal, ct) {
1941 session.mfaregstate = MfaRegState::TotpInvalidSha1(
1944 totp_token.clone(),
1945 token_sha1,
1946 label.to_string(),
1947 );
1948 Ok(session.deref().into())
1949 } else {
1950 session.mfaregstate = MfaRegState::TotpTryAgain(totp_token.clone());
1952 Ok(session.deref().into())
1953 }
1954 }
1955 }
1956 _ => Err(OperationError::InvalidRequestState),
1957 }
1958 }
1959
1960 pub fn credential_primary_accept_sha1_totp(
1961 &self,
1962 cust: &CredentialUpdateSessionToken,
1963 ct: Duration,
1964 ) -> Result<CredentialUpdateSessionStatus, OperationError> {
1965 let session_handle = self.get_current_session(cust, ct)?;
1966 let mut session = session_handle.try_lock().map_err(|_| {
1967 admin_error!("Session already locked, unable to proceed.");
1968 OperationError::InvalidState
1969 })?;
1970 trace!(?session);
1971
1972 if !matches!(session.primary_state, CredentialState::Modifiable) {
1973 error!("Session does not have permission to modify primary credential");
1974 return Err(OperationError::AccessDenied);
1975 };
1976
1977 match &session.mfaregstate {
1979 MfaRegState::TotpInvalidSha1(_, token_sha1, label) => {
1980 let ncred = session
1982 .primary
1983 .as_ref()
1984 .map(|cred| cred.append_totp(label.to_string(), token_sha1.clone()))
1985 .ok_or_else(|| {
1986 admin_error!("A TOTP was added, but no primary credential stub exists");
1987 OperationError::InvalidState
1988 })?;
1989
1990 security_info!("A SHA1 TOTP credential was accepted");
1991
1992 session.primary = Some(ncred);
1993
1994 session.mfaregstate = MfaRegState::None;
1996 Ok(session.deref().into())
1997 }
1998 _ => Err(OperationError::InvalidRequestState),
1999 }
2000 }
2001
2002 pub fn credential_primary_remove_totp(
2003 &self,
2004 cust: &CredentialUpdateSessionToken,
2005 ct: Duration,
2006 label: &str,
2007 ) -> Result<CredentialUpdateSessionStatus, OperationError> {
2008 let session_handle = self.get_current_session(cust, ct)?;
2009 let mut session = session_handle.try_lock().map_err(|_| {
2010 admin_error!("Session already locked, unable to proceed.");
2011 OperationError::InvalidState
2012 })?;
2013 trace!(?session);
2014
2015 if !matches!(session.primary_state, CredentialState::Modifiable) {
2016 error!("Session does not have permission to modify primary credential");
2017 return Err(OperationError::AccessDenied);
2018 };
2019
2020 if !matches!(session.mfaregstate, MfaRegState::None) {
2021 admin_info!("Invalid TOTP state, another update is in progress");
2022 return Err(OperationError::InvalidState);
2023 }
2024
2025 let ncred = session
2026 .primary
2027 .as_ref()
2028 .map(|cred| cred.remove_totp(label))
2029 .ok_or_else(|| {
2030 admin_error!("Try to remove TOTP, but no primary credential stub exists");
2031 OperationError::InvalidState
2032 })?;
2033
2034 session.primary = Some(ncred);
2035
2036 session.mfaregstate = MfaRegState::None;
2038 Ok(session.deref().into())
2039 }
2040
2041 pub fn credential_primary_init_backup_codes(
2042 &self,
2043 cust: &CredentialUpdateSessionToken,
2044 ct: Duration,
2045 ) -> Result<CredentialUpdateSessionStatus, OperationError> {
2046 let session_handle = self.get_current_session(cust, ct)?;
2047 let mut session = session_handle.try_lock().map_err(|_| {
2048 error!("Session already locked, unable to proceed.");
2049 OperationError::InvalidState
2050 })?;
2051 trace!(?session);
2052
2053 if !matches!(session.primary_state, CredentialState::Modifiable) {
2054 error!("Session does not have permission to modify primary credential");
2055 return Err(OperationError::AccessDenied);
2056 };
2057
2058 let codes = backup_code_from_random();
2061
2062 let ncred = session
2063 .primary
2064 .as_ref()
2065 .ok_or_else(|| {
2066 error!("Tried to add backup codes, but no primary credential stub exists");
2067 OperationError::InvalidState
2068 })
2069 .and_then(|cred|
2070 cred.update_backup_code(BackupCodes::new(codes.clone()))
2071 .map_err(|_| {
2072 error!("Tried to add backup codes, but MFA is not enabled on this credential yet");
2073 OperationError::InvalidState
2074 })
2075 )
2076 ?;
2077
2078 session.primary = Some(ncred);
2079
2080 Ok(session.deref().into()).map(|mut status: CredentialUpdateSessionStatus| {
2081 status.mfaregstate = MfaRegStateStatus::BackupCodes(codes);
2082 status
2083 })
2084 }
2085
2086 pub fn credential_primary_remove_backup_codes(
2087 &self,
2088 cust: &CredentialUpdateSessionToken,
2089 ct: Duration,
2090 ) -> Result<CredentialUpdateSessionStatus, OperationError> {
2091 let session_handle = self.get_current_session(cust, ct)?;
2092 let mut session = session_handle.try_lock().map_err(|_| {
2093 admin_error!("Session already locked, unable to proceed.");
2094 OperationError::InvalidState
2095 })?;
2096 trace!(?session);
2097
2098 if !matches!(session.primary_state, CredentialState::Modifiable) {
2099 error!("Session does not have permission to modify primary credential");
2100 return Err(OperationError::AccessDenied);
2101 };
2102
2103 let ncred = session
2104 .primary
2105 .as_ref()
2106 .ok_or_else(|| {
2107 admin_error!("Tried to add backup codes, but no primary credential stub exists");
2108 OperationError::InvalidState
2109 })
2110 .and_then(|cred|
2111 cred.remove_backup_code()
2112 .map_err(|_| {
2113 admin_error!("Tried to remove backup codes, but MFA is not enabled on this credential yet");
2114 OperationError::InvalidState
2115 })
2116 )
2117 ?;
2118
2119 session.primary = Some(ncred);
2120
2121 Ok(session.deref().into())
2122 }
2123
2124 pub fn credential_primary_delete(
2125 &self,
2126 cust: &CredentialUpdateSessionToken,
2127 ct: Duration,
2128 ) -> Result<CredentialUpdateSessionStatus, OperationError> {
2129 let session_handle = self.get_current_session(cust, ct)?;
2130 let mut session = session_handle.try_lock().map_err(|_| {
2131 admin_error!("Session already locked, unable to proceed.");
2132 OperationError::InvalidState
2133 })?;
2134 trace!(?session);
2135
2136 if !(matches!(session.primary_state, CredentialState::Modifiable)
2137 || matches!(session.primary_state, CredentialState::DeleteOnly))
2138 {
2139 error!("Session does not have permission to modify primary credential");
2140 return Err(OperationError::AccessDenied);
2141 };
2142
2143 session.primary = None;
2144 Ok(session.deref().into())
2145 }
2146
2147 pub fn credential_passkey_init(
2148 &self,
2149 cust: &CredentialUpdateSessionToken,
2150 ct: Duration,
2151 ) -> Result<CredentialUpdateSessionStatus, OperationError> {
2152 let session_handle = self.get_current_session(cust, ct)?;
2153 let mut session = session_handle.try_lock().map_err(|_| {
2154 admin_error!("Session already locked, unable to proceed.");
2155 OperationError::InvalidState
2156 })?;
2157 trace!(?session);
2158
2159 if !matches!(session.passkeys_state, CredentialState::Modifiable) {
2160 error!("Session does not have permission to modify passkeys");
2161 return Err(OperationError::AccessDenied);
2162 };
2163
2164 if !matches!(session.mfaregstate, MfaRegState::None) {
2165 debug!("Clearing incomplete mfareg");
2166 }
2167
2168 let (ccr, pk_reg) = self
2169 .webauthn
2170 .start_passkey_registration(
2171 session.account.uuid,
2172 &session.account.spn,
2173 &session.account.displayname,
2174 session.account.existing_credential_id_list(),
2175 )
2176 .map_err(|e| {
2177 error!(eclass=?e, emsg=%e, "Unable to start passkey registration");
2178 OperationError::Webauthn
2179 })?;
2180
2181 session.mfaregstate = MfaRegState::Passkey(Box::new(ccr), pk_reg);
2182 Ok(session.deref().into())
2184 }
2185
2186 pub fn credential_passkey_finish(
2187 &self,
2188 cust: &CredentialUpdateSessionToken,
2189 ct: Duration,
2190 label: String,
2191 reg: &RegisterPublicKeyCredential,
2192 ) -> Result<CredentialUpdateSessionStatus, OperationError> {
2193 let session_handle = self.get_current_session(cust, ct)?;
2194 let mut session = session_handle.try_lock().map_err(|_| {
2195 admin_error!("Session already locked, unable to proceed.");
2196 OperationError::InvalidState
2197 })?;
2198 trace!(?session);
2199
2200 if !matches!(session.passkeys_state, CredentialState::Modifiable) {
2201 error!("Session does not have permission to modify passkeys");
2202 return Err(OperationError::AccessDenied);
2203 };
2204
2205 match &session.mfaregstate {
2206 MfaRegState::Passkey(_ccr, pk_reg) => {
2207 let reg_result = self.webauthn.finish_passkey_registration(reg, pk_reg);
2208
2209 session.mfaregstate = MfaRegState::None;
2211
2212 match reg_result {
2213 Ok(passkey) => {
2214 let pk_id = Uuid::new_v4();
2215 session.passkeys.insert(pk_id, (label, passkey));
2216
2217 let cu_status: CredentialUpdateSessionStatus = session.deref().into();
2218 Ok(cu_status)
2219 }
2220 Err(WebauthnError::UserNotVerified) => {
2221 let mut cu_status: CredentialUpdateSessionStatus = session.deref().into();
2222 cu_status.append_ephemeral_warning(
2223 CredentialUpdateSessionStatusWarnings::WebauthnUserVerificationRequired,
2224 );
2225 Ok(cu_status)
2226 }
2227 Err(err) => {
2228 error!(eclass=?err, emsg=%err, "Unable to complete passkey registration");
2229 Err(OperationError::CU0002WebauthnRegistrationError)
2230 }
2231 }
2232 }
2233 invalid_state => {
2234 warn!(?invalid_state);
2235 Err(OperationError::InvalidRequestState)
2236 }
2237 }
2238 }
2239
2240 pub fn credential_passkey_remove(
2241 &self,
2242 cust: &CredentialUpdateSessionToken,
2243 ct: Duration,
2244 uuid: Uuid,
2245 ) -> Result<CredentialUpdateSessionStatus, OperationError> {
2246 let session_handle = self.get_current_session(cust, ct)?;
2247 let mut session = session_handle.try_lock().map_err(|_| {
2248 admin_error!("Session already locked, unable to proceed.");
2249 OperationError::InvalidState
2250 })?;
2251 trace!(?session);
2252
2253 if !(matches!(session.passkeys_state, CredentialState::Modifiable)
2254 || matches!(session.passkeys_state, CredentialState::DeleteOnly))
2255 {
2256 error!("Session does not have permission to modify passkeys");
2257 return Err(OperationError::AccessDenied);
2258 };
2259
2260 session.passkeys.remove(&uuid);
2262
2263 Ok(session.deref().into())
2264 }
2265
2266 pub fn credential_attested_passkey_init(
2267 &self,
2268 cust: &CredentialUpdateSessionToken,
2269 ct: Duration,
2270 ) -> Result<CredentialUpdateSessionStatus, OperationError> {
2271 let session_handle = self.get_current_session(cust, ct)?;
2272 let mut session = session_handle.try_lock().map_err(|_| {
2273 error!("Session already locked, unable to proceed.");
2274 OperationError::InvalidState
2275 })?;
2276 trace!(?session);
2277
2278 if !matches!(session.attested_passkeys_state, CredentialState::Modifiable) {
2279 error!("Session does not have permission to modify attested passkeys");
2280 return Err(OperationError::AccessDenied);
2281 };
2282
2283 if !matches!(session.mfaregstate, MfaRegState::None) {
2284 debug!("Cancelling abandoned mfareg");
2285 }
2286
2287 let att_ca_list = session
2288 .resolved_account_policy
2289 .webauthn_attestation_ca_list()
2290 .cloned()
2291 .ok_or_else(|| {
2292 error!(
2293 "No attestation CA list is available, can not proceed with attested passkeys."
2294 );
2295 OperationError::AccessDenied
2296 })?;
2297
2298 let (ccr, pk_reg) = self
2299 .webauthn
2300 .start_attested_passkey_registration(
2301 session.account.uuid,
2302 &session.account.spn,
2303 &session.account.displayname,
2304 session.account.existing_credential_id_list(),
2305 att_ca_list,
2306 None,
2307 )
2308 .map_err(|e| {
2309 error!(eclass=?e, emsg=%e, "Unable to start passkey registration");
2310 OperationError::Webauthn
2311 })?;
2312
2313 session.mfaregstate = MfaRegState::AttestedPasskey(Box::new(ccr), pk_reg);
2314 Ok(session.deref().into())
2316 }
2317
2318 pub fn credential_attested_passkey_finish(
2319 &self,
2320 cust: &CredentialUpdateSessionToken,
2321 ct: Duration,
2322 label: String,
2323 reg: &RegisterPublicKeyCredential,
2324 ) -> Result<CredentialUpdateSessionStatus, OperationError> {
2325 let session_handle = self.get_current_session(cust, ct)?;
2326 let mut session = session_handle.try_lock().map_err(|_| {
2327 admin_error!("Session already locked, unable to proceed.");
2328 OperationError::InvalidState
2329 })?;
2330 trace!(?session);
2331
2332 if !matches!(session.attested_passkeys_state, CredentialState::Modifiable) {
2333 error!("Session does not have permission to modify attested passkeys");
2334 return Err(OperationError::AccessDenied);
2335 };
2336
2337 match &session.mfaregstate {
2338 MfaRegState::AttestedPasskey(_ccr, pk_reg) => {
2339 let result = self
2340 .webauthn
2341 .finish_attested_passkey_registration(reg, pk_reg)
2342 .map_err(|e| {
2343 error!(eclass=?e, emsg=%e, "Unable to complete attested passkey registration");
2344
2345 match e {
2346 WebauthnError::AttestationChainNotTrusted(_)
2347 | WebauthnError::AttestationNotVerifiable => {
2348 OperationError::CU0001WebauthnAttestationNotTrusted
2349 },
2350 WebauthnError::UserNotVerified => {
2351 OperationError::CU0003WebauthnUserNotVerified
2352 },
2353 _ => OperationError::CU0002WebauthnRegistrationError,
2354 }
2355 });
2356
2357 session.mfaregstate = MfaRegState::None;
2359
2360 let passkey = result?;
2361 trace!(?passkey);
2362
2363 let pk_id = Uuid::new_v4();
2364 session.attested_passkeys.insert(pk_id, (label, passkey));
2365
2366 trace!(?session.attested_passkeys);
2367
2368 Ok(session.deref().into())
2369 }
2370 _ => Err(OperationError::InvalidRequestState),
2371 }
2372 }
2373
2374 pub fn credential_attested_passkey_remove(
2375 &self,
2376 cust: &CredentialUpdateSessionToken,
2377 ct: Duration,
2378 uuid: Uuid,
2379 ) -> Result<CredentialUpdateSessionStatus, OperationError> {
2380 let session_handle = self.get_current_session(cust, ct)?;
2381 let mut session = session_handle.try_lock().map_err(|_| {
2382 admin_error!("Session already locked, unable to proceed.");
2383 OperationError::InvalidState
2384 })?;
2385 trace!(?session);
2386
2387 if !(matches!(session.attested_passkeys_state, CredentialState::Modifiable)
2388 || matches!(session.attested_passkeys_state, CredentialState::DeleteOnly))
2389 {
2390 error!("Session does not have permission to modify attested passkeys");
2391 return Err(OperationError::AccessDenied);
2392 };
2393
2394 session.attested_passkeys.remove(&uuid);
2396
2397 Ok(session.deref().into())
2398 }
2399
2400 #[instrument(level = "trace", skip(cust, self))]
2401 pub fn credential_unix_set_password(
2402 &self,
2403 cust: &CredentialUpdateSessionToken,
2404 ct: Duration,
2405 pw: &str,
2406 ) -> Result<CredentialUpdateSessionStatus, OperationError> {
2407 let session_handle = self.get_current_session(cust, ct)?;
2408 let mut session = session_handle.try_lock().map_err(|_| {
2409 admin_error!("Session already locked, unable to proceed.");
2410 OperationError::InvalidState
2411 })?;
2412 trace!(?session);
2413
2414 if !matches!(session.unixcred_state, CredentialState::Modifiable) {
2415 error!("Session does not have permission to modify unix credential");
2416 return Err(OperationError::AccessDenied);
2417 };
2418
2419 self.check_password_quality(
2420 pw,
2421 &session.resolved_account_policy,
2422 session.account.related_inputs().as_slice(),
2423 session.account.radius_secret.as_deref(),
2424 )
2425 .map_err(|e| match e {
2426 PasswordQuality::TooShort(sz) => {
2427 OperationError::PasswordQuality(vec![PasswordFeedback::TooShort(sz)])
2428 }
2429 PasswordQuality::BadListed => {
2430 OperationError::PasswordQuality(vec![PasswordFeedback::BadListed])
2431 }
2432 PasswordQuality::DontReusePasswords => {
2433 OperationError::PasswordQuality(vec![PasswordFeedback::DontReusePasswords])
2434 }
2435 PasswordQuality::Feedback(feedback) => OperationError::PasswordQuality(feedback),
2436 })?;
2437
2438 let ncred = match &session.unixcred {
2439 Some(unixcred) => {
2440 unixcred.set_password(self.crypto_policy, pw)?
2442 }
2443 None => Credential::new_password_only(self.crypto_policy, pw)?,
2444 };
2445
2446 session.unixcred = Some(ncred);
2447 Ok(session.deref().into())
2448 }
2449
2450 pub fn credential_unix_delete(
2451 &self,
2452 cust: &CredentialUpdateSessionToken,
2453 ct: Duration,
2454 ) -> Result<CredentialUpdateSessionStatus, OperationError> {
2455 let session_handle = self.get_current_session(cust, ct)?;
2456 let mut session = session_handle.try_lock().map_err(|_| {
2457 admin_error!("Session already locked, unable to proceed.");
2458 OperationError::InvalidState
2459 })?;
2460 trace!(?session);
2461
2462 if !(matches!(session.unixcred_state, CredentialState::Modifiable)
2463 || matches!(session.unixcred_state, CredentialState::DeleteOnly))
2464 {
2465 error!("Session does not have permission to modify unix credential");
2466 return Err(OperationError::AccessDenied);
2467 };
2468
2469 session.unixcred = None;
2470 Ok(session.deref().into())
2471 }
2472
2473 #[instrument(level = "trace", skip(cust, self))]
2474 pub fn credential_sshkey_add(
2475 &self,
2476 cust: &CredentialUpdateSessionToken,
2477 ct: Duration,
2478 label: String,
2479 sshpubkey: SshPublicKey,
2480 ) -> Result<CredentialUpdateSessionStatus, OperationError> {
2481 let session_handle = self.get_current_session(cust, ct)?;
2482 let mut session = session_handle.try_lock().map_err(|_| {
2483 admin_error!("Session already locked, unable to proceed.");
2484 OperationError::InvalidState
2485 })?;
2486 trace!(?session);
2487
2488 if !matches!(session.unixcred_state, CredentialState::Modifiable) {
2489 error!("Session does not have permission to modify unix credential");
2490 return Err(OperationError::AccessDenied);
2491 };
2492
2493 if !LABEL_RE.is_match(&label) {
2495 error!("SSH Public Key label invalid");
2496 return Err(OperationError::InvalidLabel);
2497 }
2498
2499 if session.sshkeys.contains_key(&label) {
2500 error!("SSH Public Key label duplicate");
2501 return Err(OperationError::DuplicateLabel);
2502 }
2503
2504 if session.sshkeys.values().any(|sk| *sk == sshpubkey) {
2505 error!("SSH Public Key duplicate");
2506 return Err(OperationError::DuplicateKey);
2507 }
2508
2509 session.sshkeys.insert(label, sshpubkey);
2510
2511 Ok(session.deref().into())
2512 }
2513
2514 pub fn credential_sshkey_remove(
2515 &self,
2516 cust: &CredentialUpdateSessionToken,
2517 ct: Duration,
2518 label: &str,
2519 ) -> Result<CredentialUpdateSessionStatus, OperationError> {
2520 let session_handle = self.get_current_session(cust, ct)?;
2521 let mut session = session_handle.try_lock().map_err(|_| {
2522 admin_error!("Session already locked, unable to proceed.");
2523 OperationError::InvalidState
2524 })?;
2525 trace!(?session);
2526
2527 if !(matches!(session.sshkeys_state, CredentialState::Modifiable)
2528 || matches!(session.sshkeys_state, CredentialState::DeleteOnly))
2529 {
2530 error!("Session does not have permission to modify sshkeys");
2531 return Err(OperationError::AccessDenied);
2532 };
2533
2534 session.sshkeys.remove(label).ok_or_else(|| {
2535 error!("No such key for label");
2536 OperationError::NoMatchingEntries
2537 })?;
2538
2539 Ok(session.deref().into())
2542 }
2543
2544 pub fn credential_update_cancel_mfareg(
2545 &self,
2546 cust: &CredentialUpdateSessionToken,
2547 ct: Duration,
2548 ) -> Result<CredentialUpdateSessionStatus, OperationError> {
2549 let session_handle = self.get_current_session(cust, ct)?;
2550 let mut session = session_handle.try_lock().map_err(|_| {
2551 admin_error!("Session already locked, unable to proceed.");
2552 OperationError::InvalidState
2553 })?;
2554 trace!(?session);
2555 session.mfaregstate = MfaRegState::None;
2556 Ok(session.deref().into())
2557 }
2558
2559 }
2561
2562#[cfg(test)]
2563mod tests {
2564 use compact_jwt::JwsCompact;
2565 use std::time::Duration;
2566
2567 use kanidm_proto::internal::{CUExtPortal, CredentialDetailType, PasswordFeedback};
2568 use kanidm_proto::v1::{AuthAllowed, AuthIssueSession, AuthMech, UnixUserToken};
2569 use uuid::uuid;
2570 use webauthn_authenticator_rs::softpasskey::SoftPasskey;
2571 use webauthn_authenticator_rs::softtoken::{self, SoftToken};
2572 use webauthn_authenticator_rs::{AuthenticatorBackend, WebauthnAuthenticator};
2573 use webauthn_rs::prelude::AttestationCaListBuilder;
2574
2575 use super::{
2576 CredentialState, CredentialUpdateSessionStatus, CredentialUpdateSessionStatusWarnings,
2577 CredentialUpdateSessionToken, InitCredentialUpdateEvent, InitCredentialUpdateIntentEvent,
2578 MfaRegStateStatus, MAXIMUM_CRED_UPDATE_TTL, MAXIMUM_INTENT_TTL, MINIMUM_INTENT_TTL,
2579 };
2580 use crate::credential::totp::Totp;
2581 use crate::event::CreateEvent;
2582 use crate::idm::audit::AuditEvent;
2583 use crate::idm::delayed::DelayedAction;
2584 use crate::idm::event::{
2585 AuthEvent, AuthResult, RegenerateRadiusSecretEvent, UnixUserAuthEvent,
2586 };
2587 use crate::idm::server::{IdmServer, IdmServerCredUpdateTransaction, IdmServerDelayed};
2588 use crate::idm::AuthState;
2589 use crate::prelude::*;
2590 use crate::utils::password_from_random_len;
2591 use crate::value::CredentialType;
2592 use sshkey_attest::proto::PublicKey as SshPublicKey;
2593
2594 const TEST_CURRENT_TIME: u64 = 6000;
2595 const TESTPERSON_UUID: Uuid = uuid!("cf231fea-1a8f-4410-a520-fd9b1a379c86");
2596
2597 const SSHKEY_VALID_1: &str = "sk-ecdsa-sha2-nistp256@openssh.com AAAAInNrLWVjZHNhLXNoYTItbmlzdHAyNTZAb3BlbnNzaC5jb20AAAAIbmlzdHAyNTYAAABBBENubZikrb8hu+HeVRdZ0pp/VAk2qv4JDbuJhvD0yNdWDL2e3cBbERiDeNPkWx58Q4rVnxkbV1fa8E2waRtT91wAAAAEc3NoOg== testuser@fidokey";
2598 const SSHKEY_VALID_2: &str = "sk-ecdsa-sha2-nistp256@openssh.com AAAAInNrLWVjZHNhLXNoYTItbmlzdHAyNTZAb3BlbnNzaC5jb20AAAAIbmlzdHAyNTYAAABBBIbkSsdGCRoW6v0nO/3vNYPhG20YhWU0wQPY7x52EOb4dmYhC4IJfzVDpEPg313BxWRKQglb5RQ1PPkou7JFyCUAAAAEc3NoOg== testuser@fidokey";
2599 const SSHKEY_INVALID: &str = "sk-ecrsa-sha9000-nistp@openssh.com AAAAInNrLWVjZHNhLXNoYTItbmlzdHAyNTZAb3BlbnNzaC5jb20AAAAIbmlzdHAyNTYAAABBBIbkSsdGCRoW6v0nO/3vNYPhG20YhWU0wQPY7x52EOb4dmYhC4IJfzVDpEPg313BxWRKQglb5RQ1PPkou7JFyCUAAAAEc3NoOg== badkey@rejectme";
2600
2601 #[idm_test]
2602 async fn credential_update_session_init(
2603 idms: &IdmServer,
2604 _idms_delayed: &mut IdmServerDelayed,
2605 ) {
2606 let ct = Duration::from_secs(TEST_CURRENT_TIME);
2607 let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();
2608
2609 let testaccount_uuid = Uuid::new_v4();
2610
2611 let e1 = entry_init!(
2612 (Attribute::Class, EntryClass::Object.to_value()),
2613 (Attribute::Class, EntryClass::Account.to_value()),
2614 (Attribute::Class, EntryClass::ServiceAccount.to_value()),
2615 (Attribute::Name, Value::new_iname("user_account_only")),
2616 (Attribute::Uuid, Value::Uuid(testaccount_uuid)),
2617 (Attribute::Description, Value::new_utf8s("testaccount")),
2618 (Attribute::DisplayName, Value::new_utf8s("testaccount"))
2619 );
2620
2621 let e2 = entry_init!(
2622 (Attribute::Class, EntryClass::Object.to_value()),
2623 (Attribute::Class, EntryClass::Account.to_value()),
2624 (Attribute::Class, EntryClass::PosixAccount.to_value()),
2625 (Attribute::Class, EntryClass::Person.to_value()),
2626 (Attribute::Name, Value::new_iname("testperson")),
2627 (Attribute::Uuid, Value::Uuid(TESTPERSON_UUID)),
2628 (Attribute::Description, Value::new_utf8s("testperson")),
2629 (Attribute::DisplayName, Value::new_utf8s("testperson"))
2630 );
2631
2632 let ce = CreateEvent::new_internal(vec![e1, e2]);
2633 let cr = idms_prox_write.qs_write.create(&ce);
2634 assert!(cr.is_ok());
2635
2636 let testaccount = idms_prox_write
2637 .qs_write
2638 .internal_search_uuid(testaccount_uuid)
2639 .expect("failed");
2640
2641 let testperson = idms_prox_write
2642 .qs_write
2643 .internal_search_uuid(TESTPERSON_UUID)
2644 .expect("failed");
2645
2646 let idm_admin = idms_prox_write
2647 .qs_write
2648 .internal_search_uuid(UUID_IDM_ADMIN)
2649 .expect("failed");
2650
2651 let cur = idms_prox_write.init_credential_update(
2655 &InitCredentialUpdateEvent::new_impersonate_entry(testaccount),
2656 ct,
2657 );
2658
2659 assert!(matches!(cur, Err(OperationError::NotAuthorised)));
2660
2661 let cur = idms_prox_write.init_credential_update(
2664 &InitCredentialUpdateEvent::new_impersonate_entry(testperson),
2665 ct,
2666 );
2667
2668 assert!(cur.is_ok());
2669
2670 let cur = idms_prox_write.init_credential_update_intent(
2675 &InitCredentialUpdateIntentEvent::new_impersonate_entry(
2676 idm_admin,
2677 TESTPERSON_UUID,
2678 MINIMUM_INTENT_TTL,
2679 ),
2680 ct,
2681 );
2682
2683 assert!(cur.is_ok());
2684 let intent_tok = cur.expect("Failed to create intent token!");
2685
2686 let cur = idms_prox_write
2689 .exchange_intent_credential_update(intent_tok.clone().into(), ct + MINIMUM_INTENT_TTL);
2690
2691 assert!(matches!(cur, Err(OperationError::SessionExpired)));
2692
2693 let cur = idms_prox_write
2694 .exchange_intent_credential_update(intent_tok.clone().into(), ct + MAXIMUM_INTENT_TTL);
2695
2696 assert!(matches!(cur, Err(OperationError::SessionExpired)));
2697
2698 let (cust_a, _c_status) = idms_prox_write
2700 .exchange_intent_credential_update(intent_tok.clone().into(), ct)
2701 .unwrap();
2702
2703 let (cust_b, _c_status) = idms_prox_write
2706 .exchange_intent_credential_update(intent_tok.into(), ct + Duration::from_secs(1))
2707 .unwrap();
2708
2709 let cur = idms_prox_write.commit_credential_update(&cust_a, ct);
2710
2711 trace!(?cur);
2713 assert!(cur.is_err());
2714
2715 let _ = idms_prox_write.commit_credential_update(&cust_b, ct);
2717
2718 idms_prox_write.commit().expect("Failed to commit txn");
2719 }
2720
2721 async fn setup_test_session(
2722 idms: &IdmServer,
2723 ct: Duration,
2724 ) -> (CredentialUpdateSessionToken, CredentialUpdateSessionStatus) {
2725 let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();
2726
2727 let modlist = ModifyList::new_purge(Attribute::CredentialTypeMinimum);
2729 idms_prox_write
2730 .qs_write
2731 .internal_modify_uuid(UUID_IDM_ALL_PERSONS, &modlist)
2732 .expect("Unable to change default session exp");
2733
2734 let e2 = entry_init!(
2735 (Attribute::Class, EntryClass::Object.to_value()),
2736 (Attribute::Class, EntryClass::Account.to_value()),
2737 (Attribute::Class, EntryClass::PosixAccount.to_value()),
2738 (Attribute::Class, EntryClass::Person.to_value()),
2739 (Attribute::Name, Value::new_iname("testperson")),
2740 (Attribute::Uuid, Value::Uuid(TESTPERSON_UUID)),
2741 (Attribute::Description, Value::new_utf8s("testperson")),
2742 (Attribute::DisplayName, Value::new_utf8s("testperson"))
2743 );
2744
2745 let ce = CreateEvent::new_internal(vec![e2]);
2746 let cr = idms_prox_write.qs_write.create(&ce);
2747 assert!(cr.is_ok());
2748
2749 let testperson = idms_prox_write
2750 .qs_write
2751 .internal_search_uuid(TESTPERSON_UUID)
2752 .expect("failed");
2753
2754 let rrse = RegenerateRadiusSecretEvent::new_internal(TESTPERSON_UUID);
2756
2757 let _ = idms_prox_write
2758 .regenerate_radius_secret(&rrse)
2759 .expect("Failed to reset radius credential 1");
2760
2761 let cur = idms_prox_write.init_credential_update(
2762 &InitCredentialUpdateEvent::new_impersonate_entry(testperson),
2763 ct,
2764 );
2765
2766 idms_prox_write.commit().expect("Failed to commit txn");
2767
2768 cur.expect("Failed to start update")
2769 }
2770
2771 async fn renew_test_session(
2772 idms: &IdmServer,
2773 ct: Duration,
2774 ) -> (CredentialUpdateSessionToken, CredentialUpdateSessionStatus) {
2775 let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();
2776
2777 let testperson = idms_prox_write
2778 .qs_write
2779 .internal_search_uuid(TESTPERSON_UUID)
2780 .expect("failed");
2781
2782 let cur = idms_prox_write.init_credential_update(
2783 &InitCredentialUpdateEvent::new_impersonate_entry(testperson),
2784 ct,
2785 );
2786
2787 trace!(renew_test_session_result = ?cur);
2788
2789 idms_prox_write.commit().expect("Failed to commit txn");
2790
2791 cur.expect("Failed to start update")
2792 }
2793
2794 async fn commit_session(idms: &IdmServer, ct: Duration, cust: CredentialUpdateSessionToken) {
2795 let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();
2796
2797 idms_prox_write
2798 .commit_credential_update(&cust, ct)
2799 .expect("Failed to commit credential update.");
2800
2801 idms_prox_write.commit().expect("Failed to commit txn");
2802 }
2803
2804 async fn check_testperson_password(
2805 idms: &IdmServer,
2806 idms_delayed: &mut IdmServerDelayed,
2807 pw: &str,
2808 ct: Duration,
2809 ) -> Option<JwsCompact> {
2810 let mut idms_auth = idms.auth().await.unwrap();
2811
2812 let auth_init = AuthEvent::named_init("testperson");
2813
2814 let r1 = idms_auth
2815 .auth(&auth_init, ct, Source::Internal.into())
2816 .await;
2817 let ar = r1.unwrap();
2818 let AuthResult { sessionid, state } = ar;
2819
2820 if !matches!(state, AuthState::Choose(_)) {
2821 debug!("Can't proceed - {:?}", state);
2822 return None;
2823 };
2824
2825 let auth_begin = AuthEvent::begin_mech(sessionid, AuthMech::Password);
2826
2827 let r2 = idms_auth
2828 .auth(&auth_begin, ct, Source::Internal.into())
2829 .await;
2830 let ar = r2.unwrap();
2831 let AuthResult { sessionid, state } = ar;
2832
2833 assert!(matches!(state, AuthState::Continue(_)));
2834
2835 let pw_step = AuthEvent::cred_step_password(sessionid, pw);
2836
2837 let r2 = idms_auth.auth(&pw_step, ct, Source::Internal.into()).await;
2839 debug!("r2 ==> {:?}", r2);
2840 idms_auth.commit().expect("Must not fail");
2841
2842 match r2 {
2843 Ok(AuthResult {
2844 sessionid: _,
2845 state: AuthState::Success(token, AuthIssueSession::Token),
2846 }) => {
2847 let da = idms_delayed.try_recv().expect("invalid");
2849 assert!(matches!(da, DelayedAction::AuthSessionRecord(_)));
2850
2851 Some(*token)
2852 }
2853 _ => None,
2854 }
2855 }
2856
2857 async fn check_testperson_unix_password(
2858 idms: &IdmServer,
2859 pw: &str,
2861 ct: Duration,
2862 ) -> Option<UnixUserToken> {
2863 let mut idms_auth = idms.auth().await.unwrap();
2864
2865 let auth_event = UnixUserAuthEvent::new_internal(TESTPERSON_UUID, pw);
2866
2867 idms_auth
2868 .auth_unix(&auth_event, ct)
2869 .await
2870 .expect("Unable to perform unix authentication")
2871 }
2872
2873 async fn check_testperson_password_totp(
2874 idms: &IdmServer,
2875 idms_delayed: &mut IdmServerDelayed,
2876 pw: &str,
2877 token: &Totp,
2878 ct: Duration,
2879 ) -> Option<JwsCompact> {
2880 let mut idms_auth = idms.auth().await.unwrap();
2881
2882 let auth_init = AuthEvent::named_init("testperson");
2883
2884 let r1 = idms_auth
2885 .auth(&auth_init, ct, Source::Internal.into())
2886 .await;
2887 let ar = r1.unwrap();
2888 let AuthResult { sessionid, state } = ar;
2889
2890 if !matches!(state, AuthState::Choose(_)) {
2891 debug!("Can't proceed - {:?}", state);
2892 return None;
2893 };
2894
2895 let auth_begin = AuthEvent::begin_mech(sessionid, AuthMech::PasswordTotp);
2896
2897 let r2 = idms_auth
2898 .auth(&auth_begin, ct, Source::Internal.into())
2899 .await;
2900 let ar = r2.unwrap();
2901 let AuthResult { sessionid, state } = ar;
2902
2903 assert!(matches!(state, AuthState::Continue(_)));
2904
2905 let totp = token
2906 .do_totp_duration_from_epoch(&ct)
2907 .expect("Failed to perform totp step");
2908
2909 let totp_step = AuthEvent::cred_step_totp(sessionid, totp);
2910 let r2 = idms_auth
2911 .auth(&totp_step, ct, Source::Internal.into())
2912 .await;
2913 let ar = r2.unwrap();
2914 let AuthResult { sessionid, state } = ar;
2915
2916 assert!(matches!(state, AuthState::Continue(_)));
2917
2918 let pw_step = AuthEvent::cred_step_password(sessionid, pw);
2919
2920 let r3 = idms_auth.auth(&pw_step, ct, Source::Internal.into()).await;
2922 debug!("r3 ==> {:?}", r3);
2923 idms_auth.commit().expect("Must not fail");
2924
2925 match r3 {
2926 Ok(AuthResult {
2927 sessionid: _,
2928 state: AuthState::Success(token, AuthIssueSession::Token),
2929 }) => {
2930 let da = idms_delayed.try_recv().expect("invalid");
2932 assert!(matches!(da, DelayedAction::AuthSessionRecord(_)));
2933 Some(*token)
2934 }
2935 _ => None,
2936 }
2937 }
2938
2939 async fn check_testperson_password_backup_code(
2940 idms: &IdmServer,
2941 idms_delayed: &mut IdmServerDelayed,
2942 pw: &str,
2943 code: &str,
2944 ct: Duration,
2945 ) -> Option<JwsCompact> {
2946 let mut idms_auth = idms.auth().await.unwrap();
2947
2948 let auth_init = AuthEvent::named_init("testperson");
2949
2950 let r1 = idms_auth
2951 .auth(&auth_init, ct, Source::Internal.into())
2952 .await;
2953 let ar = r1.unwrap();
2954 let AuthResult { sessionid, state } = ar;
2955
2956 if !matches!(state, AuthState::Choose(_)) {
2957 debug!("Can't proceed - {:?}", state);
2958 return None;
2959 };
2960
2961 let auth_begin = AuthEvent::begin_mech(sessionid, AuthMech::PasswordBackupCode);
2962
2963 let r2 = idms_auth
2964 .auth(&auth_begin, ct, Source::Internal.into())
2965 .await;
2966 let ar = r2.unwrap();
2967 let AuthResult { sessionid, state } = ar;
2968
2969 assert!(matches!(state, AuthState::Continue(_)));
2970
2971 let code_step = AuthEvent::cred_step_backup_code(sessionid, code);
2972 let r2 = idms_auth
2973 .auth(&code_step, ct, Source::Internal.into())
2974 .await;
2975 let ar = r2.unwrap();
2976 let AuthResult { sessionid, state } = ar;
2977
2978 assert!(matches!(state, AuthState::Continue(_)));
2979
2980 let pw_step = AuthEvent::cred_step_password(sessionid, pw);
2981
2982 let r3 = idms_auth.auth(&pw_step, ct, Source::Internal.into()).await;
2984 debug!("r3 ==> {:?}", r3);
2985 idms_auth.commit().expect("Must not fail");
2986
2987 match r3 {
2988 Ok(AuthResult {
2989 sessionid: _,
2990 state: AuthState::Success(token, AuthIssueSession::Token),
2991 }) => {
2992 let da = idms_delayed.try_recv().expect("invalid");
2994 assert!(matches!(da, DelayedAction::BackupCodeRemoval(_)));
2995 let r = idms.delayed_action(ct, da).await;
2996 assert!(r.is_ok());
2997
2998 let da = idms_delayed.try_recv().expect("invalid");
3000 assert!(matches!(da, DelayedAction::AuthSessionRecord(_)));
3001 Some(*token)
3002 }
3003 _ => None,
3004 }
3005 }
3006
3007 async fn check_testperson_passkey<T: AuthenticatorBackend>(
3008 idms: &IdmServer,
3009 idms_delayed: &mut IdmServerDelayed,
3010 wa: &mut WebauthnAuthenticator<T>,
3011 origin: Url,
3012 ct: Duration,
3013 ) -> Option<JwsCompact> {
3014 let mut idms_auth = idms.auth().await.unwrap();
3015
3016 let auth_init = AuthEvent::named_init("testperson");
3017
3018 let r1 = idms_auth
3019 .auth(&auth_init, ct, Source::Internal.into())
3020 .await;
3021 let ar = r1.unwrap();
3022 let AuthResult { sessionid, state } = ar;
3023
3024 if !matches!(state, AuthState::Choose(_)) {
3025 debug!("Can't proceed - {:?}", state);
3026 return None;
3027 };
3028
3029 let auth_begin = AuthEvent::begin_mech(sessionid, AuthMech::Passkey);
3030
3031 let r2 = idms_auth
3032 .auth(&auth_begin, ct, Source::Internal.into())
3033 .await;
3034 let ar = r2.unwrap();
3035 let AuthResult { sessionid, state } = ar;
3036
3037 trace!(?state);
3038
3039 let rcr = match state {
3040 AuthState::Continue(mut allowed) => match allowed.pop() {
3041 Some(AuthAllowed::Passkey(rcr)) => rcr,
3042 _ => unreachable!(),
3043 },
3044 _ => unreachable!(),
3045 };
3046
3047 trace!(?rcr);
3048
3049 let resp = wa
3050 .do_authentication(origin, rcr)
3051 .expect("failed to use softtoken to authenticate");
3052
3053 let passkey_step = AuthEvent::cred_step_passkey(sessionid, resp);
3054
3055 let r3 = idms_auth
3056 .auth(&passkey_step, ct, Source::Internal.into())
3057 .await;
3058 debug!("r3 ==> {:?}", r3);
3059 idms_auth.commit().expect("Must not fail");
3060
3061 match r3 {
3062 Ok(AuthResult {
3063 sessionid: _,
3064 state: AuthState::Success(token, AuthIssueSession::Token),
3065 }) => {
3066 let da = idms_delayed.try_recv().expect("invalid");
3068 assert!(matches!(da, DelayedAction::WebauthnCounterIncrement(_)));
3069 let r = idms.delayed_action(ct, da).await;
3070 assert!(r.is_ok());
3071
3072 let da = idms_delayed.try_recv().expect("invalid");
3074 assert!(matches!(da, DelayedAction::AuthSessionRecord(_)));
3075
3076 Some(*token)
3077 }
3078 _ => None,
3079 }
3080 }
3081
3082 #[idm_test]
3083 async fn credential_update_session_cleanup(
3084 idms: &IdmServer,
3085 _idms_delayed: &mut IdmServerDelayed,
3086 ) {
3087 let ct = Duration::from_secs(TEST_CURRENT_TIME);
3088 let (cust, _) = setup_test_session(idms, ct).await;
3089
3090 let cutxn = idms.cred_update_transaction().await.unwrap();
3091 let c_status = cutxn.credential_update_status(&cust, ct);
3093 assert!(c_status.is_ok());
3094 drop(cutxn);
3095
3096 let (_cust, _) =
3098 renew_test_session(idms, ct + MAXIMUM_CRED_UPDATE_TTL + Duration::from_secs(1)).await;
3099
3100 let cutxn = idms.cred_update_transaction().await.unwrap();
3101
3102 let c_status = cutxn
3105 .credential_update_status(&cust, ct)
3106 .expect_err("Session is still valid!");
3107 assert!(matches!(c_status, OperationError::InvalidState));
3108 }
3109
3110 #[idm_test]
3111 async fn credential_update_onboarding_create_new_pw(
3112 idms: &IdmServer,
3113 idms_delayed: &mut IdmServerDelayed,
3114 ) {
3115 let test_pw = "fo3EitierohF9AelaNgiem0Ei6vup4equo1Oogeevaetehah8Tobeengae3Ci0ooh0uki";
3116 let ct = Duration::from_secs(TEST_CURRENT_TIME);
3117
3118 let (cust, _) = setup_test_session(idms, ct).await;
3119
3120 let cutxn = idms.cred_update_transaction().await.unwrap();
3121
3122 let c_status = cutxn
3126 .credential_update_status(&cust, ct)
3127 .expect("Failed to get the current session status.");
3128
3129 trace!(?c_status);
3130
3131 assert!(c_status.primary.is_none());
3132
3133 let c_status = cutxn
3136 .credential_primary_set_password(&cust, ct, test_pw)
3137 .expect("Failed to update the primary cred password");
3138
3139 assert!(c_status.can_commit);
3140
3141 drop(cutxn);
3142 commit_session(idms, ct, cust).await;
3143
3144 assert!(check_testperson_password(idms, idms_delayed, test_pw, ct)
3146 .await
3147 .is_some());
3148
3149 let (cust, _) = renew_test_session(idms, ct).await;
3151 let cutxn = idms.cred_update_transaction().await.unwrap();
3152
3153 let c_status = cutxn
3154 .credential_update_status(&cust, ct)
3155 .expect("Failed to get the current session status.");
3156 trace!(?c_status);
3157 assert!(c_status.primary.is_some());
3158
3159 let c_status = cutxn
3160 .credential_primary_delete(&cust, ct)
3161 .expect("Failed to delete the primary cred");
3162 trace!(?c_status);
3163 assert!(c_status.primary.is_none());
3164
3165 drop(cutxn);
3166 commit_session(idms, ct, cust).await;
3167
3168 assert!(check_testperson_password(idms, idms_delayed, test_pw, ct)
3170 .await
3171 .is_none());
3172 }
3173
3174 #[idm_test]
3175 async fn credential_update_password_quality_checks(
3176 idms: &IdmServer,
3177 _idms_delayed: &mut IdmServerDelayed,
3178 ) {
3179 let ct = Duration::from_secs(TEST_CURRENT_TIME);
3180 let (cust, _) = setup_test_session(idms, ct).await;
3181
3182 let mut r_txn = idms.proxy_read().await.unwrap();
3185
3186 let radius_secret = r_txn
3187 .qs_read
3188 .internal_search_uuid(TESTPERSON_UUID)
3189 .expect("No such entry")
3190 .get_ava_single_secret(Attribute::RadiusSecret)
3191 .expect("No radius secret found")
3192 .to_string();
3193
3194 drop(r_txn);
3195
3196 let cutxn = idms.cred_update_transaction().await.unwrap();
3197
3198 let c_status = cutxn
3202 .credential_update_status(&cust, ct)
3203 .expect("Failed to get the current session status.");
3204
3205 trace!(?c_status);
3206
3207 assert!(c_status.primary.is_none());
3208
3209 let err = cutxn
3213 .credential_primary_set_password(&cust, ct, "password")
3214 .unwrap_err();
3215 trace!(?err);
3216 assert!(
3217 matches!(err, OperationError::PasswordQuality(details) if details == vec!(PasswordFeedback::TooShort(PW_MIN_LENGTH),))
3218 );
3219
3220 let err = cutxn
3221 .credential_primary_set_password(&cust, ct, "password1234")
3222 .unwrap_err();
3223 trace!(?err);
3224 assert!(
3225 matches!(err, OperationError::PasswordQuality(details) if details
3226 == vec!(
3227 PasswordFeedback::AddAnotherWordOrTwo,
3228 PasswordFeedback::ThisIsACommonPassword,
3229 ))
3230 );
3231
3232 let err = cutxn
3233 .credential_primary_set_password(&cust, ct, &radius_secret)
3234 .unwrap_err();
3235 trace!(?err);
3236 assert!(
3237 matches!(err, OperationError::PasswordQuality(details) if details == vec!(PasswordFeedback::DontReusePasswords,))
3238 );
3239
3240 let err = cutxn
3241 .credential_primary_set_password(&cust, ct, "testperson2023")
3242 .unwrap_err();
3243 trace!(?err);
3244 assert!(
3245 matches!(err, OperationError::PasswordQuality(details) if details == vec!(
3246 PasswordFeedback::NamesAndSurnamesByThemselvesAreEasyToGuess,
3247 PasswordFeedback::AvoidDatesAndYearsThatAreAssociatedWithYou,
3248 ))
3249 );
3250
3251 let err = cutxn
3252 .credential_primary_set_password(
3253 &cust,
3254 ct,
3255 "demo_badlist_shohfie3aeci2oobur0aru9uushah6EiPi2woh4hohngoighaiRuepieN3ongoo1",
3256 )
3257 .unwrap_err();
3258 trace!(?err);
3259 assert!(
3260 matches!(err, OperationError::PasswordQuality(details) if details == vec!(PasswordFeedback::BadListed))
3261 );
3262
3263 assert!(c_status.can_commit);
3264
3265 drop(cutxn);
3266 }
3267
3268 #[idm_test]
3269 async fn credential_update_password_min_length_account_policy(
3270 idms: &IdmServer,
3271 _idms_delayed: &mut IdmServerDelayed,
3272 ) {
3273 let ct = Duration::from_secs(TEST_CURRENT_TIME);
3274
3275 let test_pw_min_length = PW_MIN_LENGTH * 2;
3277
3278 let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();
3279
3280 let modlist = ModifyList::new_purge_and_set(
3281 Attribute::AuthPasswordMinimumLength,
3282 Value::Uint32(test_pw_min_length),
3283 );
3284 idms_prox_write
3285 .qs_write
3286 .internal_modify_uuid(UUID_IDM_ALL_ACCOUNTS, &modlist)
3287 .expect("Unable to change default session exp");
3288
3289 assert!(idms_prox_write.commit().is_ok());
3290 let (cust, _) = setup_test_session(idms, ct).await;
3293
3294 let cutxn = idms.cred_update_transaction().await.unwrap();
3295
3296 let c_status = cutxn
3300 .credential_update_status(&cust, ct)
3301 .expect("Failed to get the current session status.");
3302
3303 trace!(?c_status);
3304
3305 assert!(c_status.primary.is_none());
3306
3307 let pw = password_from_random_len(8);
3310 let err = cutxn
3311 .credential_primary_set_password(&cust, ct, &pw)
3312 .unwrap_err();
3313 trace!(?err);
3314 assert!(
3315 matches!(err, OperationError::PasswordQuality(details) if details == vec!(PasswordFeedback::TooShort(test_pw_min_length),))
3316 );
3317
3318 let pw = password_from_random_len(test_pw_min_length - 1);
3320 let err = cutxn
3321 .credential_primary_set_password(&cust, ct, &pw)
3322 .unwrap_err();
3323 trace!(?err);
3324 assert!(matches!(err,OperationError::PasswordQuality(details)
3325 if details == vec!(PasswordFeedback::TooShort(test_pw_min_length),)));
3326
3327 let pw = password_from_random_len(test_pw_min_length);
3329 let c_status = cutxn
3330 .credential_primary_set_password(&cust, ct, &pw)
3331 .expect("Failed to update the primary cred password");
3332
3333 assert!(c_status.can_commit);
3334
3335 drop(cutxn);
3336 commit_session(idms, ct, cust).await;
3337 }
3338
3339 #[idm_test]
3345 async fn credential_update_onboarding_create_new_mfa_totp_basic(
3346 idms: &IdmServer,
3347 idms_delayed: &mut IdmServerDelayed,
3348 ) {
3349 let test_pw = "fo3EitierohF9AelaNgiem0Ei6vup4equo1Oogeevaetehah8Tobeengae3Ci0ooh0uki";
3350 let ct = Duration::from_secs(TEST_CURRENT_TIME);
3351
3352 let (cust, _) = setup_test_session(idms, ct).await;
3353 let cutxn = idms.cred_update_transaction().await.unwrap();
3354
3355 let c_status = cutxn
3357 .credential_primary_set_password(&cust, ct, test_pw)
3358 .expect("Failed to update the primary cred password");
3359
3360 assert!(c_status.can_commit);
3362
3363 let c_status = cutxn
3365 .credential_primary_init_totp(&cust, ct)
3366 .expect("Failed to update the primary cred password");
3367
3368 let totp_token: Totp = match c_status.mfaregstate {
3370 MfaRegStateStatus::TotpCheck(secret) => Some(secret.try_into().unwrap()),
3371
3372 _ => None,
3373 }
3374 .expect("Unable to retrieve totp token, invalid state.");
3375
3376 trace!(?totp_token);
3377 let chal = totp_token
3378 .do_totp_duration_from_epoch(&ct)
3379 .expect("Failed to perform totp step");
3380
3381 let c_status = cutxn
3383 .credential_primary_check_totp(&cust, ct, chal + 1, "totp")
3384 .expect("Failed to update the primary cred totp");
3385
3386 assert!(
3387 matches!(c_status.mfaregstate, MfaRegStateStatus::TotpTryAgain),
3388 "{:?}",
3389 c_status.mfaregstate
3390 );
3391
3392 let c_status = cutxn
3394 .credential_primary_check_totp(&cust, ct, chal, "")
3395 .expect("Failed to update the primary cred totp");
3396
3397 assert!(
3398 matches!(
3399 c_status.mfaregstate,
3400 MfaRegStateStatus::TotpNameTryAgain(ref val) if val.is_empty()
3401 ),
3402 "{:?}",
3403 c_status.mfaregstate
3404 );
3405
3406 let c_status = cutxn
3408 .credential_primary_check_totp(&cust, ct, chal, " ")
3409 .expect("Failed to update the primary cred totp");
3410
3411 assert!(
3412 matches!(
3413 c_status.mfaregstate,
3414 MfaRegStateStatus::TotpNameTryAgain(ref val) if val == " "
3415 ),
3416 "{:?}",
3417 c_status.mfaregstate
3418 );
3419
3420 let c_status = cutxn
3421 .credential_primary_check_totp(&cust, ct, chal, "totp")
3422 .expect("Failed to update the primary cred totp");
3423
3424 assert!(matches!(c_status.mfaregstate, MfaRegStateStatus::None));
3425 assert!(match c_status.primary.as_ref().map(|c| &c.type_) {
3426 Some(CredentialDetailType::PasswordMfa(totp, _, 0)) => !totp.is_empty(),
3427 _ => false,
3428 });
3429
3430 {
3431 let c_status = cutxn
3432 .credential_primary_init_totp(&cust, ct)
3433 .expect("Failed to update the primary cred password");
3434
3435 let totp_token: Totp = match c_status.mfaregstate {
3437 MfaRegStateStatus::TotpCheck(secret) => Some(secret.try_into().unwrap()),
3438 _ => None,
3439 }
3440 .expect("Unable to retrieve totp token, invalid state.");
3441
3442 trace!(?totp_token);
3443 let chal = totp_token
3444 .do_totp_duration_from_epoch(&ct)
3445 .expect("Failed to perform totp step");
3446
3447 let c_status = cutxn
3449 .credential_primary_check_totp(&cust, ct, chal, "totp")
3450 .expect("Failed to update the primary cred totp");
3451
3452 assert!(
3453 matches!(
3454 c_status.mfaregstate,
3455 MfaRegStateStatus::TotpNameTryAgain(ref val) if val == "totp"
3456 ),
3457 "{:?}",
3458 c_status.mfaregstate
3459 );
3460
3461 assert!(cutxn.credential_update_cancel_mfareg(&cust, ct).is_ok())
3462 }
3463
3464 drop(cutxn);
3467 commit_session(idms, ct, cust).await;
3468
3469 assert!(
3471 check_testperson_password_totp(idms, idms_delayed, test_pw, &totp_token, ct)
3472 .await
3473 .is_some()
3474 );
3475 let (cust, _) = renew_test_session(idms, ct).await;
3479 let cutxn = idms.cred_update_transaction().await.unwrap();
3480
3481 let c_status = cutxn
3482 .credential_primary_remove_totp(&cust, ct, "totp")
3483 .expect("Failed to update the primary cred password");
3484
3485 assert!(matches!(c_status.mfaregstate, MfaRegStateStatus::None));
3486 assert!(matches!(
3487 c_status.primary.as_ref().map(|c| &c.type_),
3488 Some(CredentialDetailType::Password)
3489 ));
3490
3491 drop(cutxn);
3492 commit_session(idms, ct, cust).await;
3493
3494 assert!(check_testperson_password(idms, idms_delayed, test_pw, ct)
3496 .await
3497 .is_some());
3498 }
3499
3500 #[idm_test]
3502 async fn credential_update_onboarding_create_new_mfa_totp_sha1(
3503 idms: &IdmServer,
3504 idms_delayed: &mut IdmServerDelayed,
3505 ) {
3506 let test_pw = "fo3EitierohF9AelaNgiem0Ei6vup4equo1Oogeevaetehah8Tobeengae3Ci0ooh0uki";
3507 let ct = Duration::from_secs(TEST_CURRENT_TIME);
3508
3509 let (cust, _) = setup_test_session(idms, ct).await;
3510 let cutxn = idms.cred_update_transaction().await.unwrap();
3511
3512 let c_status = cutxn
3514 .credential_primary_set_password(&cust, ct, test_pw)
3515 .expect("Failed to update the primary cred password");
3516
3517 assert!(c_status.can_commit);
3519
3520 let c_status = cutxn
3522 .credential_primary_init_totp(&cust, ct)
3523 .expect("Failed to update the primary cred password");
3524
3525 let totp_token: Totp = match c_status.mfaregstate {
3527 MfaRegStateStatus::TotpCheck(secret) => Some(secret.try_into().unwrap()),
3528
3529 _ => None,
3530 }
3531 .expect("Unable to retrieve totp token, invalid state.");
3532
3533 let totp_token = totp_token.downgrade_to_legacy();
3534
3535 trace!(?totp_token);
3536 let chal = totp_token
3537 .do_totp_duration_from_epoch(&ct)
3538 .expect("Failed to perform totp step");
3539
3540 let c_status = cutxn
3542 .credential_primary_check_totp(&cust, ct, chal, "totp")
3543 .expect("Failed to update the primary cred password");
3544
3545 assert!(matches!(
3546 c_status.mfaregstate,
3547 MfaRegStateStatus::TotpInvalidSha1
3548 ));
3549
3550 let c_status = cutxn
3552 .credential_primary_accept_sha1_totp(&cust, ct)
3553 .expect("Failed to update the primary cred password");
3554
3555 assert!(matches!(c_status.mfaregstate, MfaRegStateStatus::None));
3556 assert!(match c_status.primary.as_ref().map(|c| &c.type_) {
3557 Some(CredentialDetailType::PasswordMfa(totp, _, 0)) => !totp.is_empty(),
3558 _ => false,
3559 });
3560
3561 drop(cutxn);
3564 commit_session(idms, ct, cust).await;
3565
3566 assert!(
3568 check_testperson_password_totp(idms, idms_delayed, test_pw, &totp_token, ct)
3569 .await
3570 .is_some()
3571 );
3572 }
3574
3575 #[idm_test]
3576 async fn credential_update_onboarding_create_new_mfa_totp_backup_codes(
3577 idms: &IdmServer,
3578 idms_delayed: &mut IdmServerDelayed,
3579 ) {
3580 let test_pw = "fo3EitierohF9AelaNgiem0Ei6vup4equo1Oogeevaetehah8Tobeengae3Ci0ooh0uki";
3581 let ct = Duration::from_secs(TEST_CURRENT_TIME);
3582
3583 let (cust, _) = setup_test_session(idms, ct).await;
3584 let cutxn = idms.cred_update_transaction().await.unwrap();
3585
3586 let _c_status = cutxn
3588 .credential_primary_set_password(&cust, ct, test_pw)
3589 .expect("Failed to update the primary cred password");
3590
3591 assert!(matches!(
3593 cutxn.credential_primary_init_backup_codes(&cust, ct),
3594 Err(OperationError::InvalidState)
3595 ));
3596
3597 let c_status = cutxn
3598 .credential_primary_init_totp(&cust, ct)
3599 .expect("Failed to update the primary cred password");
3600
3601 let totp_token: Totp = match c_status.mfaregstate {
3602 MfaRegStateStatus::TotpCheck(secret) => Some(secret.try_into().unwrap()),
3603 _ => None,
3604 }
3605 .expect("Unable to retrieve totp token, invalid state.");
3606
3607 trace!(?totp_token);
3608 let chal = totp_token
3609 .do_totp_duration_from_epoch(&ct)
3610 .expect("Failed to perform totp step");
3611
3612 let c_status = cutxn
3613 .credential_primary_check_totp(&cust, ct, chal, "totp")
3614 .expect("Failed to update the primary cred totp");
3615
3616 assert!(matches!(c_status.mfaregstate, MfaRegStateStatus::None));
3617 assert!(match c_status.primary.as_ref().map(|c| &c.type_) {
3618 Some(CredentialDetailType::PasswordMfa(totp, _, 0)) => !totp.is_empty(),
3619 _ => false,
3620 });
3621
3622 let c_status = cutxn
3625 .credential_primary_init_backup_codes(&cust, ct)
3626 .expect("Failed to update the primary cred password");
3627
3628 let codes = match c_status.mfaregstate {
3629 MfaRegStateStatus::BackupCodes(codes) => Some(codes),
3630 _ => None,
3631 }
3632 .expect("Unable to retrieve backupcodes, invalid state.");
3633
3634 debug!("{:?}", c_status.primary.as_ref().map(|c| &c.type_));
3636 assert!(match c_status.primary.as_ref().map(|c| &c.type_) {
3637 Some(CredentialDetailType::PasswordMfa(totp, _, 8)) => !totp.is_empty(),
3638 _ => false,
3639 });
3640
3641 drop(cutxn);
3643 commit_session(idms, ct, cust).await;
3644
3645 let backup_code = codes.iter().next().expect("No codes available");
3646
3647 assert!(check_testperson_password_backup_code(
3649 idms,
3650 idms_delayed,
3651 test_pw,
3652 backup_code,
3653 ct
3654 )
3655 .await
3656 .is_some());
3657
3658 let (cust, _) = renew_test_session(idms, ct).await;
3660 let cutxn = idms.cred_update_transaction().await.unwrap();
3661
3662 let c_status = cutxn
3664 .credential_update_status(&cust, ct)
3665 .expect("Failed to get the current session status.");
3666
3667 assert!(match c_status.primary.as_ref().map(|c| &c.type_) {
3668 Some(CredentialDetailType::PasswordMfa(totp, _, 7)) => !totp.is_empty(),
3669 _ => false,
3670 });
3671
3672 let c_status = cutxn
3674 .credential_primary_remove_backup_codes(&cust, ct)
3675 .expect("Failed to update the primary cred password");
3676
3677 assert!(matches!(c_status.mfaregstate, MfaRegStateStatus::None));
3678 assert!(match c_status.primary.as_ref().map(|c| &c.type_) {
3679 Some(CredentialDetailType::PasswordMfa(totp, _, 0)) => !totp.is_empty(),
3680 _ => false,
3681 });
3682
3683 let c_status = cutxn
3685 .credential_primary_init_backup_codes(&cust, ct)
3686 .expect("Failed to update the primary cred password");
3687
3688 assert!(matches!(
3689 c_status.mfaregstate,
3690 MfaRegStateStatus::BackupCodes(_)
3691 ));
3692 assert!(match c_status.primary.as_ref().map(|c| &c.type_) {
3693 Some(CredentialDetailType::PasswordMfa(totp, _, 8)) => !totp.is_empty(),
3694 _ => false,
3695 });
3696
3697 let c_status = cutxn
3699 .credential_primary_remove_totp(&cust, ct, "totp")
3700 .expect("Failed to update the primary cred password");
3701
3702 assert!(matches!(c_status.mfaregstate, MfaRegStateStatus::None));
3703 assert!(matches!(
3704 c_status.primary.as_ref().map(|c| &c.type_),
3705 Some(CredentialDetailType::Password)
3706 ));
3707
3708 drop(cutxn);
3709 commit_session(idms, ct, cust).await;
3710 }
3711
3712 #[idm_test]
3713 async fn credential_update_onboarding_cancel_inprogress_totp(
3714 idms: &IdmServer,
3715 idms_delayed: &mut IdmServerDelayed,
3716 ) {
3717 let test_pw = "fo3EitierohF9AelaNgiem0Ei6vup4equo1Oogeevaetehah8Tobeengae3Ci0ooh0uki";
3718 let ct = Duration::from_secs(TEST_CURRENT_TIME);
3719
3720 let (cust, _) = setup_test_session(idms, ct).await;
3721 let cutxn = idms.cred_update_transaction().await.unwrap();
3722
3723 let c_status = cutxn
3725 .credential_primary_set_password(&cust, ct, test_pw)
3726 .expect("Failed to update the primary cred password");
3727
3728 assert!(c_status.can_commit);
3730
3731 let c_status = cutxn
3733 .credential_primary_init_totp(&cust, ct)
3734 .expect("Failed to update the primary cred totp");
3735
3736 assert!(c_status.can_commit);
3738 assert!(matches!(
3739 c_status.mfaregstate,
3740 MfaRegStateStatus::TotpCheck(_)
3741 ));
3742
3743 let c_status = cutxn
3744 .credential_update_cancel_mfareg(&cust, ct)
3745 .expect("Failed to cancel in-flight totp change");
3746
3747 assert!(matches!(c_status.mfaregstate, MfaRegStateStatus::None));
3748 assert!(c_status.can_commit);
3749
3750 drop(cutxn);
3751 commit_session(idms, ct, cust).await;
3752
3753 assert!(check_testperson_password(idms, idms_delayed, test_pw, ct)
3755 .await
3756 .is_some());
3757 }
3758
3759 async fn create_new_passkey(
3766 ct: Duration,
3767 origin: &Url,
3768 cutxn: &IdmServerCredUpdateTransaction<'_>,
3769 cust: &CredentialUpdateSessionToken,
3770 wa: &mut WebauthnAuthenticator<SoftPasskey>,
3771 ) -> CredentialUpdateSessionStatus {
3772 let c_status = cutxn
3774 .credential_passkey_init(cust, ct)
3775 .expect("Failed to initiate passkey registration");
3776
3777 assert!(c_status.passkeys.is_empty());
3778
3779 let passkey_chal = match c_status.mfaregstate {
3780 MfaRegStateStatus::Passkey(c) => Some(c),
3781 _ => None,
3782 }
3783 .expect("Unable to access passkey challenge, invalid state");
3784
3785 let passkey_resp = wa
3786 .do_registration(origin.clone(), passkey_chal)
3787 .expect("Failed to create soft passkey");
3788
3789 let label = "softtoken".to_string();
3791 let c_status = cutxn
3792 .credential_passkey_finish(cust, ct, label, &passkey_resp)
3793 .expect("Failed to initiate passkey registration");
3794
3795 assert!(matches!(c_status.mfaregstate, MfaRegStateStatus::None));
3796 assert!(c_status.primary.as_ref().is_none());
3797
3798 trace!(?c_status);
3800 assert_eq!(c_status.passkeys.len(), 1);
3801
3802 c_status
3803 }
3804
3805 #[idm_test]
3806 async fn credential_update_onboarding_create_new_passkey(
3807 idms: &IdmServer,
3808 idms_delayed: &mut IdmServerDelayed,
3809 ) {
3810 let ct = Duration::from_secs(TEST_CURRENT_TIME);
3811
3812 let (cust, _) = setup_test_session(idms, ct).await;
3813 let cutxn = idms.cred_update_transaction().await.unwrap();
3814 let origin = cutxn.get_origin().clone();
3815
3816 let mut wa = WebauthnAuthenticator::new(SoftPasskey::new(true));
3818
3819 let c_status = create_new_passkey(ct, &origin, &cutxn, &cust, &mut wa).await;
3820
3821 let pk_uuid = c_status.passkeys.first().map(|pkd| pkd.uuid).unwrap();
3823
3824 drop(cutxn);
3826 commit_session(idms, ct, cust).await;
3827
3828 assert!(
3830 check_testperson_passkey(idms, idms_delayed, &mut wa, origin.clone(), ct)
3831 .await
3832 .is_some()
3833 );
3834
3835 let (cust, _) = renew_test_session(idms, ct).await;
3837 let cutxn = idms.cred_update_transaction().await.unwrap();
3838
3839 trace!(?c_status);
3840 assert!(c_status.primary.is_none());
3841 assert_eq!(c_status.passkeys.len(), 1);
3842
3843 let c_status = cutxn
3844 .credential_passkey_remove(&cust, ct, pk_uuid)
3845 .expect("Failed to delete the passkey");
3846
3847 trace!(?c_status);
3848 assert!(c_status.primary.is_none());
3849 assert!(c_status.passkeys.is_empty());
3850
3851 drop(cutxn);
3852 commit_session(idms, ct, cust).await;
3853
3854 assert!(
3856 check_testperson_passkey(idms, idms_delayed, &mut wa, origin, ct)
3857 .await
3858 .is_none()
3859 );
3860 }
3861
3862 #[idm_test]
3863 async fn credential_update_access_denied(
3864 idms: &IdmServer,
3865 _idms_delayed: &mut IdmServerDelayed,
3866 ) {
3867 let ct = Duration::from_secs(TEST_CURRENT_TIME);
3871
3872 let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();
3873
3874 let sync_uuid = Uuid::new_v4();
3875
3876 let e1 = entry_init!(
3877 (Attribute::Class, EntryClass::Object.to_value()),
3878 (Attribute::Class, EntryClass::SyncAccount.to_value()),
3879 (Attribute::Name, Value::new_iname("test_scim_sync")),
3880 (Attribute::Uuid, Value::Uuid(sync_uuid)),
3881 (
3882 Attribute::Description,
3883 Value::new_utf8s("A test sync agreement")
3884 )
3885 );
3886
3887 let e2 = entry_init!(
3888 (Attribute::Class, EntryClass::Object.to_value()),
3889 (Attribute::Class, EntryClass::SyncObject.to_value()),
3890 (Attribute::Class, EntryClass::Account.to_value()),
3891 (Attribute::Class, EntryClass::PosixAccount.to_value()),
3892 (Attribute::Class, EntryClass::Person.to_value()),
3893 (Attribute::SyncParentUuid, Value::Refer(sync_uuid)),
3894 (Attribute::Name, Value::new_iname("testperson")),
3895 (Attribute::Uuid, Value::Uuid(TESTPERSON_UUID)),
3896 (Attribute::Description, Value::new_utf8s("testperson")),
3897 (Attribute::DisplayName, Value::new_utf8s("testperson"))
3898 );
3899
3900 let ce = CreateEvent::new_internal(vec![e1, e2]);
3901 let cr = idms_prox_write.qs_write.create(&ce);
3902 assert!(cr.is_ok());
3903
3904 let testperson = idms_prox_write
3905 .qs_write
3906 .internal_search_uuid(TESTPERSON_UUID)
3907 .expect("failed");
3908
3909 let cur = idms_prox_write.init_credential_update(
3910 &InitCredentialUpdateEvent::new_impersonate_entry(testperson),
3911 ct,
3912 );
3913
3914 idms_prox_write.commit().expect("Failed to commit txn");
3915
3916 let (cust, custatus) = cur.expect("Failed to start update");
3917
3918 trace!(?custatus);
3919
3920 let CredentialUpdateSessionStatus {
3923 spn: _,
3924 displayname: _,
3925 ext_cred_portal,
3926 mfaregstate: _,
3927 can_commit: _,
3928 warnings: _,
3929 primary: _,
3930 primary_state,
3931 passkeys: _,
3932 passkeys_state,
3933 attested_passkeys: _,
3934 attested_passkeys_state,
3935 attested_passkeys_allowed_devices: _,
3936 unixcred_state,
3937 unixcred: _,
3938 sshkeys: _,
3939 sshkeys_state,
3940 } = custatus;
3941
3942 assert!(matches!(ext_cred_portal, CUExtPortal::Hidden));
3943 assert!(matches!(primary_state, CredentialState::AccessDeny));
3944 assert!(matches!(passkeys_state, CredentialState::AccessDeny));
3945 assert!(matches!(
3946 attested_passkeys_state,
3947 CredentialState::AccessDeny
3948 ));
3949 assert!(matches!(unixcred_state, CredentialState::AccessDeny));
3950 assert!(matches!(sshkeys_state, CredentialState::AccessDeny));
3951
3952 let cutxn = idms.cred_update_transaction().await.unwrap();
3953
3954 let err = cutxn
3960 .credential_primary_set_password(&cust, ct, "password")
3961 .unwrap_err();
3962 assert!(matches!(err, OperationError::AccessDenied));
3963
3964 let err = cutxn
3965 .credential_unix_set_password(&cust, ct, "password")
3966 .unwrap_err();
3967 assert!(matches!(err, OperationError::AccessDenied));
3968
3969 let sshkey = SshPublicKey::from_string(SSHKEY_VALID_1).expect("Invalid SSHKEY_VALID_1");
3970
3971 let err = cutxn
3972 .credential_sshkey_add(&cust, ct, "label".to_string(), sshkey)
3973 .unwrap_err();
3974 assert!(matches!(err, OperationError::AccessDenied));
3975
3976 let err = cutxn.credential_primary_init_totp(&cust, ct).unwrap_err();
3978 assert!(matches!(err, OperationError::AccessDenied));
3979
3980 let err = cutxn
3982 .credential_primary_check_totp(&cust, ct, 0, "totp")
3983 .unwrap_err();
3984 assert!(matches!(err, OperationError::AccessDenied));
3985
3986 let err = cutxn
3988 .credential_primary_accept_sha1_totp(&cust, ct)
3989 .unwrap_err();
3990 assert!(matches!(err, OperationError::AccessDenied));
3991
3992 let err = cutxn
3994 .credential_primary_remove_totp(&cust, ct, "totp")
3995 .unwrap_err();
3996 assert!(matches!(err, OperationError::AccessDenied));
3997
3998 let err = cutxn
4000 .credential_primary_init_backup_codes(&cust, ct)
4001 .unwrap_err();
4002 assert!(matches!(err, OperationError::AccessDenied));
4003
4004 let err = cutxn
4006 .credential_primary_remove_backup_codes(&cust, ct)
4007 .unwrap_err();
4008 assert!(matches!(err, OperationError::AccessDenied));
4009
4010 let err = cutxn.credential_primary_delete(&cust, ct).unwrap_err();
4012 assert!(matches!(err, OperationError::AccessDenied));
4013
4014 let err = cutxn.credential_passkey_init(&cust, ct).unwrap_err();
4016 assert!(matches!(err, OperationError::AccessDenied));
4017
4018 let err = cutxn
4023 .credential_passkey_remove(&cust, ct, Uuid::new_v4())
4024 .unwrap_err();
4025 assert!(matches!(err, OperationError::AccessDenied));
4026
4027 let c_status = cutxn
4028 .credential_update_status(&cust, ct)
4029 .expect("Failed to get the current session status.");
4030 trace!(?c_status);
4031 assert!(c_status.primary.is_none());
4032 assert!(c_status.passkeys.is_empty());
4033
4034 drop(cutxn);
4035 commit_session(idms, ct, cust).await;
4036 }
4037
4038 #[idm_test]
4040 async fn credential_update_account_policy_mfa_required(
4041 idms: &IdmServer,
4042 _idms_delayed: &mut IdmServerDelayed,
4043 ) {
4044 let test_pw = "fo3EitierohF9AelaNgiem0Ei6vup4equo1Oogeevaetehah8Tobeengae3Ci0ooh0uki";
4045 let ct = Duration::from_secs(TEST_CURRENT_TIME);
4046
4047 let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();
4048
4049 let modlist = ModifyList::new_purge_and_set(
4050 Attribute::CredentialTypeMinimum,
4051 CredentialType::Mfa.into(),
4052 );
4053 idms_prox_write
4054 .qs_write
4055 .internal_modify_uuid(UUID_IDM_ALL_ACCOUNTS, &modlist)
4056 .expect("Unable to change default session exp");
4057
4058 assert!(idms_prox_write.commit().is_ok());
4059 let (cust, _) = setup_test_session(idms, ct).await;
4062
4063 let cutxn = idms.cred_update_transaction().await.unwrap();
4064
4065 let c_status = cutxn
4069 .credential_update_status(&cust, ct)
4070 .expect("Failed to get the current session status.");
4071
4072 trace!(?c_status);
4073
4074 assert!(c_status.primary.is_none());
4075
4076 let c_status = cutxn
4079 .credential_primary_set_password(&cust, ct, test_pw)
4080 .expect("Failed to update the primary cred password");
4081
4082 assert!(!c_status.can_commit);
4083 assert!(c_status
4084 .warnings
4085 .contains(&CredentialUpdateSessionStatusWarnings::MfaRequired));
4086 let c_status = cutxn
4089 .credential_primary_init_totp(&cust, ct)
4090 .expect("Failed to update the primary cred password");
4091
4092 let totp_token: Totp = match c_status.mfaregstate {
4094 MfaRegStateStatus::TotpCheck(secret) => Some(secret.try_into().unwrap()),
4095
4096 _ => None,
4097 }
4098 .expect("Unable to retrieve totp token, invalid state.");
4099
4100 trace!(?totp_token);
4101 let chal = totp_token
4102 .do_totp_duration_from_epoch(&ct)
4103 .expect("Failed to perform totp step");
4104
4105 let c_status = cutxn
4106 .credential_primary_check_totp(&cust, ct, chal, "totp")
4107 .expect("Failed to update the primary cred totp");
4108
4109 assert!(matches!(c_status.mfaregstate, MfaRegStateStatus::None));
4110 assert!(match c_status.primary.as_ref().map(|c| &c.type_) {
4111 Some(CredentialDetailType::PasswordMfa(totp, _, 0)) => !totp.is_empty(),
4112 _ => false,
4113 });
4114
4115 assert!(c_status.can_commit);
4117 assert!(c_status.warnings.is_empty());
4118
4119 drop(cutxn);
4120 commit_session(idms, ct, cust).await;
4121
4122 let (cust, _) = renew_test_session(idms, ct).await;
4124 let cutxn = idms.cred_update_transaction().await.unwrap();
4125
4126 let c_status = cutxn
4127 .credential_primary_remove_totp(&cust, ct, "totp")
4128 .expect("Failed to update the primary cred totp");
4129
4130 assert!(matches!(c_status.mfaregstate, MfaRegStateStatus::None));
4131 assert!(matches!(
4132 c_status.primary.as_ref().map(|c| &c.type_),
4133 Some(CredentialDetailType::Password)
4134 ));
4135
4136 assert!(!c_status.can_commit);
4138 assert!(c_status
4139 .warnings
4140 .contains(&CredentialUpdateSessionStatusWarnings::MfaRequired));
4141
4142 let c_status = cutxn
4144 .credential_primary_delete(&cust, ct)
4145 .expect("Failed to delete the primary credential");
4146 assert!(c_status.primary.is_none());
4147
4148 let origin = cutxn.get_origin().clone();
4149 let mut wa = WebauthnAuthenticator::new(SoftPasskey::new(true));
4150
4151 let c_status = create_new_passkey(ct, &origin, &cutxn, &cust, &mut wa).await;
4152
4153 assert!(c_status.can_commit);
4154 assert!(c_status.warnings.is_empty());
4155 assert_eq!(c_status.passkeys.len(), 1);
4156
4157 drop(cutxn);
4158 commit_session(idms, ct, cust).await;
4159 }
4160
4161 #[idm_test]
4162 async fn credential_update_account_policy_passkey_required(
4163 idms: &IdmServer,
4164 _idms_delayed: &mut IdmServerDelayed,
4165 ) {
4166 let test_pw = "fo3EitierohF9AelaNgiem0Ei6vup4equo1Oogeevaetehah8Tobeengae3Ci0ooh0uki";
4167 let ct = Duration::from_secs(TEST_CURRENT_TIME);
4168
4169 let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();
4170
4171 let modlist = ModifyList::new_purge_and_set(
4172 Attribute::CredentialTypeMinimum,
4173 CredentialType::Passkey.into(),
4174 );
4175 idms_prox_write
4176 .qs_write
4177 .internal_modify_uuid(UUID_IDM_ALL_ACCOUNTS, &modlist)
4178 .expect("Unable to change default session exp");
4179
4180 assert!(idms_prox_write.commit().is_ok());
4181 let (cust, _) = setup_test_session(idms, ct).await;
4184
4185 let cutxn = idms.cred_update_transaction().await.unwrap();
4186
4187 let c_status = cutxn
4191 .credential_update_status(&cust, ct)
4192 .expect("Failed to get the current session status.");
4193
4194 trace!(?c_status);
4195 assert!(c_status.primary.is_none());
4196 assert!(matches!(
4197 c_status.primary_state,
4198 CredentialState::PolicyDeny
4199 ));
4200
4201 let err = cutxn
4202 .credential_primary_set_password(&cust, ct, test_pw)
4203 .unwrap_err();
4204 assert!(matches!(err, OperationError::AccessDenied));
4205
4206 let origin = cutxn.get_origin().clone();
4207 let mut wa = WebauthnAuthenticator::new(SoftPasskey::new(true));
4208
4209 let c_status = create_new_passkey(ct, &origin, &cutxn, &cust, &mut wa).await;
4210
4211 assert!(c_status.can_commit);
4212 assert!(c_status.warnings.is_empty());
4213 assert_eq!(c_status.passkeys.len(), 1);
4214
4215 drop(cutxn);
4216 commit_session(idms, ct, cust).await;
4217 }
4218
4219 #[idm_test]
4222 async fn credential_update_account_policy_attested_passkey_required(
4223 idms: &IdmServer,
4224 idms_delayed: &mut IdmServerDelayed,
4225 ) {
4226 let ct = Duration::from_secs(TEST_CURRENT_TIME);
4227
4228 let (soft_token_valid, ca_root) = SoftToken::new(true).unwrap();
4230 let mut wa_token_valid = WebauthnAuthenticator::new(soft_token_valid);
4231
4232 let mut att_ca_builder = AttestationCaListBuilder::new();
4234 att_ca_builder
4235 .insert_device_x509(
4236 ca_root,
4237 softtoken::AAGUID,
4238 "softtoken".to_string(),
4239 Default::default(),
4240 )
4241 .unwrap();
4242 let att_ca_list = att_ca_builder.build();
4243
4244 let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();
4245
4246 let modlist = ModifyList::new_purge_and_set(
4247 Attribute::WebauthnAttestationCaList,
4248 Value::WebauthnAttestationCaList(att_ca_list),
4249 );
4250 idms_prox_write
4251 .qs_write
4252 .internal_modify_uuid(UUID_IDM_ALL_ACCOUNTS, &modlist)
4253 .expect("Unable to change webauthn attestation policy");
4254
4255 assert!(idms_prox_write.commit().is_ok());
4256
4257 let (soft_token_invalid, _) = SoftToken::new(true).unwrap();
4259 let mut wa_token_invalid = WebauthnAuthenticator::new(soft_token_invalid);
4260
4261 let mut wa_passkey_invalid = WebauthnAuthenticator::new(SoftPasskey::new(true));
4262
4263 let (cust, _) = setup_test_session(idms, ct).await;
4266 let cutxn = idms.cred_update_transaction().await.unwrap();
4267 let origin = cutxn.get_origin().clone();
4268
4269 let c_status = cutxn
4271 .credential_update_status(&cust, ct)
4272 .expect("Failed to get the current session status.");
4273
4274 trace!(?c_status);
4275 assert!(c_status.attested_passkeys.is_empty());
4276 assert_eq!(
4277 c_status.attested_passkeys_allowed_devices,
4278 vec!["softtoken".to_string()]
4279 );
4280
4281 let err = cutxn.credential_passkey_init(&cust, ct).unwrap_err();
4284 assert!(matches!(err, OperationError::AccessDenied));
4285
4286 let c_status = cutxn
4289 .credential_attested_passkey_init(&cust, ct)
4290 .expect("Failed to initiate attested passkey registration");
4291
4292 let passkey_chal = match c_status.mfaregstate {
4293 MfaRegStateStatus::AttestedPasskey(c) => Some(c),
4294 _ => None,
4295 }
4296 .expect("Unable to access passkey challenge, invalid state");
4297
4298 let passkey_resp = wa_passkey_invalid
4299 .do_registration(origin.clone(), passkey_chal)
4300 .expect("Failed to create soft passkey");
4301
4302 let label = "softtoken".to_string();
4304 let err = cutxn
4305 .credential_attested_passkey_finish(&cust, ct, label, &passkey_resp)
4306 .unwrap_err();
4307
4308 assert!(matches!(
4309 err,
4310 OperationError::CU0001WebauthnAttestationNotTrusted
4311 ));
4312
4313 let c_status = cutxn
4316 .credential_attested_passkey_init(&cust, ct)
4317 .expect("Failed to initiate attested passkey registration");
4318
4319 let passkey_chal = match c_status.mfaregstate {
4320 MfaRegStateStatus::AttestedPasskey(c) => Some(c),
4321 _ => None,
4322 }
4323 .expect("Unable to access passkey challenge, invalid state");
4324
4325 let passkey_resp = wa_token_invalid
4326 .do_registration(origin.clone(), passkey_chal)
4327 .expect("Failed to create soft passkey");
4328
4329 let label = "softtoken".to_string();
4331 let err = cutxn
4332 .credential_attested_passkey_finish(&cust, ct, label, &passkey_resp)
4333 .unwrap_err();
4334
4335 assert!(matches!(
4336 err,
4337 OperationError::CU0001WebauthnAttestationNotTrusted
4338 ));
4339
4340 let c_status = cutxn
4343 .credential_attested_passkey_init(&cust, ct)
4344 .expect("Failed to initiate attested passkey registration");
4345
4346 let passkey_chal = match c_status.mfaregstate {
4347 MfaRegStateStatus::AttestedPasskey(c) => Some(c),
4348 _ => None,
4349 }
4350 .expect("Unable to access passkey challenge, invalid state");
4351
4352 let passkey_resp = wa_token_valid
4353 .do_registration(origin.clone(), passkey_chal)
4354 .expect("Failed to create soft passkey");
4355
4356 let label = "softtoken".to_string();
4358 let c_status = cutxn
4359 .credential_attested_passkey_finish(&cust, ct, label, &passkey_resp)
4360 .expect("Failed to initiate passkey registration");
4361
4362 assert!(matches!(c_status.mfaregstate, MfaRegStateStatus::None));
4363 trace!(?c_status);
4364 assert_eq!(c_status.attested_passkeys.len(), 1);
4365
4366 let pk_uuid = c_status
4367 .attested_passkeys
4368 .first()
4369 .map(|pkd| pkd.uuid)
4370 .unwrap();
4371
4372 drop(cutxn);
4373 commit_session(idms, ct, cust).await;
4374
4375 assert!(check_testperson_passkey(
4377 idms,
4378 idms_delayed,
4379 &mut wa_token_valid,
4380 origin.clone(),
4381 ct
4382 )
4383 .await
4384 .is_some());
4385
4386 let (cust, _) = renew_test_session(idms, ct).await;
4388 let cutxn = idms.cred_update_transaction().await.unwrap();
4389
4390 trace!(?c_status);
4391 assert!(c_status.primary.is_none());
4392 assert!(c_status.passkeys.is_empty());
4393 assert_eq!(c_status.attested_passkeys.len(), 1);
4394
4395 let c_status = cutxn
4396 .credential_attested_passkey_remove(&cust, ct, pk_uuid)
4397 .expect("Failed to delete the attested passkey");
4398
4399 trace!(?c_status);
4400 assert!(c_status.primary.is_none());
4401 assert!(c_status.passkeys.is_empty());
4402 assert!(c_status.attested_passkeys.is_empty());
4403
4404 drop(cutxn);
4405 commit_session(idms, ct, cust).await;
4406
4407 assert!(
4409 check_testperson_passkey(idms, idms_delayed, &mut wa_token_valid, origin, ct)
4410 .await
4411 .is_none()
4412 );
4413 }
4414
4415 #[idm_test(audit = 1)]
4416 async fn credential_update_account_policy_attested_passkey_changed(
4417 idms: &IdmServer,
4418 idms_delayed: &mut IdmServerDelayed,
4419 idms_audit: &mut IdmServerAudit,
4420 ) {
4421 let ct = Duration::from_secs(TEST_CURRENT_TIME);
4422
4423 let (soft_token_1, ca_root_1) = SoftToken::new(true).unwrap();
4425 let mut wa_token_1 = WebauthnAuthenticator::new(soft_token_1);
4426
4427 let (_soft_token_2, ca_root_2) = SoftToken::new(true).unwrap();
4428
4429 let mut att_ca_builder = AttestationCaListBuilder::new();
4430 att_ca_builder
4431 .insert_device_x509(
4432 ca_root_1.clone(),
4433 softtoken::AAGUID,
4434 "softtoken_1".to_string(),
4435 Default::default(),
4436 )
4437 .unwrap();
4438 let att_ca_list = att_ca_builder.build();
4439
4440 trace!(?att_ca_list);
4441
4442 let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();
4443
4444 let modlist = ModifyList::new_purge_and_set(
4445 Attribute::WebauthnAttestationCaList,
4446 Value::WebauthnAttestationCaList(att_ca_list),
4447 );
4448 idms_prox_write
4449 .qs_write
4450 .internal_modify_uuid(UUID_IDM_ALL_ACCOUNTS, &modlist)
4451 .expect("Unable to change webauthn attestation policy");
4452
4453 assert!(idms_prox_write.commit().is_ok());
4454
4455 let mut att_ca_builder = AttestationCaListBuilder::new();
4457 att_ca_builder
4458 .insert_device_x509(
4459 ca_root_2,
4460 softtoken::AAGUID,
4461 "softtoken_2".to_string(),
4462 Default::default(),
4463 )
4464 .unwrap();
4465 let att_ca_list_post = att_ca_builder.build();
4466
4467 let (cust, _) = setup_test_session(idms, ct).await;
4469 let cutxn = idms.cred_update_transaction().await.unwrap();
4470 let origin = cutxn.get_origin().clone();
4471
4472 let c_status = cutxn
4474 .credential_attested_passkey_init(&cust, ct)
4475 .expect("Failed to initiate attested passkey registration");
4476
4477 let passkey_chal = match c_status.mfaregstate {
4478 MfaRegStateStatus::AttestedPasskey(c) => Some(c),
4479 _ => None,
4480 }
4481 .expect("Unable to access passkey challenge, invalid state");
4482
4483 let passkey_resp = wa_token_1
4484 .do_registration(origin.clone(), passkey_chal)
4485 .expect("Failed to create soft passkey");
4486
4487 let label = "softtoken".to_string();
4489 let c_status = cutxn
4490 .credential_attested_passkey_finish(&cust, ct, label, &passkey_resp)
4491 .expect("Failed to initiate passkey registration");
4492
4493 assert!(matches!(c_status.mfaregstate, MfaRegStateStatus::None));
4494 trace!(?c_status);
4495 assert_eq!(c_status.attested_passkeys.len(), 1);
4496
4497 drop(cutxn);
4500 commit_session(idms, ct, cust).await;
4501
4502 assert!(
4504 check_testperson_passkey(idms, idms_delayed, &mut wa_token_1, origin.clone(), ct)
4505 .await
4506 .is_some()
4507 );
4508
4509 let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();
4511
4512 let modlist = ModifyList::new_purge_and_set(
4513 Attribute::WebauthnAttestationCaList,
4514 Value::WebauthnAttestationCaList(att_ca_list_post),
4515 );
4516 idms_prox_write
4517 .qs_write
4518 .internal_modify_uuid(UUID_IDM_ALL_ACCOUNTS, &modlist)
4519 .expect("Unable to change webauthn attestation policy");
4520
4521 assert!(idms_prox_write.commit().is_ok());
4522
4523 assert!(
4525 check_testperson_passkey(idms, idms_delayed, &mut wa_token_1, origin.clone(), ct)
4526 .await
4527 .is_none()
4528 );
4529
4530 match idms_audit.audit_rx().try_recv() {
4533 Ok(AuditEvent::AuthenticationDenied { .. }) => {}
4534 _ => panic!("Oh no"),
4535 }
4536
4537 let (cust, _) = renew_test_session(idms, ct).await;
4539 let cutxn = idms.cred_update_transaction().await.unwrap();
4540
4541 let c_status = cutxn
4543 .credential_update_status(&cust, ct)
4544 .expect("Failed to get the current session status.");
4545
4546 trace!(?c_status);
4547 assert!(c_status.attested_passkeys.is_empty());
4548
4549 drop(cutxn);
4550 commit_session(idms, ct, cust).await;
4551
4552 assert!(
4554 check_testperson_passkey(idms, idms_delayed, &mut wa_token_1, origin.clone(), ct)
4555 .await
4556 .is_none()
4557 );
4558 }
4559
4560 #[idm_test]
4562 async fn credential_update_account_policy_attested_passkey_downgrade(
4563 idms: &IdmServer,
4564 idms_delayed: &mut IdmServerDelayed,
4565 ) {
4566 let ct = Duration::from_secs(TEST_CURRENT_TIME);
4567
4568 let (soft_token_1, ca_root_1) = SoftToken::new(true).unwrap();
4570 let mut wa_token_1 = WebauthnAuthenticator::new(soft_token_1);
4571
4572 let mut att_ca_builder = AttestationCaListBuilder::new();
4573 att_ca_builder
4574 .insert_device_x509(
4575 ca_root_1.clone(),
4576 softtoken::AAGUID,
4577 "softtoken_1".to_string(),
4578 Default::default(),
4579 )
4580 .unwrap();
4581 let att_ca_list = att_ca_builder.build();
4582
4583 trace!(?att_ca_list);
4584
4585 let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();
4586
4587 let modlist = ModifyList::new_purge_and_set(
4588 Attribute::WebauthnAttestationCaList,
4589 Value::WebauthnAttestationCaList(att_ca_list),
4590 );
4591 idms_prox_write
4592 .qs_write
4593 .internal_modify_uuid(UUID_IDM_ALL_ACCOUNTS, &modlist)
4594 .expect("Unable to change webauthn attestation policy");
4595
4596 assert!(idms_prox_write.commit().is_ok());
4597
4598 let (cust, _) = setup_test_session(idms, ct).await;
4600 let cutxn = idms.cred_update_transaction().await.unwrap();
4601 let origin = cutxn.get_origin().clone();
4602
4603 let c_status = cutxn
4605 .credential_attested_passkey_init(&cust, ct)
4606 .expect("Failed to initiate attested passkey registration");
4607
4608 let passkey_chal = match c_status.mfaregstate {
4609 MfaRegStateStatus::AttestedPasskey(c) => Some(c),
4610 _ => None,
4611 }
4612 .expect("Unable to access passkey challenge, invalid state");
4613
4614 let passkey_resp = wa_token_1
4615 .do_registration(origin.clone(), passkey_chal)
4616 .expect("Failed to create soft passkey");
4617
4618 let label = "softtoken".to_string();
4620 let c_status = cutxn
4621 .credential_attested_passkey_finish(&cust, ct, label, &passkey_resp)
4622 .expect("Failed to initiate passkey registration");
4623
4624 assert!(matches!(c_status.mfaregstate, MfaRegStateStatus::None));
4625 trace!(?c_status);
4626 assert_eq!(c_status.attested_passkeys.len(), 1);
4627
4628 drop(cutxn);
4631 commit_session(idms, ct, cust).await;
4632
4633 assert!(
4635 check_testperson_passkey(idms, idms_delayed, &mut wa_token_1, origin.clone(), ct)
4636 .await
4637 .is_some()
4638 );
4639
4640 let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();
4642
4643 let modlist = ModifyList::new_purge(Attribute::WebauthnAttestationCaList);
4644 idms_prox_write
4645 .qs_write
4646 .internal_modify_uuid(UUID_IDM_ALL_ACCOUNTS, &modlist)
4647 .expect("Unable to change webauthn attestation policy");
4648
4649 assert!(idms_prox_write.commit().is_ok());
4650
4651 assert!(
4653 check_testperson_passkey(idms, idms_delayed, &mut wa_token_1, origin.clone(), ct)
4654 .await
4655 .is_some()
4656 );
4657
4658 let (cust, _) = renew_test_session(idms, ct).await;
4660 let cutxn = idms.cred_update_transaction().await.unwrap();
4661
4662 let c_status = cutxn
4663 .credential_update_status(&cust, ct)
4664 .expect("Failed to get the current session status.");
4665
4666 trace!(?c_status);
4667 assert_eq!(c_status.attested_passkeys.len(), 1);
4668 assert!(matches!(
4669 c_status.attested_passkeys_state,
4670 CredentialState::DeleteOnly
4671 ));
4672
4673 drop(cutxn);
4674 commit_session(idms, ct, cust).await;
4675 }
4676
4677 #[idm_test]
4678 async fn credential_update_unix_password(
4679 idms: &IdmServer,
4680 _idms_delayed: &mut IdmServerDelayed,
4681 ) {
4682 let test_pw = "fo3EitierohF9AelaNgiem0Ei6vup4equo1Oogeevaetehah8Tobeengae3Ci0ooh0uki";
4683 let ct = Duration::from_secs(TEST_CURRENT_TIME);
4684
4685 let (cust, _) = setup_test_session(idms, ct).await;
4686
4687 let cutxn = idms.cred_update_transaction().await.unwrap();
4688
4689 let c_status = cutxn
4693 .credential_update_status(&cust, ct)
4694 .expect("Failed to get the current session status.");
4695
4696 trace!(?c_status);
4697
4698 assert!(c_status.unixcred.is_none());
4699
4700 let c_status = cutxn
4703 .credential_unix_set_password(&cust, ct, test_pw)
4704 .expect("Failed to update the unix cred password");
4705
4706 assert!(c_status.can_commit);
4707
4708 drop(cutxn);
4709 commit_session(idms, ct, cust).await;
4710
4711 assert!(check_testperson_unix_password(idms, test_pw, ct)
4713 .await
4714 .is_some());
4715
4716 let (cust, _) = renew_test_session(idms, ct).await;
4718 let cutxn = idms.cred_update_transaction().await.unwrap();
4719
4720 let c_status = cutxn
4721 .credential_update_status(&cust, ct)
4722 .expect("Failed to get the current session status.");
4723 trace!(?c_status);
4724 assert!(c_status.unixcred.is_some());
4725
4726 let c_status = cutxn
4727 .credential_unix_delete(&cust, ct)
4728 .expect("Failed to delete the unix cred");
4729 trace!(?c_status);
4730 assert!(c_status.unixcred.is_none());
4731
4732 drop(cutxn);
4733 commit_session(idms, ct, cust).await;
4734
4735 assert!(check_testperson_unix_password(idms, test_pw, ct)
4737 .await
4738 .is_none());
4739 }
4740
4741 #[idm_test]
4742 async fn credential_update_sshkeys(idms: &IdmServer, _idms_delayed: &mut IdmServerDelayed) {
4743 let sshkey_valid_1 =
4744 SshPublicKey::from_string(SSHKEY_VALID_1).expect("Invalid SSHKEY_VALID_1");
4745 let sshkey_valid_2 =
4746 SshPublicKey::from_string(SSHKEY_VALID_2).expect("Invalid SSHKEY_VALID_2");
4747
4748 assert!(SshPublicKey::from_string(SSHKEY_INVALID).is_err());
4749
4750 let ct = Duration::from_secs(TEST_CURRENT_TIME);
4751 let (cust, _) = setup_test_session(idms, ct).await;
4752 let cutxn = idms.cred_update_transaction().await.unwrap();
4753
4754 let c_status = cutxn
4755 .credential_update_status(&cust, ct)
4756 .expect("Failed to get the current session status.");
4757
4758 trace!(?c_status);
4759
4760 assert!(c_status.sshkeys.is_empty());
4761
4762 let result = cutxn.credential_sshkey_add(&cust, ct, "".to_string(), sshkey_valid_1.clone());
4764 assert!(matches!(result, Err(OperationError::InvalidLabel)));
4765
4766 let result =
4768 cutxn.credential_sshkey_add(&cust, ct, "🚛".to_string(), sshkey_valid_1.clone());
4769 assert!(matches!(result, Err(OperationError::InvalidLabel)));
4770
4771 let result = cutxn.credential_sshkey_remove(&cust, ct, "key1");
4773 assert!(matches!(result, Err(OperationError::NoMatchingEntries)));
4774
4775 let c_status = cutxn
4777 .credential_sshkey_add(&cust, ct, "key1".to_string(), sshkey_valid_1.clone())
4778 .expect("Failed to add sshkey_valid_1");
4779
4780 trace!(?c_status);
4781 assert_eq!(c_status.sshkeys.len(), 1);
4782 assert!(c_status.sshkeys.contains_key("key1"));
4783
4784 let c_status = cutxn
4786 .credential_sshkey_add(&cust, ct, "key2".to_string(), sshkey_valid_2.clone())
4787 .expect("Failed to add sshkey_valid_2");
4788
4789 trace!(?c_status);
4790 assert_eq!(c_status.sshkeys.len(), 2);
4791 assert!(c_status.sshkeys.contains_key("key1"));
4792 assert!(c_status.sshkeys.contains_key("key2"));
4793
4794 let c_status = cutxn
4796 .credential_sshkey_remove(&cust, ct, "key2")
4797 .expect("Failed to remove sshkey_valid_2");
4798
4799 trace!(?c_status);
4800 assert_eq!(c_status.sshkeys.len(), 1);
4801 assert!(c_status.sshkeys.contains_key("key1"));
4802
4803 let result =
4805 cutxn.credential_sshkey_add(&cust, ct, "key1".to_string(), sshkey_valid_2.clone());
4806 assert!(matches!(result, Err(OperationError::DuplicateLabel)));
4807
4808 let result =
4810 cutxn.credential_sshkey_add(&cust, ct, "key2".to_string(), sshkey_valid_1.clone());
4811 assert!(matches!(result, Err(OperationError::DuplicateKey)));
4812
4813 drop(cutxn);
4814 commit_session(idms, ct, cust).await;
4815 }
4816}