kanidmd_lib/idm/
account.rs

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    // To make this self-referential, we'll need to likely make Entry Pin<Arc<_>>
51    // so that we can make the references work.
52    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        // Check the classes
92        if !$value.attribute_equality(Attribute::Class, &EntryClass::Account.to_partialvalue()) {
93            return Err(OperationError::MissingClass(ENTRYCLASS_ACCOUNT.into()));
94        }
95
96        // Now extract our needed attributes
97        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        // Resolved by the caller
144        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        // Provide hints from groups.
154        let mut ui_hints: BTreeSet<_> = groups
155            .iter()
156            .map(|group: &Group<()>| group.ui_hints().iter())
157            .flatten()
158            .copied()
159            .collect();
160
161        // For now disable cred updates on sync accounts too.
162        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    /// Given the session_id and other metadata, create a user authentication token
322    /// that represents a users session. Since this metadata can vary from session
323    /// to session, this userauthtoken may contain some data (claims) that may yield
324    /// different privileges to the bearer.
325    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        // TODO: Apply policy to this expiry time.
333        // We have to remove the nanoseconds because when we transmit this / serialise it we drop
334        // the nanoseconds, but if we haven't done a serialise on the server our db cache has the
335        // ns value which breaks some checks.
336        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        // Note that currently the auth_session time comes from policy, but the already-privileged
343        // session bound is hardcoded.
344        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            // Issue an invalid/expired session.
357            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                // These sessions are always rw, and so have limited life.
366                (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            // application: None,
382            // groups: self.groups.iter().map(|g| g.to_proto()).collect(),
383            limit_search_max_results,
384            limit_search_max_filter_test,
385        })
386    }
387
388    /// Given the session_id and other metadata, reissue a user authentication token
389    /// that has elevated privileges. In the future we may adapt this to change what
390    /// scopes are granted per-reauth.
391    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            // Return a ReadWrite session with an inner expiry for the privileges
414            {
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                    // Needs to come from the actual original session. If we don't do this we have
423                    // to re-update the expiry in the DB. We don't want a re-auth to extend a time
424                    // bound session.
425                    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            // application: None,
441            // groups: self.groups.iter().map(|g| g.to_proto()).collect(),
442            limit_search_max_results,
443            limit_search_max_filter_test,
444        })
445    }
446
447    /// Given the currently bound client certificate, yield a user auth token that
448    /// represents the current session for the account.
449    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            // application: None,
478            // groups: self.groups.iter().map(|g| g.to_proto()).collect(),
479            limit_search_max_results,
480            limit_search_max_filter_test,
481        })
482    }
483
484    /// Determine if an entry is within it's validity period using it's `valid_from` and
485    /// `expire` attributes. `true` indicates the account is within the valid period.
486    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            // If current time greater than start time window
496            vft <= &cot
497        } else {
498            // We have no time, not expired.
499            true
500        };
501        let vmax = if let Some(ext) = expire {
502            // If exp greater than ct then expired.
503            &cot <= ext
504        } else {
505            // If not present, we are not expired
506            true
507        };
508        // Mix the results
509        vmin && vmax
510    }
511
512    /// Determine if this account is within it's validity period. `true` indicates the
513    /// account is within the valid period.
514    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    /// Get related inputs, such as account name, email, etc. This is used for password
519    /// quality checking.
520    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            // Change the cred
561            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            // Make a new credential instead
570            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            // Change the cred
588            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                    // No action, not the same pw
597                    Ok(None)
598                }
599            }
600            // Nothing to do.
601            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        // Where is the credential we need to update?
611        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        // Is it a passkey?
623        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        // Is it an attested passkey?
638        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            // Change the cred
665            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                // No credential exists, we can't supplementy it.
680                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        // In the future this will need to handle multiple credentials, not just single.
698
699        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        // TODO!!!
709        // Used in registrations only for disallowing existing credentials.
710        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        // Remember, token expiry is checked by validate_and_parse_token_to_token.
719        // If we wanted we could check other properties of the uat here?
720        // Alternatively, we could always store LESS in the uat because of this?
721
722        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        // Anonymous does NOT record it's sessions, so we simply check the expiry time
738        // of the token. This is already done for us as noted above.
739        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            // Get the sessions.
746            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            // Important - we don't have to check the expiry time against ct here since it was
751            // already checked in token_to_token. Here we just need to check it's consistent
752            // to our internal session knowledge.
753            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                        // William, if you have added a new type of credential, and end up here, you
765                        // need to look at session consistency plugin.
766                        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
872// Need to also add a "to UserAuthToken" ...
873
874// Need tests for conversion and the cred validations
875
876pub struct DestroySessionTokenEvent {
877    // Who initiated this?
878    pub ident: Identity,
879    // Who is it targeting?
880    pub target: Uuid,
881    // Which token id.
882    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        // Delete the attribute with uuid.
902        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 as executed
910                &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 as intended (acp)
918                &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                // Provide the event to impersonate. Notice how we project this with readwrite
927                // capability? This is because without this we'd force re-auths to end
928                // a session and we don't want that! you should always be able to logout!
929                &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        // Get the entry.
945        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        // Copy the current classes
954        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        // Remove the service account class.
966        // Add the person class.
967        let mut new_iutf8es = prev_classes.clone();
968        new_iutf8es.remove(EntryClass::ServiceAccount.into());
969        new_iutf8es.insert(EntryClass::Person.into());
970
971        // diff the schema attrs, and remove the ones that are service_account only.
972        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        // Now construct the modlist which:
982        // removes service_account
983        let mut modlist = ModifyList::new_remove(
984            Attribute::Class,
985            EntryClass::ServiceAccount.to_partialvalue(),
986        );
987        // add person
988        modlist.push_mod(Modify::Present(
989            Attribute::Class,
990            EntryClass::Person.to_value(),
991        ));
992        // purge the other attrs that are SA only.
993        removed
994            .into_iter()
995            .for_each(|attr| modlist.push_mod(Modify::Purged(attr.into())));
996        // purge existing sessions
997
998        // Modify
999        self.qs_write
1000            .impersonate_modify(
1001                // Filter as executed
1002                &filter!(f_eq(Attribute::Uuid, PartialValue::Uuid(target_uuid))),
1003                // Filter as intended (acp)
1004                &filter_all!(f_eq(Attribute::Uuid, PartialValue::Uuid(target_uuid))),
1005                &modlist,
1006                // Provide the entry to impersonate
1007                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    // Who initiated this?
1018    pub ident: Identity,
1019    // Who is it targeting?
1020    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        // Make an event from the request
1029        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                    // get the first entry
1046                    .and_then(|e| {
1047                        let account_id = e.get_uuid();
1048                        // From the entry, turn it into the value
1049                        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                        // No matching entry? Return none.
1081                        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        // Create a user. So far no ui hints.
1103        // Create a service account
1104        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        // Check the ui hints are as expected.
1131        assert_eq!(uat.ui_hints.len(), 1);
1132        assert!(uat.ui_hints.contains(&UiHint::CredentialUpdate));
1133
1134        // Modify the user to be a posix account, ensure they get the hint.
1135        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        // Check the ui hints are as expected.
1148        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        // Add a group with a ui hint, and then check they get the hint.
1166        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        // Check the ui hints are as expected.
1181        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}