feat: Implement basic user search beyond mail attribute

This commit is contained in:
selfhoster selfhoster 2026-09-21 11:58:24 +02:00
commit ec36ea9116
5 changed files with 78 additions and 278 deletions

View file

@ -1,12 +1,11 @@
use ldap3_proto::control::LdapControl;
use ldap3_proto::proto::{
LdapOp, LdapPartialAttribute, LdapResult, LdapSearchRequest, LdapSearchResultEntry,
LdapFilter, LdapOp, LdapPartialAttribute, LdapResult, LdapSearchRequest, LdapSearchResultEntry,
};
use ldap3_proto::{LdapMsg, LdapResultCode};
use crate::db::error::BoxedError;
use crate::db::{Database, DatabaseInterface, User};
use crate::ldap::filter::mail::{MailDomainError, MailFilter};
use crate::ldap::{Dn, LdapReturnError, LdapStream, LdapStreamError, MalformedDn};
#[derive(Debug)]
@ -70,7 +69,6 @@ impl SearchDn {
pub enum SearchError {
Db(BoxedError),
InvalidDn(InvalidSearchDn),
MailDomain(MailDomainError),
}
impl SearchError {
@ -100,7 +98,6 @@ impl LdapReturnError for SearchError {
match self {
Self::Db(_e) => LdapResultCode::Unavailable,
Self::InvalidDn(e) => e.code(),
Self::MailDomain(e) => e.code(),
}
}
@ -108,7 +105,6 @@ impl LdapReturnError for SearchError {
match self {
Self::Db(e) => format!("Database error: {e}"),
Self::InvalidDn(e) => e.message(),
Self::MailDomain(e) => e.message(),
}
}
}
@ -182,7 +178,7 @@ fn search_entry_from_user(user: &User, req_attrs: &[String]) -> LdapSearchResult
}
}
pub async fn search_by_mail_filter(
pub async fn search_by_everything(
stream: &mut LdapStream,
db: &Database,
sr: LdapSearchRequest,
@ -196,30 +192,84 @@ pub async fn search_by_mail_filter(
return Ok(());
}
let mail_filter = match MailFilter::from_search_filter(&sr.filter) {
Ok(mail_filter) => mail_filter,
Err(e) => {
SearchError::MailDomain(e)
.error_message(stream, msgid)
.await?;
return Ok(());
}
};
let maybe_user = match db.get_user(&mail_filter.to_user_ref()).await {
Ok(maybe_user) => maybe_user,
let users = match search_everything(db, &sr.filter).await {
Ok(users) => users,
Err(e) => {
SearchError::Db(e).error_message(stream, msgid).await?;
return Ok(());
}
};
if let Some(user) = maybe_user {
let entry_ctrl = (search_entry_from_user(&user, &sr.attrs), vec![]);
search_success(stream, msgid, vec![entry_ctrl]).await?;
} else {
search_success(stream, msgid, vec![]).await?;
}
// TODO: pagination ctrl
let entries = users
.into_iter()
.map(|user| (search_entry_from_user(&user, &sr.attrs), vec![]))
.collect();
search_success(stream, msgid, entries).await?;
Ok(())
}
// TODO: pagination
// TODO: support groups (objectclass=groupOfNames) and membership
/// A very unoptimized search algorithm for the LDAP database.
///
/// Key information:
/// - ignores all filters that are not AND/OR/NOT/EQ (others may be added
/// if we find a valid usecase)
/// - iterates over every single entry in the database!!!!!
pub async fn search_everything(db: &Database, sr: &LdapFilter) -> Result<Vec<User>, BoxedError> {
Ok(db
.list_all_users()
.await?
.into_iter()
.filter(|user| user_matches_filter(user, sr))
.collect())
}
/// Checks if a user matches a single `LdapFilter`.
///
/// Used recursively to see if a user matches a filter overall.
pub fn user_matches_filter(user: &User, filter: &LdapFilter) -> bool {
// TODO: we should also receive the group memberships as argument here
// so we can match that in the future
// For now, we only match mail/mailalias/uid attributes
// TODO: implement mailalias, for now it's simply mapped to the mail attribute
match filter {
LdapFilter::And(filters) => {
for sub_filter in filters {
if !user_matches_filter(user, sub_filter) {
return false;
}
}
true
}
LdapFilter::Or(filters) => {
for sub_filter in filters {
if user_matches_filter(user, sub_filter) {
return true;
}
}
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}");
false
}
},
_ => {
tracing::warn!("Unimplemented search filter, considering no match: {filter:?}");
false
}
}
}