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 let ident_memberof = ident.get_memberof();
49 let ident_uuid = ident.get_uuid();
50
51 match modify_ident_test(ident) {
55 AccessBasicResult::Deny => denied = true,
56 AccessBasicResult::Grant => grant = true,
57 AccessBasicResult::Ignore => {}
58 }
59
60 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 AccessModResult::Allow { .. } | AccessModResult::Ignore => {}
84 }
85
86 if !grant && !denied {
87 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 AccessModResult::Allow { .. } | AccessModResult::Ignore => {}
102 }
103
104 let scoped_acp: Vec<&AccessControlModify> = related_acp
106 .iter()
107 .filter_map(|acm| {
108 match &acm.receiver_condition {
109 AccessControlReceiverCondition::GroupChecked => {
110 }
113 AccessControlReceiverCondition::EntryManager => {
114 if let Some(entry_manager_uuids) =
121 entry.get_ava_refer(Attribute::EntryManagedBy)
122 {
123 let group_check = ident_memberof
124 .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 return None;
135 }
136 } else {
137 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 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 &constrain_pres & &allow_pres
186 } else {
187 allow_pres
188 };
189
190 let allowed_rem = if !constrain_rem.is_empty() {
191 &constrain_rem & &allow_rem
193 } else {
194 allow_rem
195 };
196
197 let mut allowed_pres_cls = if !constrain_pres_cls.is_empty() {
198 &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 &constrain_rem_cls & &allow_rem_cls
207 } else {
208 allow_rem_cls
209 };
210
211 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 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 }
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 AccessModResult::Ignore
297 }
298 IdentType::User(_) => {
299 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
337fn modify_protected_attrs<'a>(
339 ident: &Identity,
340 entry: &Arc<EntrySealedCommitted>,
341) -> AccessModResult<'a> {
342 match &ident.origin {
343 IdentType::Internal | IdentType::Synch(_) => {
344 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 AccessModResult::Ignore
352 } else {
353 modify_protected_entry_attrs(classes)
355 }
356 } else {
357 AccessModResult::Ignore
360 }
361 }
362 }
363}
364
365fn modify_protected_entry_attrs<'a>(classes: &BTreeSet<String>) -> AccessModResult<'a> {
366 if !classes.is_disjoint(&LOCKED_ENTRY_CLASSES) {
371 return AccessModResult::Deny;
373 }
374
375 let mut constrain_attrs = BTreeSet::default();
376
377 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 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 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 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}