kanidmd_lib/idm/
account.rs

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