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_sync_account_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    // We'll add more modules later.
55
56    // Now finalise the decision.
57
58    if denied {
59        SearchResult::Deny
60    } else if grant {
61        SearchResult::Grant
62    } else {
63        let allowed_attrs = if !constrain.is_empty() {
64            // bit_and
65            &constrain & &allow
66        } else {
67            allow
68        };
69        SearchResult::Allow(allowed_attrs)
70    }
71}
72
73fn search_filter_entry(
74    ident: &Identity,
75    related_acp: &[AccessControlSearchResolved],
76    entry: &Arc<EntrySealedCommitted>,
77) -> AccessSrchResult {
78    // If this is an internal search, return our working set.
79    match &ident.origin {
80        IdentType::Internal => {
81            trace!(uuid = ?entry.get_display_id(), "Internal operation, bypassing access check");
82            // No need to check ACS
83            return AccessSrchResult::Grant;
84        }
85        IdentType::Synch(_) => {
86            security_debug!(uuid = ?entry.get_display_id(), "Blocking sync check");
87            return AccessSrchResult::Deny;
88        }
89        IdentType::User(_) => {}
90    };
91    debug!(event = %ident, "Access check for search (filter) event");
92
93    match ident.access_scope() {
94        AccessScope::Synchronise => {
95            security_debug!(
96                "denied ❌ - identity access scope 'Synchronise' is not permitted to search"
97            );
98            return AccessSrchResult::Deny;
99        }
100        AccessScope::ReadOnly | AccessScope::ReadWrite => {
101            // As you were
102        }
103    };
104
105    // needed for checking entry manager conditions.
106    let ident_memberof = ident.get_memberof();
107    let ident_uuid = ident.get_uuid();
108
109    let allowed_attrs: BTreeSet<Attribute> = related_acp
110        .iter()
111        .filter_map(|acs| {
112            // Assert that the receiver condition applies.
113            match &acs.receiver_condition {
114                AccessControlReceiverCondition::GroupChecked => {
115                    // The groups were already checked during filter resolution. Trust
116                    // that result, and continue.
117                }
118                AccessControlReceiverCondition::EntryManager => {
119                    // This condition relies on the entry we are looking at to have a back-ref
120                    // to our uuid or a group we are in as an entry manager.
121
122                    // Note, while schema has this as single value, we currently
123                    // fetch it as a multivalue btreeset for future incase we allow
124                    // multiple entry manager by in future.
125                    if let Some(entry_manager_uuids) = entry.get_ava_refer(Attribute::EntryManagedBy) {
126                        let group_check = ident_memberof
127                            // Have at least one group allowed.
128                            .map(|imo| imo.intersection(entry_manager_uuids).next().is_some())
129                            .unwrap_or_default();
130
131                        let user_check = ident_uuid
132                            .map(|u| entry_manager_uuids.contains(&u))
133                            .unwrap_or_default();
134
135                        if !(group_check || user_check) {
136                            // Not the entry manager
137                            return None
138                        }
139                    } else {
140                        // Can not satisfy.
141                        return None
142                    }
143                }
144            };
145
146            match &acs.target_condition {
147                AccessControlTargetCondition::Scope(f_res) => {
148                    if !entry.entry_match_no_index(f_res) {
149                        security_debug!(entry = ?entry.get_display_id(), acs = %acs.acp.acp.name, "entry DOES NOT match acs");
150                        return None
151                    }
152                }
153            };
154
155            // -- Conditions pass -- release the attributes.
156
157            security_debug!(entry = ?entry.get_display_id(), acs = %acs.acp.acp.name, "acs applied to entry");
158            // add search_attrs to allowed.
159            Some(acs.acp.attrs.iter().cloned())
160        })
161        .flatten()
162        .collect();
163
164    AccessSrchResult::Allow {
165        attr: allowed_attrs,
166    }
167}
168
169fn search_oauth2_filter_entry(
170    ident: &Identity,
171    entry: &Arc<EntrySealedCommitted>,
172) -> AccessSrchResult {
173    match &ident.origin {
174        IdentType::Internal | IdentType::Synch(_) => AccessSrchResult::Ignore,
175        IdentType::User(iuser) => {
176            if iuser.entry.get_uuid() == UUID_ANONYMOUS {
177                debug!("Anonymous can't access OAuth2 entries, ignoring");
178                return AccessSrchResult::Ignore;
179            }
180
181            let contains_o2_rs = entry
182                .get_ava_as_iutf8(Attribute::Class)
183                .map(|set| {
184                    trace!(?set);
185                    set.contains(&EntryClass::OAuth2ResourceServer.to_string())
186                })
187                .unwrap_or(false);
188
189            let contains_o2_scope_member = entry
190                .get_ava_as_oauthscopemaps(Attribute::OAuth2RsScopeMap)
191                .and_then(|maps| ident.get_memberof().map(|mo| (maps, mo)))
192                .map(|(maps, mo)| maps.keys().any(|k| mo.contains(k)))
193                .unwrap_or(false);
194
195            if contains_o2_rs && contains_o2_scope_member {
196                security_debug!(entry = ?entry.get_uuid(), ident = ?iuser.entry.get_uuid2rdn(), "ident is a memberof a group granted an oauth2 scope by this entry");
197
198                return AccessSrchResult::Allow {
199                    attr: btreeset!(
200                        Attribute::Class,
201                        Attribute::DisplayName,
202                        Attribute::Uuid,
203                        Attribute::Name,
204                        Attribute::OAuth2RsOriginLanding,
205                        Attribute::Image
206                    ),
207                };
208            }
209            AccessSrchResult::Ignore
210        }
211    }
212}
213
214fn search_sync_account_filter_entry(
215    ident: &Identity,
216    entry: &Arc<EntrySealedCommitted>,
217) -> AccessSrchResult {
218    match &ident.origin {
219        IdentType::Internal | IdentType::Synch(_) => AccessSrchResult::Ignore,
220        IdentType::User(iuser) => {
221            // Is the user a synced object?
222            let is_user_sync_account = iuser
223                .entry
224                .get_ava_as_iutf8(Attribute::Class)
225                .map(|set| {
226                    trace!(?set);
227                    set.contains(&EntryClass::SyncObject.to_string())
228                        && set.contains(EntryClass::Account.into())
229                })
230                .unwrap_or(false);
231
232            if is_user_sync_account {
233                let is_target_sync_account = entry
234                    .get_ava_as_iutf8(Attribute::Class)
235                    .map(|set| {
236                        trace!(?set);
237                        set.contains(&EntryClass::SyncAccount.to_string())
238                    })
239                    .unwrap_or(false);
240
241                if is_target_sync_account {
242                    // Okay, now we need to check if the uuids line up.
243                    let sync_uuid = entry.get_uuid();
244                    let sync_source_match = iuser
245                        .entry
246                        .get_ava_single_refer(Attribute::SyncParentUuid)
247                        .map(|sync_parent_uuid| sync_parent_uuid == sync_uuid)
248                        .unwrap_or(false);
249
250                    if sync_source_match {
251                        // We finally got here!
252                        security_debug!(entry = ?entry.get_uuid(), ident = ?iuser.entry.get_uuid2rdn(), "ident is a synchronised account from this sync account");
253
254                        return AccessSrchResult::Allow {
255                            attr: btreeset!(
256                                Attribute::Class,
257                                Attribute::Uuid,
258                                Attribute::SyncCredentialPortal
259                            ),
260                        };
261                    }
262                }
263            }
264            // Fall through
265            AccessSrchResult::Ignore
266        }
267    }
268}