kanidmd_lib/server/access/
search.rs

1use crate::prelude::*;
2use std::collections::BTreeSet;
3
4use super::profiles::{
5    AccessControlReceiverCondition, AccessControlSearchResolved, AccessControlTargetCondition,
6};
7use super::AccessSrchResult;
8use std::sync::Arc;
9
10pub(super) enum SearchResult {
11    Deny,
12    Grant,
13    Allow(BTreeSet<Attribute>),
14}
15
16pub(super) fn apply_search_access(
17    ident: &Identity,
18    related_acp: &[AccessControlSearchResolved],
19    entry: &Arc<EntrySealedCommitted>,
20) -> SearchResult {
21    // This could be considered "slow" due to allocs each iter with the entry. We
22    // could move these out of the loop and reuse, but there are likely risks to
23    // that.
24    let mut denied = false;
25    let mut grant = false;
26    let constrain = BTreeSet::default();
27    let mut allow = BTreeSet::default();
28
29    // The access control profile
30    match search_filter_entry(ident, related_acp, entry) {
31        AccessSrchResult::Deny => denied = true,
32        AccessSrchResult::Grant => grant = true,
33        AccessSrchResult::Ignore => {}
34        // AccessSrchResult::Constrain { mut attr } => constrain.append(&mut attr),
35        AccessSrchResult::Allow { mut attr } => allow.append(&mut attr),
36    };
37
38    match search_oauth2_filter_entry(ident, entry) {
39        AccessSrchResult::Deny => denied = true,
40        AccessSrchResult::Grant => grant = true,
41        AccessSrchResult::Ignore => {}
42        // AccessSrchResult::Constrain { mut attr } => constrain.append(&mut attr),
43        AccessSrchResult::Allow { mut attr } => allow.append(&mut attr),
44    };
45
46    match search_applications_filter_entry(ident, entry) {
47        AccessSrchResult::Deny => denied = true,
48        AccessSrchResult::Grant => grant = true,
49        AccessSrchResult::Ignore => {}
50        // AccessSrchResult::Constrain { mut attr } => constrain.append(&mut attr),
51        AccessSrchResult::Allow { mut attr } => allow.append(&mut attr),
52    };
53
54    match search_sync_account_filter_entry(ident, entry) {
55        AccessSrchResult::Deny => denied = true,
56        AccessSrchResult::Grant => grant = true,
57        AccessSrchResult::Ignore => {}
58        // AccessSrchResult::Constrain{ mut attr } => constrain.append(&mut attr),
59        AccessSrchResult::Allow { mut attr } => allow.append(&mut attr),
60    };
61
62    // We'll add more modules later.
63
64    // Now finalise the decision.
65
66    if denied {
67        SearchResult::Deny
68    } else if grant {
69        SearchResult::Grant
70    } else {
71        let allowed_attrs = if !constrain.is_empty() {
72            // bit_and
73            &constrain & &allow
74        } else {
75            allow
76        };
77        SearchResult::Allow(allowed_attrs)
78    }
79}
80
81fn search_filter_entry(
82    ident: &Identity,
83    related_acp: &[AccessControlSearchResolved],
84    entry: &Arc<EntrySealedCommitted>,
85) -> AccessSrchResult {
86    // If this is an internal search, return our working set.
87    match &ident.origin {
88        IdentType::Internal => {
89            trace!(uuid = ?entry.get_display_id(), "Internal operation, bypassing access check");
90            // No need to check ACS
91            return AccessSrchResult::Grant;
92        }
93        IdentType::Synch(_) => {
94            security_debug!(uuid = ?entry.get_display_id(), "Blocking sync check");
95            return AccessSrchResult::Deny;
96        }
97        IdentType::User(_) => {}
98    };
99    debug!(event = %ident, "Access check for search (filter) event");
100
101    match ident.access_scope() {
102        AccessScope::Synchronise => {
103            security_debug!(
104                "denied ❌ - identity access scope 'Synchronise' is not permitted to search"
105            );
106            return AccessSrchResult::Deny;
107        }
108        AccessScope::ReadOnly | AccessScope::ReadWrite => {
109            // As you were
110        }
111    };
112
113    // needed for checking entry manager conditions.
114    let ident_memberof = ident.get_memberof();
115    let ident_uuid = ident.get_uuid();
116
117    let allowed_attrs: BTreeSet<Attribute> = related_acp
118        .iter()
119        .filter_map(|acs| {
120            // Assert that the receiver condition applies.
121            match &acs.receiver_condition {
122                AccessControlReceiverCondition::GroupChecked => {
123                    // The groups were already checked during filter resolution. Trust
124                    // that result, and continue.
125                }
126                AccessControlReceiverCondition::EntryManager => {
127                    // This condition relies on the entry we are looking at to have a back-ref
128                    // to our uuid or a group we are in as an entry manager.
129
130                    // Note, while schema has this as single value, we currently
131                    // fetch it as a multivalue btreeset for future incase we allow
132                    // multiple entry manager by in future.
133                    if let Some(entry_manager_uuids) = entry.get_ava_refer(Attribute::EntryManagedBy) {
134                        let group_check = ident_memberof
135                            // Have at least one group allowed.
136                            .map(|imo| imo.intersection(entry_manager_uuids).next().is_some())
137                            .unwrap_or_default();
138
139                        let user_check = ident_uuid
140                            .map(|u| entry_manager_uuids.contains(&u))
141                            .unwrap_or_default();
142
143                        if !(group_check || user_check) {
144                            // Not the entry manager
145                            return None
146                        }
147                    } else {
148                        // Can not satisfy.
149                        return None
150                    }
151                }
152            };
153
154            match &acs.target_condition {
155                AccessControlTargetCondition::Scope(f_res) => {
156                    if !entry.entry_match_no_index(f_res) {
157                        security_debug!(entry = ?entry.get_display_id(), acs = %acs.acp.acp.name, "entry DOES NOT match acs");
158                        return None
159                    }
160                }
161            };
162
163            // -- Conditions pass -- release the attributes.
164
165            security_debug!(entry = ?entry.get_display_id(), acs = %acs.acp.acp.name, "acs applied to entry");
166            // add search_attrs to allowed.
167            Some(acs.acp.attrs.iter().cloned())
168        })
169        .flatten()
170        .collect();
171
172    AccessSrchResult::Allow {
173        attr: allowed_attrs,
174    }
175}
176
177fn search_oauth2_filter_entry(
178    ident: &Identity,
179    entry: &Arc<EntrySealedCommitted>,
180) -> AccessSrchResult {
181    match &ident.origin {
182        IdentType::Internal | IdentType::Synch(_) => AccessSrchResult::Ignore,
183        IdentType::User(iuser) => {
184            if iuser.entry.get_uuid() == UUID_ANONYMOUS {
185                debug!("Anonymous can't access OAuth2 entries, ignoring");
186                return AccessSrchResult::Ignore;
187            }
188
189            let contains_o2_rs = entry
190                .get_ava_as_iutf8(Attribute::Class)
191                .map(|set| {
192                    trace!(?set);
193                    set.contains(&EntryClass::OAuth2ResourceServer.to_string())
194                })
195                .unwrap_or(false);
196
197            let contains_o2_scope_member = entry
198                .get_ava_as_oauthscopemaps(Attribute::OAuth2RsScopeMap)
199                .and_then(|maps| ident.get_memberof().map(|mo| (maps, mo)))
200                .map(|(maps, mo)| maps.keys().any(|k| mo.contains(k)))
201                .unwrap_or(false);
202
203            if contains_o2_rs && contains_o2_scope_member {
204                security_debug!(entry = ?entry.get_uuid(), ident = ?iuser.entry.get_uuid2rdn(), "ident is a memberof a group granted an oauth2 scope by this entry");
205
206                return AccessSrchResult::Allow {
207                    attr: btreeset!(
208                        Attribute::Class,
209                        Attribute::DisplayName,
210                        Attribute::Uuid,
211                        Attribute::Name,
212                        Attribute::OAuth2RsOriginLanding,
213                        Attribute::Image
214                    ),
215                };
216            }
217            AccessSrchResult::Ignore
218        }
219    }
220}
221
222fn search_applications_filter_entry(
223    ident: &Identity,
224    entry: &Arc<EntrySealedCommitted>,
225) -> AccessSrchResult {
226    match &ident.origin {
227        IdentType::Internal | IdentType::Synch(_) => AccessSrchResult::Ignore,
228        IdentType::User(iuser) => {
229            if iuser.entry.get_uuid() == UUID_ANONYMOUS {
230                debug!("Anonymous can't access application entries, ignoring");
231                return AccessSrchResult::Ignore;
232            }
233
234            let contains_application = entry
235                .get_ava_as_iutf8(Attribute::Class)
236                .map(|set| {
237                    trace!(?set);
238                    set.contains(&EntryClass::Application.to_string())
239                })
240                .unwrap_or(false);
241
242            let contains_application_linked_group = entry
243                .get_ava_single_refer(Attribute::LinkedGroup)
244                .and_then(|group_uuid| ident.get_memberof().map(|mo| mo.contains(&group_uuid)))
245                .unwrap_or(false);
246
247            trace!(?entry);
248
249            if contains_application && contains_application_linked_group {
250                security_debug!(entry = ?entry.get_uuid(), ident = ?iuser.entry.get_uuid2rdn(), "ident is a memberof a group granted application access for this entry");
251
252                return AccessSrchResult::Allow {
253                    attr: btreeset!(
254                        Attribute::Class,
255                        Attribute::DisplayName,
256                        Attribute::Uuid,
257                        Attribute::Name,
258                        Attribute::LinkedGroup
259                    ),
260                };
261            }
262            AccessSrchResult::Ignore
263        }
264    }
265}
266
267fn search_sync_account_filter_entry(
268    ident: &Identity,
269    entry: &Arc<EntrySealedCommitted>,
270) -> AccessSrchResult {
271    match &ident.origin {
272        IdentType::Internal | IdentType::Synch(_) => AccessSrchResult::Ignore,
273        IdentType::User(iuser) => {
274            // Is the user a synced object?
275            let is_user_sync_account = iuser
276                .entry
277                .get_ava_as_iutf8(Attribute::Class)
278                .map(|set| {
279                    trace!(?set);
280                    set.contains(&EntryClass::SyncObject.to_string())
281                        && set.contains(EntryClass::Account.into())
282                })
283                .unwrap_or(false);
284
285            if is_user_sync_account {
286                let is_target_sync_account = entry
287                    .get_ava_as_iutf8(Attribute::Class)
288                    .map(|set| {
289                        trace!(?set);
290                        set.contains(&EntryClass::SyncAccount.to_string())
291                    })
292                    .unwrap_or(false);
293
294                if is_target_sync_account {
295                    // Okay, now we need to check if the uuids line up.
296                    let sync_uuid = entry.get_uuid();
297                    let sync_source_match = iuser
298                        .entry
299                        .get_ava_single_refer(Attribute::SyncParentUuid)
300                        .map(|sync_parent_uuid| sync_parent_uuid == sync_uuid)
301                        .unwrap_or(false);
302
303                    if sync_source_match {
304                        // We finally got here!
305                        security_debug!(entry = ?entry.get_uuid(), ident = ?iuser.entry.get_uuid2rdn(), "ident is a synchronised account from this sync account");
306
307                        return AccessSrchResult::Allow {
308                            attr: btreeset!(
309                                Attribute::Class,
310                                Attribute::Uuid,
311                                Attribute::SyncCredentialPortal
312                            ),
313                        };
314                    }
315                }
316            }
317            // Fall through
318            AccessSrchResult::Ignore
319        }
320    }
321}