feat: Support case-insensitive LDAP attributes in search

This commit is contained in:
selfhoster selfhoster 2026-09-21 15:10:43 +02:00
commit 586faccc7c
3 changed files with 97 additions and 15 deletions

66
src/ldap/attr.rs Normal file
View file

@ -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<Self, Self::Err> {
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()));
}
}

View file

@ -1,3 +1,5 @@
mod attr;
pub use attr::LdapAttribute;
mod client_state;
pub use client_state::LdapClientState;
mod dn;

View file

@ -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"
),
}
}