llldap/src/ldap/op/search.rs

217 lines
5.9 KiB
Rust

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(#[expect(unused)] 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(
stream: &mut LdapStream,
db: &Database,
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(())
}