1use super::accountpolicy::ResolvedAccountPolicy;
2use super::group::{load_account_policy, load_all_groups_from_account, Group, Unix};
3use crate::constants::UUID_ANONYMOUS;
4use crate::credential::softlock::CredSoftLockPolicy;
5use crate::credential::{apppwd::ApplicationPassword, Credential};
6use crate::entry::{Entry, EntryCommitted, EntryReduced, EntrySealed};
7use crate::event::SearchEvent;
8use crate::idm::application::Application;
9use crate::idm::ldap::{LdapBoundToken, LdapSession};
10use crate::idm::server::{IdmServerProxyReadTransaction, IdmServerProxyWriteTransaction};
11use crate::modify::{ModifyInvalid, ModifyList};
12use crate::prelude::*;
13use crate::schema::SchemaTransaction;
14use crate::value::{IntentTokenState, PartialValue, SessionState, Value};
15use kanidm_lib_crypto::CryptoPolicy;
16use kanidm_proto::internal::{CredentialStatus, UatPurpose, UiHint, UserAuthToken};
17use kanidm_proto::v1::{UatStatus, UatStatusState, UnixGroupToken, UnixUserToken};
18use sshkey_attest::proto::PublicKey as SshPublicKey;
19use std::collections::{BTreeMap, BTreeSet};
20use std::time::Duration;
21use time::OffsetDateTime;
22use uuid::Uuid;
23use webauthn_rs::prelude::{
24 AttestedPasskey as AttestedPasskeyV4, AuthenticationResult, CredentialID, Passkey as PasskeyV4,
25};
26
27#[derive(Debug, Clone)]
28pub struct UnixExtensions {
29 ucred: Option<Credential>,
30 shell: Option<String>,
31 gidnumber: u32,
32 groups: Vec<Group<Unix>>,
33}
34
35impl UnixExtensions {
36 pub(crate) fn ucred(&self) -> Option<&Credential> {
37 self.ucred.as_ref()
38 }
39}
40
41#[derive(Default, Debug, Clone)]
42pub struct OAuth2AccountCredential {
43 pub(crate) provider: Uuid,
44 pub(crate) cred_id: Uuid,
45 pub(crate) user_id: String,
46 pub(crate) user_sub: String,
47}
48
49#[derive(Default, Debug, Clone)]
50pub struct Account {
51 spn: String,
54 name: Option<String>,
55 pub displayname: String,
56 pub uuid: Uuid,
57 pub sync_parent_uuid: Option<Uuid>,
58 pub groups: Vec<Group<()>>,
59 pub primary: Option<Credential>,
60 pub passkeys: BTreeMap<Uuid, (String, PasskeyV4)>,
61 pub attested_passkeys: BTreeMap<Uuid, (String, AttestedPasskeyV4)>,
62 pub valid_from: Option<OffsetDateTime>,
63 pub expire: Option<OffsetDateTime>,
64 softlock_expire: Option<OffsetDateTime>,
65 pub radius_secret: Option<String>,
66 pub ui_hints: BTreeSet<UiHint>,
67 pub mail_primary: Option<String>,
68 pub mail: Vec<String>,
69 pub credential_update_intent_tokens: BTreeMap<String, IntentTokenState>,
70 pub(crate) unix_extn: Option<UnixExtensions>,
71 pub(crate) sshkeys: BTreeMap<String, SshPublicKey>,
72 pub apps_pwds: BTreeMap<Uuid, Vec<ApplicationPassword>>,
73 pub(crate) oauth2_client_provider: Option<OAuth2AccountCredential>,
74 pub updated_at: Option<Cid>,
75}
76
77#[cfg(test)]
78impl From<crate::migration_data::BuiltinAccount> for crate::idm::account::Account {
79 fn from(value: crate::migration_data::BuiltinAccount) -> Self {
80 Self {
81 name: Some(value.name.to_string()),
82 uuid: value.uuid,
83 displayname: value.displayname.to_string(),
84 spn: format!("{}@example.com", value.name),
85 mail_primary: None,
86 mail: Vec::with_capacity(0),
87 ..Default::default()
88 }
89 }
90}
91
92macro_rules! try_from_entry {
93 ($value:expr, $groups:expr, $unix_groups:expr) => {{
94 if !$value.attribute_equality(Attribute::Class, &EntryClass::Account.to_partialvalue()) {
96 return Err(OperationError::MissingClass(ENTRYCLASS_ACCOUNT.into()));
97 }
98
99 let name = $value
101 .get_ava_single_iname(Attribute::Name)
102 .map(|s| s.to_string());
103
104 let displayname = $value
105 .get_ava_single_utf8(Attribute::DisplayName)
106 .map(|s| s.to_string())
107 .ok_or(OperationError::MissingAttribute(Attribute::DisplayName))?;
108
109 let sync_parent_uuid = $value.get_ava_single_refer(Attribute::SyncParentUuid);
110
111 let primary = $value
112 .get_ava_single_credential(Attribute::PrimaryCredential)
113 .cloned();
114
115 let passkeys = $value
116 .get_ava_passkeys(Attribute::PassKeys)
117 .cloned()
118 .unwrap_or_default();
119
120 let attested_passkeys = $value
121 .get_ava_attestedpasskeys(Attribute::AttestedPasskeys)
122 .cloned()
123 .unwrap_or_default();
124
125 let spn = $value
126 .get_ava_single_proto_string(Attribute::Spn)
127 .ok_or(OperationError::MissingAttribute(Attribute::Spn))?;
128
129 let mail_primary = $value
130 .get_ava_mail_primary(Attribute::Mail)
131 .map(str::to_string);
132
133 let mail = $value
134 .get_ava_iter_mail(Attribute::Mail)
135 .map(|i| i.map(str::to_string).collect())
136 .unwrap_or_default();
137
138 let valid_from = $value.get_ava_single_datetime(Attribute::AccountValidFrom);
139
140 let expire = $value.get_ava_single_datetime(Attribute::AccountExpire);
141
142 let softlock_expire = $value.get_ava_single_datetime(Attribute::AccountSoftlockExpire);
143
144 let radius_secret = $value
145 .get_ava_single_secret(Attribute::RadiusSecret)
146 .map(str::to_string);
147
148 let groups = $groups;
150
151 let uuid = $value.get_uuid().clone();
152
153 let credential_update_intent_tokens = $value
154 .get_ava_as_intenttokens(Attribute::CredentialUpdateIntentToken)
155 .cloned()
156 .unwrap_or_default();
157
158 let mut ui_hints: BTreeSet<_> = groups
160 .iter()
161 .map(|group: &Group<()>| group.ui_hints().iter())
162 .flatten()
163 .copied()
164 .collect();
165
166 if $value.attribute_equality(Attribute::Class, &EntryClass::Person.to_partialvalue()) {
168 ui_hints.insert(UiHint::CredentialUpdate);
169 }
170
171 if $value.attribute_equality(Attribute::Class, &EntryClass::SyncObject.to_partialvalue()) {
172 ui_hints.insert(UiHint::SynchronisedAccount);
173 }
174
175 let sshkeys = $value
176 .get_ava_set(Attribute::SshPublicKey)
177 .and_then(|vs| vs.as_sshkey_map())
178 .cloned()
179 .unwrap_or_default();
180
181 let unix_extn = if $value.attribute_equality(
182 Attribute::Class,
183 &EntryClass::PosixAccount.to_partialvalue(),
184 ) {
185 ui_hints.insert(UiHint::PosixAccount);
186
187 let ucred = $value
188 .get_ava_single_credential(Attribute::UnixPassword)
189 .cloned();
190
191 let shell = $value
192 .get_ava_single_iutf8(Attribute::LoginShell)
193 .map(|s| s.to_string());
194
195 let gidnumber = $value
196 .get_ava_single_uint32(Attribute::GidNumber)
197 .ok_or_else(|| OperationError::MissingAttribute(Attribute::GidNumber))?;
198
199 let groups = $unix_groups;
200
201 Some(UnixExtensions {
202 ucred,
203 shell,
204 gidnumber,
205 groups,
206 })
207 } else {
208 None
209 };
210
211 let apps_pwds = $value
212 .get_ava_application_password(Attribute::ApplicationPassword)
213 .cloned()
214 .unwrap_or_default();
215
216 let maybe_account_provider = $value.get_ava_single_refer(Attribute::OAuth2AccountProvider);
217
218 let maybe_account_unique_user_id =
219 $value.get_ava_single_utf8(Attribute::OAuth2AccountUniqueUserId);
220
221 let maybe_account_unique_user_sub =
222 $value.get_ava_single_utf8(Attribute::OAuth2AccountUniqueUserSub);
223
224 let maybe_account_credential_id =
225 $value.get_ava_single_uuid(Attribute::OAuth2AccountCredentialUuid);
226
227 let oauth2_client_provider = match (
228 maybe_account_provider,
229 maybe_account_unique_user_id,
230 maybe_account_unique_user_sub,
231 maybe_account_credential_id,
232 ) {
233 (Some(provider), Some(user_id), Some(user_sub), Some(cred_id)) => {
234 Some(OAuth2AccountCredential {
235 provider,
236 cred_id,
237 user_id: user_id.to_string(),
238 user_sub: user_sub.to_string(),
239 })
240 }
241 _ => None,
242 };
243
244 let updated_at: Option<Cid> = $value
245 .get_ava_set(Attribute::LastModifiedCid)
246 .cloned()
247 .and_then(|u| u.to_cid_single());
248
249 Ok(Account {
250 uuid,
251 name,
252 sync_parent_uuid,
253 displayname,
254 groups,
255 primary,
256 passkeys,
257 attested_passkeys,
258 valid_from,
259 expire,
260 softlock_expire,
261 radius_secret,
262 spn,
263 ui_hints,
264 mail_primary,
265 mail,
266 credential_update_intent_tokens,
267 unix_extn,
268 sshkeys,
269 apps_pwds,
270 oauth2_client_provider,
271 updated_at,
272 })
273 }};
274}
275
276impl Account {
277 pub(crate) fn unix_extn(&self) -> Option<&UnixExtensions> {
278 self.unix_extn.as_ref()
279 }
280
281 pub(crate) fn primary(&self) -> Option<&Credential> {
282 self.primary.as_ref()
283 }
284
285 pub(crate) fn sshkeys(&self) -> &BTreeMap<String, SshPublicKey> {
286 &self.sshkeys
287 }
288
289 pub(crate) fn uuid(&self) -> Uuid {
290 self.uuid
291 }
292
293 pub(crate) fn spn(&self) -> &str {
294 self.spn.as_str()
295 }
296
297 pub(crate) fn name(&self) -> &str {
298 self.name.as_deref().unwrap_or(self.spn.as_str())
299 }
300
301 pub(crate) fn display_name(&self) -> &str {
302 &self.displayname
303 }
304
305 pub(crate) fn mail_primary(&self) -> Option<&str> {
306 self.mail_primary.as_deref()
307 }
308
309 pub(crate) fn mail(&self) -> &[String] {
310 self.mail.as_slice()
311 }
312
313 pub(crate) fn softlock_expire(&self) -> Option<OffsetDateTime> {
314 self.softlock_expire
315 }
316
317 #[instrument(level = "trace", skip_all)]
318 pub(crate) fn try_from_entry_ro(
319 value: &Entry<EntrySealed, EntryCommitted>,
320 qs: &mut QueryServerReadTransaction,
321 ) -> Result<Self, OperationError> {
322 let (groups, unix_groups) = load_all_groups_from_account(value, qs)?;
323
324 try_from_entry!(value, groups, unix_groups)
325 }
326
327 #[instrument(level = "trace", skip_all)]
328 pub(crate) fn try_from_entry_with_policy<'a, TXN>(
329 value: &Entry<EntrySealed, EntryCommitted>,
330 qs: &mut TXN,
331 ) -> Result<(Self, ResolvedAccountPolicy), OperationError>
332 where
333 TXN: QueryServerTransaction<'a>,
334 {
335 let (groups, unix_groups) = load_all_groups_from_account(value, qs)?;
336 let rap = load_account_policy(value, qs)?;
337
338 try_from_entry!(value, groups, unix_groups).map(|acct| (acct, rap))
339 }
340
341 #[instrument(level = "trace", skip_all)]
342 pub(crate) fn try_from_entry_rw(
343 value: &Entry<EntrySealed, EntryCommitted>,
344 qs: &mut QueryServerWriteTransaction,
345 ) -> Result<Self, OperationError> {
346 let (groups, unix_groups) = load_all_groups_from_account(value, qs)?;
347
348 try_from_entry!(value, groups, unix_groups)
349 }
350
351 #[instrument(level = "trace", skip_all)]
352 pub(crate) fn try_from_entry_reduced(
353 value: &Entry<EntryReduced, EntryCommitted>,
354 qs: &mut QueryServerReadTransaction,
355 ) -> Result<Self, OperationError> {
356 let (groups, unix_groups) = load_all_groups_from_account(value, qs)?;
357 try_from_entry!(value, groups, unix_groups)
358 }
359
360 pub(crate) fn to_userauthtoken(
365 &self,
366 session_id: Uuid,
367 scope: SessionScope,
368 ct: Duration,
369 account_policy: &ResolvedAccountPolicy,
370 ) -> Option<UserAuthToken> {
371 let ct = ct - Duration::from_nanos(ct.subsec_nanos() as u64);
375 let issued_at = OffsetDateTime::UNIX_EPOCH + ct;
376
377 let limit_search_max_results = account_policy.limit_search_max_results();
378 let limit_search_max_filter_test = account_policy.limit_search_max_filter_test();
379
380 let expiry = OffsetDateTime::UNIX_EPOCH
383 + ct
384 + Duration::from_secs(account_policy.authsession_expiry() as u64);
385 let limited_expiry = OffsetDateTime::UNIX_EPOCH
386 + ct
387 + Duration::from_secs(DEFAULT_AUTH_SESSION_LIMITED_EXPIRY as u64);
388
389 let (purpose, expiry) = match scope {
390 SessionScope::Synchronise => {
392 warn!(
393 "Should be impossible to issue sync sessions with a uat. Refusing to proceed."
394 );
395 return None;
396 }
397 SessionScope::ReadOnly => (UatPurpose::ReadOnly, expiry),
398 SessionScope::ReadWrite => {
399 let capped = std::cmp::min(expiry, limited_expiry);
402
403 (
404 UatPurpose::ReadWrite {
405 expiry: Some(capped),
406 },
407 capped,
408 )
409 }
410 SessionScope::PrivilegeCapable => (UatPurpose::ReadWrite { expiry: None }, expiry),
411 };
412
413 Some(UserAuthToken {
414 session_id,
415 expiry: Some(expiry),
416 issued_at,
417 purpose,
418 uuid: self.uuid,
419 displayname: self.displayname.clone(),
420 spn: self.spn.clone(),
421 mail_primary: self.mail_primary.clone(),
422 ui_hints: self.ui_hints.clone(),
423 limit_search_max_results,
426 limit_search_max_filter_test,
427 })
428 }
429
430 pub(crate) fn to_reissue_userauthtoken(
434 &self,
435 session_id: Uuid,
436 session_expiry: Option<OffsetDateTime>,
437 scope: SessionScope,
438 read_write: bool,
439 ct: Duration,
440 account_policy: &ResolvedAccountPolicy,
441 ) -> Option<UserAuthToken> {
442 let issued_at = OffsetDateTime::UNIX_EPOCH + ct;
443
444 let limit_search_max_results = account_policy.limit_search_max_results();
445 let limit_search_max_filter_test = account_policy.limit_search_max_filter_test();
446
447 let (purpose, expiry) = match scope {
448 SessionScope::Synchronise | SessionScope::ReadOnly | SessionScope::ReadWrite => {
449 warn!(
450 "Impossible state, should not be re-issuing for session scope {:?}",
451 scope
452 );
453 return None;
454 }
455 SessionScope::PrivilegeCapable if read_write => {
456 let expiry = Some(
458 OffsetDateTime::UNIX_EPOCH
459 + ct
460 + Duration::from_secs(account_policy.privilege_expiry().into()),
461 );
462 (UatPurpose::ReadWrite { expiry }, session_expiry)
466 }
467 SessionScope::PrivilegeCapable => {
468 (UatPurpose::ReadWrite { expiry: None }, session_expiry)
470 }
471 };
472
473 Some(UserAuthToken {
474 session_id,
475 expiry,
476 issued_at,
477 purpose,
478 uuid: self.uuid,
479 displayname: self.displayname.clone(),
480 spn: self.spn.clone(),
481 mail_primary: self.mail_primary.clone(),
482 ui_hints: self.ui_hints.clone(),
483 limit_search_max_results,
486 limit_search_max_filter_test,
487 })
488 }
489
490 pub(crate) fn client_cert_info_to_userauthtoken(
493 &self,
494 certificate_id: Uuid,
495 session_is_rw: bool,
496 ct: Duration,
497 account_policy: &ResolvedAccountPolicy,
498 ) -> Option<UserAuthToken> {
499 let issued_at = OffsetDateTime::UNIX_EPOCH + ct;
500
501 let limit_search_max_results = account_policy.limit_search_max_results();
502 let limit_search_max_filter_test = account_policy.limit_search_max_filter_test();
503
504 let purpose = if session_is_rw {
505 UatPurpose::ReadWrite { expiry: None }
506 } else {
507 UatPurpose::ReadOnly
508 };
509
510 Some(UserAuthToken {
511 session_id: certificate_id,
512 expiry: None,
513 issued_at,
514 purpose,
515 uuid: self.uuid,
516 displayname: self.displayname.clone(),
517 spn: self.spn.clone(),
518 mail_primary: self.mail_primary.clone(),
519 ui_hints: self.ui_hints.clone(),
520 limit_search_max_results,
523 limit_search_max_filter_test,
524 })
525 }
526
527 pub fn check_within_valid_time(
530 ct: Duration,
531 valid_from: Option<&OffsetDateTime>,
532 expire: Option<&OffsetDateTime>,
533 ) -> bool {
534 let cot = OffsetDateTime::UNIX_EPOCH + ct;
535 trace!("Checking within valid time: {:?} {:?}", valid_from, expire);
536
537 let vmin = if let Some(vft) = valid_from {
538 vft <= &cot
540 } else {
541 true
543 };
544 let vmax = if let Some(ext) = expire {
545 &cot <= ext
547 } else {
548 true
550 };
551 vmin && vmax
553 }
554
555 pub fn is_within_valid_time(&self, ct: Duration) -> bool {
558 Self::check_within_valid_time(ct, self.valid_from.as_ref(), self.expire.as_ref())
559 }
560
561 pub fn related_inputs(&self) -> Vec<&str> {
564 let mut inputs = Vec::with_capacity(4 + self.mail.len());
565 self.mail.iter().for_each(|m| {
566 inputs.push(m.as_str());
567 });
568 inputs.push(self.spn.as_str());
569 if let Some(name) = self.name.as_ref() {
570 inputs.push(name)
571 }
572 inputs.push(self.displayname.as_str());
573 if let Some(s) = self.radius_secret.as_deref() {
574 inputs.push(s);
575 }
576 inputs
577 }
578
579 pub fn primary_cred_uuid_and_policy(&self) -> Option<(Uuid, CredSoftLockPolicy)> {
580 self.primary
581 .as_ref()
582 .map(|cred| (cred.uuid, cred.softlock_policy()))
583 .or_else(|| {
584 if self.is_anonymous() {
585 Some((UUID_ANONYMOUS, CredSoftLockPolicy::Unrestricted))
586 } else {
587 None
588 }
589 })
590 }
591
592 pub fn is_anonymous(&self) -> bool {
593 self.uuid == UUID_ANONYMOUS
594 }
595
596 #[cfg(test)]
597 pub(crate) fn gen_password_mod(
598 &self,
599 cleartext: &str,
600 crypto_policy: &CryptoPolicy,
601 ct: OffsetDateTime,
602 ) -> Result<ModifyList<ModifyInvalid>, OperationError> {
603 match &self.primary {
604 Some(primary) => {
606 let ncred = primary.set_password(crypto_policy, cleartext, ct)?;
607 let vcred = Value::new_credential("primary", ncred);
608 Ok(ModifyList::new_purge_and_set(
609 Attribute::PrimaryCredential,
610 vcred,
611 ))
612 }
613 None => {
615 let ncred = Credential::new_password_only(
616 crypto_policy,
617 cleartext,
618 OffsetDateTime::UNIX_EPOCH,
619 )?;
620 let vcred = Value::new_credential("primary", ncred);
621 Ok(ModifyList::new_purge_and_set(
622 Attribute::PrimaryCredential,
623 vcred,
624 ))
625 }
626 }
627 }
628
629 pub(crate) fn gen_password_upgrade_mod(
630 &self,
631 cleartext: &str,
632 crypto_policy: &CryptoPolicy,
633 ) -> Result<Option<ModifyList<ModifyInvalid>>, OperationError> {
634 match &self.primary {
635 Some(primary) => {
637 if let Some(ncred) = primary.upgrade_password(crypto_policy, cleartext)? {
638 let vcred = Value::new_credential("primary", ncred);
639 Ok(Some(ModifyList::new_purge_and_set(
640 Attribute::PrimaryCredential,
641 vcred,
642 )))
643 } else {
644 Ok(None)
646 }
647 }
648 None => Ok(None),
650 }
651 }
652
653 pub(crate) fn gen_webauthn_counter_mod(
654 &mut self,
655 auth_result: &AuthenticationResult,
656 ) -> Result<Option<ModifyList<ModifyInvalid>>, OperationError> {
657 let mut ml = Vec::with_capacity(2);
658 let opt_ncred = match self.primary.as_ref() {
660 Some(primary) => primary.update_webauthn_properties(auth_result)?,
661 None => None,
662 };
663
664 if let Some(ncred) = opt_ncred {
665 let vcred = Value::new_credential("primary", ncred);
666 ml.push(Modify::Purged(Attribute::PrimaryCredential));
667 ml.push(Modify::Present(Attribute::PrimaryCredential, vcred));
668 }
669
670 self.passkeys.iter_mut().for_each(|(u, (t, k))| {
672 if let Some(true) = k.update_credential(auth_result) {
673 ml.push(Modify::Removed(
674 Attribute::PassKeys,
675 PartialValue::Passkey(*u),
676 ));
677
678 ml.push(Modify::Present(
679 Attribute::PassKeys,
680 Value::Passkey(*u, t.clone(), k.clone()),
681 ));
682 }
683 });
684
685 self.attested_passkeys.iter_mut().for_each(|(u, (t, k))| {
687 if let Some(true) = k.update_credential(auth_result) {
688 ml.push(Modify::Removed(
689 Attribute::AttestedPasskeys,
690 PartialValue::AttestedPasskey(*u),
691 ));
692
693 ml.push(Modify::Present(
694 Attribute::AttestedPasskeys,
695 Value::AttestedPasskey(*u, t.clone(), k.clone()),
696 ));
697 }
698 });
699
700 if ml.is_empty() {
701 Ok(None)
702 } else {
703 Ok(Some(ModifyList::new_list(ml)))
704 }
705 }
706
707 pub(crate) fn invalidate_backup_code_mod(
708 self,
709 code_to_remove: &str,
710 ) -> Result<ModifyList<ModifyInvalid>, OperationError> {
711 match self.primary {
712 Some(primary) => {
714 let r_ncred = primary.invalidate_backup_code(code_to_remove);
715 match r_ncred {
716 Ok(ncred) => {
717 let vcred = Value::new_credential("primary", ncred);
718 Ok(ModifyList::new_purge_and_set(
719 Attribute::PrimaryCredential,
720 vcred,
721 ))
722 }
723 Err(e) => Err(e),
724 }
725 }
726 None => {
727 Err(OperationError::InvalidState)
729 }
730 }
731 }
732
733 pub(crate) fn regenerate_radius_secret_mod(
734 &self,
735 cleartext: &str,
736 ) -> Result<ModifyList<ModifyInvalid>, OperationError> {
737 let vcred = Value::new_secret_str(cleartext);
738 Ok(ModifyList::new_purge_and_set(
739 Attribute::RadiusSecret,
740 vcred,
741 ))
742 }
743
744 pub(crate) fn to_credentialstatus(&self) -> Result<CredentialStatus, OperationError> {
745 self.primary
748 .as_ref()
749 .map(|cred| CredentialStatus {
750 creds: vec![cred.into()],
751 })
752 .ok_or(OperationError::NoMatchingAttributes)
753 }
754
755 pub(crate) fn existing_credential_id_list(&self) -> Option<Vec<CredentialID>> {
756 None
759 }
760
761 pub(crate) fn check_user_auth_token_valid(
762 ct: Duration,
763 uat: &UserAuthToken,
764 entry: &Entry<EntrySealed, EntryCommitted>,
765 ) -> bool {
766 let within_valid_window = Account::check_within_valid_time(
771 ct,
772 entry
773 .get_ava_single_datetime(Attribute::AccountValidFrom)
774 .as_ref(),
775 entry
776 .get_ava_single_datetime(Attribute::AccountExpire)
777 .as_ref(),
778 );
779
780 if !within_valid_window {
781 security_info!("Account has expired or is not yet valid, not allowing to proceed");
782 return false;
783 }
784
785 trace!("{}", &uat);
788
789 if uat.uuid == UUID_ANONYMOUS {
790 security_debug!("Anonymous sessions do not have session records, session is valid.");
791 true
792 } else {
793 let session_present = entry
795 .get_ava_as_session_map(Attribute::UserAuthTokenSession)
796 .and_then(|session_map| session_map.get(&uat.session_id));
797
798 if let Some(session) = session_present {
802 match (&session.state, &uat.expiry) {
803 (SessionState::ExpiresAt(s_exp), Some(u_exp)) if s_exp == u_exp => {
804 security_info!("A valid limited session value exists for this token");
805 true
806 }
807 (SessionState::NeverExpires, None) => {
808 security_info!("A valid unbound session value exists for this token");
809 true
810 }
811 (SessionState::RevokedAt(_), _) => {
812 security_info!("Session has been revoked");
815 false
816 }
817 _ => {
818 security_info!("Session and uat expiry are not consistent, rejecting.");
819 debug!(ses_st = ?session.state, uat_exp = ?uat.expiry);
820 false
821 }
822 }
823 } else {
824 let grace = uat.issued_at + AUTH_TOKEN_GRACE_WINDOW;
825 let current = time::OffsetDateTime::UNIX_EPOCH + ct;
826 trace!(%grace, %current);
827 if current >= grace {
828 security_info!(
829 "The token grace window has passed, and no session exists. Assuming invalid."
830 );
831 false
832 } else {
833 security_info!("The token grace window is in effect. Assuming valid.");
834 true
835 }
836 }
837 }
838 }
839
840 pub(crate) fn verify_application_password(
841 &self,
842 application: &Application,
843 cleartext: &str,
844 ) -> Result<Option<LdapBoundToken>, OperationError> {
845 if let Some(v) = self.apps_pwds.get(&application.uuid) {
846 for ap in v.iter() {
847 let password_verified = ap.password.verify(cleartext).map_err(|e| {
848 error!(crypto_err = ?e);
849 OperationError::CryptographyError
850 })?;
851
852 if password_verified {
853 let session_id = uuid::Uuid::new_v4();
854 security_info!(
855 "Starting session {} for {} {}",
856 session_id,
857 self.spn,
858 self.uuid
859 );
860
861 return Ok(Some(LdapBoundToken {
862 spn: self.spn.clone(),
863 session_id,
864 effective_session: LdapSession::ApplicationPasswordBind(
865 application.uuid,
866 self.uuid,
867 ),
868 }));
869 }
870 }
871 }
872 Ok(None)
873 }
874
875 pub(crate) fn to_unixusertoken(&self, ct: Duration) -> Result<UnixUserToken, OperationError> {
876 let (gidnumber, shell, sshkeys, groups) = match &self.unix_extn {
877 Some(ue) => {
878 let sshkeys: Vec<_> = self.sshkeys.values().cloned().collect();
879 (ue.gidnumber, ue.shell.clone(), sshkeys, ue.groups.clone())
880 }
881 None => {
882 return Err(OperationError::MissingClass(
883 ENTRYCLASS_POSIX_ACCOUNT.into(),
884 ));
885 }
886 };
887
888 let groups: Vec<UnixGroupToken> = groups.iter().map(|g| g.to_unixgrouptoken()).collect();
889
890 Ok(UnixUserToken {
891 name: self.name().into(),
892 spn: self.spn.clone(),
893 displayname: self.displayname.clone(),
894 gidnumber,
895 uuid: self.uuid,
896 shell: shell.clone(),
897 groups,
898 sshkeys,
899 valid: self.is_within_valid_time(ct),
900 })
901 }
902
903 pub(crate) fn oauth2_client_provider(&self) -> Option<&OAuth2AccountCredential> {
904 self.oauth2_client_provider.as_ref()
905 }
906
907 #[cfg(test)]
908 pub(crate) fn setup_oauth2_client_provider(
909 &mut self,
910 client_provider: &crate::idm::oauth2_client::OAuth2ClientProvider,
911 ) {
912 self.oauth2_client_provider = Some(OAuth2AccountCredential {
913 provider: client_provider.uuid,
914 cred_id: Uuid::new_v4(),
915 user_id: self.spn.clone(),
916 user_sub: self.uuid.to_string(),
917 });
918 }
919}
920
921pub struct DestroySessionTokenEvent {
926 pub ident: Identity,
928 pub target: Uuid,
930 pub token_id: Uuid,
932}
933
934impl DestroySessionTokenEvent {
935 #[cfg(test)]
936 pub fn new_internal(target: Uuid, token_id: Uuid) -> Self {
937 DestroySessionTokenEvent {
938 ident: Identity::from_internal(),
939 target,
940 token_id,
941 }
942 }
943}
944
945impl IdmServerProxyWriteTransaction<'_> {
946 pub fn account_destroy_session_token(
947 &mut self,
948 dte: &DestroySessionTokenEvent,
949 ) -> Result<(), OperationError> {
950 let modlist = ModifyList::new_list(vec![Modify::Removed(
952 Attribute::UserAuthTokenSession,
953 PartialValue::Refer(dte.token_id),
954 )]);
955
956 self.qs_write
957 .impersonate_modify(
958 &filter!(f_and!([
960 f_eq(Attribute::Uuid, PartialValue::Uuid(dte.target)),
961 f_eq(
962 Attribute::UserAuthTokenSession,
963 PartialValue::Refer(dte.token_id)
964 )
965 ])),
966 &filter_all!(f_and!([
968 f_eq(Attribute::Uuid, PartialValue::Uuid(dte.target)),
969 f_eq(
970 Attribute::UserAuthTokenSession,
971 PartialValue::Refer(dte.token_id)
972 )
973 ])),
974 &modlist,
975 &dte.ident.project_with_scope(AccessScope::ReadWrite),
979 )
980 .map_err(|e| {
981 admin_error!("Failed to destroy user auth token {:?}", e);
982 e
983 })
984 }
985
986 pub fn service_account_into_person(
987 &mut self,
988 ident: &Identity,
989 target_uuid: Uuid,
990 ) -> Result<(), OperationError> {
991 let schema_ref = self.qs_write.get_schema();
992
993 let account_entry = self
995 .qs_write
996 .internal_search_uuid(target_uuid)
997 .map_err(|e| {
998 admin_error!("Failed to start service account into person -> {:?}", e);
999 e
1000 })?;
1001
1002 let prev_classes: BTreeSet<_> = account_entry
1004 .get_ava_as_iutf8_iter(Attribute::Class)
1005 .ok_or_else(|| {
1006 error!(
1007 "Invalid entry, {} attribute is not present or not iutf8",
1008 Attribute::Class
1009 );
1010 OperationError::MissingAttribute(Attribute::Class)
1011 })?
1012 .collect();
1013
1014 let mut new_iutf8es = prev_classes.clone();
1017 new_iutf8es.remove(EntryClass::ServiceAccount.into());
1018 new_iutf8es.insert(EntryClass::Person.into());
1019
1020 let (_added, removed) = schema_ref
1022 .query_attrs_difference(&prev_classes, &new_iutf8es)
1023 .map_err(|se| {
1024 admin_error!("While querying the schema, it reported that requested classes may not be present indicating a possible corruption");
1025 OperationError::SchemaViolation(
1026 se
1027 )
1028 })?;
1029
1030 let mut modlist = ModifyList::new_remove(
1033 Attribute::Class,
1034 EntryClass::ServiceAccount.to_partialvalue(),
1035 );
1036 modlist.push_mod(Modify::Present(
1038 Attribute::Class,
1039 EntryClass::Person.to_value(),
1040 ));
1041 removed
1043 .into_iter()
1044 .for_each(|attr| modlist.push_mod(Modify::Purged(attr.into())));
1045 self.qs_write
1049 .impersonate_modify(
1050 &filter!(f_eq(Attribute::Uuid, PartialValue::Uuid(target_uuid))),
1052 &filter_all!(f_eq(Attribute::Uuid, PartialValue::Uuid(target_uuid))),
1054 &modlist,
1055 ident,
1057 )
1058 .map_err(|e| {
1059 admin_error!("Failed to migrate service account to person - {:?}", e);
1060 e
1061 })
1062 }
1063}
1064
1065pub struct ListUserAuthTokenEvent {
1066 pub ident: Identity,
1068 pub target: Uuid,
1070}
1071
1072impl IdmServerProxyReadTransaction<'_> {
1073 pub fn account_list_user_auth_tokens(
1074 &mut self,
1075 lte: &ListUserAuthTokenEvent,
1076 ) -> Result<Vec<UatStatus>, OperationError> {
1077 let srch = match SearchEvent::from_target_uuid_request(
1079 lte.ident.clone(),
1080 lte.target,
1081 &self.qs_read,
1082 ) {
1083 Ok(s) => s,
1084 Err(e) => {
1085 admin_error!("Failed to begin account list user auth tokens: {:?}", e);
1086 return Err(e);
1087 }
1088 };
1089
1090 match self.qs_read.search_ext(&srch) {
1091 Ok(mut entries) => {
1092 entries
1093 .pop()
1094 .and_then(|e| {
1096 let account_id = e.get_uuid();
1097 e.get_ava_as_session_map(Attribute::UserAuthTokenSession)
1099 .map(|smap| {
1100 smap.iter()
1101 .map(|(u, s)| {
1102 let state = match s.state {
1103 SessionState::ExpiresAt(odt) => {
1104 UatStatusState::ExpiresAt(odt)
1105 }
1106 SessionState::NeverExpires => {
1107 UatStatusState::NeverExpires
1108 }
1109 SessionState::RevokedAt(_) => UatStatusState::Revoked,
1110 };
1111
1112 s.scope
1113 .try_into()
1114 .map(|purpose| UatStatus {
1115 account_id,
1116 session_id: *u,
1117 state,
1118 issued_at: s.issued_at,
1119 purpose,
1120 })
1121 .inspect_err(|_e| {
1122 admin_error!("Invalid user auth token {}", u);
1123 })
1124 })
1125 .collect::<Result<Vec<_>, _>>()
1126 })
1127 })
1128 .unwrap_or_else(|| {
1129 Ok(Vec::with_capacity(0))
1131 })
1132 }
1133 Err(e) => Err(e),
1134 }
1135 }
1136}
1137
1138#[cfg(test)]
1139mod tests {
1140 use crate::idm::accountpolicy::ResolvedAccountPolicy;
1141 use crate::prelude::*;
1142 use kanidm_proto::internal::UiHint;
1143
1144 #[idm_test]
1145 async fn test_idm_account_ui_hints(idms: &IdmServer, _idms_delayed: &mut IdmServerDelayed) {
1146 let ct = duration_from_epoch_now();
1147 let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();
1148
1149 let target_uuid = Uuid::new_v4();
1150
1151 let e = entry_init!(
1154 (Attribute::Class, EntryClass::Object.to_value()),
1155 (Attribute::Class, EntryClass::Account.to_value()),
1156 (Attribute::Class, EntryClass::Person.to_value()),
1157 (Attribute::Name, Value::new_iname("testaccount")),
1158 (Attribute::Uuid, Value::Uuid(target_uuid)),
1159 (Attribute::Description, Value::new_utf8s("testaccount")),
1160 (Attribute::DisplayName, Value::new_utf8s("Test Account"))
1161 );
1162
1163 let ce = CreateEvent::new_internal(vec![e]);
1164 assert!(idms_prox_write.qs_write.create(&ce).is_ok());
1165
1166 let account = idms_prox_write
1167 .target_to_account(target_uuid)
1168 .expect("account must exist");
1169 let session_id = uuid::Uuid::new_v4();
1170 let uat = account
1171 .to_userauthtoken(
1172 session_id,
1173 SessionScope::ReadWrite,
1174 ct,
1175 &ResolvedAccountPolicy::test_policy(),
1176 )
1177 .expect("Unable to create uat");
1178
1179 assert_eq!(uat.ui_hints.len(), 1);
1181 assert!(uat.ui_hints.contains(&UiHint::CredentialUpdate));
1182
1183 let me_posix = ModifyEvent::new_internal_invalid(
1185 filter!(f_eq(
1186 Attribute::Name,
1187 PartialValue::new_iname("testaccount")
1188 )),
1189 ModifyList::new_list(vec![
1190 Modify::Present(Attribute::Class, EntryClass::PosixAccount.into()),
1191 Modify::Present(Attribute::GidNumber, Value::new_uint32(2001)),
1192 ]),
1193 );
1194 assert!(idms_prox_write.qs_write.modify(&me_posix).is_ok());
1195
1196 let account = idms_prox_write
1198 .target_to_account(target_uuid)
1199 .expect("account must exist");
1200 let session_id = uuid::Uuid::new_v4();
1201 let uat = account
1202 .to_userauthtoken(
1203 session_id,
1204 SessionScope::ReadWrite,
1205 ct,
1206 &ResolvedAccountPolicy::test_policy(),
1207 )
1208 .expect("Unable to create uat");
1209
1210 assert_eq!(uat.ui_hints.len(), 2);
1211 assert!(uat.ui_hints.contains(&UiHint::PosixAccount));
1212 assert!(uat.ui_hints.contains(&UiHint::CredentialUpdate));
1213
1214 let e = entry_init!(
1216 (Attribute::Class, EntryClass::Object.to_value()),
1217 (Attribute::Class, EntryClass::Group.to_value()),
1218 (Attribute::Name, Value::new_iname("test_uihint_group")),
1219 (Attribute::Member, Value::Refer(target_uuid)),
1220 (
1221 Attribute::GrantUiHint,
1222 Value::UiHint(UiHint::ExperimentalFeatures)
1223 )
1224 );
1225
1226 let ce = CreateEvent::new_internal(vec![e]);
1227 assert!(idms_prox_write.qs_write.create(&ce).is_ok());
1228
1229 let account = idms_prox_write
1231 .target_to_account(target_uuid)
1232 .expect("account must exist");
1233 let session_id = uuid::Uuid::new_v4();
1234 let uat = account
1235 .to_userauthtoken(
1236 session_id,
1237 SessionScope::ReadWrite,
1238 ct,
1239 &ResolvedAccountPolicy::test_policy(),
1240 )
1241 .expect("Unable to create uat");
1242
1243 assert_eq!(uat.ui_hints.len(), 3);
1244 assert!(uat.ui_hints.contains(&UiHint::PosixAccount));
1245 assert!(uat.ui_hints.contains(&UiHint::ExperimentalFeatures));
1246 assert!(uat.ui_hints.contains(&UiHint::CredentialUpdate));
1247
1248 assert!(idms_prox_write.commit().is_ok());
1249 }
1250}