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