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        // We have to remove the nanoseconds because when we transmit this / serialise it we drop
333        // the nanoseconds, but if we haven't done a serialise on the server our db cache has the
334        // ns value which breaks some checks.
335        let ct = ct - Duration::from_nanos(ct.subsec_nanos() as u64);
336        let issued_at = OffsetDateTime::UNIX_EPOCH + ct;
337
338        let limit_search_max_results = account_policy.limit_search_max_results();
339        let limit_search_max_filter_test = account_policy.limit_search_max_filter_test();
340
341        // Note that currently the auth_session time comes from policy, but the already-privileged
342        // session bound is hardcoded. This mostly affects admin/idm_admin breakglass accounts.
343        let expiry = OffsetDateTime::UNIX_EPOCH
344            + ct
345            + Duration::from_secs(account_policy.authsession_expiry() as u64);
346        let limited_expiry = OffsetDateTime::UNIX_EPOCH
347            + ct
348            + Duration::from_secs(DEFAULT_AUTH_SESSION_LIMITED_EXPIRY as u64);
349
350        let (purpose, expiry) = match scope {
351            // Issue an invalid/expired session.
352            SessionScope::Synchronise => {
353                warn!(
354                    "Should be impossible to issue sync sessions with a uat. Refusing to proceed."
355                );
356                return None;
357            }
358            SessionScope::ReadOnly => (UatPurpose::ReadOnly, expiry),
359            SessionScope::ReadWrite => {
360                // These sessions are always rw, and so have limited life.
361                // Ensure that we take the lower of the two bounds.
362                let capped = std::cmp::min(expiry, limited_expiry);
363
364                (
365                    UatPurpose::ReadWrite {
366                        expiry: Some(capped),
367                    },
368                    capped,
369                )
370            }
371            SessionScope::PrivilegeCapable => (UatPurpose::ReadWrite { expiry: None }, expiry),
372        };
373
374        Some(UserAuthToken {
375            session_id,
376            expiry: Some(expiry),
377            issued_at,
378            purpose,
379            uuid: self.uuid,
380            displayname: self.displayname.clone(),
381            spn: self.spn.clone(),
382            mail_primary: self.mail_primary.clone(),
383            ui_hints: self.ui_hints.clone(),
384            // application: None,
385            // groups: self.groups.iter().map(|g| g.to_proto()).collect(),
386            limit_search_max_results,
387            limit_search_max_filter_test,
388        })
389    }
390
391    /// Given the session_id and other metadata, reissue a user authentication token
392    /// that has elevated privileges. In the future we may adapt this to change what
393    /// scopes are granted per-reauth.
394    pub(crate) fn to_reissue_userauthtoken(
395        &self,
396        session_id: Uuid,
397        session_expiry: Option<OffsetDateTime>,
398        scope: SessionScope,
399        ct: Duration,
400        account_policy: &ResolvedAccountPolicy,
401    ) -> Option<UserAuthToken> {
402        let issued_at = OffsetDateTime::UNIX_EPOCH + ct;
403
404        let limit_search_max_results = account_policy.limit_search_max_results();
405        let limit_search_max_filter_test = account_policy.limit_search_max_filter_test();
406
407        let (purpose, expiry) = match scope {
408            SessionScope::Synchronise | SessionScope::ReadOnly | SessionScope::ReadWrite => {
409                warn!(
410                    "Impossible state, should not be re-issuing for session scope {:?}",
411                    scope
412                );
413                return None;
414            }
415            SessionScope::PrivilegeCapable =>
416            // Return a ReadWrite session with an inner expiry for the privileges
417            {
418                let expiry = Some(
419                    OffsetDateTime::UNIX_EPOCH
420                        + ct
421                        + Duration::from_secs(account_policy.privilege_expiry().into()),
422                );
423                (
424                    UatPurpose::ReadWrite { expiry },
425                    // Needs to come from the actual original session. If we don't do this we have
426                    // to re-update the expiry in the DB. We don't want a re-auth to extend a time
427                    // bound session.
428                    session_expiry,
429                )
430            }
431        };
432
433        Some(UserAuthToken {
434            session_id,
435            expiry,
436            issued_at,
437            purpose,
438            uuid: self.uuid,
439            displayname: self.displayname.clone(),
440            spn: self.spn.clone(),
441            mail_primary: self.mail_primary.clone(),
442            ui_hints: self.ui_hints.clone(),
443            // application: None,
444            // groups: self.groups.iter().map(|g| g.to_proto()).collect(),
445            limit_search_max_results,
446            limit_search_max_filter_test,
447        })
448    }
449
450    /// Given the currently bound client certificate, yield a user auth token that
451    /// represents the current session for the account.
452    pub(crate) fn client_cert_info_to_userauthtoken(
453        &self,
454        certificate_id: Uuid,
455        session_is_rw: bool,
456        ct: Duration,
457        account_policy: &ResolvedAccountPolicy,
458    ) -> Option<UserAuthToken> {
459        let issued_at = OffsetDateTime::UNIX_EPOCH + ct;
460
461        let limit_search_max_results = account_policy.limit_search_max_results();
462        let limit_search_max_filter_test = account_policy.limit_search_max_filter_test();
463
464        let purpose = if session_is_rw {
465            UatPurpose::ReadWrite { expiry: None }
466        } else {
467            UatPurpose::ReadOnly
468        };
469
470        Some(UserAuthToken {
471            session_id: certificate_id,
472            expiry: None,
473            issued_at,
474            purpose,
475            uuid: self.uuid,
476            displayname: self.displayname.clone(),
477            spn: self.spn.clone(),
478            mail_primary: self.mail_primary.clone(),
479            ui_hints: self.ui_hints.clone(),
480            // application: None,
481            // groups: self.groups.iter().map(|g| g.to_proto()).collect(),
482            limit_search_max_results,
483            limit_search_max_filter_test,
484        })
485    }
486
487    /// Determine if an entry is within it's validity period using it's `valid_from` and
488    /// `expire` attributes. `true` indicates the account is within the valid period.
489    pub fn check_within_valid_time(
490        ct: Duration,
491        valid_from: Option<&OffsetDateTime>,
492        expire: Option<&OffsetDateTime>,
493    ) -> bool {
494        let cot = OffsetDateTime::UNIX_EPOCH + ct;
495        trace!("Checking within valid time: {:?} {:?}", valid_from, expire);
496
497        let vmin = if let Some(vft) = valid_from {
498            // If current time greater than start time window
499            vft <= &cot
500        } else {
501            // We have no time, not expired.
502            true
503        };
504        let vmax = if let Some(ext) = expire {
505            // If exp greater than ct then expired.
506            &cot <= ext
507        } else {
508            // If not present, we are not expired
509            true
510        };
511        // Mix the results
512        vmin && vmax
513    }
514
515    /// Determine if this account is within it's validity period. `true` indicates the
516    /// account is within the valid period.
517    pub fn is_within_valid_time(&self, ct: Duration) -> bool {
518        Self::check_within_valid_time(ct, self.valid_from.as_ref(), self.expire.as_ref())
519    }
520
521    /// Get related inputs, such as account name, email, etc. This is used for password
522    /// quality checking.
523    pub fn related_inputs(&self) -> Vec<&str> {
524        let mut inputs = Vec::with_capacity(4 + self.mail.len());
525        self.mail.iter().for_each(|m| {
526            inputs.push(m.as_str());
527        });
528        inputs.push(self.spn.as_str());
529        if let Some(name) = self.name.as_ref() {
530            inputs.push(name)
531        }
532        inputs.push(self.displayname.as_str());
533        if let Some(s) = self.radius_secret.as_deref() {
534            inputs.push(s);
535        }
536        inputs
537    }
538
539    pub fn primary_cred_uuid_and_policy(&self) -> Option<(Uuid, CredSoftLockPolicy)> {
540        self.primary
541            .as_ref()
542            .map(|cred| (cred.uuid, cred.softlock_policy()))
543            .or_else(|| {
544                if self.is_anonymous() {
545                    Some((UUID_ANONYMOUS, CredSoftLockPolicy::Unrestricted))
546                } else {
547                    None
548                }
549            })
550    }
551
552    pub fn is_anonymous(&self) -> bool {
553        self.uuid == UUID_ANONYMOUS
554    }
555
556    #[cfg(test)]
557    pub(crate) fn gen_password_mod(
558        &self,
559        cleartext: &str,
560        crypto_policy: &CryptoPolicy,
561    ) -> Result<ModifyList<ModifyInvalid>, OperationError> {
562        match &self.primary {
563            // Change the cred
564            Some(primary) => {
565                let ncred = primary.set_password(crypto_policy, cleartext)?;
566                let vcred = Value::new_credential("primary", ncred);
567                Ok(ModifyList::new_purge_and_set(
568                    Attribute::PrimaryCredential,
569                    vcred,
570                ))
571            }
572            // Make a new credential instead
573            None => {
574                let ncred = Credential::new_password_only(crypto_policy, cleartext)?;
575                let vcred = Value::new_credential("primary", ncred);
576                Ok(ModifyList::new_purge_and_set(
577                    Attribute::PrimaryCredential,
578                    vcred,
579                ))
580            }
581        }
582    }
583
584    pub(crate) fn gen_password_upgrade_mod(
585        &self,
586        cleartext: &str,
587        crypto_policy: &CryptoPolicy,
588    ) -> Result<Option<ModifyList<ModifyInvalid>>, OperationError> {
589        match &self.primary {
590            // Change the cred
591            Some(primary) => {
592                if let Some(ncred) = primary.upgrade_password(crypto_policy, cleartext)? {
593                    let vcred = Value::new_credential("primary", ncred);
594                    Ok(Some(ModifyList::new_purge_and_set(
595                        Attribute::PrimaryCredential,
596                        vcred,
597                    )))
598                } else {
599                    // No action, not the same pw
600                    Ok(None)
601                }
602            }
603            // Nothing to do.
604            None => Ok(None),
605        }
606    }
607
608    pub(crate) fn gen_webauthn_counter_mod(
609        &mut self,
610        auth_result: &AuthenticationResult,
611    ) -> Result<Option<ModifyList<ModifyInvalid>>, OperationError> {
612        let mut ml = Vec::with_capacity(2);
613        // Where is the credential we need to update?
614        let opt_ncred = match self.primary.as_ref() {
615            Some(primary) => primary.update_webauthn_properties(auth_result)?,
616            None => None,
617        };
618
619        if let Some(ncred) = opt_ncred {
620            let vcred = Value::new_credential("primary", ncred);
621            ml.push(Modify::Purged(Attribute::PrimaryCredential));
622            ml.push(Modify::Present(Attribute::PrimaryCredential, vcred));
623        }
624
625        // Is it a passkey?
626        self.passkeys.iter_mut().for_each(|(u, (t, k))| {
627            if let Some(true) = k.update_credential(auth_result) {
628                ml.push(Modify::Removed(
629                    Attribute::PassKeys,
630                    PartialValue::Passkey(*u),
631                ));
632
633                ml.push(Modify::Present(
634                    Attribute::PassKeys,
635                    Value::Passkey(*u, t.clone(), k.clone()),
636                ));
637            }
638        });
639
640        // Is it an attested passkey?
641        self.attested_passkeys.iter_mut().for_each(|(u, (t, k))| {
642            if let Some(true) = k.update_credential(auth_result) {
643                ml.push(Modify::Removed(
644                    Attribute::AttestedPasskeys,
645                    PartialValue::AttestedPasskey(*u),
646                ));
647
648                ml.push(Modify::Present(
649                    Attribute::AttestedPasskeys,
650                    Value::AttestedPasskey(*u, t.clone(), k.clone()),
651                ));
652            }
653        });
654
655        if ml.is_empty() {
656            Ok(None)
657        } else {
658            Ok(Some(ModifyList::new_list(ml)))
659        }
660    }
661
662    pub(crate) fn invalidate_backup_code_mod(
663        self,
664        code_to_remove: &str,
665    ) -> Result<ModifyList<ModifyInvalid>, OperationError> {
666        match self.primary {
667            // Change the cred
668            Some(primary) => {
669                let r_ncred = primary.invalidate_backup_code(code_to_remove);
670                match r_ncred {
671                    Ok(ncred) => {
672                        let vcred = Value::new_credential("primary", ncred);
673                        Ok(ModifyList::new_purge_and_set(
674                            Attribute::PrimaryCredential,
675                            vcred,
676                        ))
677                    }
678                    Err(e) => Err(e),
679                }
680            }
681            None => {
682                // No credential exists, we can't supplementy it.
683                Err(OperationError::InvalidState)
684            }
685        }
686    }
687
688    pub(crate) fn regenerate_radius_secret_mod(
689        &self,
690        cleartext: &str,
691    ) -> Result<ModifyList<ModifyInvalid>, OperationError> {
692        let vcred = Value::new_secret_str(cleartext);
693        Ok(ModifyList::new_purge_and_set(
694            Attribute::RadiusSecret,
695            vcred,
696        ))
697    }
698
699    pub(crate) fn to_credentialstatus(&self) -> Result<CredentialStatus, OperationError> {
700        // In the future this will need to handle multiple credentials, not just single.
701
702        self.primary
703            .as_ref()
704            .map(|cred| CredentialStatus {
705                creds: vec![cred.into()],
706            })
707            .ok_or(OperationError::NoMatchingAttributes)
708    }
709
710    pub(crate) fn existing_credential_id_list(&self) -> Option<Vec<CredentialID>> {
711        // TODO!!!
712        // Used in registrations only for disallowing existing credentials.
713        None
714    }
715
716    pub(crate) fn check_user_auth_token_valid(
717        ct: Duration,
718        uat: &UserAuthToken,
719        entry: &Entry<EntrySealed, EntryCommitted>,
720    ) -> bool {
721        // Remember, token expiry is checked by validate_and_parse_token_to_token.
722        // If we wanted we could check other properties of the uat here?
723        // Alternatively, we could always store LESS in the uat because of this?
724
725        let within_valid_window = Account::check_within_valid_time(
726            ct,
727            entry
728                .get_ava_single_datetime(Attribute::AccountValidFrom)
729                .as_ref(),
730            entry
731                .get_ava_single_datetime(Attribute::AccountExpire)
732                .as_ref(),
733        );
734
735        if !within_valid_window {
736            security_info!("Account has expired or is not yet valid, not allowing to proceed");
737            return false;
738        }
739
740        // Anonymous does NOT record it's sessions, so we simply check the expiry time
741        // of the token. This is already done for us as noted above.
742        trace!("{}", &uat);
743
744        if uat.uuid == UUID_ANONYMOUS {
745            security_debug!("Anonymous sessions do not have session records, session is valid.");
746            true
747        } else {
748            // Get the sessions.
749            let session_present = entry
750                .get_ava_as_session_map(Attribute::UserAuthTokenSession)
751                .and_then(|session_map| session_map.get(&uat.session_id));
752
753            // Important - we don't have to check the expiry time against ct here since it was
754            // already checked in token_to_token. Here we just need to check it's consistent
755            // to our internal session knowledge.
756            if let Some(session) = session_present {
757                match (&session.state, &uat.expiry) {
758                    (SessionState::ExpiresAt(s_exp), Some(u_exp)) if s_exp == u_exp => {
759                        security_info!("A valid limited session value exists for this token");
760                        true
761                    }
762                    (SessionState::NeverExpires, None) => {
763                        security_info!("A valid unbound session value exists for this token");
764                        true
765                    }
766                    (SessionState::RevokedAt(_), _) => {
767                        // William, if you have added a new type of credential, and end up here, you
768                        // need to look at session consistency plugin.
769                        security_info!("Session has been revoked");
770                        false
771                    }
772                    _ => {
773                        security_info!("Session and uat expiry are not consistent, rejecting.");
774                        debug!(ses_st = ?session.state, uat_exp = ?uat.expiry);
775                        false
776                    }
777                }
778            } else {
779                let grace = uat.issued_at + AUTH_TOKEN_GRACE_WINDOW;
780                let current = time::OffsetDateTime::UNIX_EPOCH + ct;
781                trace!(%grace, %current);
782                if current >= grace {
783                    security_info!(
784                        "The token grace window has passed, and no session exists. Assuming invalid."
785                    );
786                    false
787                } else {
788                    security_info!("The token grace window is in effect. Assuming valid.");
789                    true
790                }
791            }
792        }
793    }
794
795    pub(crate) fn verify_application_password(
796        &self,
797        application: &Application,
798        cleartext: &str,
799    ) -> Result<Option<LdapBoundToken>, OperationError> {
800        if let Some(v) = self.apps_pwds.get(&application.uuid) {
801            for ap in v.iter() {
802                let password_verified = ap.password.verify(cleartext).map_err(|e| {
803                    error!(crypto_err = ?e);
804                    OperationError::CryptographyError
805                })?;
806
807                if password_verified {
808                    let session_id = uuid::Uuid::new_v4();
809                    security_info!(
810                        "Starting session {} for {} {}",
811                        session_id,
812                        self.spn,
813                        self.uuid
814                    );
815
816                    return Ok(Some(LdapBoundToken {
817                        spn: self.spn.clone(),
818                        session_id,
819                        effective_session: LdapSession::ApplicationPasswordBind(
820                            application.uuid,
821                            self.uuid,
822                        ),
823                    }));
824                }
825            }
826        }
827        Ok(None)
828    }
829
830    pub(crate) fn to_unixusertoken(&self, ct: Duration) -> Result<UnixUserToken, OperationError> {
831        let (gidnumber, shell, sshkeys, groups) = match &self.unix_extn {
832            Some(ue) => {
833                let sshkeys: Vec<_> = self.sshkeys.values().cloned().collect();
834                (ue.gidnumber, ue.shell.clone(), sshkeys, ue.groups.clone())
835            }
836            None => {
837                return Err(OperationError::MissingClass(
838                    ENTRYCLASS_POSIX_ACCOUNT.into(),
839                ));
840            }
841        };
842
843        let groups: Vec<UnixGroupToken> = groups.iter().map(|g| g.to_unixgrouptoken()).collect();
844
845        Ok(UnixUserToken {
846            name: self.name().into(),
847            spn: self.spn.clone(),
848            displayname: self.displayname.clone(),
849            gidnumber,
850            uuid: self.uuid,
851            shell: shell.clone(),
852            groups,
853            sshkeys,
854            valid: self.is_within_valid_time(ct),
855        })
856    }
857
858    pub(crate) fn oauth2_client_provider(&self) -> Option<&OAuth2AccountCredential> {
859        self.oauth2_client_provider.as_ref()
860    }
861
862    #[cfg(test)]
863    pub(crate) fn setup_oauth2_client_provider(
864        &mut self,
865        client_provider: &crate::idm::oauth2_client::OAuth2ClientProvider,
866    ) {
867        self.oauth2_client_provider = Some(OAuth2AccountCredential {
868            provider: client_provider.uuid,
869            cred_id: Uuid::new_v4(),
870            user_id: self.spn.clone(),
871        });
872    }
873}
874
875// Need to also add a "to UserAuthToken" ...
876
877// Need tests for conversion and the cred validations
878
879pub struct DestroySessionTokenEvent {
880    // Who initiated this?
881    pub ident: Identity,
882    // Who is it targeting?
883    pub target: Uuid,
884    // Which token id.
885    pub token_id: Uuid,
886}
887
888impl DestroySessionTokenEvent {
889    #[cfg(test)]
890    pub fn new_internal(target: Uuid, token_id: Uuid) -> Self {
891        DestroySessionTokenEvent {
892            ident: Identity::from_internal(),
893            target,
894            token_id,
895        }
896    }
897}
898
899impl IdmServerProxyWriteTransaction<'_> {
900    pub fn account_destroy_session_token(
901        &mut self,
902        dte: &DestroySessionTokenEvent,
903    ) -> Result<(), OperationError> {
904        // Delete the attribute with uuid.
905        let modlist = ModifyList::new_list(vec![Modify::Removed(
906            Attribute::UserAuthTokenSession,
907            PartialValue::Refer(dte.token_id),
908        )]);
909
910        self.qs_write
911            .impersonate_modify(
912                // Filter as executed
913                &filter!(f_and!([
914                    f_eq(Attribute::Uuid, PartialValue::Uuid(dte.target)),
915                    f_eq(
916                        Attribute::UserAuthTokenSession,
917                        PartialValue::Refer(dte.token_id)
918                    )
919                ])),
920                // Filter as intended (acp)
921                &filter_all!(f_and!([
922                    f_eq(Attribute::Uuid, PartialValue::Uuid(dte.target)),
923                    f_eq(
924                        Attribute::UserAuthTokenSession,
925                        PartialValue::Refer(dte.token_id)
926                    )
927                ])),
928                &modlist,
929                // Provide the event to impersonate. Notice how we project this with readwrite
930                // capability? This is because without this we'd force re-auths to end
931                // a session and we don't want that! you should always be able to logout!
932                &dte.ident.project_with_scope(AccessScope::ReadWrite),
933            )
934            .map_err(|e| {
935                admin_error!("Failed to destroy user auth token {:?}", e);
936                e
937            })
938    }
939
940    pub fn service_account_into_person(
941        &mut self,
942        ident: &Identity,
943        target_uuid: Uuid,
944    ) -> Result<(), OperationError> {
945        let schema_ref = self.qs_write.get_schema();
946
947        // Get the entry.
948        let account_entry = self
949            .qs_write
950            .internal_search_uuid(target_uuid)
951            .map_err(|e| {
952                admin_error!("Failed to start service account into person -> {:?}", e);
953                e
954            })?;
955
956        // Copy the current classes
957        let prev_classes: BTreeSet<_> = account_entry
958            .get_ava_as_iutf8_iter(Attribute::Class)
959            .ok_or_else(|| {
960                error!(
961                    "Invalid entry, {} attribute is not present or not iutf8",
962                    Attribute::Class
963                );
964                OperationError::MissingAttribute(Attribute::Class)
965            })?
966            .collect();
967
968        // Remove the service account class.
969        // Add the person class.
970        let mut new_iutf8es = prev_classes.clone();
971        new_iutf8es.remove(EntryClass::ServiceAccount.into());
972        new_iutf8es.insert(EntryClass::Person.into());
973
974        // diff the schema attrs, and remove the ones that are service_account only.
975        let (_added, removed) = schema_ref
976            .query_attrs_difference(&prev_classes, &new_iutf8es)
977            .map_err(|se| {
978                admin_error!("While querying the schema, it reported that requested classes may not be present indicating a possible corruption");
979                OperationError::SchemaViolation(
980                    se
981                )
982            })?;
983
984        // Now construct the modlist which:
985        // removes service_account
986        let mut modlist = ModifyList::new_remove(
987            Attribute::Class,
988            EntryClass::ServiceAccount.to_partialvalue(),
989        );
990        // add person
991        modlist.push_mod(Modify::Present(
992            Attribute::Class,
993            EntryClass::Person.to_value(),
994        ));
995        // purge the other attrs that are SA only.
996        removed
997            .into_iter()
998            .for_each(|attr| modlist.push_mod(Modify::Purged(attr.into())));
999        // purge existing sessions
1000
1001        // Modify
1002        self.qs_write
1003            .impersonate_modify(
1004                // Filter as executed
1005                &filter!(f_eq(Attribute::Uuid, PartialValue::Uuid(target_uuid))),
1006                // Filter as intended (acp)
1007                &filter_all!(f_eq(Attribute::Uuid, PartialValue::Uuid(target_uuid))),
1008                &modlist,
1009                // Provide the entry to impersonate
1010                ident,
1011            )
1012            .map_err(|e| {
1013                admin_error!("Failed to migrate service account to person - {:?}", e);
1014                e
1015            })
1016    }
1017}
1018
1019pub struct ListUserAuthTokenEvent {
1020    // Who initiated this?
1021    pub ident: Identity,
1022    // Who is it targeting?
1023    pub target: Uuid,
1024}
1025
1026impl IdmServerProxyReadTransaction<'_> {
1027    pub fn account_list_user_auth_tokens(
1028        &mut self,
1029        lte: &ListUserAuthTokenEvent,
1030    ) -> Result<Vec<UatStatus>, OperationError> {
1031        // Make an event from the request
1032        let srch = match SearchEvent::from_target_uuid_request(
1033            lte.ident.clone(),
1034            lte.target,
1035            &self.qs_read,
1036        ) {
1037            Ok(s) => s,
1038            Err(e) => {
1039                admin_error!("Failed to begin account list user auth tokens: {:?}", e);
1040                return Err(e);
1041            }
1042        };
1043
1044        match self.qs_read.search_ext(&srch) {
1045            Ok(mut entries) => {
1046                entries
1047                    .pop()
1048                    // get the first entry
1049                    .and_then(|e| {
1050                        let account_id = e.get_uuid();
1051                        // From the entry, turn it into the value
1052                        e.get_ava_as_session_map(Attribute::UserAuthTokenSession)
1053                            .map(|smap| {
1054                                smap.iter()
1055                                    .map(|(u, s)| {
1056                                        let state = match s.state {
1057                                            SessionState::ExpiresAt(odt) => {
1058                                                UatStatusState::ExpiresAt(odt)
1059                                            }
1060                                            SessionState::NeverExpires => {
1061                                                UatStatusState::NeverExpires
1062                                            }
1063                                            SessionState::RevokedAt(_) => UatStatusState::Revoked,
1064                                        };
1065
1066                                        s.scope
1067                                            .try_into()
1068                                            .map(|purpose| UatStatus {
1069                                                account_id,
1070                                                session_id: *u,
1071                                                state,
1072                                                issued_at: s.issued_at,
1073                                                purpose,
1074                                            })
1075                                            .inspect_err(|_e| {
1076                                                admin_error!("Invalid user auth token {}", u);
1077                                            })
1078                                    })
1079                                    .collect::<Result<Vec<_>, _>>()
1080                            })
1081                    })
1082                    .unwrap_or_else(|| {
1083                        // No matching entry? Return none.
1084                        Ok(Vec::with_capacity(0))
1085                    })
1086            }
1087            Err(e) => Err(e),
1088        }
1089    }
1090}
1091
1092#[cfg(test)]
1093mod tests {
1094    use crate::idm::accountpolicy::ResolvedAccountPolicy;
1095    use crate::prelude::*;
1096    use kanidm_proto::internal::UiHint;
1097
1098    #[idm_test]
1099    async fn test_idm_account_ui_hints(idms: &IdmServer, _idms_delayed: &mut IdmServerDelayed) {
1100        let ct = duration_from_epoch_now();
1101        let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();
1102
1103        let target_uuid = Uuid::new_v4();
1104
1105        // Create a user. So far no ui hints.
1106        // Create a service account
1107        let e = entry_init!(
1108            (Attribute::Class, EntryClass::Object.to_value()),
1109            (Attribute::Class, EntryClass::Account.to_value()),
1110            (Attribute::Class, EntryClass::Person.to_value()),
1111            (Attribute::Name, Value::new_iname("testaccount")),
1112            (Attribute::Uuid, Value::Uuid(target_uuid)),
1113            (Attribute::Description, Value::new_utf8s("testaccount")),
1114            (Attribute::DisplayName, Value::new_utf8s("Test Account"))
1115        );
1116
1117        let ce = CreateEvent::new_internal(vec![e]);
1118        assert!(idms_prox_write.qs_write.create(&ce).is_ok());
1119
1120        let account = idms_prox_write
1121            .target_to_account(target_uuid)
1122            .expect("account must exist");
1123        let session_id = uuid::Uuid::new_v4();
1124        let uat = account
1125            .to_userauthtoken(
1126                session_id,
1127                SessionScope::ReadWrite,
1128                ct,
1129                &ResolvedAccountPolicy::test_policy(),
1130            )
1131            .expect("Unable to create uat");
1132
1133        // Check the ui hints are as expected.
1134        assert_eq!(uat.ui_hints.len(), 1);
1135        assert!(uat.ui_hints.contains(&UiHint::CredentialUpdate));
1136
1137        // Modify the user to be a posix account, ensure they get the hint.
1138        let me_posix = ModifyEvent::new_internal_invalid(
1139            filter!(f_eq(
1140                Attribute::Name,
1141                PartialValue::new_iname("testaccount")
1142            )),
1143            ModifyList::new_list(vec![
1144                Modify::Present(Attribute::Class, EntryClass::PosixAccount.into()),
1145                Modify::Present(Attribute::GidNumber, Value::new_uint32(2001)),
1146            ]),
1147        );
1148        assert!(idms_prox_write.qs_write.modify(&me_posix).is_ok());
1149
1150        // Check the ui hints are as expected.
1151        let account = idms_prox_write
1152            .target_to_account(target_uuid)
1153            .expect("account must exist");
1154        let session_id = uuid::Uuid::new_v4();
1155        let uat = account
1156            .to_userauthtoken(
1157                session_id,
1158                SessionScope::ReadWrite,
1159                ct,
1160                &ResolvedAccountPolicy::test_policy(),
1161            )
1162            .expect("Unable to create uat");
1163
1164        assert_eq!(uat.ui_hints.len(), 2);
1165        assert!(uat.ui_hints.contains(&UiHint::PosixAccount));
1166        assert!(uat.ui_hints.contains(&UiHint::CredentialUpdate));
1167
1168        // Add a group with a ui hint, and then check they get the hint.
1169        let e = entry_init!(
1170            (Attribute::Class, EntryClass::Object.to_value()),
1171            (Attribute::Class, EntryClass::Group.to_value()),
1172            (Attribute::Name, Value::new_iname("test_uihint_group")),
1173            (Attribute::Member, Value::Refer(target_uuid)),
1174            (
1175                Attribute::GrantUiHint,
1176                Value::UiHint(UiHint::ExperimentalFeatures)
1177            )
1178        );
1179
1180        let ce = CreateEvent::new_internal(vec![e]);
1181        assert!(idms_prox_write.qs_write.create(&ce).is_ok());
1182
1183        // Check the ui hints are as expected.
1184        let account = idms_prox_write
1185            .target_to_account(target_uuid)
1186            .expect("account must exist");
1187        let session_id = uuid::Uuid::new_v4();
1188        let uat = account
1189            .to_userauthtoken(
1190                session_id,
1191                SessionScope::ReadWrite,
1192                ct,
1193                &ResolvedAccountPolicy::test_policy(),
1194            )
1195            .expect("Unable to create uat");
1196
1197        assert_eq!(uat.ui_hints.len(), 3);
1198        assert!(uat.ui_hints.contains(&UiHint::PosixAccount));
1199        assert!(uat.ui_hints.contains(&UiHint::ExperimentalFeatures));
1200        assert!(uat.ui_hints.contains(&UiHint::CredentialUpdate));
1201
1202        assert!(idms_prox_write.commit().is_ok());
1203    }
1204}