208 lines
5.8 KiB
Rust
208 lines
5.8 KiB
Rust
use ldap3_proto::proto::{LdapBindCred, LdapBindRequest, LdapBindResponse, LdapOp, LdapResult};
|
|
use ldap3_proto::{LdapMsg, LdapResultCode};
|
|
|
|
use crate::db::error::BoxedError;
|
|
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(InvalidBindDn),
|
|
UnsupportedSASL,
|
|
}
|
|
|
|
impl BindError {
|
|
pub async fn error_message(
|
|
&self,
|
|
stream: &mut LdapStream,
|
|
msgid: i32,
|
|
) -> Result<(), LdapStreamError> {
|
|
tracing::debug!(error=?self, "Bind failed");
|
|
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 LdapReturnError for BindError {
|
|
fn code(&self) -> LdapResultCode {
|
|
match self {
|
|
Self::Db(_e) => LdapResultCode::Unavailable,
|
|
Self::InvalidCredentials => LdapResultCode::InvalidCredentials,
|
|
Self::InvalidDn(e) => e.code(),
|
|
Self::UnsupportedSASL => LdapResultCode::OperationsError,
|
|
}
|
|
}
|
|
|
|
fn message(&self) -> String {
|
|
match self {
|
|
Self::Db(e) => format!("Database error: {e}"),
|
|
Self::InvalidCredentials => "Wrong username or password".to_string(),
|
|
Self::InvalidDn(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<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?;
|
|
return Ok(None);
|
|
}
|
|
};
|
|
|
|
let LdapBindCred::Simple(password) = req.cred else {
|
|
BindError::UnsupportedSASL
|
|
.error_message(stream, msgid)
|
|
.await?;
|
|
return Ok(None);
|
|
};
|
|
|
|
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");
|
|
tracing::error!(error = e, "Database failure");
|
|
BindError::Db(e).error_message(stream, msgid).await?;
|
|
return Ok(None);
|
|
}
|
|
};
|
|
|
|
if success {
|
|
bind_success(stream, msgid).await?;
|
|
Ok(Some(dn))
|
|
} else {
|
|
BindError::InvalidCredentials
|
|
.error_message(stream, msgid)
|
|
.await?;
|
|
Ok(None)
|
|
}
|
|
}
|