mirror of
https://github.com/lldap/lldap.git
synced 2026-06-26 01:00:38 +00:00
By making it a &'static, we can have a single allocation for all the threads/async contexts. This also normalizes the whitespace from the user input; a trailing \n can cause weird issues with clients
451 lines
16 KiB
Rust
451 lines
16 KiB
Rust
use crate::{
|
|
compare,
|
|
core::{
|
|
error::{LdapError, LdapResult},
|
|
utils::LdapInfo,
|
|
},
|
|
create, delete, modify,
|
|
password::{self, do_password_modification},
|
|
search::{
|
|
self, is_root_dse_request, is_subschema_entry_request, make_ldap_subschema_entry,
|
|
make_search_error, make_search_request, make_search_success, root_dse_response,
|
|
},
|
|
};
|
|
use ldap3_proto::proto::{
|
|
LdapAddRequest, LdapBindRequest, LdapBindResponse, LdapCompareRequest, LdapExtendedRequest,
|
|
LdapExtendedResponse, LdapFilter, LdapModifyRequest, LdapOp, LdapPasswordModifyRequest,
|
|
LdapResult as LdapResultOp, LdapResultCode, LdapSearchRequest, OID_PASSWORD_MODIFY, OID_WHOAMI,
|
|
};
|
|
use lldap_access_control::AccessControlledBackendHandler;
|
|
use lldap_auth::access_control::ValidationResults;
|
|
use lldap_domain::public_schema::PublicSchema;
|
|
use lldap_domain_handlers::handler::{BackendHandler, LoginHandler, ReadSchemaBackendHandler};
|
|
use lldap_opaque_handler::OpaqueHandler;
|
|
use tracing::{debug, instrument};
|
|
|
|
use super::delete::make_del_response;
|
|
|
|
pub(crate) fn make_add_response(code: LdapResultCode, message: String) -> LdapOp {
|
|
LdapOp::AddResponse(LdapResultOp {
|
|
code,
|
|
matcheddn: "".to_string(),
|
|
message,
|
|
referral: vec![],
|
|
})
|
|
}
|
|
|
|
pub(crate) fn make_extended_response(code: LdapResultCode, message: String) -> LdapOp {
|
|
LdapOp::ExtendedResponse(LdapExtendedResponse {
|
|
res: LdapResultOp {
|
|
code,
|
|
matcheddn: "".to_string(),
|
|
message,
|
|
referral: vec![],
|
|
},
|
|
name: None,
|
|
value: None,
|
|
})
|
|
}
|
|
|
|
pub(crate) fn make_modify_response(code: LdapResultCode, message: String) -> LdapOp {
|
|
LdapOp::ModifyResponse(LdapResultOp {
|
|
code,
|
|
matcheddn: "".to_string(),
|
|
message,
|
|
referral: vec![],
|
|
})
|
|
}
|
|
|
|
pub struct LdapHandler<Backend> {
|
|
user_info: Option<ValidationResults>,
|
|
backend_handler: AccessControlledBackendHandler<Backend>,
|
|
ldap_info: &'static LdapInfo,
|
|
session_uuid: uuid::Uuid,
|
|
}
|
|
|
|
impl<Backend> LdapHandler<Backend> {
|
|
pub fn session_uuid(&self) -> &uuid::Uuid {
|
|
&self.session_uuid
|
|
}
|
|
}
|
|
|
|
impl<Backend: LoginHandler> LdapHandler<Backend> {
|
|
pub fn get_login_handler(&self) -> &(impl LoginHandler + use<Backend>) {
|
|
self.backend_handler.unsafe_get_handler()
|
|
}
|
|
}
|
|
|
|
impl<Backend: OpaqueHandler> LdapHandler<Backend> {
|
|
pub fn get_opaque_handler(&self) -> &(impl OpaqueHandler + use<Backend>) {
|
|
self.backend_handler.unsafe_get_handler()
|
|
}
|
|
}
|
|
|
|
enum Credentials<'s> {
|
|
Bound(&'s ValidationResults),
|
|
Unbound(Vec<LdapOp>),
|
|
}
|
|
|
|
impl<Backend: BackendHandler + LoginHandler + OpaqueHandler> LdapHandler<Backend> {
|
|
pub fn new(
|
|
backend_handler: AccessControlledBackendHandler<Backend>,
|
|
ldap_info: &'static LdapInfo,
|
|
session_uuid: uuid::Uuid,
|
|
) -> Self {
|
|
Self {
|
|
user_info: None,
|
|
backend_handler,
|
|
ldap_info,
|
|
session_uuid,
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
pub fn new_for_tests(backend_handler: Backend, ldap_base_dn: &str) -> Self {
|
|
Self::new(
|
|
AccessControlledBackendHandler::new(backend_handler),
|
|
Box::leak(Box::new(
|
|
LdapInfo::new(ldap_base_dn, Vec::new(), Vec::new()).unwrap(),
|
|
)),
|
|
uuid::Uuid::parse_str("550e8400-e29b-41d4-a716-446655440000").unwrap(),
|
|
)
|
|
}
|
|
|
|
fn get_credentials(&self) -> Credentials<'_> {
|
|
match self.user_info.as_ref() {
|
|
Some(user_info) => Credentials::Bound(user_info),
|
|
None => Credentials::Unbound(vec![make_extended_response(
|
|
LdapResultCode::InsufficentAccessRights,
|
|
"No user currently bound".to_string(),
|
|
)]),
|
|
}
|
|
}
|
|
|
|
pub async fn do_search_or_dse(&self, request: &LdapSearchRequest) -> LdapResult<Vec<LdapOp>> {
|
|
if is_root_dse_request(request) {
|
|
debug!("rootDSE request");
|
|
return Ok(vec![
|
|
root_dse_response(&self.ldap_info.base_dn_str),
|
|
make_search_success(),
|
|
]);
|
|
} else if is_subschema_entry_request(request) {
|
|
// See RFC4512 section 4.4 "Subschema discovery"
|
|
debug!("Schema request");
|
|
let backend_handler = self
|
|
.user_info
|
|
.as_ref()
|
|
.and_then(|u| self.backend_handler.get_schema_only_handler(u))
|
|
.ok_or_else(|| LdapError {
|
|
code: LdapResultCode::InsufficentAccessRights,
|
|
message: "No user currently bound".to_string(),
|
|
})?;
|
|
|
|
let schema = backend_handler.get_schema().await.map_err(|e| LdapError {
|
|
code: LdapResultCode::OperationsError,
|
|
message: format!("Unable to get schema: {e:#}"),
|
|
})?;
|
|
return Ok(vec![
|
|
make_ldap_subschema_entry(PublicSchema::from(schema)),
|
|
make_search_success(),
|
|
]);
|
|
}
|
|
self.do_search(request).await
|
|
}
|
|
|
|
#[instrument(skip_all, level = "debug")]
|
|
async fn do_search(&self, request: &LdapSearchRequest) -> LdapResult<Vec<LdapOp>> {
|
|
let user_info = self.user_info.as_ref().ok_or_else(|| LdapError {
|
|
code: LdapResultCode::InsufficentAccessRights,
|
|
message: "No user currently bound".to_string(),
|
|
})?;
|
|
let backend_handler = self
|
|
.backend_handler
|
|
.get_user_restricted_lister_handler(user_info);
|
|
search::do_search(&backend_handler, self.ldap_info, request).await
|
|
}
|
|
|
|
#[instrument(skip_all, level = "debug", fields(dn = %request.dn))]
|
|
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,
|
|
})]
|
|
}
|
|
|
|
#[instrument(skip_all, level = "debug")]
|
|
async fn do_extended_request(&self, request: &LdapExtendedRequest) -> Vec<LdapOp> {
|
|
match request.name.as_str() {
|
|
OID_PASSWORD_MODIFY => match LdapPasswordModifyRequest::try_from(request) {
|
|
Ok(password_request) => {
|
|
let credentials = match self.get_credentials() {
|
|
Credentials::Bound(cred) => cred,
|
|
Credentials::Unbound(err) => return err,
|
|
};
|
|
do_password_modification(
|
|
credentials,
|
|
self.ldap_info,
|
|
&self.backend_handler,
|
|
self.get_opaque_handler(),
|
|
&password_request,
|
|
)
|
|
.await
|
|
.unwrap_or_else(|e: LdapError| vec![make_extended_response(e.code, e.message)])
|
|
}
|
|
Err(e) => vec![make_extended_response(
|
|
LdapResultCode::ProtocolError,
|
|
format!("Error while parsing password modify request: {e:#?}"),
|
|
)],
|
|
},
|
|
OID_WHOAMI => {
|
|
let authz_id = self
|
|
.user_info
|
|
.as_ref()
|
|
.map(|user_info| {
|
|
format!(
|
|
"dn:uid={},ou=people,{}",
|
|
user_info.user.as_str(),
|
|
self.ldap_info.base_dn_str
|
|
)
|
|
})
|
|
.unwrap_or_default();
|
|
vec![make_extended_response(LdapResultCode::Success, authz_id)]
|
|
}
|
|
_ => vec![make_extended_response(
|
|
LdapResultCode::UnwillingToPerform,
|
|
format!("Unsupported extended operation: {}", &request.name),
|
|
)],
|
|
}
|
|
}
|
|
|
|
#[instrument(skip_all, level = "debug", fields(dn = %request.dn))]
|
|
pub async fn do_modify_request(&self, request: &LdapModifyRequest) -> Vec<LdapOp> {
|
|
let credentials = match self.get_credentials() {
|
|
Credentials::Bound(cred) => cred,
|
|
Credentials::Unbound(err) => return err,
|
|
};
|
|
modify::handle_modify_request(
|
|
self.get_opaque_handler(),
|
|
|credentials, user_id| {
|
|
self.backend_handler
|
|
.get_readable_handler(credentials, &user_id)
|
|
},
|
|
self.ldap_info,
|
|
credentials,
|
|
request,
|
|
)
|
|
.await
|
|
.unwrap_or_else(|e: LdapError| vec![make_modify_response(e.code, e.message)])
|
|
}
|
|
|
|
#[instrument(skip_all, level = "debug")]
|
|
pub async fn create_user_or_group(&self, request: LdapAddRequest) -> LdapResult<Vec<LdapOp>> {
|
|
let backend_handler = self
|
|
.user_info
|
|
.as_ref()
|
|
.and_then(|u| self.backend_handler.get_admin_handler(u))
|
|
.ok_or_else(|| LdapError {
|
|
code: LdapResultCode::InsufficentAccessRights,
|
|
message: "Unauthorized write".to_string(),
|
|
})?;
|
|
create::create_user_or_group(backend_handler, self.ldap_info, request).await
|
|
}
|
|
|
|
#[instrument(skip_all, level = "debug")]
|
|
pub async fn delete_user_or_group(&self, request: String) -> LdapResult<Vec<LdapOp>> {
|
|
let backend_handler = self
|
|
.user_info
|
|
.as_ref()
|
|
.and_then(|u| self.backend_handler.get_admin_handler(u))
|
|
.ok_or_else(|| LdapError {
|
|
code: LdapResultCode::InsufficentAccessRights,
|
|
message: "Unauthorized write".to_string(),
|
|
})?;
|
|
delete::delete_user_or_group(backend_handler, self.ldap_info, request).await
|
|
}
|
|
|
|
#[instrument(skip_all, level = "debug")]
|
|
pub async fn do_compare(&self, request: LdapCompareRequest) -> LdapResult<Vec<LdapOp>> {
|
|
let req = make_search_request::<String>(
|
|
&self.ldap_info.base_dn_str,
|
|
LdapFilter::Equality("dn".to_string(), request.dn.to_string()),
|
|
vec![request.atype.clone()],
|
|
);
|
|
compare::compare(
|
|
request,
|
|
self.do_search(&req).await?,
|
|
&self.ldap_info.base_dn_str,
|
|
)
|
|
}
|
|
|
|
pub async fn handle_ldap_message(&mut self, ldap_op: LdapOp) -> Option<Vec<LdapOp>> {
|
|
Some(match ldap_op {
|
|
LdapOp::BindRequest(request) => self.do_bind(&request).await,
|
|
LdapOp::SearchRequest(request) => self
|
|
.do_search_or_dse(&request)
|
|
.await
|
|
.unwrap_or_else(|e: LdapError| vec![make_search_error(e.code, e.message)]),
|
|
LdapOp::UnbindRequest => {
|
|
debug!(
|
|
"Unbind request for {}",
|
|
self.user_info
|
|
.as_ref()
|
|
.map(|u| u.user.as_str())
|
|
.unwrap_or("<not bound>"),
|
|
);
|
|
self.user_info = None;
|
|
// No need to notify on unbind (per rfc4511)
|
|
return None;
|
|
}
|
|
LdapOp::ModifyRequest(request) => self.do_modify_request(&request).await,
|
|
LdapOp::ExtendedRequest(request) => self.do_extended_request(&request).await,
|
|
LdapOp::AddRequest(request) => self
|
|
.create_user_or_group(request)
|
|
.await
|
|
.unwrap_or_else(|e: LdapError| vec![make_add_response(e.code, e.message)]),
|
|
LdapOp::DelRequest(request) => self
|
|
.delete_user_or_group(request)
|
|
.await
|
|
.unwrap_or_else(|e: LdapError| vec![make_del_response(e.code, e.message)]),
|
|
LdapOp::CompareRequest(request) => self
|
|
.do_compare(request)
|
|
.await
|
|
.unwrap_or_else(|e: LdapError| vec![make_search_error(e.code, e.message)]),
|
|
op => vec![make_extended_response(
|
|
LdapResultCode::UnwillingToPerform,
|
|
format!("Unsupported operation: {op:#?}"),
|
|
)],
|
|
})
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
pub mod tests {
|
|
use super::*;
|
|
use crate::password::tests::make_bind_success;
|
|
use chrono::TimeZone;
|
|
use ldap3_proto::proto::{LdapBindCred, LdapWhoamiRequest};
|
|
use lldap_domain::{
|
|
types::{GroupDetails, GroupId, UserId},
|
|
uuid,
|
|
};
|
|
use lldap_domain_handlers::handler::*;
|
|
use lldap_test_utils::{MockTestBackendHandler, setup_default_schema};
|
|
use mockall::predicate::eq;
|
|
use pretty_assertions::assert_eq;
|
|
use std::collections::HashSet;
|
|
use tokio;
|
|
|
|
pub fn make_user_search_request<S: Into<String>>(
|
|
filter: LdapFilter,
|
|
attrs: Vec<S>,
|
|
) -> LdapSearchRequest {
|
|
make_search_request::<S>("ou=people,Dc=example,dc=com", filter, attrs)
|
|
}
|
|
|
|
pub fn make_group_search_request<S: Into<String>>(
|
|
filter: LdapFilter,
|
|
attrs: Vec<S>,
|
|
) -> LdapSearchRequest {
|
|
make_search_request::<S>("ou=groups,dc=example,dc=com", filter, attrs)
|
|
}
|
|
|
|
pub async fn setup_bound_handler_with_group(
|
|
mut mock: MockTestBackendHandler,
|
|
group: &str,
|
|
) -> LdapHandler<MockTestBackendHandler> {
|
|
mock.expect_bind()
|
|
.with(eq(BindRequest {
|
|
name: UserId::new("test"),
|
|
password: "pass".to_string(),
|
|
}))
|
|
.return_once(|_| Ok(()));
|
|
let group = group.to_string();
|
|
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: group.into(),
|
|
creation_date: chrono::Utc.timestamp_opt(42, 42).unwrap().naive_utc(),
|
|
uuid: uuid!("a1a2a3a4b1b2c1c2d1d2d3d4d5d6d7d8"),
|
|
attributes: Vec::new(),
|
|
modified_date: chrono::Utc.timestamp_opt(42, 42).unwrap().naive_utc(),
|
|
});
|
|
Ok(set)
|
|
});
|
|
setup_default_schema(&mut mock);
|
|
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());
|
|
ldap_handler
|
|
}
|
|
|
|
pub async fn setup_bound_readonly_handler(
|
|
mock: MockTestBackendHandler,
|
|
) -> LdapHandler<MockTestBackendHandler> {
|
|
setup_bound_handler_with_group(mock, "lldap_strict_readonly").await
|
|
}
|
|
|
|
pub async fn setup_bound_password_manager_handler(
|
|
mock: MockTestBackendHandler,
|
|
) -> LdapHandler<MockTestBackendHandler> {
|
|
setup_bound_handler_with_group(mock, "lldap_password_manager").await
|
|
}
|
|
|
|
pub async fn setup_bound_admin_handler(
|
|
mock: MockTestBackendHandler,
|
|
) -> LdapHandler<MockTestBackendHandler> {
|
|
setup_bound_handler_with_group(mock, "lldap_admin").await
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_whoami_empty() {
|
|
let mut ldap_handler =
|
|
LdapHandler::new_for_tests(MockTestBackendHandler::new(), "dc=example,dc=com");
|
|
let request = LdapOp::ExtendedRequest(LdapWhoamiRequest {}.into());
|
|
assert_eq!(
|
|
ldap_handler.handle_ldap_message(request).await,
|
|
Some(vec![make_extended_response(
|
|
LdapResultCode::Success,
|
|
"".to_string(),
|
|
)])
|
|
);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_whoami_bound() {
|
|
let mock = MockTestBackendHandler::new();
|
|
let mut ldap_handler = setup_bound_admin_handler(mock).await;
|
|
let request = LdapOp::ExtendedRequest(LdapWhoamiRequest {}.into());
|
|
assert_eq!(
|
|
ldap_handler.handle_ldap_message(request).await,
|
|
Some(vec![make_extended_response(
|
|
LdapResultCode::Success,
|
|
"dn:uid=test,ou=people,dc=example,dc=com".to_string(),
|
|
)])
|
|
);
|
|
}
|
|
}
|