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 softlock_expire(&self) -> Option<OffsetDateTime> {
290 self.softlock_expire
291 }
292
293 #[instrument(level = "trace", skip_all)]
294 pub(crate) fn try_from_entry_ro(
295 value: &Entry<EntrySealed, EntryCommitted>,
296 qs: &mut QueryServerReadTransaction,
297 ) -> Result<Self, OperationError> {
298 let (groups, unix_groups) = load_all_groups_from_account(value, qs)?;
299
300 try_from_entry!(value, groups, unix_groups)
301 }
302
303 #[instrument(level = "trace", skip_all)]
304 pub(crate) fn try_from_entry_with_policy<'a, TXN>(
305 value: &Entry<EntrySealed, EntryCommitted>,
306 qs: &mut TXN,
307 ) -> Result<(Self, ResolvedAccountPolicy), OperationError>
308 where
309 TXN: QueryServerTransaction<'a>,
310 {
311 let (groups, unix_groups) = load_all_groups_from_account(value, qs)?;
312 let rap = load_account_policy(value, qs)?;
313
314 try_from_entry!(value, groups, unix_groups).map(|acct| (acct, rap))
315 }
316
317 #[instrument(level = "trace", skip_all)]
318 pub(crate) fn try_from_entry_rw(
319 value: &Entry<EntrySealed, EntryCommitted>,
320 qs: &mut QueryServerWriteTransaction,
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_reduced(
329 value: &Entry<EntryReduced, EntryCommitted>,
330 qs: &mut QueryServerReadTransaction,
331 ) -> Result<Self, OperationError> {
332 let (groups, unix_groups) = load_all_groups_from_account(value, qs)?;
333 try_from_entry!(value, groups, unix_groups)
334 }
335
336 pub(crate) fn to_userauthtoken(
341 &self,
342 session_id: Uuid,
343 scope: SessionScope,
344 ct: Duration,
345 account_policy: &ResolvedAccountPolicy,
346 ) -> Option<UserAuthToken> {
347 let ct = ct - Duration::from_nanos(ct.subsec_nanos() as u64);
351 let issued_at = OffsetDateTime::UNIX_EPOCH + ct;
352
353 let limit_search_max_results = account_policy.limit_search_max_results();
354 let limit_search_max_filter_test = account_policy.limit_search_max_filter_test();
355
356 let expiry = OffsetDateTime::UNIX_EPOCH
359 + ct
360 + Duration::from_secs(account_policy.authsession_expiry() as u64);
361 let limited_expiry = OffsetDateTime::UNIX_EPOCH
362 + ct
363 + Duration::from_secs(DEFAULT_AUTH_SESSION_LIMITED_EXPIRY as u64);
364
365 let (purpose, expiry) = match scope {
366 SessionScope::Synchronise => {
368 warn!(
369 "Should be impossible to issue sync sessions with a uat. Refusing to proceed."
370 );
371 return None;
372 }
373 SessionScope::ReadOnly => (UatPurpose::ReadOnly, expiry),
374 SessionScope::ReadWrite => {
375 let capped = std::cmp::min(expiry, limited_expiry);
378
379 (
380 UatPurpose::ReadWrite {
381 expiry: Some(capped),
382 },
383 capped,
384 )
385 }
386 SessionScope::PrivilegeCapable => (UatPurpose::ReadWrite { expiry: None }, expiry),
387 };
388
389 Some(UserAuthToken {
390 session_id,
391 expiry: Some(expiry),
392 issued_at,
393 purpose,
394 uuid: self.uuid,
395 displayname: self.displayname.clone(),
396 spn: self.spn.clone(),
397 mail_primary: self.mail_primary.clone(),
398 ui_hints: self.ui_hints.clone(),
399 limit_search_max_results,
402 limit_search_max_filter_test,
403 })
404 }
405
406 pub(crate) fn to_reissue_userauthtoken(
410 &self,
411 session_id: Uuid,
412 session_expiry: Option<OffsetDateTime>,
413 scope: SessionScope,
414 ct: Duration,
415 account_policy: &ResolvedAccountPolicy,
416 ) -> Option<UserAuthToken> {
417 let issued_at = OffsetDateTime::UNIX_EPOCH + ct;
418
419 let limit_search_max_results = account_policy.limit_search_max_results();
420 let limit_search_max_filter_test = account_policy.limit_search_max_filter_test();
421
422 let (purpose, expiry) = match scope {
423 SessionScope::Synchronise | SessionScope::ReadOnly | SessionScope::ReadWrite => {
424 warn!(
425 "Impossible state, should not be re-issuing for session scope {:?}",
426 scope
427 );
428 return None;
429 }
430 SessionScope::PrivilegeCapable =>
431 {
433 let expiry = Some(
434 OffsetDateTime::UNIX_EPOCH
435 + ct
436 + Duration::from_secs(account_policy.privilege_expiry().into()),
437 );
438 (
439 UatPurpose::ReadWrite { expiry },
440 session_expiry,
444 )
445 }
446 };
447
448 Some(UserAuthToken {
449 session_id,
450 expiry,
451 issued_at,
452 purpose,
453 uuid: self.uuid,
454 displayname: self.displayname.clone(),
455 spn: self.spn.clone(),
456 mail_primary: self.mail_primary.clone(),
457 ui_hints: self.ui_hints.clone(),
458 limit_search_max_results,
461 limit_search_max_filter_test,
462 })
463 }
464
465 pub(crate) fn client_cert_info_to_userauthtoken(
468 &self,
469 certificate_id: Uuid,
470 session_is_rw: bool,
471 ct: Duration,
472 account_policy: &ResolvedAccountPolicy,
473 ) -> Option<UserAuthToken> {
474 let issued_at = OffsetDateTime::UNIX_EPOCH + ct;
475
476 let limit_search_max_results = account_policy.limit_search_max_results();
477 let limit_search_max_filter_test = account_policy.limit_search_max_filter_test();
478
479 let purpose = if session_is_rw {
480 UatPurpose::ReadWrite { expiry: None }
481 } else {
482 UatPurpose::ReadOnly
483 };
484
485 Some(UserAuthToken {
486 session_id: certificate_id,
487 expiry: None,
488 issued_at,
489 purpose,
490 uuid: self.uuid,
491 displayname: self.displayname.clone(),
492 spn: self.spn.clone(),
493 mail_primary: self.mail_primary.clone(),
494 ui_hints: self.ui_hints.clone(),
495 limit_search_max_results,
498 limit_search_max_filter_test,
499 })
500 }
501
502 pub fn check_within_valid_time(
505 ct: Duration,
506 valid_from: Option<&OffsetDateTime>,
507 expire: Option<&OffsetDateTime>,
508 ) -> bool {
509 let cot = OffsetDateTime::UNIX_EPOCH + ct;
510 trace!("Checking within valid time: {:?} {:?}", valid_from, expire);
511
512 let vmin = if let Some(vft) = valid_from {
513 vft <= &cot
515 } else {
516 true
518 };
519 let vmax = if let Some(ext) = expire {
520 &cot <= ext
522 } else {
523 true
525 };
526 vmin && vmax
528 }
529
530 pub fn is_within_valid_time(&self, ct: Duration) -> bool {
533 Self::check_within_valid_time(ct, self.valid_from.as_ref(), self.expire.as_ref())
534 }
535
536 pub fn related_inputs(&self) -> Vec<&str> {
539 let mut inputs = Vec::with_capacity(4 + self.mail.len());
540 self.mail.iter().for_each(|m| {
541 inputs.push(m.as_str());
542 });
543 inputs.push(self.spn.as_str());
544 if let Some(name) = self.name.as_ref() {
545 inputs.push(name)
546 }
547 inputs.push(self.displayname.as_str());
548 if let Some(s) = self.radius_secret.as_deref() {
549 inputs.push(s);
550 }
551 inputs
552 }
553
554 pub fn primary_cred_uuid_and_policy(&self) -> Option<(Uuid, CredSoftLockPolicy)> {
555 self.primary
556 .as_ref()
557 .map(|cred| (cred.uuid, cred.softlock_policy()))
558 .or_else(|| {
559 if self.is_anonymous() {
560 Some((UUID_ANONYMOUS, CredSoftLockPolicy::Unrestricted))
561 } else {
562 None
563 }
564 })
565 }
566
567 pub fn is_anonymous(&self) -> bool {
568 self.uuid == UUID_ANONYMOUS
569 }
570
571 #[cfg(test)]
572 pub(crate) fn gen_password_mod(
573 &self,
574 cleartext: &str,
575 crypto_policy: &CryptoPolicy,
576 ) -> Result<ModifyList<ModifyInvalid>, OperationError> {
577 match &self.primary {
578 Some(primary) => {
580 let ncred = primary.set_password(crypto_policy, cleartext)?;
581 let vcred = Value::new_credential("primary", ncred);
582 Ok(ModifyList::new_purge_and_set(
583 Attribute::PrimaryCredential,
584 vcred,
585 ))
586 }
587 None => {
589 let ncred = Credential::new_password_only(crypto_policy, cleartext)?;
590 let vcred = Value::new_credential("primary", ncred);
591 Ok(ModifyList::new_purge_and_set(
592 Attribute::PrimaryCredential,
593 vcred,
594 ))
595 }
596 }
597 }
598
599 pub(crate) fn gen_password_upgrade_mod(
600 &self,
601 cleartext: &str,
602 crypto_policy: &CryptoPolicy,
603 ) -> Result<Option<ModifyList<ModifyInvalid>>, OperationError> {
604 match &self.primary {
605 Some(primary) => {
607 if let Some(ncred) = primary.upgrade_password(crypto_policy, cleartext)? {
608 let vcred = Value::new_credential("primary", ncred);
609 Ok(Some(ModifyList::new_purge_and_set(
610 Attribute::PrimaryCredential,
611 vcred,
612 )))
613 } else {
614 Ok(None)
616 }
617 }
618 None => Ok(None),
620 }
621 }
622
623 pub(crate) fn gen_webauthn_counter_mod(
624 &mut self,
625 auth_result: &AuthenticationResult,
626 ) -> Result<Option<ModifyList<ModifyInvalid>>, OperationError> {
627 let mut ml = Vec::with_capacity(2);
628 let opt_ncred = match self.primary.as_ref() {
630 Some(primary) => primary.update_webauthn_properties(auth_result)?,
631 None => None,
632 };
633
634 if let Some(ncred) = opt_ncred {
635 let vcred = Value::new_credential("primary", ncred);
636 ml.push(Modify::Purged(Attribute::PrimaryCredential));
637 ml.push(Modify::Present(Attribute::PrimaryCredential, vcred));
638 }
639
640 self.passkeys.iter_mut().for_each(|(u, (t, k))| {
642 if let Some(true) = k.update_credential(auth_result) {
643 ml.push(Modify::Removed(
644 Attribute::PassKeys,
645 PartialValue::Passkey(*u),
646 ));
647
648 ml.push(Modify::Present(
649 Attribute::PassKeys,
650 Value::Passkey(*u, t.clone(), k.clone()),
651 ));
652 }
653 });
654
655 self.attested_passkeys.iter_mut().for_each(|(u, (t, k))| {
657 if let Some(true) = k.update_credential(auth_result) {
658 ml.push(Modify::Removed(
659 Attribute::AttestedPasskeys,
660 PartialValue::AttestedPasskey(*u),
661 ));
662
663 ml.push(Modify::Present(
664 Attribute::AttestedPasskeys,
665 Value::AttestedPasskey(*u, t.clone(), k.clone()),
666 ));
667 }
668 });
669
670 if ml.is_empty() {
671 Ok(None)
672 } else {
673 Ok(Some(ModifyList::new_list(ml)))
674 }
675 }
676
677 pub(crate) fn invalidate_backup_code_mod(
678 self,
679 code_to_remove: &str,
680 ) -> Result<ModifyList<ModifyInvalid>, OperationError> {
681 match self.primary {
682 Some(primary) => {
684 let r_ncred = primary.invalidate_backup_code(code_to_remove);
685 match r_ncred {
686 Ok(ncred) => {
687 let vcred = Value::new_credential("primary", ncred);
688 Ok(ModifyList::new_purge_and_set(
689 Attribute::PrimaryCredential,
690 vcred,
691 ))
692 }
693 Err(e) => Err(e),
694 }
695 }
696 None => {
697 Err(OperationError::InvalidState)
699 }
700 }
701 }
702
703 pub(crate) fn regenerate_radius_secret_mod(
704 &self,
705 cleartext: &str,
706 ) -> Result<ModifyList<ModifyInvalid>, OperationError> {
707 let vcred = Value::new_secret_str(cleartext);
708 Ok(ModifyList::new_purge_and_set(
709 Attribute::RadiusSecret,
710 vcred,
711 ))
712 }
713
714 pub(crate) fn to_credentialstatus(&self) -> Result<CredentialStatus, OperationError> {
715 self.primary
718 .as_ref()
719 .map(|cred| CredentialStatus {
720 creds: vec![cred.into()],
721 })
722 .ok_or(OperationError::NoMatchingAttributes)
723 }
724
725 pub(crate) fn existing_credential_id_list(&self) -> Option<Vec<CredentialID>> {
726 None
729 }
730
731 pub(crate) fn check_user_auth_token_valid(
732 ct: Duration,
733 uat: &UserAuthToken,
734 entry: &Entry<EntrySealed, EntryCommitted>,
735 ) -> bool {
736 let within_valid_window = Account::check_within_valid_time(
741 ct,
742 entry
743 .get_ava_single_datetime(Attribute::AccountValidFrom)
744 .as_ref(),
745 entry
746 .get_ava_single_datetime(Attribute::AccountExpire)
747 .as_ref(),
748 );
749
750 if !within_valid_window {
751 security_info!("Account has expired or is not yet valid, not allowing to proceed");
752 return false;
753 }
754
755 trace!("{}", &uat);
758
759 if uat.uuid == UUID_ANONYMOUS {
760 security_debug!("Anonymous sessions do not have session records, session is valid.");
761 true
762 } else {
763 let session_present = entry
765 .get_ava_as_session_map(Attribute::UserAuthTokenSession)
766 .and_then(|session_map| session_map.get(&uat.session_id));
767
768 if let Some(session) = session_present {
772 match (&session.state, &uat.expiry) {
773 (SessionState::ExpiresAt(s_exp), Some(u_exp)) if s_exp == u_exp => {
774 security_info!("A valid limited session value exists for this token");
775 true
776 }
777 (SessionState::NeverExpires, None) => {
778 security_info!("A valid unbound session value exists for this token");
779 true
780 }
781 (SessionState::RevokedAt(_), _) => {
782 security_info!("Session has been revoked");
785 false
786 }
787 _ => {
788 security_info!("Session and uat expiry are not consistent, rejecting.");
789 debug!(ses_st = ?session.state, uat_exp = ?uat.expiry);
790 false
791 }
792 }
793 } else {
794 let grace = uat.issued_at + AUTH_TOKEN_GRACE_WINDOW;
795 let current = time::OffsetDateTime::UNIX_EPOCH + ct;
796 trace!(%grace, %current);
797 if current >= grace {
798 security_info!(
799 "The token grace window has passed, and no session exists. Assuming invalid."
800 );
801 false
802 } else {
803 security_info!("The token grace window is in effect. Assuming valid.");
804 true
805 }
806 }
807 }
808 }
809
810 pub(crate) fn verify_application_password(
811 &self,
812 application: &Application,
813 cleartext: &str,
814 ) -> Result<Option<LdapBoundToken>, OperationError> {
815 if let Some(v) = self.apps_pwds.get(&application.uuid) {
816 for ap in v.iter() {
817 let password_verified = ap.password.verify(cleartext).map_err(|e| {
818 error!(crypto_err = ?e);
819 OperationError::CryptographyError
820 })?;
821
822 if password_verified {
823 let session_id = uuid::Uuid::new_v4();
824 security_info!(
825 "Starting session {} for {} {}",
826 session_id,
827 self.spn,
828 self.uuid
829 );
830
831 return Ok(Some(LdapBoundToken {
832 spn: self.spn.clone(),
833 session_id,
834 effective_session: LdapSession::ApplicationPasswordBind(
835 application.uuid,
836 self.uuid,
837 ),
838 }));
839 }
840 }
841 }
842 Ok(None)
843 }
844
845 pub(crate) fn to_unixusertoken(&self, ct: Duration) -> Result<UnixUserToken, OperationError> {
846 let (gidnumber, shell, sshkeys, groups) = match &self.unix_extn {
847 Some(ue) => {
848 let sshkeys: Vec<_> = self.sshkeys.values().cloned().collect();
849 (ue.gidnumber, ue.shell.clone(), sshkeys, ue.groups.clone())
850 }
851 None => {
852 return Err(OperationError::MissingClass(
853 ENTRYCLASS_POSIX_ACCOUNT.into(),
854 ));
855 }
856 };
857
858 let groups: Vec<UnixGroupToken> = groups.iter().map(|g| g.to_unixgrouptoken()).collect();
859
860 Ok(UnixUserToken {
861 name: self.name().into(),
862 spn: self.spn.clone(),
863 displayname: self.displayname.clone(),
864 gidnumber,
865 uuid: self.uuid,
866 shell: shell.clone(),
867 groups,
868 sshkeys,
869 valid: self.is_within_valid_time(ct),
870 })
871 }
872
873 pub(crate) fn oauth2_client_provider(&self) -> Option<&OAuth2AccountCredential> {
874 self.oauth2_client_provider.as_ref()
875 }
876
877 #[cfg(test)]
878 pub(crate) fn setup_oauth2_client_provider(
879 &mut self,
880 client_provider: &crate::idm::oauth2_client::OAuth2ClientProvider,
881 ) {
882 self.oauth2_client_provider = Some(OAuth2AccountCredential {
883 provider: client_provider.uuid,
884 cred_id: Uuid::new_v4(),
885 user_id: self.spn.clone(),
886 });
887 }
888}
889
890pub struct DestroySessionTokenEvent {
895 pub ident: Identity,
897 pub target: Uuid,
899 pub token_id: Uuid,
901}
902
903impl DestroySessionTokenEvent {
904 #[cfg(test)]
905 pub fn new_internal(target: Uuid, token_id: Uuid) -> Self {
906 DestroySessionTokenEvent {
907 ident: Identity::from_internal(),
908 target,
909 token_id,
910 }
911 }
912}
913
914impl IdmServerProxyWriteTransaction<'_> {
915 pub fn account_destroy_session_token(
916 &mut self,
917 dte: &DestroySessionTokenEvent,
918 ) -> Result<(), OperationError> {
919 let modlist = ModifyList::new_list(vec![Modify::Removed(
921 Attribute::UserAuthTokenSession,
922 PartialValue::Refer(dte.token_id),
923 )]);
924
925 self.qs_write
926 .impersonate_modify(
927 &filter!(f_and!([
929 f_eq(Attribute::Uuid, PartialValue::Uuid(dte.target)),
930 f_eq(
931 Attribute::UserAuthTokenSession,
932 PartialValue::Refer(dte.token_id)
933 )
934 ])),
935 &filter_all!(f_and!([
937 f_eq(Attribute::Uuid, PartialValue::Uuid(dte.target)),
938 f_eq(
939 Attribute::UserAuthTokenSession,
940 PartialValue::Refer(dte.token_id)
941 )
942 ])),
943 &modlist,
944 &dte.ident.project_with_scope(AccessScope::ReadWrite),
948 )
949 .map_err(|e| {
950 admin_error!("Failed to destroy user auth token {:?}", e);
951 e
952 })
953 }
954
955 pub fn service_account_into_person(
956 &mut self,
957 ident: &Identity,
958 target_uuid: Uuid,
959 ) -> Result<(), OperationError> {
960 let schema_ref = self.qs_write.get_schema();
961
962 let account_entry = self
964 .qs_write
965 .internal_search_uuid(target_uuid)
966 .map_err(|e| {
967 admin_error!("Failed to start service account into person -> {:?}", e);
968 e
969 })?;
970
971 let prev_classes: BTreeSet<_> = account_entry
973 .get_ava_as_iutf8_iter(Attribute::Class)
974 .ok_or_else(|| {
975 error!(
976 "Invalid entry, {} attribute is not present or not iutf8",
977 Attribute::Class
978 );
979 OperationError::MissingAttribute(Attribute::Class)
980 })?
981 .collect();
982
983 let mut new_iutf8es = prev_classes.clone();
986 new_iutf8es.remove(EntryClass::ServiceAccount.into());
987 new_iutf8es.insert(EntryClass::Person.into());
988
989 let (_added, removed) = schema_ref
991 .query_attrs_difference(&prev_classes, &new_iutf8es)
992 .map_err(|se| {
993 admin_error!("While querying the schema, it reported that requested classes may not be present indicating a possible corruption");
994 OperationError::SchemaViolation(
995 se
996 )
997 })?;
998
999 let mut modlist = ModifyList::new_remove(
1002 Attribute::Class,
1003 EntryClass::ServiceAccount.to_partialvalue(),
1004 );
1005 modlist.push_mod(Modify::Present(
1007 Attribute::Class,
1008 EntryClass::Person.to_value(),
1009 ));
1010 removed
1012 .into_iter()
1013 .for_each(|attr| modlist.push_mod(Modify::Purged(attr.into())));
1014 self.qs_write
1018 .impersonate_modify(
1019 &filter!(f_eq(Attribute::Uuid, PartialValue::Uuid(target_uuid))),
1021 &filter_all!(f_eq(Attribute::Uuid, PartialValue::Uuid(target_uuid))),
1023 &modlist,
1024 ident,
1026 )
1027 .map_err(|e| {
1028 admin_error!("Failed to migrate service account to person - {:?}", e);
1029 e
1030 })
1031 }
1032}
1033
1034pub struct ListUserAuthTokenEvent {
1035 pub ident: Identity,
1037 pub target: Uuid,
1039}
1040
1041impl IdmServerProxyReadTransaction<'_> {
1042 pub fn account_list_user_auth_tokens(
1043 &mut self,
1044 lte: &ListUserAuthTokenEvent,
1045 ) -> Result<Vec<UatStatus>, OperationError> {
1046 let srch = match SearchEvent::from_target_uuid_request(
1048 lte.ident.clone(),
1049 lte.target,
1050 &self.qs_read,
1051 ) {
1052 Ok(s) => s,
1053 Err(e) => {
1054 admin_error!("Failed to begin account list user auth tokens: {:?}", e);
1055 return Err(e);
1056 }
1057 };
1058
1059 match self.qs_read.search_ext(&srch) {
1060 Ok(mut entries) => {
1061 entries
1062 .pop()
1063 .and_then(|e| {
1065 let account_id = e.get_uuid();
1066 e.get_ava_as_session_map(Attribute::UserAuthTokenSession)
1068 .map(|smap| {
1069 smap.iter()
1070 .map(|(u, s)| {
1071 let state = match s.state {
1072 SessionState::ExpiresAt(odt) => {
1073 UatStatusState::ExpiresAt(odt)
1074 }
1075 SessionState::NeverExpires => {
1076 UatStatusState::NeverExpires
1077 }
1078 SessionState::RevokedAt(_) => UatStatusState::Revoked,
1079 };
1080
1081 s.scope
1082 .try_into()
1083 .map(|purpose| UatStatus {
1084 account_id,
1085 session_id: *u,
1086 state,
1087 issued_at: s.issued_at,
1088 purpose,
1089 })
1090 .inspect_err(|_e| {
1091 admin_error!("Invalid user auth token {}", u);
1092 })
1093 })
1094 .collect::<Result<Vec<_>, _>>()
1095 })
1096 })
1097 .unwrap_or_else(|| {
1098 Ok(Vec::with_capacity(0))
1100 })
1101 }
1102 Err(e) => Err(e),
1103 }
1104 }
1105}
1106
1107#[cfg(test)]
1108mod tests {
1109 use crate::idm::accountpolicy::ResolvedAccountPolicy;
1110 use crate::prelude::*;
1111 use kanidm_proto::internal::UiHint;
1112
1113 #[idm_test]
1114 async fn test_idm_account_ui_hints(idms: &IdmServer, _idms_delayed: &mut IdmServerDelayed) {
1115 let ct = duration_from_epoch_now();
1116 let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();
1117
1118 let target_uuid = Uuid::new_v4();
1119
1120 let e = entry_init!(
1123 (Attribute::Class, EntryClass::Object.to_value()),
1124 (Attribute::Class, EntryClass::Account.to_value()),
1125 (Attribute::Class, EntryClass::Person.to_value()),
1126 (Attribute::Name, Value::new_iname("testaccount")),
1127 (Attribute::Uuid, Value::Uuid(target_uuid)),
1128 (Attribute::Description, Value::new_utf8s("testaccount")),
1129 (Attribute::DisplayName, Value::new_utf8s("Test Account"))
1130 );
1131
1132 let ce = CreateEvent::new_internal(vec![e]);
1133 assert!(idms_prox_write.qs_write.create(&ce).is_ok());
1134
1135 let account = idms_prox_write
1136 .target_to_account(target_uuid)
1137 .expect("account must exist");
1138 let session_id = uuid::Uuid::new_v4();
1139 let uat = account
1140 .to_userauthtoken(
1141 session_id,
1142 SessionScope::ReadWrite,
1143 ct,
1144 &ResolvedAccountPolicy::test_policy(),
1145 )
1146 .expect("Unable to create uat");
1147
1148 assert_eq!(uat.ui_hints.len(), 1);
1150 assert!(uat.ui_hints.contains(&UiHint::CredentialUpdate));
1151
1152 let me_posix = ModifyEvent::new_internal_invalid(
1154 filter!(f_eq(
1155 Attribute::Name,
1156 PartialValue::new_iname("testaccount")
1157 )),
1158 ModifyList::new_list(vec![
1159 Modify::Present(Attribute::Class, EntryClass::PosixAccount.into()),
1160 Modify::Present(Attribute::GidNumber, Value::new_uint32(2001)),
1161 ]),
1162 );
1163 assert!(idms_prox_write.qs_write.modify(&me_posix).is_ok());
1164
1165 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(), 2);
1180 assert!(uat.ui_hints.contains(&UiHint::PosixAccount));
1181 assert!(uat.ui_hints.contains(&UiHint::CredentialUpdate));
1182
1183 let e = entry_init!(
1185 (Attribute::Class, EntryClass::Object.to_value()),
1186 (Attribute::Class, EntryClass::Group.to_value()),
1187 (Attribute::Name, Value::new_iname("test_uihint_group")),
1188 (Attribute::Member, Value::Refer(target_uuid)),
1189 (
1190 Attribute::GrantUiHint,
1191 Value::UiHint(UiHint::ExperimentalFeatures)
1192 )
1193 );
1194
1195 let ce = CreateEvent::new_internal(vec![e]);
1196 assert!(idms_prox_write.qs_write.create(&ce).is_ok());
1197
1198 let account = idms_prox_write
1200 .target_to_account(target_uuid)
1201 .expect("account must exist");
1202 let session_id = uuid::Uuid::new_v4();
1203 let uat = account
1204 .to_userauthtoken(
1205 session_id,
1206 SessionScope::ReadWrite,
1207 ct,
1208 &ResolvedAccountPolicy::test_policy(),
1209 )
1210 .expect("Unable to create uat");
1211
1212 assert_eq!(uat.ui_hints.len(), 3);
1213 assert!(uat.ui_hints.contains(&UiHint::PosixAccount));
1214 assert!(uat.ui_hints.contains(&UiHint::ExperimentalFeatures));
1215 assert!(uat.ui_hints.contains(&UiHint::CredentialUpdate));
1216
1217 assert!(idms_prox_write.commit().is_ok());
1218 }
1219}