kanidmd_lib/server/access/
search.rs1use 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    let mut denied = false;
25    let mut grant = false;
26    let constrain = BTreeSet::default();
27    let mut allow = BTreeSet::default();
28
29    match search_filter_entry(ident, related_acp, entry) {
31        AccessSrchResult::Deny => denied = true,
32        AccessSrchResult::Grant => grant = true,
33        AccessSrchResult::Ignore => {}
34        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::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::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::Allow { mut attr } => allow.append(&mut attr),
60    };
61
62    if denied {
67        SearchResult::Deny
68    } else if grant {
69        SearchResult::Grant
70    } else {
71        let allowed_attrs = if !constrain.is_empty() {
72            &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    match &ident.origin {
88        IdentType::Internal => {
89            trace!(uuid = ?entry.get_display_id(), "Internal operation, bypassing access check");
90            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            }
111    };
112
113    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            match &acs.receiver_condition {
122                AccessControlReceiverCondition::GroupChecked => {
123                    }
126                AccessControlReceiverCondition::EntryManager => {
127                    if let Some(entry_manager_uuids) = entry.get_ava_refer(Attribute::EntryManagedBy) {
134                        let group_check = ident_memberof
135                            .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                            return None
146                        }
147                    } else {
148                        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                        debug!(entry = ?entry.get_display_id(), acs = %acs.acp.acp.name, action="search_filter", "entry DOES NOT match acs");
158                        return None
159                    }
160                }
161            };
162
163            debug!(entry = ?entry.get_display_id(), acs = %acs.acp.acp.name, "acs applied to entry");
165            Some(acs.acp.attrs.iter().cloned())
167        })
168        .flatten()
169        .collect();
170
171    AccessSrchResult::Allow {
172        attr: allowed_attrs,
173    }
174}
175
176fn search_oauth2_filter_entry(
177    ident: &Identity,
178    entry: &Arc<EntrySealedCommitted>,
179) -> AccessSrchResult {
180    match &ident.origin {
181        IdentType::Internal | IdentType::Synch(_) => AccessSrchResult::Ignore,
182        IdentType::User(iuser) => {
183            if iuser.entry.get_uuid() == UUID_ANONYMOUS {
184                debug!("Anonymous can't access OAuth2 entries, ignoring");
185                return AccessSrchResult::Ignore;
186            }
187
188            let contains_o2_rs = entry
189                .get_ava_as_iutf8(Attribute::Class)
190                .map(|set| {
191                    trace!(?set);
192                    set.contains(&EntryClass::OAuth2ResourceServer.to_string())
193                })
194                .unwrap_or(false);
195
196            let contains_o2_scope_member = entry
197                .get_ava_as_oauthscopemaps(Attribute::OAuth2RsScopeMap)
198                .and_then(|maps| ident.get_memberof().map(|mo| (maps, mo)))
199                .map(|(maps, mo)| maps.keys().any(|k| mo.contains(k)))
200                .unwrap_or(false);
201
202            if contains_o2_rs && contains_o2_scope_member {
203                security_debug!(entry = ?entry.get_uuid(), ident = ?iuser.entry.get_uuid2rdn(), "ident is a memberof a group granted an oauth2 scope by this entry");
204
205                return AccessSrchResult::Allow {
206                    attr: btreeset!(
207                        Attribute::Class,
208                        Attribute::DisplayName,
209                        Attribute::Uuid,
210                        Attribute::Name,
211                        Attribute::OAuth2RsOriginLanding,
212                        Attribute::Image
213                    ),
214                };
215            }
216            AccessSrchResult::Ignore
217        }
218    }
219}
220
221fn search_applications_filter_entry(
222    ident: &Identity,
223    entry: &Arc<EntrySealedCommitted>,
224) -> AccessSrchResult {
225    match &ident.origin {
226        IdentType::Internal | IdentType::Synch(_) => AccessSrchResult::Ignore,
227        IdentType::User(iuser) => {
228            if iuser.entry.get_uuid() == UUID_ANONYMOUS {
229                debug!("Anonymous can't access application entries, ignoring");
230                return AccessSrchResult::Ignore;
231            }
232
233            let contains_application = entry
234                .get_ava_as_iutf8(Attribute::Class)
235                .map(|set| {
236                    trace!(?set);
237                    set.contains(&EntryClass::Application.to_string())
238                })
239                .unwrap_or(false);
240
241            let contains_application_linked_group = entry
242                .get_ava_single_refer(Attribute::LinkedGroup)
243                .and_then(|group_uuid| ident.get_memberof().map(|mo| mo.contains(&group_uuid)))
244                .unwrap_or(false);
245
246            trace!(?entry);
247
248            if contains_application && contains_application_linked_group {
249                security_debug!(entry = ?entry.get_uuid(), ident = ?iuser.entry.get_uuid2rdn(), "ident is a memberof a group granted application access for this entry");
250
251                return AccessSrchResult::Allow {
252                    attr: btreeset!(
253                        Attribute::Class,
254                        Attribute::DisplayName,
255                        Attribute::Uuid,
256                        Attribute::Name,
257                        Attribute::LinkedGroup
258                    ),
259                };
260            }
261            AccessSrchResult::Ignore
262        }
263    }
264}
265
266fn search_sync_account_filter_entry(
267    ident: &Identity,
268    entry: &Arc<EntrySealedCommitted>,
269) -> AccessSrchResult {
270    match &ident.origin {
271        IdentType::Internal | IdentType::Synch(_) => AccessSrchResult::Ignore,
272        IdentType::User(iuser) => {
273            let is_user_sync_account = iuser
275                .entry
276                .get_ava_as_iutf8(Attribute::Class)
277                .map(|set| {
278                    trace!(?set);
279                    set.contains(&EntryClass::SyncObject.to_string())
280                        && set.contains(EntryClass::Account.into())
281                })
282                .unwrap_or(false);
283
284            if is_user_sync_account {
285                let is_target_sync_account = entry
286                    .get_ava_as_iutf8(Attribute::Class)
287                    .map(|set| {
288                        trace!(?set);
289                        set.contains(&EntryClass::SyncAccount.to_string())
290                    })
291                    .unwrap_or(false);
292
293                if is_target_sync_account {
294                    let sync_uuid = entry.get_uuid();
296                    let sync_source_match = iuser
297                        .entry
298                        .get_ava_single_refer(Attribute::SyncParentUuid)
299                        .map(|sync_parent_uuid| sync_parent_uuid == sync_uuid)
300                        .unwrap_or(false);
301
302                    if sync_source_match {
303                        security_debug!(entry = ?entry.get_uuid(), ident = ?iuser.entry.get_uuid2rdn(), "ident is a synchronised account from this sync account");
305
306                        return AccessSrchResult::Allow {
307                            attr: btreeset!(
308                                Attribute::Class,
309                                Attribute::Uuid,
310                                Attribute::SyncCredentialPortal
311                            ),
312                        };
313                    }
314                }
315            }
316            AccessSrchResult::Ignore
318        }
319    }
320}