From 586faccc7c0866e48c83c57e3fd36b4b7082c17a Mon Sep 17 00:00:00 2001 From: selfhoster1312 Date: Mon, 21 Sep 2026 15:10:43 +0200 Subject: [PATCH] feat: Support case-insensitive LDAP attributes in search --- src/ldap/attr.rs | 66 +++++++++++++++++++++++++++++++++++++++++++ src/ldap/mod.rs | 2 ++ src/ldap/op/search.rs | 44 +++++++++++++++++++---------- 3 files changed, 97 insertions(+), 15 deletions(-) create mode 100644 src/ldap/attr.rs diff --git a/src/ldap/attr.rs b/src/ldap/attr.rs new file mode 100644 index 0000000..14bd72a --- /dev/null +++ b/src/ldap/attr.rs @@ -0,0 +1,66 @@ +use std::fmt; +use std::str::FromStr; + +#[derive(Clone, Debug, PartialEq)] +pub struct UnknownLdapAttribute(String); + +impl fmt::Display for UnknownLdapAttribute { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "Unknown LDAP attribute: {}", self.0) + } +} + +impl std::error::Error for UnknownLdapAttribute {} + +/// An LDAP attribute that is requested, or requested to be matched against an entry. +/// +/// Attributes are case-insensitive when parsing from a string. +/// +/// In the future, we may want to support custom attributes, but that is not +/// implemented for now. +#[derive(Clone, Debug, PartialEq)] +pub enum LdapAttribute { + Uid, + CommonName, + MemberOf, + ObjectClass, + Mail, + MailAlias, +} + +impl FromStr for LdapAttribute { + type Err = UnknownLdapAttribute; + + fn from_str(s: &str) -> Result { + match s.to_lowercase().as_str() { + "uid" => Ok(Self::Uid), + "cn" => Ok(Self::CommonName), + "memberof" => Ok(Self::MemberOf), + "objectclass" => Ok(Self::ObjectClass), + "mail" => Ok(Self::Mail), + "mailalias" => Ok(Self::MailAlias), + _ => Err(UnknownLdapAttribute(s.to_string())), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn uppercased_attribute() { + let s = "MemberOF"; + let attr = LdapAttribute::from_str(s); + println!("{attr:?}"); + assert_eq!(attr.unwrap(), LdapAttribute::MemberOf); + } + + #[test] + fn unknown_attribute() { + let s = "foobar"; + let attr = LdapAttribute::from_str(s); + println!("{attr:?}"); + assert_eq!(attr.unwrap_err(), UnknownLdapAttribute(s.to_string())); + } +} diff --git a/src/ldap/mod.rs b/src/ldap/mod.rs index 59da6d2..00f6174 100644 --- a/src/ldap/mod.rs +++ b/src/ldap/mod.rs @@ -1,3 +1,5 @@ +mod attr; +pub use attr::LdapAttribute; mod client_state; pub use client_state::LdapClientState; mod dn; diff --git a/src/ldap/op/search.rs b/src/ldap/op/search.rs index 5d77fcf..86ef63d 100644 --- a/src/ldap/op/search.rs +++ b/src/ldap/op/search.rs @@ -4,9 +4,11 @@ use ldap3_proto::proto::{ }; use ldap3_proto::{LdapMsg, LdapResultCode}; +use std::str::FromStr; + use crate::db::error::BoxedError; use crate::db::{Database, DatabaseInterface, User}; -use crate::ldap::{Dn, LdapReturnError, LdapStream, LdapStreamError, MalformedDn}; +use crate::ldap::{Dn, LdapAttribute, LdapReturnError, LdapStream, LdapStreamError, MalformedDn}; #[derive(Debug)] pub struct InvalidSearchDn { @@ -252,24 +254,36 @@ pub fn user_matches_filter(user: &User, filter: &LdapFilter) -> bool { false } LdapFilter::Not(sub_filter) => !user_matches_filter(user, sub_filter), - LdapFilter::Equality(attr, value) => match attr.as_ref() { - "uid" => user.username == *value, - // TODO: should CN be different than the mail? - "cn" | "mail" | "mailAlias" => user.mail == *value, - // TODO: group membership - "memberof" => false, - "objectClass" => matches!( - value.as_ref(), - "inetOrgPerson" | "posixAccount" | "mailAccount" | "person" - ), - _ => { - tracing::warn!("Unknown user attribute filter, considering no match: {attr}"); + LdapFilter::Equality(attr, value) => LdapAttribute::from_str(attr).map_or_else( + |e| { + tracing::warn!("Unrecognized attribute, considering no match: {e}"); false - } - }, + }, + |attr| user_matches_attribute(user, &attr, value), + ), _ => { tracing::warn!("Unimplemented search filter, considering no match: {filter:?}"); false } } } + +// TODO: we want to support group relations here as argument soon +pub fn user_matches_attribute(user: &User, attribute: &LdapAttribute, value: &str) -> bool { + match attribute { + // TODO: should we lowercase the value here? + LdapAttribute::Uid => user.username == *value, + // TODO: should CN be different than the mail? + // TODO: should we lowercase the value here? + LdapAttribute::CommonName | LdapAttribute::Mail | LdapAttribute::MailAlias => { + user.mail == *value + } + // TODO: group membership + LdapAttribute::MemberOf => false, + LdapAttribute::ObjectClass => matches!( + // We lowercase the value here because there's no ambiguity + value.to_lowercase().as_ref(), + "inetorgperson" | "posixaccount" | "mailaccount" | "person" + ), + } +}