server: split off do_bind from ldap_handler

This commit is contained in:
Valentin Tolmer
2025-04-04 17:31:08 -05:00
committed by nitnelave
parent c4aca0dad7
commit 63f8b51c88
4 changed files with 234 additions and 183 deletions

View File

@@ -24,7 +24,7 @@ where
);
if let Some(e) = iter.next() {
Err(format!(
r#"Too many elements in distinguished name: "{:?}", "{:?}", "{:?}""#,
r#"Too many elements in distinguished name: "{}", "{}", "{}""#,
pair.0, pair.1, e
))
} else {

View File

@@ -15,27 +15,29 @@ use crate::{
AccessControlledBackendHandler, AdminBackendHandler, UserReadableBackendHandler,
},
ldap::search::{
self, make_search_error, make_search_request, make_search_success, root_dse_response, is_root_dse_request
self, is_root_dse_request, make_search_error, make_search_request, make_search_success,
root_dse_response,
},
},
};
use anyhow::Result;
use ldap3_proto::proto::{
LdapAddRequest, LdapAttribute, LdapBindCred, LdapBindRequest, LdapBindResponse,
LdapCompareRequest, LdapExtendedRequest, LdapExtendedResponse, LdapFilter, LdapModify,
LdapModifyRequest, LdapModifyType, LdapOp, LdapPartialAttribute, LdapPasswordModifyRequest,
LdapResult as LdapResultOp, LdapResultCode, LdapSearchRequest, OID_PASSWORD_MODIFY,
OID_WHOAMI,
};
LdapAddRequest, LdapAttribute, LdapBindRequest, LdapBindResponse, LdapCompareRequest,
LdapExtendedRequest, LdapExtendedResponse, LdapFilter, LdapModify, LdapModifyRequest,
LdapModifyType, LdapOp, LdapPartialAttribute, LdapPasswordModifyRequest,
LdapResult as LdapResultOp, LdapResultCode, LdapSearchRequest, OID_PASSWORD_MODIFY, OID_WHOAMI,
};
use lldap_auth::access_control::ValidationResults;
use lldap_domain::{
requests::{CreateGroupRequest, CreateUserRequest},
types::{Attribute, AttributeName, AttributeType, Email, GroupName, UserId},
};
use lldap_domain_handlers::handler::{BackendHandler, BindRequest, LoginHandler};
use lldap_domain_handlers::handler::{BackendHandler, LoginHandler};
use std::collections::HashMap;
use tracing::{debug, instrument};
use super::password;
fn make_add_error(code: LdapResultCode, message: String) -> LdapOp {
LdapOp::AddResponse(LdapResultOp {
code,
@@ -157,48 +159,29 @@ impl<Backend: BackendHandler + LoginHandler + OpaqueHandler> LdapHandler<Backend
}
#[instrument(skip_all, level = "debug", fields(dn = %request.dn))]
pub async fn do_bind(&mut self, request: &LdapBindRequest) -> (LdapResultCode, String) {
if request.dn.is_empty() {
return (
LdapResultCode::InappropriateAuthentication,
"Anonymous bind not allowed".to_string(),
);
}
let user_id = match get_user_id_from_distinguished_name(
&request.dn.to_ascii_lowercase(),
&self.ldap_info.base_dn,
&self.ldap_info.base_dn_str,
) {
Ok(s) => s,
Err(e) => return (LdapResultCode::NamingViolation, e.to_string()),
};
let password = if let LdapBindCred::Simple(password) = &request.cred {
password
} else {
return (
LdapResultCode::UnwillingToPerform,
"SASL not supported".to_string(),
);
};
match self
.get_login_handler()
.bind(BindRequest {
name: user_id.clone(),
password: password.clone(),
})
.await
{
Ok(()) => {
self.user_info = self
.backend_handler
.get_permissions_for_user(user_id)
.await
.ok();
debug!("Success!");
(LdapResultCode::Success, "".to_string())
}
Err(_) => (LdapResultCode::InvalidCredentials, "".to_string()),
}
pub async fn do_bind(&mut self, request: &LdapBindRequest) -> Vec<LdapOp> {
let (code, message) =
match password::do_bind(&self.ldap_info, request, self.get_login_handler()).await {
Ok(user_id) => {
self.user_info = self
.backend_handler
.get_permissions_for_user(user_id)
.await
.ok();
debug!("Success!");
(LdapResultCode::Success, "".to_string())
}
Err(err) => (err.code, err.message),
};
vec![LdapOp::BindResponse(LdapBindResponse {
res: LdapResultOp {
code,
matcheddn: "".to_string(),
message,
referral: vec![],
},
saslcreds: None,
})]
}
async fn change_password<B: OpaqueHandler>(
@@ -633,18 +616,7 @@ impl<Backend: BackendHandler + LoginHandler + OpaqueHandler> LdapHandler<Backend
pub async fn handle_ldap_message(&mut self, ldap_op: LdapOp) -> Option<Vec<LdapOp>> {
Some(match ldap_op {
LdapOp::BindRequest(request) => {
let (code, message) = self.do_bind(&request).await;
vec![LdapOp::BindResponse(LdapBindResponse {
res: LdapResultOp {
code,
matcheddn: "".to_string(),
message,
referral: vec![],
},
saslcreds: None,
})]
}
LdapOp::BindRequest(request) => self.do_bind(&request).await,
LdapOp::SearchRequest(request) => self
.do_search_or_dse(&request)
.await
@@ -682,9 +654,12 @@ impl<Backend: BackendHandler + LoginHandler + OpaqueHandler> LdapHandler<Backend
#[cfg(test)]
pub mod tests {
use super::*;
use crate::infra::test_utils::{MockTestBackendHandler, setup_default_schema};
use crate::infra::{
ldap::password::tests::make_bind_success,
test_utils::{MockTestBackendHandler, setup_default_schema},
};
use chrono::TimeZone;
use ldap3_proto::proto::LdapWhoamiRequest;
use ldap3_proto::proto::{LdapBindCred, LdapWhoamiRequest};
use lldap_domain::{types::*, uuid};
use lldap_domain_handlers::handler::*;
use mockall::predicate::eq;
@@ -736,10 +711,7 @@ pub mod tests {
dn: "uid=test,ou=people,dc=example,dc=coM".to_string(),
cred: LdapBindCred::Simple("pass".to_string()),
};
assert_eq!(
ldap_handler.do_bind(&request).await.0,
LdapResultCode::Success
);
assert_eq!(ldap_handler.do_bind(&request).await, make_bind_success());
ldap_handler
}
@@ -761,120 +733,6 @@ pub mod tests {
setup_bound_handler_with_group(mock, "lldap_admin").await
}
#[tokio::test]
async fn test_bind() {
let mut mock = MockTestBackendHandler::new();
mock.expect_bind()
.with(eq(lldap_domain_handlers::handler::BindRequest {
name: UserId::new("bob"),
password: "pass".to_string(),
}))
.times(1)
.return_once(|_| Ok(()));
mock.expect_get_user_groups()
.with(eq(UserId::new("bob")))
.return_once(|_| Ok(HashSet::new()));
let mut ldap_handler = LdapHandler::new_for_tests(mock, "dc=eXample,dc=com");
let request = LdapOp::BindRequest(LdapBindRequest {
dn: "uid=bob,ou=people,dc=example,dc=com".to_string(),
cred: LdapBindCred::Simple("pass".to_string()),
});
assert_eq!(
ldap_handler.handle_ldap_message(request).await,
Some(vec![LdapOp::BindResponse(LdapBindResponse {
res: LdapResultOp {
code: LdapResultCode::Success,
matcheddn: "".to_string(),
message: "".to_string(),
referral: vec![],
},
saslcreds: None,
})]),
);
}
#[tokio::test]
async fn test_admin_bind() {
let mut mock = MockTestBackendHandler::new();
mock.expect_bind()
.with(eq(lldap_domain_handlers::handler::BindRequest {
name: UserId::new("test"),
password: "pass".to_string(),
}))
.times(1)
.return_once(|_| Ok(()));
mock.expect_get_user_groups()
.with(eq(UserId::new("test")))
.return_once(|_| {
let mut set = HashSet::new();
set.insert(GroupDetails {
group_id: GroupId(42),
display_name: "lldap_admin".into(),
creation_date: chrono::Utc.timestamp_opt(42, 42).unwrap().naive_utc(),
uuid: uuid!("a1a2a3a4b1b2c1c2d1d2d3d4d5d6d7d8"),
attributes: Vec::new(),
});
Ok(set)
});
let mut ldap_handler = LdapHandler::new_for_tests(mock, "dc=example,dc=com");
let request = LdapBindRequest {
dn: "uid=test,ou=people,dc=example,dc=com".to_string(),
cred: LdapBindCred::Simple("pass".to_string()),
};
assert_eq!(
ldap_handler.do_bind(&request).await.0,
LdapResultCode::Success
);
}
#[tokio::test]
async fn test_bind_invalid_dn() {
let mock = MockTestBackendHandler::new();
let mut ldap_handler = LdapHandler::new_for_tests(mock, "dc=example,dc=com");
let request = LdapBindRequest {
dn: "cn=bob,dc=example,dc=com".to_string(),
cred: LdapBindCred::Simple("pass".to_string()),
};
assert_eq!(
ldap_handler.do_bind(&request).await.0,
LdapResultCode::NamingViolation,
);
let request = LdapBindRequest {
dn: "uid=bob,dc=example,dc=com".to_string(),
cred: LdapBindCred::Simple("pass".to_string()),
};
assert_eq!(
ldap_handler.do_bind(&request).await.0,
LdapResultCode::NamingViolation,
);
let request = LdapBindRequest {
dn: "uid=bob,ou=groups,dc=example,dc=com".to_string(),
cred: LdapBindCred::Simple("pass".to_string()),
};
assert_eq!(
ldap_handler.do_bind(&request).await.0,
LdapResultCode::NamingViolation,
);
let request = LdapBindRequest {
dn: "uid=bob,ou=people,dc=example,dc=fr".to_string(),
cred: LdapBindCred::Simple("pass".to_string()),
};
assert_eq!(
ldap_handler.do_bind(&request).await.0,
LdapResultCode::NamingViolation,
);
let request = LdapBindRequest {
dn: "uid=bob=test,ou=people,dc=example,dc=com".to_string(),
cred: LdapBindCred::Simple("pass".to_string()),
};
assert_eq!(
ldap_handler.do_bind(&request).await.0,
LdapResultCode::NamingViolation,
);
}
#[tokio::test]
async fn test_password_change() {
let mut mock = MockTestBackendHandler::new();

View File

@@ -1,2 +1,3 @@
pub mod handler;
pub mod password;
pub mod search;

View File

@@ -0,0 +1,192 @@
use crate::domain::ldap::{
error::{LdapError, LdapResult},
utils::{LdapInfo, get_user_id_from_distinguished_name},
};
use ldap3_proto::proto::{LdapBindCred, LdapBindRequest, LdapResultCode};
use lldap_domain::types::UserId;
use lldap_domain_handlers::handler::{BindRequest, LoginHandler};
pub(crate) async fn do_bind(
ldap_info: &LdapInfo,
request: &LdapBindRequest,
login_handler: &impl LoginHandler,
) -> LdapResult<UserId> {
if request.dn.is_empty() {
return Err(LdapError {
code: LdapResultCode::InappropriateAuthentication,
message: "Anonymous bind not allowed".to_string(),
});
}
let user_id = match get_user_id_from_distinguished_name(
&request.dn.to_ascii_lowercase(),
&ldap_info.base_dn,
&ldap_info.base_dn_str,
) {
Ok(s) => s,
Err(e) => {
return Err(LdapError {
code: LdapResultCode::NamingViolation,
message: e.to_string(),
});
}
};
let password = if let LdapBindCred::Simple(password) = &request.cred {
password
} else {
return Err(LdapError {
code: LdapResultCode::UnwillingToPerform,
message: "SASL not supported".to_string(),
});
};
match login_handler
.bind(BindRequest {
name: user_id.clone(),
password: password.clone(),
})
.await
{
Ok(()) => Ok(user_id),
Err(_) => Err(LdapError {
code: LdapResultCode::InvalidCredentials,
message: "".to_string(),
}),
}
}
#[cfg(test)]
pub mod tests {
use super::*;
use crate::infra::ldap::handler::LdapHandler;
use crate::infra::test_utils::MockTestBackendHandler;
use chrono::TimeZone;
use ldap3_proto::proto::{LdapBindResponse, LdapOp, LdapResult as LdapResultOp};
use lldap_domain::{types::*, uuid};
use mockall::predicate::eq;
use pretty_assertions::assert_eq;
use std::collections::HashSet;
use tokio;
pub fn make_bind_result(code: LdapResultCode, message: &str) -> Vec<LdapOp> {
vec![LdapOp::BindResponse(LdapBindResponse {
res: LdapResultOp {
code,
matcheddn: "".to_string(),
message: message.to_string(),
referral: vec![],
},
saslcreds: None,
})]
}
pub fn make_bind_success() -> Vec<LdapOp> {
make_bind_result(LdapResultCode::Success, "")
}
#[tokio::test]
async fn test_bind() {
let mut mock = MockTestBackendHandler::new();
mock.expect_bind()
.with(eq(lldap_domain_handlers::handler::BindRequest {
name: UserId::new("bob"),
password: "pass".to_string(),
}))
.times(1)
.return_once(|_| Ok(()));
mock.expect_get_user_groups()
.with(eq(UserId::new("bob")))
.return_once(|_| Ok(HashSet::new()));
let mut ldap_handler = LdapHandler::new_for_tests(mock, "dc=eXample,dc=com");
let request = LdapOp::BindRequest(LdapBindRequest {
dn: "uid=bob,ou=people,dc=example,dc=com".to_string(),
cred: LdapBindCred::Simple("pass".to_string()),
});
assert_eq!(
ldap_handler
.handle_ldap_message(request)
.await
.unwrap(),
make_bind_success()
);
}
#[tokio::test]
async fn test_admin_bind() {
let mut mock = MockTestBackendHandler::new();
mock.expect_bind()
.with(eq(lldap_domain_handlers::handler::BindRequest {
name: UserId::new("test"),
password: "pass".to_string(),
}))
.times(1)
.return_once(|_| Ok(()));
mock.expect_get_user_groups()
.with(eq(UserId::new("test")))
.return_once(|_| {
let mut set = HashSet::new();
set.insert(GroupDetails {
group_id: GroupId(42),
display_name: "lldap_admin".into(),
creation_date: chrono::Utc.timestamp_opt(42, 42).unwrap().naive_utc(),
uuid: uuid!("a1a2a3a4b1b2c1c2d1d2d3d4d5d6d7d8"),
attributes: Vec::new(),
});
Ok(set)
});
let mut ldap_handler = LdapHandler::new_for_tests(mock, "dc=example,dc=com");
let request = LdapBindRequest {
dn: "uid=test,ou=people,dc=example,dc=com".to_string(),
cred: LdapBindCred::Simple("pass".to_string()),
};
assert_eq!(ldap_handler.do_bind(&request).await,
make_bind_success());
}
#[tokio::test]
async fn test_bind_invalid_dn() {
let mock = MockTestBackendHandler::new();
let mut ldap_handler = LdapHandler::new_for_tests(mock, "dc=example,dc=com");
let request = LdapBindRequest {
dn: "cn=bob,dc=example,dc=com".to_string(),
cred: LdapBindCred::Simple("pass".to_string()),
};
assert_eq!(
ldap_handler.do_bind(&request).await,
make_bind_result(LdapResultCode::NamingViolation, r#"Unexpected DN format. Got "cn=bob,dc=example,dc=com", expected: "uid=id,ou=people,dc=example,dc=com""#),
);
let request = LdapBindRequest {
dn: "uid=bob,dc=example,dc=com".to_string(),
cred: LdapBindCred::Simple("pass".to_string()),
};
assert_eq!(
ldap_handler.do_bind(&request).await,
make_bind_result(LdapResultCode::NamingViolation, r#"Unexpected DN format. Got "uid=bob,dc=example,dc=com", expected: "uid=id,ou=people,dc=example,dc=com""#),
);
let request = LdapBindRequest {
dn: "uid=bob,ou=groups,dc=example,dc=com".to_string(),
cred: LdapBindCred::Simple("pass".to_string()),
};
assert_eq!(
ldap_handler.do_bind(&request).await,
make_bind_result(LdapResultCode::NamingViolation, r#"Unexpected DN format. Got "uid=bob,ou=groups,dc=example,dc=com", expected: "uid=id,ou=people,dc=example,dc=com""#),
);
let request = LdapBindRequest {
dn: "uid=bob,ou=people,dc=example,dc=fr".to_string(),
cred: LdapBindCred::Simple("pass".to_string()),
};
assert_eq!(
ldap_handler.do_bind(&request).await,
make_bind_result(LdapResultCode::NamingViolation, r#"Not a subtree of the base tree"#),
);
let request = LdapBindRequest {
dn: "uid=bob=test,ou=people,dc=example,dc=com".to_string(),
cred: LdapBindCred::Simple("pass".to_string()),
};
assert_eq!(
ldap_handler.do_bind(&request).await,
make_bind_result(LdapResultCode::NamingViolation, r#"Too many elements in distinguished name: "uid", "bob", "test""#),
);
}
}