feat: Search queries by mail attribute

This commit is contained in:
selfhoster selfhoster 2026-09-03 15:21:26 +02:00
commit c77223048c
9 changed files with 592 additions and 78 deletions

View file

@ -1,4 +1,4 @@
use crate::ldap::Dn; use crate::ldap::BindDn;
/// Whether a client is successfully logged in (bound). /// Whether a client is successfully logged in (bound).
/// ///
@ -6,7 +6,7 @@ use crate::ldap::Dn;
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub enum LdapClientState { pub enum LdapClientState {
Unbound, Unbound,
Bound(Dn), Bound(BindDn),
} }
impl LdapClientState { impl LdapClientState {
@ -18,11 +18,11 @@ impl LdapClientState {
*self = Self::Unbound; *self = Self::Unbound;
} }
pub fn bind(&mut self, dn: Dn) { pub fn bind(&mut self, dn: BindDn) {
*self = Self::Bound(dn); *self = Self::Bound(dn);
} }
pub fn bound_dn(&self) -> Option<&Dn> { pub fn bound_dn(&self) -> Option<&BindDn> {
match self { match self {
Self::Unbound => None, Self::Unbound => None,
Self::Bound(dn) => Some(dn), Self::Bound(dn) => Some(dn),
@ -30,6 +30,6 @@ impl LdapClientState {
} }
pub fn bound_dn_string(&self) -> String { pub fn bound_dn_string(&self) -> String {
self.bound_dn().map_or(String::new(), Dn::to_dn_string) self.bound_dn().map_or(String::new(), BindDn::to_dn_string)
} }
} }

View file

@ -1,8 +1,6 @@
use dn_escape::dn_escape; use dn_escape::dn_escape;
use ldap3_proto::LdapResultCode;
use crate::db::UserRef; use crate::db::User;
use crate::ldap::LdapReturnError;
/// A simple multi-value Map powered by a vector, to respect /// A simple multi-value Map powered by a vector, to respect
/// order of first appearance of keys. /// order of first appearance of keys.
@ -52,30 +50,7 @@ impl VecMap {
} }
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct InvalidDnError(pub String); pub struct MalformedDn;
impl LdapReturnError for InvalidDnError {
fn code(&self) -> LdapResultCode {
LdapResultCode::InvalidDNSyntax
}
fn message(&self) -> String {
format!("Invalid dn: {}", self.0)
}
}
#[derive(Clone, Debug)]
pub struct NotUserDnError(pub String);
impl LdapReturnError for NotUserDnError {
fn code(&self) -> LdapResultCode {
LdapResultCode::InvalidDNSyntax
}
fn message(&self) -> String {
format!("Not a user dn containing uid/dc: {}", self.0)
}
}
/// A Dn is a key-value mapping which can contain the same key several times. /// A Dn is a key-value mapping which can contain the same key several times.
/// ///
@ -96,7 +71,7 @@ impl Dn {
/// ///
/// However, if we find a really funny request such as `dc=foo=bar`, then /// However, if we find a really funny request such as `dc=foo=bar`, then
/// we return an error to the client. /// we return an error to the client.
pub fn from_dn_str(input: &str) -> Result<Self, InvalidDnError> { pub fn from_dn_str(input: &str) -> Result<Self, MalformedDn> {
let mut keys = VecMap::new(); let mut keys = VecMap::new();
if input.is_empty() { if input.is_empty() {
@ -108,7 +83,7 @@ impl Dn {
// Here we have key=val pairs // Here we have key=val pairs
if query_parts.clone().count() != 2 { if query_parts.clone().count() != 2 {
// Bad request // Bad request
return Err(InvalidDnError(input.to_string())); return Err(MalformedDn);
} }
let key = query_parts.next().unwrap(); let key = query_parts.next().unwrap();
@ -169,21 +144,15 @@ impl Dn {
self.keys.insert("dc", domain_components, force); self.keys.insert("dc", domain_components, force);
} }
pub fn to_user_ref(&self) -> Result<UserRef, NotUserDnError> { pub fn from_user(user: &User) -> Self {
let Some(username_vals) = self.keys.get("uid") else { let mut keys = VecMap::new();
return Err(NotUserDnError(self.to_dn_string())); keys.insert_or_append("uid", &user.username);
}; keys.insert_or_append("ou", "people");
if username_vals.len() != 1 { for domain_component in user.domain.split('.') {
return Err(NotUserDnError(self.to_dn_string())); keys.insert_or_append("dc", domain_component);
} }
let username = username_vals[0].clone();
let Some(domain_vals) = self.keys.get("dc") else { Self { keys }
return Err(NotUserDnError(self.to_dn_string()));
};
let domain = domain_vals.join(".");
Ok(UserRef { username, domain })
} }
} }

248
src/ldap/filter/mail.rs Normal file
View file

@ -0,0 +1,248 @@
use ldap3_proto::{LdapFilter, LdapResultCode};
use std::fmt;
use crate::db::UserRef;
use crate::ldap::LdapReturnError;
#[derive(Clone, Debug)]
pub struct MailFilter {
pub complete: String,
pub username: String,
pub domain: String,
}
impl fmt::Display for MailFilter {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.complete)
}
}
impl MailFilter {
/// Tries to parse a string into an email address.
///
/// This method is not really RFC-compliant because it merely splits by `@`,
/// but is good enough for what we do.
pub fn new(value: &str) -> Result<Self, MailDomainError> {
let original = value;
// Here we normalize the mail attribute to lowercase to prevent
// useless mismatches.
// TODO: investigate if that's supposed to be a problem for anyone?
let value = value.to_lowercase();
if value != original {
tracing::debug!("Normalized search email from {original} to {value}");
}
let mut parts = value.split('@');
let username = parts.next().unwrap();
let Some(domain) = parts.next() else {
tracing::debug!("Not a valid username@domain email: {value}");
return Err(MailDomainError::InvalidMail);
};
if parts.next().is_some() {
tracing::debug!("Too many parts in mail address");
return Err(MailDomainError::InvalidMail);
}
Ok(Self {
complete: value.clone(),
username: username.to_string(),
domain: domain.to_string(),
})
}
/// Extract any mail filter from an LDAP search filter
/// which may contain other criteria, which we overall don't care about
/// at the moment.
pub fn from_search_filter(filter: &LdapFilter) -> Result<Self, MailDomainError> {
let res = match filter {
LdapFilter::And(filters) | LdapFilter::Or(filters) => {
Self::from_multiple_filters(filters)
}
LdapFilter::Equality(attr, value) => {
if let Some(domain) = Self::from_equality_filter(attr, value)? {
Ok(domain)
} else {
Err(MailDomainError::NoMailFilter)
}
}
_ => Err(MailDomainError::InvalidFilter),
};
match &res {
Ok(mail) => tracing::debug!("Found email in search filter: {mail}"),
Err(e) => tracing::debug!("Not found email in search filter: {}", e.message()),
}
res
}
/// Extract any mail filter from a bunch of LDAP filters.
///
/// Any filter that is not an equality check is discarded.
fn from_multiple_filters(filters: &[LdapFilter]) -> Result<Self, MailDomainError> {
for filter in filters {
if let LdapFilter::Equality(attr, value) = filter {
// Here if we receive None, it means the filter was not checking for the
// `mail` attr so we continue iterating.
if let Some(found) = Self::from_equality_filter(attr, value)? {
return Ok(found);
}
}
}
Err(MailDomainError::NoMailFilter)
}
/// Extract any mail filter from an LDAP search equality filter extracted
/// from a global search filter.
fn from_equality_filter(attr: &str, value: &str) -> Result<Option<Self>, MailDomainError> {
if attr != "mail" {
return Ok(None);
}
let mail = Self::new(value)?;
Ok(Some(mail))
}
pub fn to_user_ref(&self) -> UserRef {
UserRef {
username: self.username.clone(),
domain: self.domain.clone(),
}
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum MailDomainError {
/// mail value not user@domain format
InvalidMail,
/// No mail filter found in the search query
NoMailFilter,
/// Filter is not And/Or/Equality for which we can find a mail filter
InvalidFilter,
}
impl fmt::Display for MailDomainError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let msg = match self {
Self::InvalidMail => {
"No valid email address requested in mail filter in search request"
}
Self::NoMailFilter => "No mail filter found in search request",
Self::InvalidFilter => "No AND/OR/EQUALITY filter found in search request",
};
write!(f, "{msg}")
}
}
impl LdapReturnError for MailDomainError {
fn code(&self) -> LdapResultCode {
match self {
Self::InvalidMail => LdapResultCode::InvalidAttributeSyntax,
Self::NoMailFilter => LdapResultCode::InappropriateMatching,
Self::InvalidFilter => LdapResultCode::UnwillingToPerform,
}
}
fn message(&self) -> String {
self.to_string()
}
}
#[cfg(test)]
mod tests {
use crate::filter::{search_filter_and, search_filter_eq};
use super::*;
#[test]
fn no_mail_filter() {
let filter = search_filter_and(&[
search_filter_eq("uid", "a"),
search_filter_eq("objectClass", "inetOrgPerson"),
]);
let mail_filter = MailFilter::from_search_filter(&filter);
println!("{:?}", mail_filter);
let e = mail_filter.unwrap_err();
assert_eq!(e, MailDomainError::NoMailFilter);
}
#[test]
fn invalid_filter() {
let filter = LdapFilter::Approx("mail".to_string(), "a@a.localhost".to_string());
let mail_filter = MailFilter::from_search_filter(&filter);
println!("{:?}", mail_filter);
let e = mail_filter.unwrap_err();
assert_eq!(e, MailDomainError::InvalidFilter);
}
#[test]
fn invalid_mail_no_domain() {
let filter = search_filter_eq("mail", "a");
let mail_filter = MailFilter::from_search_filter(&filter);
println!("{:?}", mail_filter);
let e = mail_filter.unwrap_err();
assert_eq!(e, MailDomainError::InvalidMail);
}
#[test]
fn invalid_mail_too_many_parts() {
let filter = search_filter_eq("mail", "a@a.localhost@a.localhost");
let mail_filter = MailFilter::from_search_filter(&filter);
println!("{:?}", mail_filter);
let e = mail_filter.unwrap_err();
assert_eq!(e, MailDomainError::InvalidMail);
}
#[test]
fn valid_basic() {
let filter = search_filter_eq("mail", "a@a.localhost");
let mail_filter = MailFilter::from_search_filter(&filter);
println!("{:?}", mail_filter);
let mail = mail_filter.unwrap();
assert_eq!(mail.complete, "a@a.localhost");
assert_eq!(mail.username, "a");
assert_eq!(mail.domain, "a.localhost");
}
#[test]
fn valid_basic_normalization() {
let filter = search_filter_eq("mail", "A@A.localhost");
let mail_filter = MailFilter::from_search_filter(&filter);
println!("{:?}", mail_filter);
let mail = mail_filter.unwrap();
assert_eq!(mail.complete, "a@a.localhost");
assert_eq!(mail.username, "a");
assert_eq!(mail.domain, "a.localhost");
}
#[test]
fn stalwart_default() {
// (&(objectClass=inetOrgPerson)(mail=?))
let filter = search_filter_and(&[
search_filter_eq("mail", "a@a.localhost"),
search_filter_eq("objectClass", "inetOrgPerson"),
]);
let mail_filter = MailFilter::from_search_filter(&filter);
println!("{:?}", mail_filter);
let mail = mail_filter.unwrap();
assert_eq!(mail.complete, "a@a.localhost");
assert_eq!(mail.username, "a");
assert_eq!(mail.domain, "a.localhost");
}
// fn stalwart_lldap_example() {
// // &(|(objectClass=person)(member=cn=mail,ou=groups,dc=example,dc=org))(uid=?))
// let filter = search_filter_and(&[
// search_filter_eq("uid", "a"),
// search_filter_or(&[
// search_filter_eq("objectClass", "person"),
// search_filter_eq("member", "cn=mail,ou=groups,dc=a,dc=localhost"),
// ]),
// ]);
// }
}

1
src/ldap/filter/mod.rs Normal file
View file

@ -0,0 +1 @@
pub mod mail;

View file

@ -2,7 +2,9 @@ use ldap3_proto::LdapMsg;
use ldap3_proto::proto::LdapOp; use ldap3_proto::proto::LdapOp;
use crate::db::{Database, DatabaseInterface}; use crate::db::{Database, DatabaseInterface};
use crate::ldap::{LdapClientState, LdapStream, LdapStreamError, op_bind, op_ext}; use crate::ldap::{
LdapClientState, LdapStream, LdapStreamError, op_bind, op_ext, search_by_mail_filter,
};
#[tracing::instrument(name = "ldap", skip(stream, db), fields(session = %stream.session))] #[tracing::instrument(name = "ldap", skip(stream, db), fields(session = %stream.session))]
pub async fn ldap_handler<D: DatabaseInterface>(mut stream: LdapStream, mut db: Database<D>) { pub async fn ldap_handler<D: DatabaseInterface>(mut stream: LdapStream, mut db: Database<D>) {
@ -79,11 +81,21 @@ pub async fn ldap_handler_inner<D: DatabaseInterface>(
client_state.bind(bound_dn); client_state.bind(bound_dn);
Ok(true) Ok(true)
} else { } else {
tracing::debug!("Unsuccessful bind"); client_state.unbind();
// TODO: abort connection here? tracing::debug!("Failed bind or anonymous bind");
Ok(false) // We keep the connection open in case it's an anonymous bind
Ok(true)
} }
} }
LdapMsg {
msgid,
op: LdapOp::SearchRequest(sr),
// TODO: ctrl for pagination
ctrl: _,
} => {
search_by_mail_filter(stream, db, sr, msgid).await?;
Ok(true)
}
// Unsupported message // Unsupported message
_ => { _ => {
tracing::warn!("Unsupported client message, closing connection"); tracing::warn!("Unsupported client message, closing connection");

View file

@ -1,12 +1,14 @@
mod client_state; mod client_state;
pub use client_state::LdapClientState; pub use client_state::LdapClientState;
mod dn; mod dn;
pub use dn::{Dn, InvalidDnError, NotUserDnError}; pub use dn::{Dn, MalformedDn};
mod filter;
mod handler; mod handler;
mod op; mod op;
pub use handler::ldap_handler; pub use handler::ldap_handler;
pub use op::bind::op_bind; pub use op::bind::{BindDn, op_bind};
pub use op::ext::op_ext; pub use op::ext::op_ext;
pub use op::search::search_by_mail_filter;
mod return_error; mod return_error;
pub use return_error::LdapReturnError; pub use return_error::LdapReturnError;
mod stream; mod stream;

View file

@ -2,17 +2,91 @@ use ldap3_proto::proto::{LdapBindCred, LdapBindRequest, LdapBindResponse, LdapOp
use ldap3_proto::{LdapMsg, LdapResultCode}; use ldap3_proto::{LdapMsg, LdapResultCode};
use crate::db::error::BoxedError; use crate::db::error::BoxedError;
use crate::db::{Database, DatabaseInterface}; use crate::db::{Database, DatabaseInterface, UserRef};
use crate::ldap::{ use crate::ldap::{Dn, LdapReturnError, LdapStream, LdapStreamError, MalformedDn};
Dn, InvalidDnError, LdapReturnError, LdapStream, LdapStreamError, NotUserDnError,
#[derive(Debug)]
pub struct InvalidBindDn {
dn: String,
kind: InvalidBindDnKind,
}
#[derive(Debug)]
pub enum InvalidBindDnKind {
Malformed(MalformedDn),
NoUid,
MultipleUid,
NoDc,
}
impl LdapReturnError for InvalidBindDn {
fn code(&self) -> LdapResultCode {
LdapResultCode::InvalidDNSyntax
}
fn message(&self) -> String {
match &self.kind {
InvalidBindDnKind::Malformed(_e) => format!("Malformed dn: {}", self.dn),
InvalidBindDnKind::NoUid => format!("Missing `uid` in bind dn: {}", self.dn),
InvalidBindDnKind::MultipleUid => {
format!("Multiple `uid` accounts provided in bind dn: {}", self.dn)
}
InvalidBindDnKind::NoDc => format!("Missing `dc` in bind dn: {}", self.dn),
}
}
}
#[derive(Clone, Debug)]
pub struct BindDn(Dn);
impl BindDn {
pub fn from_dn_str(input: &str) -> Result<Self, InvalidBindDn> {
let dn = Dn::from_dn_str(input).map_err(|e| InvalidBindDn {
dn: input.to_string(),
kind: InvalidBindDnKind::Malformed(e),
})?;
let Some(uid) = dn.keys.get("uid") else {
return Err(InvalidBindDn {
dn: input.to_string(),
kind: InvalidBindDnKind::NoUid,
});
}; };
if uid.len() != 1 {
return Err(InvalidBindDn {
dn: input.to_string(),
kind: InvalidBindDnKind::MultipleUid,
});
}
if dn.get_hostname().is_none() {
return Err(InvalidBindDn {
dn: input.to_string(),
kind: InvalidBindDnKind::NoDc,
});
}
Ok(Self(dn))
}
pub fn to_dn_string(&self) -> String {
self.0.to_dn_string()
}
pub fn to_user_ref(&self) -> UserRef {
let username = self.0.keys.get("uid").unwrap()[0].clone();
let domain = self.0.keys.get("dc").unwrap().join(".");
UserRef { username, domain }
}
}
#[derive(Debug)] #[derive(Debug)]
pub enum BindError { pub enum BindError {
Db(BoxedError), Db(BoxedError),
InvalidCredentials, InvalidCredentials,
InvalidDn(InvalidDnError), InvalidDn(InvalidBindDn),
NotUserDn(NotUserDnError),
UnsupportedSASL, UnsupportedSASL,
} }
@ -42,19 +116,12 @@ impl BindError {
} }
} }
impl From<InvalidDnError> for BindError {
fn from(e: InvalidDnError) -> Self {
Self::InvalidDn(e)
}
}
impl LdapReturnError for BindError { impl LdapReturnError for BindError {
fn code(&self) -> LdapResultCode { fn code(&self) -> LdapResultCode {
match self { match self {
Self::Db(_e) => LdapResultCode::Unavailable, Self::Db(_e) => LdapResultCode::Unavailable,
Self::InvalidCredentials => LdapResultCode::InvalidCredentials, Self::InvalidCredentials => LdapResultCode::InvalidCredentials,
Self::InvalidDn(e) => e.code(), Self::InvalidDn(e) => e.code(),
Self::NotUserDn(e) => e.code(),
Self::UnsupportedSASL => LdapResultCode::OperationsError, Self::UnsupportedSASL => LdapResultCode::OperationsError,
} }
} }
@ -64,7 +131,6 @@ impl LdapReturnError for BindError {
Self::Db(e) => format!("Database error: {e}"), Self::Db(e) => format!("Database error: {e}"),
Self::InvalidCredentials => "Wrong username or password".to_string(), Self::InvalidCredentials => "Wrong username or password".to_string(),
Self::InvalidDn(e) => e.message(), Self::InvalidDn(e) => e.message(),
Self::NotUserDn(e) => e.message(),
Self::UnsupportedSASL => "SASL login is not supported".to_string(), Self::UnsupportedSASL => "SASL login is not supported".to_string(),
} }
} }
@ -98,8 +164,14 @@ pub async fn op_bind<D: DatabaseInterface>(
db: &Database<D>, db: &Database<D>,
req: LdapBindRequest, req: LdapBindRequest,
msgid: i32, msgid: i32,
) -> Result<Option<Dn>, LdapStreamError> { ) -> Result<Option<BindDn>, LdapStreamError> {
let dn = match Dn::from_dn_str(&req.dn) { // Anonymous bind
if req.dn.is_empty() {
bind_success(stream, msgid).await?;
return Ok(None);
}
let dn = match BindDn::from_dn_str(&req.dn) {
Ok(dn) => dn, Ok(dn) => dn,
Err(e) => { Err(e) => {
BindError::InvalidDn(e).error_message(stream, msgid).await?; BindError::InvalidDn(e).error_message(stream, msgid).await?;
@ -114,15 +186,7 @@ pub async fn op_bind<D: DatabaseInterface>(
return Ok(None); return Ok(None);
}; };
let user_ref = match dn.to_user_ref() { let success = match db.check_password(&dn.to_user_ref(), &password).await {
Ok(user_ref) => user_ref,
Err(e) => {
BindError::NotUserDn(e).error_message(stream, msgid).await?;
return Ok(None);
}
};
let success = match db.check_password(&user_ref, &password).await {
Ok(success) => success, Ok(success) => success,
Err(e) => { Err(e) => {
// tracing::error!(error = &*e as &dyn std::error::Error, "Database failure"); // tracing::error!(error = &*e as &dyn std::error::Error, "Database failure");

View file

@ -1,2 +1,3 @@
pub mod bind; pub mod bind;
pub mod ext; pub mod ext;
pub mod search;

217
src/ldap/op/search.rs Normal file
View file

@ -0,0 +1,217 @@
use ldap3_proto::control::LdapControl;
use ldap3_proto::proto::{
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)]
pub struct InvalidSearchDn {
dn: String,
kind: InvalidSearchDnKind,
}
#[derive(Debug)]
pub enum InvalidSearchDnKind {
Malformed(MalformedDn),
NoOu,
MultipleOu,
}
impl LdapReturnError for InvalidSearchDn {
fn code(&self) -> LdapResultCode {
LdapResultCode::InvalidDNSyntax
}
fn message(&self) -> String {
match &self.kind {
InvalidSearchDnKind::Malformed(_e) => format!("Malformed dn: {}", self.dn),
InvalidSearchDnKind::NoOu => format!("Missing `ou` in search dn: {}", self.dn),
InvalidSearchDnKind::MultipleOu => {
format!("Multiple `ou` tables provided in search dn: {}", self.dn)
}
}
}
}
#[derive(Clone, Debug)]
pub struct SearchDn(Dn);
impl SearchDn {
pub fn from_dn_str(input: &str) -> Result<Self, InvalidSearchDn> {
let dn = Dn::from_dn_str(input).map_err(|e| InvalidSearchDn {
dn: input.to_string(),
kind: InvalidSearchDnKind::Malformed(e),
})?;
let Some(ou) = dn.keys.get("ou") else {
return Err(InvalidSearchDn {
dn: input.to_string(),
kind: InvalidSearchDnKind::NoOu,
});
};
if ou.len() != 1 {
return Err(InvalidSearchDn {
dn: input.to_string(),
kind: InvalidSearchDnKind::MultipleOu,
});
}
Ok(Self(dn))
}
}
#[derive(Debug)]
pub enum SearchError {
Db(BoxedError),
InvalidDn(InvalidSearchDn),
MailDomain(MailDomainError),
}
impl SearchError {
pub async fn error_message(
&self,
stream: &mut LdapStream,
msgid: i32,
) -> Result<(), LdapStreamError> {
let resp_msg = LdapMsg {
msgid,
op: LdapOp::SearchResultDone(LdapResult {
code: self.code(),
matcheddn: String::new(),
message: self.message(),
referral: vec![],
}),
ctrl: vec![],
};
stream.send(resp_msg).await?;
Ok(())
}
}
impl LdapReturnError for SearchError {
fn code(&self) -> LdapResultCode {
match self {
Self::Db(_e) => LdapResultCode::Unavailable,
Self::InvalidDn(e) => e.code(),
Self::MailDomain(e) => e.code(),
}
}
fn message(&self) -> String {
match self {
Self::Db(e) => format!("Database error: {e}"),
Self::InvalidDn(e) => e.message(),
Self::MailDomain(e) => e.message(),
}
}
}
pub async fn search_success(
stream: &mut LdapStream,
msgid: i32,
entries: Vec<(LdapSearchResultEntry, Vec<LdapControl>)>,
) -> Result<(), LdapStreamError> {
let count = entries.len();
for (entry, ctrl) in entries {
tracing::debug!("Search result: {entry:?}");
stream
.send(LdapMsg {
msgid,
op: LdapOp::SearchResultEntry(entry),
ctrl,
})
.await?;
}
stream
.send(LdapMsg {
msgid,
op: LdapOp::SearchResultDone(LdapResult {
code: LdapResultCode::Success,
matcheddn: String::new(),
message: format!("Found {count} result(s)"),
referral: vec![],
}),
// TODO: implement LdapControl for pagination
ctrl: vec![],
})
.await?;
Ok(())
}
fn search_entry_from_user(user: &User, req_attrs: &[String]) -> LdapSearchResultEntry {
let mut res: Vec<LdapPartialAttribute> = vec![];
for attr in req_attrs {
if let Some(attr_value) = match attr.as_str() {
"uid" => Some(user.username.clone()),
"cn"|"mail" => Some(user.mail.clone()),
_ => {
tracing::warn!("Ignoring unknown attr in search query: {attr}");
None
}
} {
res.push(LdapPartialAttribute {
atype: attr.clone(),
// TODO: there may be multiple values here in the future,
// eg. mailaliases
vals: vec![Vec::from(attr_value)],
});
}
}
LdapSearchResultEntry {
dn: Dn::from_user(user).to_dn_string(),
attributes: res,
}
}
pub async fn search_by_mail_filter<D: DatabaseInterface>(
stream: &mut LdapStream,
db: &Database<D>,
sr: LdapSearchRequest,
msgid: i32,
) -> Result<(), LdapStreamError> {
// TODO: We should probably reuse the search DN somehow
if let Err(e) = SearchDn::from_dn_str(&sr.base) {
SearchError::InvalidDn(e)
.error_message(stream, msgid)
.await?;
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,
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?;
}
Ok(())
}