feat: Implement LDAP bind/whoami

This commit is contained in:
selfhoster selfhoster 2026-09-01 21:18:19 +02:00
commit b4330b37fa
19 changed files with 664 additions and 37 deletions

129
src/ldap/op/bind.rs Normal file
View file

@ -0,0 +1,129 @@
use ldap3_proto::proto::{LdapBindCred, LdapBindRequest, LdapBindResponse, LdapOp, LdapResult};
use ldap3_proto::{LdapMsg, LdapResultCode};
use crate::db::{Database, DatabaseInterface};
use crate::ldap::{
Dn, InvalidDnError, LdapReturnError, LdapStream, LdapStreamError, NotUserDnError,
};
pub enum BindError {
// TODO: DB Error
InvalidCredentials,
InvalidDn(InvalidDnError),
NotUserDn(NotUserDnError),
UnsupportedSASL,
}
impl BindError {
pub async fn error_message(
&self,
stream: &mut LdapStream,
msgid: i32,
) -> Result<(), LdapStreamError> {
let resp_msg = LdapMsg {
msgid,
op: LdapOp::BindResponse(LdapBindResponse {
res: LdapResult {
code: self.code(),
matcheddn: String::new(),
message: self.message(),
referral: vec![],
},
saslcreds: None,
}),
ctrl: vec![],
};
stream.send(resp_msg).await?;
Ok(())
}
}
impl From<InvalidDnError> for BindError {
fn from(e: InvalidDnError) -> Self {
Self::InvalidDn(e)
}
}
impl LdapReturnError for BindError {
fn code(&self) -> LdapResultCode {
match self {
Self::InvalidCredentials => LdapResultCode::InvalidCredentials,
Self::InvalidDn(e) => e.code(),
Self::NotUserDn(e) => e.code(),
Self::UnsupportedSASL => LdapResultCode::OperationsError,
}
}
fn message(&self) -> String {
match self {
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(),
}
}
}
pub async fn bind_success(stream: &mut LdapStream, msgid: i32) -> Result<(), LdapStreamError> {
let resp_msg = LdapMsg {
msgid,
op: LdapOp::BindResponse(LdapBindResponse {
res: LdapResult {
code: LdapResultCode::Success,
matcheddn: String::new(),
message: String::new(),
referral: vec![],
},
saslcreds: None,
}),
ctrl: vec![],
};
stream.send(resp_msg).await?;
Ok(())
}
/// Tries to bind the user.
///
/// On success, returns `Ok(Some(bound_dn))`. `Ok(None)` means credentials failed,
/// either because the account does not exist, or the password is wrong.
pub async fn op_bind<D: DatabaseInterface>(
stream: &mut LdapStream,
db: &Database<D>,
req: LdapBindRequest,
msgid: i32,
) -> Result<Option<Dn>, LdapStreamError> {
let dn = match Dn::from_dn_str(&req.dn) {
Ok(dn) => dn,
Err(e) => {
BindError::InvalidDn(e).error_message(stream, msgid).await?;
return Ok(None);
}
};
let LdapBindCred::Simple(password) = req.cred else {
BindError::UnsupportedSASL
.error_message(stream, msgid)
.await?;
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);
}
};
if db.check_password(&user_ref, &password).await {
bind_success(stream, msgid).await?;
Ok(Some(dn))
} else {
BindError::InvalidCredentials
.error_message(stream, msgid)
.await?;
Ok(None)
}
}