Separate ldap_handler, add tests

This commit is contained in:
Valentin Tolmer 2021-03-11 10:50:15 +01:00
parent ff4e986a0d
commit 86b89a00cc
4 changed files with 143 additions and 78 deletions

View File

@ -2,13 +2,16 @@ use crate::infra::configuration::Configuration;
use anyhow::{bail, Result}; use anyhow::{bail, Result};
use sqlx::any::AnyPool; use sqlx::any::AnyPool;
#[cfg_attr(test, derive(PartialEq, Eq, Debug))]
pub struct BindRequest { pub struct BindRequest {
pub name: String, pub name: String,
pub password: String, pub password: String,
} }
#[cfg_attr(test, derive(PartialEq, Eq, Debug))]
pub struct SearchRequest {} pub struct SearchRequest {}
#[cfg_attr(test, derive(PartialEq, Eq, Debug))]
pub struct SearchResponse {} pub struct SearchResponse {}
pub trait BackendHandler: Clone + Send { pub trait BackendHandler: Clone + Send {
@ -47,3 +50,15 @@ impl BackendHandler for SqlBackendHandler {
Ok(SearchResponse {}) Ok(SearchResponse {})
} }
} }
#[cfg(test)]
mockall::mock! {
pub TestBackendHandler{}
impl Clone for TestBackendHandler {
fn clone(&self) -> Self;
}
impl BackendHandler for TestBackendHandler {
fn bind(&mut self, request: BindRequest) -> Result<()>;
fn search(&mut self, request: SearchRequest) -> Result<SearchResponse>;
}
}

123
src/infra/ldap_handler.rs Normal file
View File

