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