kanidmd_lib/server/access/
modify.rs

1use super::profiles::{
2    AccessControlModify, AccessControlModifyResolved, AccessControlReceiverCondition,
3    AccessControlTargetCondition,
4};
5use super::protected::{
6    LOCKED_ENTRY_CLASSES, PROTECTED_MOD_ENTRY_CLASSES, PROTECTED_MOD_PRES_ENTRY_CLASSES,
7    PROTECTED_MOD_REM_ENTRY_CLASSES,
8};
9use super::{AccessBasicResult, AccessModResult};
10use crate::prelude::*;
11use hashbrown::HashMap;
12use std::collections::BTreeSet;
13use std::sync::Arc;
14
15pub(super) enum ModifyResult<'a> {
16    Deny,
17    Grant,
18    Allow {
19        pres: BTreeSet<Attribute>,
20        rem: BTreeSet<Attribute>,
21        pres_cls: BTreeSet<&'a str>,
22        rem_cls: BTreeSet<&'a str>,
23    },
24}
25
26pub(super) fn apply_modify_access<'a>(
27    ident: &Identity,
28    related_acp: &'a [AccessControlModifyResolved],
29    sync_agreements: &HashMap<Uuid, BTreeSet<Attribute>>,
30    entry: &Arc<EntrySealedCommitted>,
31) -> ModifyResult<'a> {
32    let mut denied = false;
33    let mut grant = false;
34
35    let mut constrain_pres = BTreeSet::default();
36    let mut allow_pres = BTreeSet::default();
37    let mut constrain_rem = BTreeSet::default();
38    let mut allow_rem = BTreeSet::default();
39
40    let mut constrain_pres_cls = BTreeSet::default();
41    let mut allow_pres_cls = BTreeSet::default();
42
43    let mut constrain_rem_cls = BTreeSet::default();
44    let mut allow_rem_cls = BTreeSet::default();
45
46    // Some useful references.
47    //  - needed for checking entry manager conditions.
48    let ident_memberof = ident.get_memberof();
49    let ident_uuid = ident.get_uuid();
50
51    // run each module. These have to be broken down further due to modify
52    // kind of being three operations all in one.
53
54    match modify_ident_test(ident) {
55        AccessBasicResult::Deny => denied = true,
56        AccessBasicResult::Grant => grant = true,
57        AccessBasicResult::Ignore => {}
58    }
59
60    // Check with protected if we should proceed.
61    match modify_protected_attrs(ident, entry) {
62        AccessModResult::Deny => denied = true,
63        AccessModResult::Constrain {
64            mut pres_attr,
65            mut rem_attr,
66            pres_cls,
67            rem_cls,
68        } => {
69            constrain_rem.append(&mut rem_attr);
70            constrain_pres.append(&mut pres_attr);
71
72            if let Some(mut pres_cls) = pres_cls {
73                constrain_pres_cls.append(&mut pres_cls);
74            }
75
76            if let Some(mut rem_cls) = rem_cls {
77                constrain_rem_cls.append(&mut rem_cls);
78            }
79        }
80        // Can't grant.
81        // AccessModResult::Grant |
82        // Can't allow
83        AccessModResult::Allow { .. } | AccessModResult::Ignore => {}
84    }
85
86    if !grant && !denied {
87        // If it's a sync entry, constrain it.
88        match modify_sync_constrain(ident, entry, sync_agreements) {
89            AccessModResult::Deny => denied = true,
90            AccessModResult::Constrain {
91                mut pres_attr,
92                mut rem_attr,
93                ..
94            } => {
95                constrain_rem.append(&mut rem_attr);
96                constrain_pres.append(&mut pres_attr);
97            }
98            // Can't grant.
99            // AccessModResult::Grant |
100            // Can't allow
101            AccessModResult::Allow { .. } | AccessModResult::Ignore => {}
102        }
103
104        // Setup the acp's here
105        let scoped_acp: Vec<&AccessControlModify> = related_acp
106            .iter()
107            .filter_map(|acm| {
108                match &acm.receiver_condition {
109                    AccessControlReceiverCondition::GroupChecked => {
110                        // The groups were already checked during filter resolution. Trust
111                        // that result, and continue.
112                    }
113                    AccessControlReceiverCondition::EntryManager => {
114                        // This condition relies on the entry we are looking at to have a back-ref
115                        // to our uuid or a group we are in as an entry manager.
116
117                        // Note, while schema has this as single value, we currently
118                        // fetch it as a multivalue btreeset for future incase we allow
119                        // multiple entry manager by in future.
120                        if let Some(entry_manager_uuids) =
121                            entry.get_ava_refer(Attribute::EntryManagedBy)
122                        {
123                            let group_check = ident_memberof
124                                // Have at least one group allowed.
125                                .map(|imo| imo.intersection(entry_manager_uuids).next().is_some())
126                                .unwrap_or_default();
127
128                            let user_check = ident_uuid
129                                .map(|u| entry_manager_uuids.contains(&u))
130                                .unwrap_or_default();
131
132                            if !(group_check || user_check) {
133                                // Not the entry manager
134                                return None;
135                            }
136                        } else {
137                            // Can not satisfy.
138                            return None;
139                        }
140                    }
141                };
142
143                match &acm.target_condition {
144                    AccessControlTargetCondition::Scope(f_res) => {
145                        if !entry.entry_match_no_index(f_res) {
146                            debug!(entry = ?entry.get_display_id(), acm = %acm.acp.acp.name, "entry DOES NOT match acs");
147                            return None;
148                        }
149                    }
150                };
151
152                debug!(entry = ?entry.get_display_id(), acs = %acm.acp.acp.name, "acs applied to entry");
153
154                Some(acm.acp)
155            })
156            .collect();
157
158        match modify_pres_test(scoped_acp.as_slice()) {
159            AccessModResult::Deny => denied = true,
160            // Can never return a unilateral grant.
161            // AccessModResult::Grant => {}
162            AccessModResult::Ignore => {}
163            AccessModResult::Constrain { .. } => {}
164            AccessModResult::Allow {
165                mut pres_attr,
166                mut rem_attr,
167                mut pres_class,
168                mut rem_class,
169            } => {
170                allow_pres.append(&mut pres_attr);
171                allow_rem.append(&mut rem_attr);
172                allow_pres_cls.append(&mut pres_class);
173                allow_rem_cls.append(&mut rem_class);
174            }
175        }
176    }
177
178    if denied {
179        ModifyResult::Deny
180    } else if grant {
181        ModifyResult::Grant
182    } else {
183        let allowed_pres = if !constrain_pres.is_empty() {
184            // bit_and
185            &constrain_pres & &allow_pres
186        } else {
187            allow_pres
188        };
189
190        let allowed_rem = if !constrain_rem.is_empty() {
191            // bit_and
192            &constrain_rem & &allow_rem
193        } else {
194            allow_rem
195        };
196
197        let mut allowed_pres_cls = if !constrain_pres_cls.is_empty() {
198            // bit_and
199            &constrain_pres_cls & &allow_pres_cls
200        } else {
201            allow_pres_cls
202        };
203
204        let mut allowed_rem_cls = if !constrain_rem_cls.is_empty() {
205            // bit_and
206            &constrain_rem_cls & &allow_rem_cls
207        } else {
208            allow_rem_cls
209        };
210
211        // Deny these classes from being part of any addition or removal to an entry
212        for protected_cls in PROTECTED_MOD_PRES_ENTRY_CLASSES.iter() {
213            allowed_pres_cls.remove(protected_cls.as_str());
214        }
215
216        for protected_cls in PROTECTED_MOD_REM_ENTRY_CLASSES.iter() {
217            allowed_rem_cls.remove(protected_cls.as_str());
218        }
219
220        ModifyResult::Allow {
221            pres: allowed_pres,
222            rem: allowed_rem,
223            pres_cls: allowed_pres_cls,
224            rem_cls: allowed_rem_cls,
225        }
226    }
227}
228
229fn modify_ident_test(ident: &Identity) -> AccessBasicResult {
230    match &ident.origin {
231        IdentType::Internal => {
232            trace!("Internal operation, bypassing access check");
233            // No need to check ACS
234            return AccessBasicResult::Grant;
235        }
236        IdentType::Synch(_) => {
237            security_critical!("Blocking sync check");
238            return AccessBasicResult::Deny;
239        }
240        IdentType::User(_) => {}
241    };
242    debug!(event = %ident, "Access check for modify event");
243
244    match ident.access_scope() {
245        AccessScope::ReadOnly | AccessScope::Synchronise => {
246            security_access!("denied ❌ - identity access scope is not permitted to modify");
247            return AccessBasicResult::Deny;
248        }
249        AccessScope::ReadWrite => {
250            // As you were
251        }
252    };
253
254    AccessBasicResult::Ignore
255}
256
257fn modify_pres_test<'a>(scoped_acp: &[&'a AccessControlModify]) -> AccessModResult<'a> {
258    let pres_attr: BTreeSet<Attribute> = scoped_acp
259        .iter()
260        .flat_map(|acp| acp.presattrs.iter().cloned())
261        .collect();
262
263    let rem_attr: BTreeSet<Attribute> = scoped_acp
264        .iter()
265        .flat_map(|acp| acp.remattrs.iter().cloned())
266        .collect();
267
268    let pres_class: BTreeSet<&'a str> = scoped_acp
269        .iter()
270        .flat_map(|acp| acp.pres_classes.iter().map(|s| s.as_str()))
271        .collect();
272
273    let rem_class: BTreeSet<&'a str> = scoped_acp
274        .iter()
275        .flat_map(|acp| acp.rem_classes.iter().map(|s| s.as_str()))
276        .collect();
277
278    AccessModResult::Allow {
279        pres_attr,
280        rem_attr,
281        pres_class,
282        rem_class,
283    }
284}
285
286fn modify_sync_constrain<'a>(
287    ident: &Identity,
288    entry: &Arc<EntrySealedCommitted>,
289    sync_agreements: &HashMap<Uuid, BTreeSet<Attribute>>,
290) -> AccessModResult<'a> {
291    match &ident.origin {
292        IdentType::Internal => AccessModResult::Ignore,
293        IdentType::Synch(_) => {
294            // Allowed to mod sync objects. Later we'll probably need to check the limits of what
295            // it can do if we go that way.
296            AccessModResult::Ignore
297        }
298        IdentType::User(_) => {
299            // We need to meet these conditions.
300            // * We are a sync object
301            // * We have a sync_parent_uuid
302            let is_sync = entry
303                .get_ava_set(Attribute::Class)
304                .map(|classes| classes.contains(&EntryClass::SyncObject.into()))
305                .unwrap_or(false);
306
307            if !is_sync {
308                return AccessModResult::Ignore;
309            }
310
311            if let Some(sync_uuid) = entry.get_ava_single_refer(Attribute::SyncParentUuid) {
312                let mut set = btreeset![
313                    Attribute::UserAuthTokenSession,
314                    Attribute::OAuth2Session,
315                    Attribute::OAuth2ConsentScopeMap,
316                    Attribute::CredentialUpdateIntentToken
317                ];
318
319                if let Some(sync_yield_authority) = sync_agreements.get(&sync_uuid) {
320                    set.extend(sync_yield_authority.iter().cloned())
321                }
322
323                AccessModResult::Constrain {
324                    pres_attr: set.clone(),
325                    rem_attr: set,
326                    pres_cls: None,
327                    rem_cls: None,
328                }
329            } else {
330                warn!(entry = ?entry.get_uuid(), "sync_parent_uuid not found on sync object, preventing all access");
331                AccessModResult::Deny
332            }
333        }
334    }
335}
336
337/// Verify if the modification runs into limits that are defined by our protection rules.
338fn modify_protected_attrs<'a>(
339    ident: &Identity,
340    entry: &Arc<EntrySealedCommitted>,
341) -> AccessModResult<'a> {
342    match &ident.origin {
343        IdentType::Internal | IdentType::Synch(_) => {
344            // We don't constraint or influence these.
345            AccessModResult::Ignore
346        }
347        IdentType::User(_) => {
348            if let Some(classes) = entry.get_ava_as_iutf8(Attribute::Class) {
349                if classes.is_disjoint(&PROTECTED_MOD_ENTRY_CLASSES) {
350                    // Not protected, go ahead
351                    AccessModResult::Ignore
352                } else {
353                    // Okay, the entry is protected, apply the full ruleset.
354                    modify_protected_entry_attrs(classes)
355                }
356            } else {
357                // Nothing to check - this entry will fail to modify anyway because it has
358                // no classes
359                AccessModResult::Ignore
360            }
361        }
362    }
363}
364
365fn modify_protected_entry_attrs<'a>(classes: &BTreeSet<String>) -> AccessModResult<'a> {
366    // This is where the majority of the logic is - this contains the modification
367    // rules as they apply.
368
369    // First check for the hard-deny rules.
370    if !classes.is_disjoint(&LOCKED_ENTRY_CLASSES) {
371        // Hard deny attribute modifications to these types.
372        return AccessModResult::Deny;
373    }
374
375    let mut constrain_attrs = BTreeSet::default();
376
377    // Allows removal of the recycled class specifically on recycled entries.
378    if classes.contains(EntryClass::Recycled.into()) {
379        constrain_attrs.extend([Attribute::Class]);
380    }
381
382    if classes.contains(EntryClass::ClassType.into()) {
383        constrain_attrs.extend([Attribute::May, Attribute::Must]);
384    }
385
386    if classes.contains(EntryClass::SystemConfig.into()) {
387        constrain_attrs.extend([Attribute::BadlistPassword]);
388    }
389
390    // Allow domain settings.
391    if classes.contains(EntryClass::DomainInfo.into()) {
392        constrain_attrs.extend([
393            Attribute::DomainSsid,
394            Attribute::DomainLdapBasedn,
395            Attribute::LdapMaxQueryableAttrs,
396            Attribute::LdapAllowUnixPwBind,
397            Attribute::FernetPrivateKeyStr,
398            Attribute::Es256PrivateKeyDer,
399            Attribute::KeyActionRevoke,
400            Attribute::KeyActionRotate,
401            Attribute::IdVerificationEcKey,
402            Attribute::DeniedName,
403            Attribute::DomainDisplayName,
404            Attribute::Image,
405        ]);
406    }
407
408    // Allow account policy related attributes to be changed on dyngroup
409    if classes.contains(EntryClass::DynGroup.into()) {
410        constrain_attrs.extend([
411            Attribute::AuthSessionExpiry,
412            Attribute::AuthPasswordMinimumLength,
413            Attribute::CredentialTypeMinimum,
414            Attribute::PrivilegeExpiry,
415            Attribute::WebauthnAttestationCaList,
416            Attribute::LimitSearchMaxResults,
417            Attribute::LimitSearchMaxFilterTest,
418            Attribute::AllowPrimaryCredFallback,
419        ]);
420    }
421
422    // If we don't constrain the attributes at all, we have to deny the change
423    // from proceeding.
424    if constrain_attrs.is_empty() {
425        AccessModResult::Deny
426    } else {
427        AccessModResult::Constrain {
428            pres_attr: constrain_attrs.clone(),
429            rem_attr: constrain_attrs,
430            pres_cls: None,
431            rem_cls: None,
432        }
433    }
434}