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