kanidmd_lib/idm/
account.rs

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    // To make this self-referential, we'll need to likely make Entry Pin<Arc<_>>
48    // so that we can make the references work.
49    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        // Check the classes
73        if !$value.attribute_equality(Attribute::Class, &EntryClass::Account.to_partialvalue()) {
74            return Err(OperationError::MissingClass(ENTRYCLASS_ACCOUNT.into()));
75        }
76
77        // Now extract our needed attributes
78        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        // Resolved by the caller
126        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        // Provide hints from groups.
136        let mut ui_hints: BTreeSet<_> = groups
137            .iter()
138            .map(|group: &Group<()>| group.ui_hints().iter())
139            .flatten()
140            .copied()
141            .collect();
142
143        // For now disable cred updates on sync accounts too.
144        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    /// Given the session_id and other metadata, create a user authentication token
274    /// that represents a users session. Since this metadata can vary from session
275    /// to session, this userauthtoken may contain some data (claims) that may yield
276    /// different privileges to the bearer.
277    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        // TODO: Apply policy to this expiry time.
285        // We have to remove the nanoseconds because when we transmit this / serialise it we drop
286        // the nanoseconds, but if we haven't done a serialise on the server our db cache has the
287        // ns value which breaks some checks.
288        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        // Note that currently the auth_session time comes from policy, but the already-privileged
295        // session bound is hardcoded.
296        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            // Issue an invalid/expired session.
309            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                // These sessions are always rw, and so have limited life.
318                (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            // application: None,
334            // groups: self.groups.iter().map(|g| g.to_proto()).collect(),
335            limit_search_max_results,
336            limit_search_max_filter_test,
337        })
338    }
339
340    /// Given the session_id and other metadata, reissue a user authentication token
341    /// that has elevated privileges. In the future we may adapt this to change what
342    /// scopes are granted per-reauth.
343    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            // Return a ReadWrite session with an inner expiry for the privileges
366            {
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                    // Needs to come from the actual original session. If we don't do this we have
375                    // to re-update the expiry in the DB. We don't want a re-auth to extend a time
376                    // bound session.
377                    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            // application: None,
393            // groups: self.groups.iter().map(|g| g.to_proto()).collect(),
394            limit_search_max_results,
395            limit_search_max_filter_test,
396        })
397    }
398
399    /// Given the currently bound client certificate, yield a user auth token that
400    /// represents the current session for the account.
401    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            // application: None,
430            // groups: self.groups.iter().map(|g| g.to_proto()).collect(),
431            limit_search_max_results,
432            limit_search_max_filter_test,
433        })
434    }
435
436    /// Determine if an entry is within it's validity period using it's `valid_from` and
437    /// `expire` attributes. `true` indicates the account is within the valid period.
438    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            // If current time greater than start time window
448            vft <= &cot
449        } else {
450            // We have no time, not expired.
451            true
452        };
453        let vmax = if let Some(ext) = expire {
454            // If exp greater than ct then expired.
455            &cot <= ext
456        } else {
457            // If not present, we are not expired
458            true
459        };
460        // Mix the results
461        vmin && vmax
462    }
463
464    /// Determine if this account is within it's validity period. `true` indicates the
465    /// account is within the valid period.
466    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    /// Get related inputs, such as account name, email, etc. This is used for password
471    /// quality checking.
472    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            // Change the cred
511            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            // Make a new credential instead
520            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            // Change the cred
538            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                    // No action, not the same pw
547                    Ok(None)
548                }
549            }
550            // Nothing to do.
551            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        // Where is the credential we need to update?
561        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        // Is it a passkey?
573        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        // Is it an attested passkey?
588        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            // Change the cred
615            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                // No credential exists, we can't supplementy it.
630                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        // In the future this will need to handle multiple credentials, not just single.
648
649        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        // TODO!!!
666        // Used in registrations only for disallowing existing credentials.
667        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        // Remember, token expiry is checked by validate_and_parse_token_to_token.
676        // If we wanted we could check other properties of the uat here?
677        // Alternatively, we could always store LESS in the uat because of this?
678
679        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        // Anonymous does NOT record it's sessions, so we simply check the expiry time
695        // of the token. This is already done for us as noted above.
696        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            // Get the sessions.
703            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            // Important - we don't have to check the expiry time against ct here since it was
708            // already checked in token_to_token. Here we just need to check it's consistent
709            // to our internal session knowledge.
710            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                        // William, if you have added a new type of credential, and end up here, you
722                        // need to look at session consistency plugin.
723                        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
825// Need to also add a "to UserAuthToken" ...
826
827// Need tests for conversion and the cred validations
828
829pub struct DestroySessionTokenEvent {
830    // Who initiated this?
831    pub ident: Identity,
832    // Who is it targeting?
833    pub target: Uuid,
834    // Which token id.
835    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        // Delete the attribute with uuid.
855        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 as executed
863                &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 as intended (acp)
871                &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                // Provide the event to impersonate. Notice how we project this with readwrite
880                // capability? This is because without this we'd force re-auths to end
881                // a session and we don't want that! you should always be able to logout!
882                &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        // Get the entry.
898        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        // Copy the current classes
907        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        // Remove the service account class.
919        // Add the person class.
920        let mut new_classes = prev_classes.clone();
921        new_classes.remove(EntryClass::ServiceAccount.into());
922        new_classes.insert(EntryClass::Person.into());
923
924        // diff the schema attrs, and remove the ones that are service_account only.
925        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        // Now construct the modlist which:
935        // removes service_account
936        let mut modlist = ModifyList::new_remove(
937            Attribute::Class,
938            EntryClass::ServiceAccount.to_partialvalue(),
939        );
940        // add person
941        modlist.push_mod(Modify::Present(
942            Attribute::Class,
943            EntryClass::Person.to_value(),
944        ));
945        // purge the other attrs that are SA only.
946        removed
947            .into_iter()
948            .for_each(|attr| modlist.push_mod(Modify::Purged(attr.into())));
949        // purge existing sessions
950
951        // Modify
952        self.qs_write
953            .impersonate_modify(
954                // Filter as executed
955                &filter!(f_eq(Attribute::Uuid, PartialValue::Uuid(target_uuid))),
956                // Filter as intended (acp)
957                &filter_all!(f_eq(Attribute::Uuid, PartialValue::Uuid(target_uuid))),
958                &modlist,
959                // Provide the entry to impersonate
960                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    // Who initiated this?
971    pub ident: Identity,
972    // Who is it targeting?
973    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        // Make an event from the request
982        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                    // get the first entry
999                    .and_then(|e| {
1000                        let account_id = e.get_uuid();
1001                        // From the entry, turn it into the value
1002                        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                        // No matching entry? Return none.
1034                        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        // Create a user. So far no ui hints.
1056        // Create a service account
1057        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        // Check the ui hints are as expected.
1084        assert_eq!(uat.ui_hints.len(), 1);
1085        assert!(uat.ui_hints.contains(&UiHint::CredentialUpdate));
1086
1087        // Modify the user to be a posix account, ensure they get the hint.
1088        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        // Check the ui hints are as expected.
1101        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        // Add a group with a ui hint, and then check they get the hint.
1119        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        // Check the ui hints are as expected.
1134        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}