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