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 ct: Duration,
427 account_policy: &ResolvedAccountPolicy,
428 ) -> Option<UserAuthToken> {
429 let issued_at = OffsetDateTime::UNIX_EPOCH + ct;
430
431 let limit_search_max_results = account_policy.limit_search_max_results();
432 let limit_search_max_filter_test = account_policy.limit_search_max_filter_test();
433
434 let (purpose, expiry) = match scope {
435 SessionScope::Synchronise | SessionScope::ReadOnly | SessionScope::ReadWrite => {
436 warn!(
437 "Impossible state, should not be re-issuing for session scope {:?}",
438 scope
439 );
440 return None;
441 }
442 SessionScope::PrivilegeCapable =>
443 {
445 let expiry = Some(
446 OffsetDateTime::UNIX_EPOCH
447 + ct
448 + Duration::from_secs(account_policy.privilege_expiry().into()),
449 );
450 (
451 UatPurpose::ReadWrite { expiry },
452 session_expiry,
456 )
457 }
458 };
459
460 Some(UserAuthToken {
461 session_id,
462 expiry,
463 issued_at,
464 purpose,
465 uuid: self.uuid,
466 displayname: self.displayname.clone(),
467 spn: self.spn.clone(),
468 mail_primary: self.mail_primary.clone(),
469 ui_hints: self.ui_hints.clone(),
470 limit_search_max_results,
473 limit_search_max_filter_test,
474 })
475 }
476
477 pub(crate) fn client_cert_info_to_userauthtoken(
480 &self,
481 certificate_id: Uuid,
482 session_is_rw: bool,
483 ct: Duration,
484 account_policy: &ResolvedAccountPolicy,
485 ) -> Option<UserAuthToken> {
486 let issued_at = OffsetDateTime::UNIX_EPOCH + ct;
487
488 let limit_search_max_results = account_policy.limit_search_max_results();
489 let limit_search_max_filter_test = account_policy.limit_search_max_filter_test();
490
491 let purpose = if session_is_rw {
492 UatPurpose::ReadWrite { expiry: None }
493 } else {
494 UatPurpose::ReadOnly
495 };
496
497 Some(UserAuthToken {
498 session_id: certificate_id,
499 expiry: None,
500 issued_at,
501 purpose,
502 uuid: self.uuid,
503 displayname: self.displayname.clone(),
504 spn: self.spn.clone(),
505 mail_primary: self.mail_primary.clone(),
506 ui_hints: self.ui_hints.clone(),
507 limit_search_max_results,
510 limit_search_max_filter_test,
511 })
512 }
513
514 pub fn check_within_valid_time(
517 ct: Duration,
518 valid_from: Option<&OffsetDateTime>,
519 expire: Option<&OffsetDateTime>,
520 ) -> bool {
521 let cot = OffsetDateTime::UNIX_EPOCH + ct;
522 trace!("Checking within valid time: {:?} {:?}", valid_from, expire);
523
524 let vmin = if let Some(vft) = valid_from {
525 vft <= &cot
527 } else {
528 true
530 };
531 let vmax = if let Some(ext) = expire {
532 &cot <= ext
534 } else {
535 true
537 };
538 vmin && vmax
540 }
541
542 pub fn is_within_valid_time(&self, ct: Duration) -> bool {
545 Self::check_within_valid_time(ct, self.valid_from.as_ref(), self.expire.as_ref())
546 }
547
548 pub fn related_inputs(&self) -> Vec<&str> {
551 let mut inputs = Vec::with_capacity(4 + self.mail.len());
552 self.mail.iter().for_each(|m| {
553 inputs.push(m.as_str());
554 });
555 inputs.push(self.spn.as_str());
556 if let Some(name) = self.name.as_ref() {
557 inputs.push(name)
558 }
559 inputs.push(self.displayname.as_str());
560 if let Some(s) = self.radius_secret.as_deref() {
561 inputs.push(s);
562 }
563 inputs
564 }
565
566 pub fn primary_cred_uuid_and_policy(&self) -> Option<(Uuid, CredSoftLockPolicy)> {
567 self.primary
568 .as_ref()
569 .map(|cred| (cred.uuid, cred.softlock_policy()))
570 .or_else(|| {
571 if self.is_anonymous() {
572 Some((UUID_ANONYMOUS, CredSoftLockPolicy::Unrestricted))
573 } else {
574 None
575 }
576 })
577 }
578
579 pub fn is_anonymous(&self) -> bool {
580 self.uuid == UUID_ANONYMOUS
581 }
582
583 #[cfg(test)]
584 pub(crate) fn gen_password_mod(
585 &self,
586 cleartext: &str,
587 crypto_policy: &CryptoPolicy,
588 ct: OffsetDateTime,
589 ) -> Result<ModifyList<ModifyInvalid>, OperationError> {
590 match &self.primary {
591 Some(primary) => {
593 let ncred = primary.set_password(crypto_policy, cleartext, ct)?;
594 let vcred = Value::new_credential("primary", ncred);
595 Ok(ModifyList::new_purge_and_set(
596 Attribute::PrimaryCredential,
597 vcred,
598 ))
599 }
600 None => {
602 let ncred = Credential::new_password_only(
603 crypto_policy,
604 cleartext,
605 OffsetDateTime::UNIX_EPOCH,
606 )?;
607 let vcred = Value::new_credential("primary", ncred);
608 Ok(ModifyList::new_purge_and_set(
609 Attribute::PrimaryCredential,
610 vcred,
611 ))
612 }
613 }
614 }
615
616 pub(crate) fn gen_password_upgrade_mod(
617 &self,
618 cleartext: &str,
619 crypto_policy: &CryptoPolicy,
620 ) -> Result<Option<ModifyList<ModifyInvalid>>, OperationError> {
621 match &self.primary {
622 Some(primary) => {
624 if let Some(ncred) = primary.upgrade_password(crypto_policy, cleartext)? {
625 let vcred = Value::new_credential("primary", ncred);
626 Ok(Some(ModifyList::new_purge_and_set(
627 Attribute::PrimaryCredential,
628 vcred,
629 )))
630 } else {
631 Ok(None)
633 }
634 }
635 None => Ok(None),
637 }
638 }
639
640 pub(crate) fn gen_webauthn_counter_mod(
641 &mut self,
642 auth_result: &AuthenticationResult,
643 ) -> Result<Option<ModifyList<ModifyInvalid>>, OperationError> {
644 let mut ml = Vec::with_capacity(2);
645 let opt_ncred = match self.primary.as_ref() {
647 Some(primary) => primary.update_webauthn_properties(auth_result)?,
648 None => None,
649 };
650
651 if let Some(ncred) = opt_ncred {
652 let vcred = Value::new_credential("primary", ncred);
653 ml.push(Modify::Purged(Attribute::PrimaryCredential));
654 ml.push(Modify::Present(Attribute::PrimaryCredential, vcred));
655 }
656
657 self.passkeys.iter_mut().for_each(|(u, (t, k))| {
659 if let Some(true) = k.update_credential(auth_result) {
660 ml.push(Modify::Removed(
661 Attribute::PassKeys,
662 PartialValue::Passkey(*u),
663 ));
664
665 ml.push(Modify::Present(
666 Attribute::PassKeys,
667 Value::Passkey(*u, t.clone(), k.clone()),
668 ));
669 }
670 });
671
672 self.attested_passkeys.iter_mut().for_each(|(u, (t, k))| {
674 if let Some(true) = k.update_credential(auth_result) {
675 ml.push(Modify::Removed(
676 Attribute::AttestedPasskeys,
677 PartialValue::AttestedPasskey(*u),
678 ));
679
680 ml.push(Modify::Present(
681 Attribute::AttestedPasskeys,
682 Value::AttestedPasskey(*u, t.clone(), k.clone()),
683 ));
684 }
685 });
686
687 if ml.is_empty() {
688 Ok(None)
689 } else {
690 Ok(Some(ModifyList::new_list(ml)))
691 }
692 }
693
694 pub(crate) fn invalidate_backup_code_mod(
695 self,
696 code_to_remove: &str,
697 ) -> Result<ModifyList<ModifyInvalid>, OperationError> {
698 match self.primary {
699 Some(primary) => {
701 let r_ncred = primary.invalidate_backup_code(code_to_remove);
702 match r_ncred {
703 Ok(ncred) => {
704 let vcred = Value::new_credential("primary", ncred);
705 Ok(ModifyList::new_purge_and_set(
706 Attribute::PrimaryCredential,
707 vcred,
708 ))
709 }
710 Err(e) => Err(e),
711 }
712 }
713 None => {
714 Err(OperationError::InvalidState)
716 }
717 }
718 }
719
720 pub(crate) fn regenerate_radius_secret_mod(
721 &self,
722 cleartext: &str,
723 ) -> Result<ModifyList<ModifyInvalid>, OperationError> {
724 let vcred = Value::new_secret_str(cleartext);
725 Ok(ModifyList::new_purge_and_set(
726 Attribute::RadiusSecret,
727 vcred,
728 ))
729 }
730
731 pub(crate) fn to_credentialstatus(&self) -> Result<CredentialStatus, OperationError> {
732 self.primary
735 .as_ref()
736 .map(|cred| CredentialStatus {
737 creds: vec![cred.into()],
738 })
739 .ok_or(OperationError::NoMatchingAttributes)
740 }
741
742 pub(crate) fn existing_credential_id_list(&self) -> Option<Vec<CredentialID>> {
743 None
746 }
747
748 pub(crate) fn check_user_auth_token_valid(
749 ct: Duration,
750 uat: &UserAuthToken,
751 entry: &Entry<EntrySealed, EntryCommitted>,
752 ) -> bool {
753 let within_valid_window = Account::check_within_valid_time(
758 ct,
759 entry
760 .get_ava_single_datetime(Attribute::AccountValidFrom)
761 .as_ref(),
762 entry
763 .get_ava_single_datetime(Attribute::AccountExpire)
764 .as_ref(),
765 );
766
767 if !within_valid_window {
768 security_info!("Account has expired or is not yet valid, not allowing to proceed");
769 return false;
770 }
771
772 trace!("{}", &uat);
775
776 if uat.uuid == UUID_ANONYMOUS {
777 security_debug!("Anonymous sessions do not have session records, session is valid.");
778 true
779 } else {
780 let session_present = entry
782 .get_ava_as_session_map(Attribute::UserAuthTokenSession)
783 .and_then(|session_map| session_map.get(&uat.session_id));
784
785 if let Some(session) = session_present {
789 match (&session.state, &uat.expiry) {
790 (SessionState::ExpiresAt(s_exp), Some(u_exp)) if s_exp == u_exp => {
791 security_info!("A valid limited session value exists for this token");
792 true
793 }
794 (SessionState::NeverExpires, None) => {
795 security_info!("A valid unbound session value exists for this token");
796 true
797 }
798 (SessionState::RevokedAt(_), _) => {
799 security_info!("Session has been revoked");
802 false
803 }
804 _ => {
805 security_info!("Session and uat expiry are not consistent, rejecting.");
806 debug!(ses_st = ?session.state, uat_exp = ?uat.expiry);
807 false
808 }
809 }
810 } else {
811 let grace = uat.issued_at + AUTH_TOKEN_GRACE_WINDOW;
812 let current = time::OffsetDateTime::UNIX_EPOCH + ct;
813 trace!(%grace, %current);
814 if current >= grace {
815 security_info!(
816 "The token grace window has passed, and no session exists. Assuming invalid."
817 );
818 false
819 } else {
820 security_info!("The token grace window is in effect. Assuming valid.");
821 true
822 }
823 }
824 }
825 }
826
827 pub(crate) fn verify_application_password(
828 &self,
829 application: &Application,
830 cleartext: &str,
831 ) -> Result<Option<LdapBoundToken>, OperationError> {
832 if let Some(v) = self.apps_pwds.get(&application.uuid) {
833 for ap in v.iter() {
834 let password_verified = ap.password.verify(cleartext).map_err(|e| {
835 error!(crypto_err = ?e);
836 OperationError::CryptographyError
837 })?;
838
839 if password_verified {
840 let session_id = uuid::Uuid::new_v4();
841 security_info!(
842 "Starting session {} for {} {}",
843 session_id,
844 self.spn,
845 self.uuid
846 );
847
848 return Ok(Some(LdapBoundToken {
849 spn: self.spn.clone(),
850 session_id,
851 effective_session: LdapSession::ApplicationPasswordBind(
852 application.uuid,
853 self.uuid,
854 ),
855 }));
856 }
857 }
858 }
859 Ok(None)
860 }
861
862 pub(crate) fn to_unixusertoken(&self, ct: Duration) -> Result<UnixUserToken, OperationError> {
863 let (gidnumber, shell, sshkeys, groups) = match &self.unix_extn {
864 Some(ue) => {
865 let sshkeys: Vec<_> = self.sshkeys.values().cloned().collect();
866 (ue.gidnumber, ue.shell.clone(), sshkeys, ue.groups.clone())
867 }
868 None => {
869 return Err(OperationError::MissingClass(
870 ENTRYCLASS_POSIX_ACCOUNT.into(),
871 ));
872 }
873 };
874
875 let groups: Vec<UnixGroupToken> = groups.iter().map(|g| g.to_unixgrouptoken()).collect();
876
877 Ok(UnixUserToken {
878 name: self.name().into(),
879 spn: self.spn.clone(),
880 displayname: self.displayname.clone(),
881 gidnumber,
882 uuid: self.uuid,
883 shell: shell.clone(),
884 groups,
885 sshkeys,
886 valid: self.is_within_valid_time(ct),
887 })
888 }
889
890 pub(crate) fn oauth2_client_provider(&self) -> Option<&OAuth2AccountCredential> {
891 self.oauth2_client_provider.as_ref()
892 }
893
894 #[cfg(test)]
895 pub(crate) fn setup_oauth2_client_provider(
896 &mut self,
897 client_provider: &crate::idm::oauth2_client::OAuth2ClientProvider,
898 ) {
899 self.oauth2_client_provider = Some(OAuth2AccountCredential {
900 provider: client_provider.uuid,
901 cred_id: Uuid::new_v4(),
902 user_id: self.spn.clone(),
903 });
904 }
905}
906
907pub struct DestroySessionTokenEvent {
912 pub ident: Identity,
914 pub target: Uuid,
916 pub token_id: Uuid,
918}
919
920impl DestroySessionTokenEvent {
921 #[cfg(test)]
922 pub fn new_internal(target: Uuid, token_id: Uuid) -> Self {
923 DestroySessionTokenEvent {
924 ident: Identity::from_internal(),
925 target,
926 token_id,
927 }
928 }
929}
930
931impl IdmServerProxyWriteTransaction<'_> {
932 pub fn account_destroy_session_token(
933 &mut self,
934 dte: &DestroySessionTokenEvent,
935 ) -> Result<(), OperationError> {
936 let modlist = ModifyList::new_list(vec![Modify::Removed(
938 Attribute::UserAuthTokenSession,
939 PartialValue::Refer(dte.token_id),
940 )]);
941
942 self.qs_write
943 .impersonate_modify(
944 &filter!(f_and!([
946 f_eq(Attribute::Uuid, PartialValue::Uuid(dte.target)),
947 f_eq(
948 Attribute::UserAuthTokenSession,
949 PartialValue::Refer(dte.token_id)
950 )
951 ])),
952 &filter_all!(f_and!([
954 f_eq(Attribute::Uuid, PartialValue::Uuid(dte.target)),
955 f_eq(
956 Attribute::UserAuthTokenSession,
957 PartialValue::Refer(dte.token_id)
958 )
959 ])),
960 &modlist,
961 &dte.ident.project_with_scope(AccessScope::ReadWrite),
965 )
966 .map_err(|e| {
967 admin_error!("Failed to destroy user auth token {:?}", e);
968 e
969 })
970 }
971
972 pub fn service_account_into_person(
973 &mut self,
974 ident: &Identity,
975 target_uuid: Uuid,
976 ) -> Result<(), OperationError> {
977 let schema_ref = self.qs_write.get_schema();
978
979 let account_entry = self
981 .qs_write
982 .internal_search_uuid(target_uuid)
983 .map_err(|e| {
984 admin_error!("Failed to start service account into person -> {:?}", e);
985 e
986 })?;
987
988 let prev_classes: BTreeSet<_> = account_entry
990 .get_ava_as_iutf8_iter(Attribute::Class)
991 .ok_or_else(|| {
992 error!(
993 "Invalid entry, {} attribute is not present or not iutf8",
994 Attribute::Class
995 );
996 OperationError::MissingAttribute(Attribute::Class)
997 })?
998 .collect();
999
1000 let mut new_iutf8es = prev_classes.clone();
1003 new_iutf8es.remove(EntryClass::ServiceAccount.into());
1004 new_iutf8es.insert(EntryClass::Person.into());
1005
1006 let (_added, removed) = schema_ref
1008 .query_attrs_difference(&prev_classes, &new_iutf8es)
1009 .map_err(|se| {
1010 admin_error!("While querying the schema, it reported that requested classes may not be present indicating a possible corruption");
1011 OperationError::SchemaViolation(
1012 se
1013 )
1014 })?;
1015
1016 let mut modlist = ModifyList::new_remove(
1019 Attribute::Class,
1020 EntryClass::ServiceAccount.to_partialvalue(),
1021 );
1022 modlist.push_mod(Modify::Present(
1024 Attribute::Class,
1025 EntryClass::Person.to_value(),
1026 ));
1027 removed
1029 .into_iter()
1030 .for_each(|attr| modlist.push_mod(Modify::Purged(attr.into())));
1031 self.qs_write
1035 .impersonate_modify(
1036 &filter!(f_eq(Attribute::Uuid, PartialValue::Uuid(target_uuid))),
1038 &filter_all!(f_eq(Attribute::Uuid, PartialValue::Uuid(target_uuid))),
1040 &modlist,
1041 ident,
1043 )
1044 .map_err(|e| {
1045 admin_error!("Failed to migrate service account to person - {:?}", e);
1046 e
1047 })
1048 }
1049}
1050
1051pub struct ListUserAuthTokenEvent {
1052 pub ident: Identity,
1054 pub target: Uuid,
1056}
1057
1058impl IdmServerProxyReadTransaction<'_> {
1059 pub fn account_list_user_auth_tokens(
1060 &mut self,
1061 lte: &ListUserAuthTokenEvent,
1062 ) -> Result<Vec<UatStatus>, OperationError> {
1063 let srch = match SearchEvent::from_target_uuid_request(
1065 lte.ident.clone(),
1066 lte.target,
1067 &self.qs_read,
1068 ) {
1069 Ok(s) => s,
1070 Err(e) => {
1071 admin_error!("Failed to begin account list user auth tokens: {:?}", e);
1072 return Err(e);
1073 }
1074 };
1075
1076 match self.qs_read.search_ext(&srch) {
1077 Ok(mut entries) => {
1078 entries
1079 .pop()
1080 .and_then(|e| {
1082 let account_id = e.get_uuid();
1083 e.get_ava_as_session_map(Attribute::UserAuthTokenSession)
1085 .map(|smap| {
1086 smap.iter()
1087 .map(|(u, s)| {
1088 let state = match s.state {
1089 SessionState::ExpiresAt(odt) => {
1090 UatStatusState::ExpiresAt(odt)
1091 }
1092 SessionState::NeverExpires => {
1093 UatStatusState::NeverExpires
1094 }
1095 SessionState::RevokedAt(_) => UatStatusState::Revoked,
1096 };
1097
1098 s.scope
1099 .try_into()
1100 .map(|purpose| UatStatus {
1101 account_id,
1102 session_id: *u,
1103 state,
1104 issued_at: s.issued_at,
1105 purpose,
1106 })
1107 .inspect_err(|_e| {
1108 admin_error!("Invalid user auth token {}", u);
1109 })
1110 })
1111 .collect::<Result<Vec<_>, _>>()
1112 })
1113 })
1114 .unwrap_or_else(|| {
1115 Ok(Vec::with_capacity(0))
1117 })
1118 }
1119 Err(e) => Err(e),
1120 }
1121 }
1122}
1123
1124#[cfg(test)]
1125mod tests {
1126 use crate::idm::accountpolicy::ResolvedAccountPolicy;
1127 use crate::prelude::*;
1128 use kanidm_proto::internal::UiHint;
1129
1130 #[idm_test]
1131 async fn test_idm_account_ui_hints(idms: &IdmServer, _idms_delayed: &mut IdmServerDelayed) {
1132 let ct = duration_from_epoch_now();
1133 let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();
1134
1135 let target_uuid = Uuid::new_v4();
1136
1137 let e = entry_init!(
1140 (Attribute::Class, EntryClass::Object.to_value()),
1141 (Attribute::Class, EntryClass::Account.to_value()),
1142 (Attribute::Class, EntryClass::Person.to_value()),
1143 (Attribute::Name, Value::new_iname("testaccount")),
1144 (Attribute::Uuid, Value::Uuid(target_uuid)),
1145 (Attribute::Description, Value::new_utf8s("testaccount")),
1146 (Attribute::DisplayName, Value::new_utf8s("Test Account"))
1147 );
1148
1149 let ce = CreateEvent::new_internal(vec![e]);
1150 assert!(idms_prox_write.qs_write.create(&ce).is_ok());
1151
1152 let account = idms_prox_write
1153 .target_to_account(target_uuid)
1154 .expect("account must exist");
1155 let session_id = uuid::Uuid::new_v4();
1156 let uat = account
1157 .to_userauthtoken(
1158 session_id,
1159 SessionScope::ReadWrite,
1160 ct,
1161 &ResolvedAccountPolicy::test_policy(),
1162 )
1163 .expect("Unable to create uat");
1164
1165 assert_eq!(uat.ui_hints.len(), 1);
1167 assert!(uat.ui_hints.contains(&UiHint::CredentialUpdate));
1168
1169 let me_posix = ModifyEvent::new_internal_invalid(
1171 filter!(f_eq(
1172 Attribute::Name,
1173 PartialValue::new_iname("testaccount")
1174 )),
1175 ModifyList::new_list(vec![
1176 Modify::Present(Attribute::Class, EntryClass::PosixAccount.into()),
1177 Modify::Present(Attribute::GidNumber, Value::new_uint32(2001)),
1178 ]),
1179 );
1180 assert!(idms_prox_write.qs_write.modify(&me_posix).is_ok());
1181
1182 let account = idms_prox_write
1184 .target_to_account(target_uuid)
1185 .expect("account must exist");
1186 let session_id = uuid::Uuid::new_v4();
1187 let uat = account
1188 .to_userauthtoken(
1189 session_id,
1190 SessionScope::ReadWrite,
1191 ct,
1192 &ResolvedAccountPolicy::test_policy(),
1193 )
1194 .expect("Unable to create uat");
1195
1196 assert_eq!(uat.ui_hints.len(), 2);
1197 assert!(uat.ui_hints.contains(&UiHint::PosixAccount));
1198 assert!(uat.ui_hints.contains(&UiHint::CredentialUpdate));
1199
1200 let e = entry_init!(
1202 (Attribute::Class, EntryClass::Object.to_value()),
1203 (Attribute::Class, EntryClass::Group.to_value()),
1204 (Attribute::Name, Value::new_iname("test_uihint_group")),
1205 (Attribute::Member, Value::Refer(target_uuid)),
1206 (
1207 Attribute::GrantUiHint,
1208 Value::UiHint(UiHint::ExperimentalFeatures)
1209 )
1210 );
1211
1212 let ce = CreateEvent::new_internal(vec![e]);
1213 assert!(idms_prox_write.qs_write.create(&ce).is_ok());
1214
1215 let account = idms_prox_write
1217 .target_to_account(target_uuid)
1218 .expect("account must exist");
1219 let session_id = uuid::Uuid::new_v4();
1220 let uat = account
1221 .to_userauthtoken(
1222 session_id,
1223 SessionScope::ReadWrite,
1224 ct,
1225 &ResolvedAccountPolicy::test_policy(),
1226 )
1227 .expect("Unable to create uat");
1228
1229 assert_eq!(uat.ui_hints.len(), 3);
1230 assert!(uat.ui_hints.contains(&UiHint::PosixAccount));
1231 assert!(uat.ui_hints.contains(&UiHint::ExperimentalFeatures));
1232 assert!(uat.ui_hints.contains(&UiHint::CredentialUpdate));
1233
1234 assert!(idms_prox_write.commit().is_ok());
1235 }
1236}