1use crate::entry::LdapEntry;
4
5#[cfg(feature = "serde")]
6use serde::{Deserialize, Serialize};
7
8use std::cmp::Ordering;
9
10#[derive(Debug, Clone, PartialEq, Eq)]
19#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
20#[non_exhaustive]
21pub enum TargetFilter {
22 All,
24 DnPattern(String),
26 ObjectClass(String),
28 HasAttribute(String),
30 And(Vec<TargetFilter>),
32 Or(Vec<TargetFilter>),
34 Not(Box<TargetFilter>),
36 Raw(String),
38}
39
40impl TargetFilter {
41 pub fn matches(&self, entry: &LdapEntry) -> bool {
43 match self {
44 Self::All => true,
45 Self::DnPattern(pattern) => entry.dn_matches(pattern),
46 Self::ObjectClass(oc) => entry.has_object_class(oc),
47 Self::HasAttribute(attr) => entry.attributes.contains_key(&attr.to_lowercase()),
48 Self::And(filters) => filters.iter().all(|f| f.matches(entry)),
49 Self::Or(filters) => filters.iter().any(|f| f.matches(entry)),
50 Self::Not(inner) => !inner.matches(entry),
51 Self::Raw(_) => false,
52 }
53 }
54
55 pub fn has_member_exclusion(&self, group_dn: &str) -> bool {
58 match self {
59 Self::And(filters) => filters.iter().any(|f| f.has_member_exclusion(group_dn)),
60 Self::Or(filters) => filters.iter().all(|f| f.has_member_exclusion(group_dn)),
61 Self::Not(inner) => inner.is_member_of_filter(group_dn),
62 _ => false,
63 }
64 }
65
66 pub fn has_filter_exclusion(&self, pattern: &str) -> bool {
71 match self {
72 Self::And(filters) => filters.iter().any(|f| f.has_filter_exclusion(pattern)),
73 Self::Or(filters) => filters.iter().all(|f| f.has_filter_exclusion(pattern)),
74 Self::Not(inner) => inner.matches_filter_pattern(pattern),
75 _ => false,
76 }
77 }
78
79 fn matches_filter_pattern(&self, pattern: &str) -> bool {
80 let pattern_lower = pattern.to_lowercase();
81 match self {
82 Self::Raw(s) => s.to_lowercase().contains(&pattern_lower),
83 Self::And(filters) | Self::Or(filters) => {
84 filters.iter().any(|f| f.matches_filter_pattern(pattern))
85 }
86 Self::ObjectClass(oc) => {
87 let check = format!("objectclass={}", oc).to_lowercase();
88 check.contains(&pattern_lower) || pattern_lower.contains(&check)
89 }
90 Self::HasAttribute(attr) => {
91 let check = format!("{}=", attr).to_lowercase();
92 pattern_lower.contains(&check)
93 }
94 _ => false,
95 }
96 }
97
98 fn is_member_of_filter(&self, group_dn: &str) -> bool {
100 let group_lower = group_dn.to_lowercase();
101 match self {
102 Self::Raw(s) => {
103 let s_lower = s.to_lowercase();
104 s_lower.contains("memberof=") && s_lower.contains(&group_lower)
105 }
106 Self::HasAttribute(attr) => attr.eq_ignore_ascii_case("memberof"),
107 _ => false,
108 }
109 }
110}
111
112impl PartialOrd for TargetFilter {
113 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
114 if self == other {
115 return Some(Ordering::Equal);
116 }
117 match (self, other) {
118 (Self::All, _) => Some(Ordering::Greater),
119 (_, Self::All) => Some(Ordering::Less),
120
121 (Self::Raw(_), _) | (_, Self::Raw(_)) => None,
122 (Self::Not(_), _) | (_, Self::Not(_)) => None,
123
124 (Self::And(a), Self::And(b)) => {
125 let a_has_all_b = b.iter().all(|bi| a.contains(bi));
126 let b_has_all_a = a.iter().all(|ai| b.contains(ai));
127 match (a_has_all_b, b_has_all_a) {
128 (true, true) => Some(Ordering::Equal),
129 (true, false) => Some(Ordering::Less),
130 (false, true) => Some(Ordering::Greater),
131 (false, false) => None,
132 }
133 }
134
135 (Self::Or(a), Self::Or(b)) => {
136 let a_has_all_b = b.iter().all(|bi| a.contains(bi));
137 let b_has_all_a = a.iter().all(|ai| b.contains(ai));
138 match (a_has_all_b, b_has_all_a) {
139 (true, true) => Some(Ordering::Equal),
140 (true, false) => Some(Ordering::Greater),
141 (false, true) => Some(Ordering::Less),
142 (false, false) => None,
143 }
144 }
145
146 (Self::And(items), x) if items.iter().any(|i| i == x) => Some(Ordering::Less),
147 (x, Self::And(items)) if items.iter().any(|i| i == x) => Some(Ordering::Greater),
148
149 (Self::Or(items), x) if items.iter().any(|i| i == x) => Some(Ordering::Greater),
150 (x, Self::Or(items)) if items.iter().any(|i| i == x) => Some(Ordering::Less),
151
152 _ => None,
153 }
154 }
155}
156
157#[cfg(test)]
158mod tests {
159 use super::*;
160
161 mod target_filter_ordering {
162 use super::*;
163
164 #[test]
165 fn all_is_greatest() {
166 let filters = [
167 TargetFilter::ObjectClass("posixAccount".into()),
168 TargetFilter::DnPattern("uid=*,dc=example,dc=com".into()),
169 TargetFilter::HasAttribute("uid".into()),
170 TargetFilter::And(vec![
171 TargetFilter::ObjectClass("posixAccount".into()),
172 TargetFilter::HasAttribute("uid".into()),
173 ]),
174 TargetFilter::Or(vec![
175 TargetFilter::ObjectClass("posixAccount".into()),
176 TargetFilter::ObjectClass("inetOrgPerson".into()),
177 ]),
178 ];
179 for f in &filters {
180 assert!(TargetFilter::All > *f, "All should be greater than {:?}", f);
181 }
182 }
183
184 #[test]
185 fn reflexive() {
186 let filters = [
187 TargetFilter::All,
188 TargetFilter::ObjectClass("posixAccount".into()),
189 TargetFilter::Raw("(uid=*)".into()),
190 TargetFilter::And(vec![TargetFilter::ObjectClass("x".into())]),
191 ];
192 for f in &filters {
193 assert_eq!(f.partial_cmp(f), Some(Ordering::Equal));
194 }
195 }
196
197 #[test]
198 fn raw_is_incomparable() {
199 let raw = TargetFilter::Raw("(uid=*)".into());
200 let oc = TargetFilter::ObjectClass("posixAccount".into());
201 assert_eq!(raw.partial_cmp(&oc), None);
202 assert_eq!(oc.partial_cmp(&raw), None);
203 assert_eq!(raw.partial_cmp(&TargetFilter::All), Some(Ordering::Less));
204 }
205
206 #[test]
207 fn and_narrows() {
208 let a = TargetFilter::ObjectClass("posixAccount".into());
209 let b = TargetFilter::HasAttribute("uid".into());
210 let and = TargetFilter::And(vec![a.clone(), b.clone()]);
211 assert!(and < a, "And should be narrower than any conjunct");
212 assert!(and < b);
213 }
214
215 #[test]
216 fn or_widens() {
217 let a = TargetFilter::ObjectClass("posixAccount".into());
218 let b = TargetFilter::ObjectClass("inetOrgPerson".into());
219 let or = TargetFilter::Or(vec![a.clone(), b.clone()]);
220 assert!(or > a, "Or should be wider than any disjunct");
221 assert!(or > b);
222 }
223
224 #[test]
225 fn and_more_conjuncts_is_narrower() {
226 let ab = TargetFilter::And(vec![
227 TargetFilter::ObjectClass("a".into()),
228 TargetFilter::ObjectClass("b".into()),
229 ]);
230 let abc = TargetFilter::And(vec![
231 TargetFilter::ObjectClass("a".into()),
232 TargetFilter::ObjectClass("b".into()),
233 TargetFilter::ObjectClass("c".into()),
234 ]);
235 assert!(abc < ab, "More conjuncts = narrower = less");
236 }
237
238 #[test]
239 fn or_more_disjuncts_is_wider() {
240 let ab = TargetFilter::Or(vec![
241 TargetFilter::ObjectClass("a".into()),
242 TargetFilter::ObjectClass("b".into()),
243 ]);
244 let abc = TargetFilter::Or(vec![
245 TargetFilter::ObjectClass("a".into()),
246 TargetFilter::ObjectClass("b".into()),
247 TargetFilter::ObjectClass("c".into()),
248 ]);
249 assert!(abc > ab, "More disjuncts = wider = greater");
250 }
251
252 #[test]
253 fn different_leaves_incomparable() {
254 let oc = TargetFilter::ObjectClass("posixAccount".into());
255 let ha = TargetFilter::HasAttribute("uid".into());
256 assert_eq!(oc.partial_cmp(&ha), None);
257 }
258
259 #[test]
260 fn disjoint_ands_incomparable() {
261 let and1 = TargetFilter::And(vec![TargetFilter::ObjectClass("a".into())]);
262 let and2 = TargetFilter::And(vec![TargetFilter::ObjectClass("b".into())]);
263 assert_eq!(and1.partial_cmp(&and2), None);
264 }
265
266 #[test]
267 fn not_is_incomparable() {
268 let not = TargetFilter::Not(Box::new(TargetFilter::ObjectClass("a".into())));
269 let oc = TargetFilter::ObjectClass("b".into());
270 assert_eq!(not.partial_cmp(&oc), None);
271 assert_eq!(oc.partial_cmp(¬), None);
272 assert!(not < TargetFilter::All);
273 }
274 }
275
276 mod member_exclusion {
277 use super::*;
278
279 #[test]
280 fn and_with_not_memberof_raw() {
281 let filter = TargetFilter::And(vec![
282 TargetFilter::Not(Box::new(TargetFilter::Raw(
283 "memberOf=cn=admins,cn=groups,cn=accounts,dc=example,dc=com".into(),
284 ))),
285 TargetFilter::ObjectClass("posixaccount".into()),
286 ]);
287 assert!(
288 filter.has_member_exclusion("cn=admins,cn=groups,cn=accounts,dc=example,dc=com")
289 );
290 assert!(
291 !filter.has_member_exclusion("cn=editors,cn=groups,cn=accounts,dc=example,dc=com")
292 );
293 }
294
295 #[test]
296 fn simple_objectclass_no_exclusion() {
297 let filter = TargetFilter::ObjectClass("posixaccount".into());
298 assert!(
299 !filter.has_member_exclusion("cn=admins,cn=groups,cn=accounts,dc=example,dc=com")
300 );
301 }
302
303 #[test]
304 fn all_no_exclusion() {
305 assert!(!TargetFilter::All.has_member_exclusion("cn=admins"));
306 }
307
308 #[test]
309 fn case_insensitive_match() {
310 let filter = TargetFilter::Not(Box::new(TargetFilter::Raw(
311 "memberOf=CN=Admins,CN=Groups,CN=Accounts,DC=Example,DC=COM".into(),
312 )));
313 assert!(
314 filter.has_member_exclusion("cn=admins,cn=groups,cn=accounts,dc=example,dc=com")
315 );
316 }
317
318 #[test]
319 fn or_requires_all_branches() {
320 let filter = TargetFilter::Or(vec![
321 TargetFilter::And(vec![
322 TargetFilter::Not(Box::new(TargetFilter::Raw("memberOf=cn=admins".into()))),
323 TargetFilter::ObjectClass("posixaccount".into()),
324 ]),
325 TargetFilter::ObjectClass("inetorgperson".into()),
326 ]);
327 assert!(!filter.has_member_exclusion("cn=admins"));
328 }
329
330 #[test]
331 fn or_with_exclusion_in_all_branches() {
332 let filter = TargetFilter::Or(vec![
333 TargetFilter::Not(Box::new(TargetFilter::Raw("memberOf=cn=admins".into()))),
334 TargetFilter::Not(Box::new(TargetFilter::Raw("memberOf=cn=admins".into()))),
335 ]);
336 assert!(filter.has_member_exclusion("cn=admins"));
337 }
338 }
339}