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 { 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( stream: &mut LdapStream, db: &Database, req: LdapBindRequest, msgid: i32, ) -> Result, 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) } }