@ -0,0 +1,123 @@
use crate::domain::handler::BackendHandler;
use ldap3_server::simple::*;
pub struct LdapHandler<Backend: BackendHandler> {
dn: String,
backend_handler: Backend,
}
impl<Backend: BackendHandler> LdapHandler<Backend> {
pub fn new(backend_handler: Backend) -> Self {
Self {
dn: "Unauthenticated".to_string(),
backend_handler,
}
}
pub fn do_bind(&mut self, sbr: &SimpleBindRequest) -> LdapMsg {
match self
.backend_handler
.bind(crate::domain::handler::BindRequest {
name: sbr.dn.clone(),
password: sbr.pw.clone(),
}) {
Ok(()) => {
self.dn = sbr.dn.clone();
sbr.gen_success()
}
Err(_) => sbr.gen_invalid_cred(),
}
}
pub fn do_search(&mut self, lsr: &SearchRequest) -> Vec<LdapMsg> {
vec![
lsr.gen_result_entry(LdapSearchResultEntry {
dn: "cn=hello,dc=example,dc=com".to_string(),
attributes: vec![
LdapPartialAttribute {
atype: "objectClass".to_string(),
vals: vec!["cursed".to_string()],
},
LdapPartialAttribute {
atype: "cn".to_string(),
vals: vec!["hello".to_string()],
},
],
}),
lsr.gen_result_entry(LdapSearchResultEntry {
dn: "cn=world,dc=example,dc=com".to_string(),
attributes: vec![
LdapPartialAttribute {
atype: "objectClass".to_string(),
vals: vec!["cursed".to_string()],
},
LdapPartialAttribute {
atype: "cn".to_string(),
vals: vec!["world".to_string()],
},
],
}),
lsr.gen_success(),
]
}
pub fn do_whoami(&mut self, wr: &WhoamiRequest) -> LdapMsg {
if self.dn == "Unauthenticated" {
wr.gen_operror("Unauthenticated")
} else {
wr.gen_success(format!("dn: {}", self.dn).as_str())
}
}
pub fn handle_ldap_message(&mut self, server_op: ServerOps) -> Option<Vec<LdapMsg>> {
let result = match server_op {
ServerOps::SimpleBind(sbr) => vec![self.do_bind(&sbr)],
ServerOps::Search(sr) => self.do_search(&sr),
ServerOps::Unbind(_) => {
// No need to notify on unbind (per rfc4511)
return None;
}
ServerOps::Whoami(wr) => vec![self.do_whoami(&wr)],
};
Some(result)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::domain::handler::MockTestBackendHandler;
use mockall::{mock, predicate::*};
#[test]
fn test_bind() {
let mut mock = MockTestBackendHandler::new();
mock.expect_bind()
.with(eq(crate::domain::handler::BindRequest {
name: "test".to_string(),
password: "pass".to_string(),
}))
.times(1)
.return_once(|_| Ok(()));
let mut ldap_handler = LdapHandler::new(mock);
let request = WhoamiRequest { msgid: 1 };
assert_eq!(
ldap_handler.do_whoami(&request),
request.gen_operror("Unauthenticated")
);
let request = SimpleBindRequest {
msgid: 2,
dn: "test".to_string(),
pw: "pass".to_string(),
};
assert_eq!(ldap_handler.do_bind(&request), request.gen_success());
let request = WhoamiRequest { msgid: 3 };
assert_eq!(
ldap_handler.do_whoami(&request),
request.gen_success("dn: test")
);
}
}

View File

@ -1,89 +1,18 @@
use crate::domain::handler::BackendHandler; use crate::domain::handler::BackendHandler;
use crate::infra::configuration::Configuration; use crate::infra::configuration::Configuration;
use crate::infra::ldap_handler::LdapHandler;
use actix_rt::net::TcpStream; use actix_rt::net::TcpStream;
use actix_server::ServerBuilder; use actix_server::ServerBuilder;
use actix_service::{fn_service, pipeline_factory}; use actix_service::{fn_service, pipeline_factory};
use anyhow::bail; use anyhow::bail;
use anyhow::Result; use anyhow::Result;
use futures_util::future::ok; use futures_util::future::ok;
use ldap3_server::simple::*;
use ldap3_server::LdapCodec;
use log::*; use log::*;
use tokio::net::tcp::WriteHalf; use tokio::net::tcp::WriteHalf;
use tokio_util::codec::{FramedRead, FramedWrite}; use tokio_util::codec::{FramedRead, FramedWrite};
use ldap3_server::simple::*;
use ldap3_server::LdapCodec;
pub struct LdapHandler<Backend: BackendHandler> {
dn: String,
backend_handler: Backend,
}
impl<Backend: BackendHandler> LdapHandler<Backend> {
pub fn do_bind(&mut self, sbr: &SimpleBindRequest) -> LdapMsg {
match self
.backend_handler
.bind(crate::domain::handler::BindRequest {
name: sbr.dn.clone(),
password: sbr.pw.clone(),
}) {
Ok(()) => {
self.dn = sbr.dn.clone();
sbr.gen_success()
}
Err(_) => sbr.gen_invalid_cred(),
}
}
pub fn do_search(&mut self, lsr: &SearchRequest) -> Vec<LdapMsg> {
vec![
lsr.gen_result_entry(LdapSearchResultEntry {
dn: "cn=hello,dc=example,dc=com".to_string(),
attributes: vec![
LdapPartialAttribute {
atype: "objectClass".to_string(),
vals: vec!["cursed".to_string()],
},
LdapPartialAttribute {
atype: "cn".to_string(),
vals: vec!["hello".to_string()],
},
],
}),
lsr.gen_result_entry(LdapSearchResultEntry {
dn: "cn=world,dc=example,dc=com".to_string(),
attributes: vec![
LdapPartialAttribute {
atype: "objectClass".to_string(),
vals: vec!["cursed".to_string()],
},
LdapPartialAttribute {
atype: "cn".to_string(),
vals: vec!["world".to_string()],
},
],
}),
lsr.gen_success(),
]
}
pub fn do_whoami(&mut self, wr: &WhoamiRequest) -> LdapMsg {
wr.gen_success(format!("dn: {}", self.dn).as_str())
}
pub fn handle_ldap_message(&mut self, server_op: ServerOps) -> Option<Vec<LdapMsg>> {
let result = match server_op {
ServerOps::SimpleBind(sbr) => vec![self.do_bind(&sbr)],
ServerOps::Search(sr) => self.do_search(&sr),
ServerOps::Unbind(_) => {
// No need to notify on unbind (per rfc4511)
return None;
}
ServerOps::Whoami(wr) => vec![self.do_whoami(&wr)],
};
Some(result)
}
}
async fn handle_incoming_message<Backend: BackendHandler>( async fn handle_incoming_message<Backend: BackendHandler>(
msg: Result<LdapMsg, std::io::Error>, msg: Result<LdapMsg, std::io::Error>,
resp: &mut FramedWrite<WriteHalf<'_>, LdapCodec>, resp: &mut FramedWrite<WriteHalf<'_>, LdapCodec>,
@ -146,10 +75,7 @@ where
let mut requests = FramedRead::new(r, LdapCodec); let mut requests = FramedRead::new(r, LdapCodec);
let mut resp = FramedWrite::new(w, LdapCodec); let mut resp = FramedWrite::new(w, LdapCodec);
let mut session = LdapHandler { let mut session = LdapHandler::new(backend_handler);
dn: "Unauthenticated".to_string(),
backend_handler,
};
while let Some(msg) = requests.next().await { while let Some(msg) = requests.next().await {
if !handle_incoming_message(msg, &mut resp, &mut session).await? { if !handle_incoming_message(msg, &mut resp, &mut session).await? {

View File

@ -1,5 +1,6 @@
pub mod cli; pub mod cli;
pub mod configuration; pub mod configuration;
pub mod ldap_handler;
pub mod ldap_server; pub mod ldap_server;
pub mod logging; pub mod logging;
pub mod tcp_server; pub mod tcp_server;