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