Skip to main content

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