use super::CompOp;
use crate::dn::matches_dn_pattern;
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 BindRule {
Anyone,
Authenticated,
UserDn(String),
SelfUser,
GroupDn(String),
RoleAttribute(String, String),
ParentDn,
RoleDn(String),
UserDnAttr(String),
GroupDnAttr(String),
Ip(String),
Dns(String),
AuthMethod(String),
DayOfWeek(String),
TimeOfDay(CompOp, String),
Ssf(CompOp, u32),
And(Vec<BindRule>),
Or(Vec<BindRule>),
Not(Box<BindRule>),
}
impl BindRule {
pub fn matches(
&self,
user_dn: Option<&str>,
user_entry: Option<&LdapEntry>,
target_dn: &str,
) -> bool {
match self {
Self::Anyone => true,
Self::Authenticated => user_dn.is_some(),
Self::UserDn(dn) => user_dn.map(|u| u.eq_ignore_ascii_case(dn)).unwrap_or(false),
Self::SelfUser => user_dn
.map(|u| u.eq_ignore_ascii_case(target_dn))
.unwrap_or(false),
Self::GroupDn(group) => user_entry
.and_then(|e| e.get_attr("memberOf"))
.map(|groups| groups.iter().any(|g| matches_dn_pattern(g, group)))
.unwrap_or(false),
Self::RoleAttribute(attr, value) => user_entry
.and_then(|e| e.get_attr(attr))
.map(|values| values.contains(value))
.unwrap_or(false),
Self::ParentDn => {
if let Some(user) = user_dn {
if let Some(comma_pos) = target_dn.find(',') {
user.eq_ignore_ascii_case(&target_dn[comma_pos + 1..])
} else {
false
}
} else {
false
}
}
Self::RoleDn(role) => user_entry
.and_then(|e| e.get_attr("nsRole"))
.map(|roles| roles.iter().any(|r| r.eq_ignore_ascii_case(role)))
.unwrap_or(false),
Self::UserDnAttr(attr) => user_entry
.and_then(|e| e.get_attr(attr))
.map(|vals| {
user_dn
.map(|u| vals.iter().any(|v| v.eq_ignore_ascii_case(u)))
.unwrap_or(false)
})
.unwrap_or(false),
Self::GroupDnAttr(_)
| Self::Ip(_)
| Self::Dns(_)
| Self::AuthMethod(_)
| Self::DayOfWeek(_)
| Self::TimeOfDay(_, _)
| Self::Ssf(_, _) => false,
Self::And(rules) => rules
.iter()
.all(|r| r.matches(user_dn, user_entry, target_dn)),
Self::Or(rules) => rules
.iter()
.any(|r| r.matches(user_dn, user_entry, target_dn)),
Self::Not(rule) => !rule.matches(user_dn, user_entry, target_dn),
}
}
pub fn is_identity_rule(&self) -> bool {
matches!(
self,
Self::Anyone
| Self::Authenticated
| Self::UserDn(_)
| Self::SelfUser
| Self::GroupDn(_)
| Self::RoleDn(_)
| Self::RoleAttribute(_, _)
| Self::ParentDn
| Self::UserDnAttr(_)
| Self::GroupDnAttr(_)
)
}
pub fn is_environment_rule(&self) -> bool {
matches!(
self,
Self::Ip(_)
| Self::Dns(_)
| Self::AuthMethod(_)
| Self::DayOfWeek(_)
| Self::TimeOfDay(_, _)
| Self::Ssf(_, _)
)
}
pub fn contains_environment_rule(&self) -> bool {
match self {
Self::And(rules) | Self::Or(rules) => {
rules.iter().any(|r| r.contains_environment_rule())
}
Self::Not(inner) => inner.contains_environment_rule(),
other => other.is_environment_rule(),
}
}
}
impl PartialOrd for BindRule {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
if self == other {
return Some(Ordering::Equal);
}
match (self, other) {
(Self::Anyone, _) => Some(Ordering::Greater),
(_, Self::Anyone) => Some(Ordering::Less),
(Self::Authenticated, x) if x.is_identity_rule() => Some(Ordering::Greater),
(x, Self::Authenticated) if x.is_identity_rule() => Some(Ordering::Less),
(Self::Not(a), Self::Not(b)) => b.partial_cmp(a),
(Self::Not(inner), _) if **inner == Self::Anyone => Some(Ordering::Less),
(_, Self::Not(inner)) if **inner == Self::Anyone => Some(Ordering::Greater),
(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::*;
#[test]
fn anyone_is_greatest() {
let rules = [
BindRule::Authenticated,
BindRule::UserDn("uid=admin".into()),
BindRule::SelfUser,
BindRule::GroupDn("cn=admins".into()),
BindRule::Ip("192.168.*".into()),
];
for r in &rules {
assert!(
BindRule::Anyone > *r,
"Anyone should be greater than {:?}",
r
);
}
}
#[test]
fn reflexive() {
let rules = [
BindRule::Anyone,
BindRule::Authenticated,
BindRule::UserDn("uid=test".into()),
BindRule::SelfUser,
BindRule::GroupDn("cn=group".into()),
BindRule::Ip("10.0.0.*".into()),
];
for r in &rules {
assert_eq!(r.partial_cmp(r), Some(Ordering::Equal));
}
}
#[test]
fn authenticated_above_identity_rules() {
assert!(BindRule::Authenticated > BindRule::UserDn("uid=x".into()));
assert!(BindRule::Authenticated > BindRule::SelfUser);
assert!(BindRule::Authenticated > BindRule::GroupDn("cn=g".into()));
assert!(BindRule::Authenticated > BindRule::RoleDn("cn=r".into()));
assert!(BindRule::Authenticated > BindRule::ParentDn);
}
#[test]
fn authenticated_vs_environment_incomparable() {
assert_eq!(
BindRule::Authenticated.partial_cmp(&BindRule::Ip("10.0.0.*".into())),
None
);
assert_eq!(
BindRule::Authenticated.partial_cmp(&BindRule::Ssf(CompOp::GreaterEqual, 128)),
None
);
}
#[test]
fn different_identity_rules_incomparable() {
assert_eq!(
BindRule::UserDn("uid=a".into()).partial_cmp(&BindRule::GroupDn("cn=g".into())),
None
);
assert_eq!(
BindRule::SelfUser.partial_cmp(&BindRule::UserDn("uid=x".into())),
None
);
}
#[test]
fn identity_vs_environment_incomparable() {
assert_eq!(
BindRule::UserDn("uid=a".into()).partial_cmp(&BindRule::Ip("10.*".into())),
None
);
}
#[test]
fn not_anyone_is_least() {
let not_anyone = BindRule::Not(Box::new(BindRule::Anyone));
assert!(not_anyone < BindRule::Authenticated);
assert!(not_anyone < BindRule::UserDn("uid=x".into()));
assert!(not_anyone < BindRule::Ip("10.*".into()));
}
#[test]
fn not_reverses_order() {
let not_auth = BindRule::Not(Box::new(BindRule::Authenticated));
let not_user = BindRule::Not(Box::new(BindRule::UserDn("uid=x".into())));
assert!(not_auth < not_user);
}
#[test]
fn and_narrows() {
let user = BindRule::UserDn("uid=x".into());
let ip = BindRule::Ip("10.*".into());
let and = BindRule::And(vec![user.clone(), ip.clone()]);
assert!(and < user);
assert!(and < ip);
}
#[test]
fn or_widens() {
let user = BindRule::UserDn("uid=x".into());
let group = BindRule::GroupDn("cn=g".into());
let or = BindRule::Or(vec![user.clone(), group.clone()]);
assert!(or > user);
assert!(or > group);
}
#[test]
fn and_more_conjuncts_narrower() {
let ab = BindRule::And(vec![
BindRule::UserDn("uid=x".into()),
BindRule::Ip("10.*".into()),
]);
let abc = BindRule::And(vec![
BindRule::UserDn("uid=x".into()),
BindRule::Ip("10.*".into()),
BindRule::Ssf(CompOp::GreaterEqual, 128),
]);
assert!(abc < ab);
}
#[test]
fn or_more_disjuncts_wider() {
let ab = BindRule::Or(vec![
BindRule::UserDn("uid=x".into()),
BindRule::UserDn("uid=y".into()),
]);
let abc = BindRule::Or(vec![
BindRule::UserDn("uid=x".into()),
BindRule::UserDn("uid=y".into()),
BindRule::UserDn("uid=z".into()),
]);
assert!(abc > ab);
}
#[test]
fn classification() {
assert!(BindRule::Anyone.is_identity_rule());
assert!(BindRule::Authenticated.is_identity_rule());
assert!(BindRule::SelfUser.is_identity_rule());
assert!(BindRule::UserDn("uid=x".into()).is_identity_rule());
assert!(BindRule::GroupDn("cn=g".into()).is_identity_rule());
assert!(!BindRule::Anyone.is_environment_rule());
assert!(BindRule::Ip("10.*".into()).is_environment_rule());
assert!(BindRule::Dns("*.example.com".into()).is_environment_rule());
assert!(BindRule::Ssf(CompOp::GreaterEqual, 128).is_environment_rule());
assert!(BindRule::DayOfWeek("mon".into()).is_environment_rule());
}
#[test]
fn chain_anyone_authenticated_user() {
let anyone = BindRule::Anyone;
let auth = BindRule::Authenticated;
let user = BindRule::UserDn("uid=admin,dc=example,dc=com".into());
assert!(anyone > auth);
assert!(auth > user);
assert!(anyone > user);
}
}