feat: Search queries by mail attribute
This commit is contained in:
parent
7527ba7a09
commit
c77223048c
9 changed files with 592 additions and 78 deletions
|
|
@ -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");
|
||||
|
|
|
|||
|
|
@ -1,2 +1,3 @@
|
|||
pub mod bind;
|
||||
pub mod ext;
|
||||
pub mod search;
|
||||
|
|
|
|||
217
src/ldap/op/search.rs
Normal file
217
src/ldap/op/search.rs
Normal 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(())
|
||||
}
|
||||
Loading…
Reference in a new issue