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