use crate::entry::LdapEntry;
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
use std::cmp::Ordering;
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[non_exhaustive]
pub enum TargetFilter {
All,
DnPattern(String),
ObjectClass(String),
HasAttribute(String),
And(Vec<TargetFilter>),
Or(Vec<TargetFilter>),
Not(Box<TargetFilter>),
Raw(String),
}
impl TargetFilter {
pub fn matches(&self, entry: &LdapEntry) -> bool {
match self {
Self::All => true,
Self::DnPattern(pattern) => entry.dn_matches(pattern),
Self::ObjectClass(oc) => entry.has_object_class(oc),
Self::HasAttribute(attr) => entry.attributes.contains_key(&attr.to_lowercase()),
Self::And(filters) => filters.iter().all(|f| f.matches(entry)),
Self::Or(filters) => filters.iter().any(|f| f.matches(entry)),
Self::Not(inner) => !inner.matches(entry),
Self::Raw(_) => false,
}
}
pub fn has_member_exclusion(&self, group_dn: &str) -> bool {
match self {
Self::And(filters) => filters.iter().any(|f| f.has_member_exclusion(group_dn)),
Self::Or(filters) => filters.iter().all(|f| f.has_member_exclusion(group_dn)),
Self::Not(inner) => inner.is_member_of_filter(group_dn),
_ => false,
}
}
pub fn has_filter_exclusion(&self, pattern: &str) -> bool {
match self {
Self::And(filters) => filters.iter().any(|f| f.has_filter_exclusion(pattern)),
Self::Or(filters) => filters.iter().all(|f| f.has_filter_exclusion(pattern)),
Self::Not(inner) => inner.matches_filter_pattern(pattern),
_ => false,
}
}
fn matches_filter_pattern(&self, pattern: &str) -> bool {
let pattern_lower = pattern.to_lowercase();
match self {
Self::Raw(s) => s.to_lowercase().contains(&pattern_lower),
Self::And(filters) | Self::Or(filters) => {
filters.iter().any(|f| f.matches_filter_pattern(pattern))
}
Self::ObjectClass(oc) => {
let check = format!("objectclass={}", oc).to_lowercase();
check.contains(&pattern_lower) || pattern_lower.contains(&check)
}
Self::HasAttribute(attr) => {
let check = format!("{}=", attr).to_lowercase();
pattern_lower.contains(&check)
}
_ => false,
}
}
fn is_member_of_filter(&self, group_dn: &str) -> bool {
let group_lower = group_dn.to_lowercase();
match self {
Self::Raw(s) => {
let s_lower = s.to_lowercase();
s_lower.contains("memberof=") && s_lower.contains(&group_lower)
}
Self::HasAttribute(attr) => attr.eq_ignore_ascii_case("memberof"),
_ => false,
}
}
}
impl PartialOrd for TargetFilter {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
if self == other {
return Some(Ordering::Equal);
}
match (self, other) {
(Self::All, _) => Some(Ordering::Greater),
(_, Self::All) => Some(Ordering::Less),
(Self::Raw(_), _) | (_, Self::Raw(_)) => None,
(Self::Not(_), _) | (_, Self::Not(_)) => None,
(Self::And(a), Self::And(b)) => {
let a_has_all_b = b.iter().all(|bi| a.contains(bi));
let b_has_all_a = a.iter().all(|ai| b.contains(ai));
match (a_has_all_b, b_has_all_a) {
(true, true) => Some(Ordering::Equal),
(true, false) => Some(Ordering::Less),
(false, true) => Some(Ordering::Greater),
(false, false) => None,
}
}
(Self::Or(a), Self::Or(b)) => {
let a_has_all_b = b.iter().all(|bi| a.contains(bi));
let b_has_all_a = a.iter().all(|ai| b.contains(ai));
match (a_has_all_b, b_has_all_a) {
(true, true) => Some(Ordering::Equal),
(true, false) => Some(Ordering::Greater),
(false, true) => Some(Ordering::Less),
(false, false) => None,
}
}
(Self::And(items), x) if items.iter().any(|i| i == x) => Some(Ordering::Less),
(x, Self::And(items)) if items.iter().any(|i| i == x) => Some(Ordering::Greater),
(Self::Or(items), x) if items.iter().any(|i| i == x) => Some(Ordering::Greater),
(x, Self::Or(items)) if items.iter().any(|i| i == x) => Some(Ordering::Less),
_ => None,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
mod target_filter_ordering {
use super::*;
#[test]
fn all_is_greatest() {
let filters = [
TargetFilter::ObjectClass("posixAccount".into()),
TargetFilter::DnPattern("uid=*,dc=example,dc=com".into()),
TargetFilter::HasAttribute("uid".into()),
TargetFilter::And(vec![
TargetFilter::ObjectClass("posixAccount".into()),
TargetFilter::HasAttribute("uid".into()),
]),
TargetFilter::Or(vec![
TargetFilter::ObjectClass("posixAccount".into()),
TargetFilter::ObjectClass("inetOrgPerson".into()),
]),
];
for f in &filters {
assert!(TargetFilter::All > *f, "All should be greater than {:?}", f);
}
}
#[test]
fn reflexive() {
let filters = [
TargetFilter::All,
TargetFilter::ObjectClass("posixAccount".into()),
TargetFilter::Raw("(uid=*)".into()),
TargetFilter::And(vec![TargetFilter::ObjectClass("x".into())]),
];
for f in &filters {
assert_eq!(f.partial_cmp(f), Some(Ordering::Equal));
}
}
#[test]
fn raw_is_incomparable() {
let raw = TargetFilter::Raw("(uid=*)".into());
let oc = TargetFilter::ObjectClass("posixAccount".into());
assert_eq!(raw.partial_cmp(&oc), None);
assert_eq!(oc.partial_cmp(&raw), None);
assert_eq!(raw.partial_cmp(&TargetFilter::All), Some(Ordering::Less));
}
#[test]
fn and_narrows() {
let a = TargetFilter::ObjectClass("posixAccount".into());
let b = TargetFilter::HasAttribute("uid".into());
let and = TargetFilter::And(vec![a.clone(), b.clone()]);
assert!(and < a, "And should be narrower than any conjunct");
assert!(and < b);
}
#[test]
fn or_widens() {
let a = TargetFilter::ObjectClass("posixAccount".into());
let b = TargetFilter::ObjectClass("inetOrgPerson".into());
let or = TargetFilter::Or(vec![a.clone(), b.clone()]);
assert!(or > a, "Or should be wider than any disjunct");
assert!(or > b);
}
#[test]
fn and_more_conjuncts_is_narrower() {
let ab = TargetFilter::And(vec![
TargetFilter::ObjectClass("a".into()),
TargetFilter::ObjectClass("b".into()),
]);
let abc = TargetFilter::And(vec![
TargetFilter::ObjectClass("a".into()),
TargetFilter::ObjectClass("b".into()),
TargetFilter::ObjectClass("c".into()),
]);
assert!(abc < ab, "More conjuncts = narrower = less");
}
#[test]
fn or_more_disjuncts_is_wider() {
let ab = TargetFilter::Or(vec![
TargetFilter::ObjectClass("a".into()),
TargetFilter::ObjectClass("b".into()),
]);
let abc = TargetFilter::Or(vec![
TargetFilter::ObjectClass("a".into()),
TargetFilter::ObjectClass("b".into()),
TargetFilter::ObjectClass("c".into()),
]);
assert!(abc > ab, "More disjuncts = wider = greater");
}
#[test]
fn different_leaves_incomparable() {
let oc = TargetFilter::ObjectClass("posixAccount".into());
let ha = TargetFilter::HasAttribute("uid".into());
assert_eq!(oc.partial_cmp(&ha), None);
}
#[test]
fn disjoint_ands_incomparable() {
let and1 = TargetFilter::And(vec![TargetFilter::ObjectClass("a".into())]);
let and2 = TargetFilter::And(vec![TargetFilter::ObjectClass("b".into())]);
assert_eq!(and1.partial_cmp(&and2), None);
}
#[test]
fn not_is_incomparable() {
let not = TargetFilter::Not(Box::new(TargetFilter::ObjectClass("a".into())));
let oc = TargetFilter::ObjectClass("b".into());
assert_eq!(not.partial_cmp(&oc), None);
assert_eq!(oc.partial_cmp(¬), None);
assert!(not < TargetFilter::All);
}
}
mod member_exclusion {
use super::*;
#[test]
fn and_with_not_memberof_raw() {
let filter = TargetFilter::And(vec![
TargetFilter::Not(Box::new(TargetFilter::Raw(
"memberOf=cn=admins,cn=groups,cn=accounts,dc=example,dc=com".into(),
))),
TargetFilter::ObjectClass("posixaccount".into()),
]);
assert!(
filter.has_member_exclusion("cn=admins,cn=groups,cn=accounts,dc=example,dc=com")
);
assert!(
!filter.has_member_exclusion("cn=editors,cn=groups,cn=accounts,dc=example,dc=com")
);
}
#[test]
fn simple_objectclass_no_exclusion() {
let filter = TargetFilter::ObjectClass("posixaccount".into());
assert!(
!filter.has_member_exclusion("cn=admins,cn=groups,cn=accounts,dc=example,dc=com")
);
}
#[test]
fn all_no_exclusion() {
assert!(!TargetFilter::All.has_member_exclusion("cn=admins"));
}
#[test]
fn case_insensitive_match() {
let filter = TargetFilter::Not(Box::new(TargetFilter::Raw(
"memberOf=CN=Admins,CN=Groups,CN=Accounts,DC=Example,DC=COM".into(),
)));
assert!(
filter.has_member_exclusion("cn=admins,cn=groups,cn=accounts,dc=example,dc=com")
);
}
#[test]
fn or_requires_all_branches() {
let filter = TargetFilter::Or(vec![
TargetFilter::And(vec![
TargetFilter::Not(Box::new(TargetFilter::Raw("memberOf=cn=admins".into()))),
TargetFilter::ObjectClass("posixaccount".into()),
]),
TargetFilter::ObjectClass("inetorgperson".into()),
]);
assert!(!filter.has_member_exclusion("cn=admins"));
}
#[test]
fn or_with_exclusion_in_all_branches() {
let filter = TargetFilter::Or(vec![
TargetFilter::Not(Box::new(TargetFilter::Raw("memberOf=cn=admins".into()))),
TargetFilter::Not(Box::new(TargetFilter::Raw("memberOf=cn=admins".into()))),
]);
assert!(filter.has_member_exclusion("cn=admins"));
}
}
}