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