mirror of
https://github.com/nitnelave/lldap.git
synced 2023-04-12 14:25:13 +00:00
Add support for non-admin bind
This commit is contained in:
parent
31e8998ac3
commit
6abe94af13
@ -27,6 +27,7 @@ tracing = "*"
|
|||||||
tracing-actix-web = "0.3.0-beta.2"
|
tracing-actix-web = "0.3.0-beta.2"
|
||||||
tracing-log = "*"
|
tracing-log = "*"
|
||||||
tracing-subscriber = "*"
|
tracing-subscriber = "*"
|
||||||
|
async-trait = "0.1.48"
|
||||||
|
|
||||||
[dependencies.figment]
|
[dependencies.figment]
|
||||||
features = ["toml", "env"]
|
features = ["toml", "env"]
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
use crate::infra::configuration::Configuration;
|
use crate::infra::configuration::Configuration;
|
||||||
use anyhow::{bail, Result};
|
use anyhow::{bail, Result};
|
||||||
|
use async_trait::async_trait;
|
||||||
use sqlx::any::AnyPool;
|
use sqlx::any::AnyPool;
|
||||||
|
use sqlx::Row;
|
||||||
|
|
||||||
#[cfg_attr(test, derive(PartialEq, Eq, Debug))]
|
#[cfg_attr(test, derive(PartialEq, Eq, Debug))]
|
||||||
pub struct BindRequest {
|
pub struct BindRequest {
|
||||||
@ -24,9 +26,10 @@ pub struct User {
|
|||||||
pub creation_date: chrono::NaiveDateTime,
|
pub creation_date: chrono::NaiveDateTime,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
pub trait BackendHandler: Clone + Send {
|
pub trait BackendHandler: Clone + Send {
|
||||||
fn bind(&mut self, request: BindRequest) -> Result<()>;
|
async fn bind(&mut self, request: BindRequest) -> Result<()>;
|
||||||
fn list_users(&mut self, request: ListUsersRequest) -> Result<Vec<User>>;
|
async fn list_users(&mut self, request: ListUsersRequest) -> Result<Vec<User>>;
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
@ -46,19 +49,34 @@ impl SqlBackendHandler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn passwords_match(encrypted_password: &str, clear_password: &str) -> bool {
|
||||||
|
encrypted_password == clear_password
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
impl BackendHandler for SqlBackendHandler {
|
impl BackendHandler for SqlBackendHandler {
|
||||||
fn bind(&mut self, request: BindRequest) -> Result<()> {
|
async fn bind(&mut self, request: BindRequest) -> Result<()> {
|
||||||
if request.name == self.config.ldap_user_dn
|
if request.name == self.config.ldap_user_dn {
|
||||||
&& request.password == self.config.ldap_user_pass
|
if request.password == self.config.ldap_user_pass {
|
||||||
{
|
|
||||||
self.authenticated = true;
|
self.authenticated = true;
|
||||||
Ok(())
|
return Ok(());
|
||||||
} else {
|
} else {
|
||||||
bail!(r#"Authentication error for "{}""#, request.name)
|
bail!(r#"Authentication error for "{}""#, request.name)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if let Ok(row) = sqlx::query("SELECT password FROM users WHERE user_id = ?")
|
||||||
|
.bind(&request.name)
|
||||||
|
.fetch_one(&self.sql_pool)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
if passwords_match(&request.password, &row.get::<String, _>("password")) {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
bail!(r#"Authentication error for "{}""#, request.name)
|
||||||
|
}
|
||||||
|
|
||||||
fn list_users(&mut self, request: ListUsersRequest) -> Result<Vec<User>> {
|
async fn list_users(&mut self, request: ListUsersRequest) -> Result<Vec<User>> {
|
||||||
Ok(Vec::new())
|
Ok(Vec::new())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -69,8 +87,9 @@ mockall::mock! {
|
|||||||
impl Clone for TestBackendHandler {
|
impl Clone for TestBackendHandler {
|
||||||
fn clone(&self) -> Self;
|
fn clone(&self) -> Self;
|
||||||
}
|
}
|
||||||
|
#[async_trait]
|
||||||
impl BackendHandler for TestBackendHandler {
|
impl BackendHandler for TestBackendHandler {
|
||||||
fn bind(&mut self, request: BindRequest) -> Result<()>;
|
async fn bind(&mut self, request: BindRequest) -> Result<()>;
|
||||||
fn list_users(&mut self, request: ListUsersRequest) -> Result<Vec<User>>;
|
async fn list_users(&mut self, request: ListUsersRequest) -> Result<Vec<User>>;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -82,10 +82,11 @@ pub struct LdapHandler<Backend: BackendHandler> {
|
|||||||
backend_handler: Backend,
|
backend_handler: Backend,
|
||||||
pub base_dn: Vec<(String, String)>,
|
pub base_dn: Vec<(String, String)>,
|
||||||
base_dn_str: String,
|
base_dn_str: String,
|
||||||
|
ldap_user_dn: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<Backend: BackendHandler> LdapHandler<Backend> {
|
impl<Backend: BackendHandler> LdapHandler<Backend> {
|
||||||
pub fn new(backend_handler: Backend, ldap_base_dn: String) -> Self {
|
pub fn new(backend_handler: Backend, ldap_base_dn: String, ldap_user_dn: String) -> Self {
|
||||||
Self {
|
Self {
|
||||||
dn: "Unauthenticated".to_string(),
|
dn: "Unauthenticated".to_string(),
|
||||||
backend_handler,
|
backend_handler,
|
||||||
@ -96,16 +97,19 @@ impl<Backend: BackendHandler> LdapHandler<Backend> {
|
|||||||
)
|
)
|
||||||
}),
|
}),
|
||||||
base_dn_str: ldap_base_dn,
|
base_dn_str: ldap_base_dn,
|
||||||
|
ldap_user_dn,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn do_bind(&mut self, sbr: &SimpleBindRequest) -> LdapMsg {
|
pub async fn do_bind(&mut self, sbr: &SimpleBindRequest) -> LdapMsg {
|
||||||
match self
|
match self
|
||||||
.backend_handler
|
.backend_handler
|
||||||
.bind(crate::domain::handler::BindRequest {
|
.bind(crate::domain::handler::BindRequest {
|
||||||
name: sbr.dn.clone(),
|
name: sbr.dn.clone(),
|
||||||
password: sbr.pw.clone(),
|
password: sbr.pw.clone(),
|
||||||
}) {
|
})
|
||||||
|
.await
|
||||||
|
{
|
||||||
Ok(()) => {
|
Ok(()) => {
|
||||||
self.dn = sbr.dn.clone();
|
self.dn = sbr.dn.clone();
|
||||||
sbr.gen_success()
|
sbr.gen_success()
|
||||||
@ -114,7 +118,13 @@ impl<Backend: BackendHandler> LdapHandler<Backend> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn do_search(&mut self, lsr: &SearchRequest) -> Vec<LdapMsg> {
|
pub async fn do_search(&mut self, lsr: &SearchRequest) -> Vec<LdapMsg> {
|
||||||
|
if self.dn != self.ldap_user_dn {
|
||||||
|
return vec![lsr.gen_error(
|
||||||
|
LdapResultCode::InsufficentAccessRights,
|
||||||
|
r#"Current user is not allowed to query LDAP"#.to_string(),
|
||||||
|
)];
|
||||||
|
}
|
||||||
let dn_parts = match parse_distinguished_name(&lsr.base) {
|
let dn_parts = match parse_distinguished_name(&lsr.base) {
|
||||||
Ok(dn) => dn,
|
Ok(dn) => dn,
|
||||||
Err(_) => {
|
Err(_) => {
|
||||||
@ -128,7 +138,7 @@ impl<Backend: BackendHandler> LdapHandler<Backend> {
|
|||||||
// Search path is not in our tree, just return an empty success.
|
// Search path is not in our tree, just return an empty success.
|
||||||
return vec![lsr.gen_success()];
|
return vec![lsr.gen_success()];
|
||||||
}
|
}
|
||||||
let users = match self.backend_handler.list_users(ListUsersRequest {}) {
|
let users = match self.backend_handler.list_users(ListUsersRequest {}).await {
|
||||||
Ok(users) => users,
|
Ok(users) => users,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
return vec![lsr.gen_error(
|
return vec![lsr.gen_error(
|
||||||
@ -156,10 +166,10 @@ impl<Backend: BackendHandler> LdapHandler<Backend> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn handle_ldap_message(&mut self, server_op: ServerOps) -> Option<Vec<LdapMsg>> {
|
pub async fn handle_ldap_message(&mut self, server_op: ServerOps) -> Option<Vec<LdapMsg>> {
|
||||||
let result = match server_op {
|
let result = match server_op {
|
||||||
ServerOps::SimpleBind(sbr) => vec![self.do_bind(&sbr)],
|
ServerOps::SimpleBind(sbr) => vec![self.do_bind(&sbr).await],
|
||||||
ServerOps::Search(sr) => self.do_search(&sr),
|
ServerOps::Search(sr) => self.do_search(&sr).await,
|
||||||
ServerOps::Unbind(_) => {
|
ServerOps::Unbind(_) => {
|
||||||
// No need to notify on unbind (per rfc4511)
|
// No need to notify on unbind (per rfc4511)
|
||||||
return None;
|
return None;
|
||||||
@ -176,9 +186,10 @@ mod tests {
|
|||||||
use crate::domain::handler::MockTestBackendHandler;
|
use crate::domain::handler::MockTestBackendHandler;
|
||||||
use chrono::NaiveDateTime;
|
use chrono::NaiveDateTime;
|
||||||
use mockall::predicate::eq;
|
use mockall::predicate::eq;
|
||||||
|
use tokio;
|
||||||
|
|
||||||
#[test]
|
#[tokio::test]
|
||||||
fn test_bind() {
|
async fn test_bind() {
|
||||||
let mut mock = MockTestBackendHandler::new();
|
let mut mock = MockTestBackendHandler::new();
|
||||||
mock.expect_bind()
|
mock.expect_bind()
|
||||||
.with(eq(crate::domain::handler::BindRequest {
|
.with(eq(crate::domain::handler::BindRequest {
|
||||||
@ -187,7 +198,8 @@ mod tests {
|
|||||||
}))
|
}))
|
||||||
.times(1)
|
.times(1)
|
||||||
.return_once(|_| Ok(()));
|
.return_once(|_| Ok(()));
|
||||||
let mut ldap_handler = LdapHandler::new(mock, "dc=example,dc=com".to_string());
|
let mut ldap_handler =
|
||||||
|
LdapHandler::new(mock, "dc=example,dc=com".to_string(), "test".to_string());
|
||||||
|
|
||||||
let request = WhoamiRequest { msgid: 1 };
|
let request = WhoamiRequest { msgid: 1 };
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
@ -200,7 +212,7 @@ mod tests {
|
|||||||
dn: "test".to_string(),
|
dn: "test".to_string(),
|
||||||
pw: "pass".to_string(),
|
pw: "pass".to_string(),
|
||||||
};
|
};
|
||||||
assert_eq!(ldap_handler.do_bind(&request), request.gen_success());
|
assert_eq!(ldap_handler.do_bind(&request).await, request.gen_success());
|
||||||
|
|
||||||
let request = WhoamiRequest { msgid: 3 };
|
let request = WhoamiRequest { msgid: 3 };
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
@ -209,6 +221,54 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_bind_invalid_credentials() {
|
||||||
|
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, "dc=example,dc=com".to_string(), "admin".to_string());
|
||||||
|
|
||||||
|
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).await, request.gen_success());
|
||||||
|
|
||||||
|
let request = WhoamiRequest { msgid: 3 };
|
||||||
|
assert_eq!(
|
||||||
|
ldap_handler.do_whoami(&request),
|
||||||
|
request.gen_success("dn: test")
|
||||||
|
);
|
||||||
|
|
||||||
|
let request = SearchRequest {
|
||||||
|
msgid: 2,
|
||||||
|
base: "ou=people,dc=example,dc=com".to_string(),
|
||||||
|
scope: LdapSearchScope::Base,
|
||||||
|
filter: LdapFilter::And(vec![]),
|
||||||
|
attrs: vec![],
|
||||||
|
};
|
||||||
|
assert_eq!(
|
||||||
|
ldap_handler.do_search(&request).await,
|
||||||
|
vec![request.gen_error(
|
||||||
|
LdapResultCode::InsufficentAccessRights,
|
||||||
|
r#"Current user is not allowed to query LDAP"#.to_string()
|
||||||
|
)]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_is_subtree() {
|
fn test_is_subtree() {
|
||||||
let subtree1 = &[
|
let subtree1 = &[
|
||||||
@ -237,8 +297,8 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[tokio::test]
|
||||||
fn test_search() {
|
async fn test_search() {
|
||||||
let mut mock = MockTestBackendHandler::new();
|
let mut mock = MockTestBackendHandler::new();
|
||||||
mock.expect_bind().return_once(|_| Ok(()));
|
mock.expect_bind().return_once(|_| Ok(()));
|
||||||
mock.expect_list_users()
|
mock.expect_list_users()
|
||||||
@ -264,13 +324,14 @@ mod tests {
|
|||||||
},
|
},
|
||||||
])
|
])
|
||||||
});
|
});
|
||||||
let mut ldap_handler = LdapHandler::new(mock, "dc=example,dc=com".to_string());
|
let mut ldap_handler =
|
||||||
|
LdapHandler::new(mock, "dc=example,dc=com".to_string(), "test".to_string());
|
||||||
let request = SimpleBindRequest {
|
let request = SimpleBindRequest {
|
||||||
msgid: 1,
|
msgid: 1,
|
||||||
dn: "test".to_string(),
|
dn: "test".to_string(),
|
||||||
pw: "pass".to_string(),
|
pw: "pass".to_string(),
|
||||||
};
|
};
|
||||||
assert_eq!(ldap_handler.do_bind(&request), request.gen_success());
|
assert_eq!(ldap_handler.do_bind(&request).await, request.gen_success());
|
||||||
let request = SearchRequest {
|
let request = SearchRequest {
|
||||||
msgid: 2,
|
msgid: 2,
|
||||||
base: "ou=people,dc=example,dc=com".to_string(),
|
base: "ou=people,dc=example,dc=com".to_string(),
|
||||||
@ -286,7 +347,7 @@ mod tests {
|
|||||||
],
|
],
|
||||||
};
|
};
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
ldap_handler.do_search(&request),
|
ldap_handler.do_search(&request).await,
|
||||||
vec![
|
vec![
|
||||||
request.gen_result_entry(LdapSearchResultEntry {
|
request.gen_result_entry(LdapSearchResultEntry {
|
||||||
dn: "cn=bob_1,dc=example,dc=com".to_string(),
|
dn: "cn=bob_1,dc=example,dc=com".to_string(),
|
||||||
|
@ -34,7 +34,7 @@ async fn handle_incoming_message<Backend: BackendHandler>(
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
match session.handle_ldap_message(server_op) {
|
match session.handle_ldap_message(server_op).await {
|
||||||
None => return Ok(false),
|
None => return Ok(false),
|
||||||
Some(result) => {
|
Some(result) => {
|
||||||
for rmsg in result.into_iter() {
|
for rmsg in result.into_iter() {
|
||||||
@ -62,20 +62,23 @@ where
|
|||||||
use futures_util::StreamExt;
|
use futures_util::StreamExt;
|
||||||
|
|
||||||
let ldap_base_dn = config.ldap_base_dn.clone();
|
let ldap_base_dn = config.ldap_base_dn.clone();
|
||||||
|
let ldap_user_dn = config.ldap_user_dn.clone();
|
||||||
Ok(
|
Ok(
|
||||||
server_builder.bind("ldap", ("0.0.0.0", config.ldap_port), move || {
|
server_builder.bind("ldap", ("0.0.0.0", config.ldap_port), move || {
|
||||||
let backend_handler = backend_handler.clone();
|
let backend_handler = backend_handler.clone();
|
||||||
let ldap_base_dn = ldap_base_dn.clone();
|
let ldap_base_dn = ldap_base_dn.clone();
|
||||||
|
let ldap_user_dn = ldap_user_dn.clone();
|
||||||
pipeline_factory(fn_service(move |mut stream: TcpStream| {
|
pipeline_factory(fn_service(move |mut stream: TcpStream| {
|
||||||
let backend_handler = backend_handler.clone();
|
let backend_handler = backend_handler.clone();
|
||||||
let ldap_base_dn = ldap_base_dn.clone();
|
let ldap_base_dn = ldap_base_dn.clone();
|
||||||
|
let ldap_user_dn = ldap_user_dn.clone();
|
||||||
async move {
|
async move {
|
||||||
// Configure the codec etc.
|
// Configure the codec etc.
|
||||||
let (r, w) = stream.split();
|
let (r, w) = stream.split();
|
||||||
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::new(backend_handler, ldap_base_dn);
|
let mut session = LdapHandler::new(backend_handler, ldap_base_dn, ldap_user_dn);
|
||||||
|
|
||||||
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? {
|
||||||
|
Loading…
Reference in New Issue
Block a user