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

@ -2,17 +2,91 @@ use ldap3_proto::proto::{LdapBindCred, LdapBindRequest, LdapBindResponse, LdapOp
use ldap3_proto::{LdapMsg, LdapResultCode};
use crate::db::error::BoxedError;
use crate::db::{Database, DatabaseInterface};
use crate::ldap::{
Dn, InvalidDnError, LdapReturnError, LdapStream, LdapStreamError, NotUserDnError,
};
use crate::db::{Database, DatabaseInterface, UserRef};
use crate::ldap::{Dn, LdapReturnError, LdapStream, LdapStreamError, MalformedDn};
#[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)]
pub enum BindError {
Db(BoxedError),
InvalidCredentials,
InvalidDn(InvalidDnError),
NotUserDn(NotUserDnError),
InvalidDn(InvalidBindDn),
UnsupportedSASL,
}
@ -42,19 +116,12 @@ impl BindError {
}
}
impl From<InvalidDnError> for BindError {
fn from(e: InvalidDnError) -> Self {
Self::InvalidDn(e)
}
}
impl LdapReturnError for BindError {
fn code(&self) -> LdapResultCode {
match self {
Self::Db(_e) => LdapResultCode::Unavailable,
Self::InvalidCredentials => LdapResultCode::InvalidCredentials,
Self::InvalidDn(e) => e.code(),
Self::NotUserDn(e) => e.code(),
Self::UnsupportedSASL => LdapResultCode::OperationsError,
}
}
@ -64,7 +131,6 @@ impl LdapReturnError for BindError {
Self::Db(e) => format!("Database error: {e}"),
Self::InvalidCredentials => "Wrong username or password".to_string(),
Self::InvalidDn(e) => e.message(),
Self::NotUserDn(e) => e.message(),
Self::UnsupportedSASL => "SASL login is not supported".to_string(),
}
}
@ -98,8 +164,14 @@ pub async fn op_bind<D: DatabaseInterface>(
db: &Database<D>,
req: LdapBindRequest,
msgid: i32,
) -> Result<Option<Dn>, LdapStreamError> {
let dn = match Dn::from_dn_str(&req.dn) {
) -> Result<Option<BindDn>, LdapStreamError> {
// 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,
Err(e) => {
BindError::InvalidDn(e).error_message(stream, msgid).await?;
@ -114,15 +186,7 @@ pub async fn op_bind<D: DatabaseInterface>(
return Ok(None);
};
let user_ref = match dn.to_user_ref() {
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 {
let success = match db.check_password(&dn.to_user_ref(), &password).await {
Ok(success) => success,
Err(e) => {
// tracing::error!(error = &*e as &dyn std::error::Error, "Database failure");