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