diff --git a/server/src/infra/ldap/create.rs b/server/src/infra/ldap/create.rs index d9142eb..1fafdeb 100644 --- a/server/src/infra/ldap/create.rs +++ b/server/src/infra/ldap/create.rs @@ -6,7 +6,7 @@ use crate::{ utils::{LdapInfo, UserOrGroupName, get_user_or_group_id_from_distinguished_name}, }, }, - infra::{access_control::AdminBackendHandler, ldap::handler::make_add_error}, + infra::{access_control::AdminBackendHandler, ldap::handler::make_add_response}, }; use ldap3_proto::proto::{ LdapAddRequest, LdapAttribute, LdapOp, LdapPartialAttribute, LdapResultCode, @@ -137,7 +137,10 @@ async fn create_user( code: LdapResultCode::OperationsError, message: format!("Could not create user: {:#?}", e), })?; - Ok(vec![make_add_error(LdapResultCode::Success, String::new())]) + Ok(vec![make_add_response( + LdapResultCode::Success, + String::new(), + )]) } #[instrument(skip_all, level = "debug")] @@ -156,7 +159,10 @@ async fn create_group( code: LdapResultCode::OperationsError, message: format!("Could not create group: {:#?}", e), })?; - Ok(vec![make_add_error(LdapResultCode::Success, String::new())]) + Ok(vec![make_add_response( + LdapResultCode::Success, + String::new(), + )]) } #[cfg(test)] @@ -192,7 +198,10 @@ mod tests { }; assert_eq!( ldap_handler.create_user_or_group(request).await, - Ok(vec![make_add_error(LdapResultCode::Success, String::new())]) + Ok(vec![make_add_response( + LdapResultCode::Success, + String::new() + )]) ); } @@ -216,7 +225,10 @@ mod tests { }; assert_eq!( ldap_handler.create_user_or_group(request).await, - Ok(vec![make_add_error(LdapResultCode::Success, String::new())]) + Ok(vec![make_add_response( + LdapResultCode::Success, + String::new() + )]) ); } @@ -252,7 +264,10 @@ mod tests { }; assert_eq!( ldap_handler.create_user_or_group(request).await, - Ok(vec![make_add_error(LdapResultCode::Success, String::new())]) + Ok(vec![make_add_response( + LdapResultCode::Success, + String::new() + )]) ); } } diff --git a/server/src/infra/ldap/delete.rs b/server/src/infra/ldap/delete.rs new file mode 100644 index 0000000..0ce64a0 --- /dev/null +++ b/server/src/infra/ldap/delete.rs @@ -0,0 +1,310 @@ +use crate::{ + domain::ldap::{ + error::{LdapError, LdapResult}, + utils::{LdapInfo, UserOrGroupName, get_user_or_group_id_from_distinguished_name}, + }, + infra::access_control::AdminBackendHandler, +}; +use ldap3_proto::proto::{LdapOp, LdapResult as LdapResultOp, LdapResultCode}; +use lldap_domain::types::{GroupName, UserId}; +use lldap_domain_handlers::handler::GroupRequestFilter; +use lldap_domain_model::error::DomainError; +use tracing::instrument; + +pub(crate) fn make_del_response(code: LdapResultCode, message: String) -> LdapOp { + LdapOp::DelResponse(LdapResultOp { + code, + matcheddn: "".to_string(), + message, + referral: vec![], + }) +} + +#[instrument(skip_all, level = "debug")] +pub(crate) async fn delete_user_or_group( + backend_handler: &impl AdminBackendHandler, + ldap_info: &LdapInfo, + request: String, +) -> LdapResult> { + let base_dn_str = &ldap_info.base_dn_str; + match get_user_or_group_id_from_distinguished_name(&request, &ldap_info.base_dn) { + UserOrGroupName::User(user_id) => delete_user(backend_handler, user_id).await, + UserOrGroupName::Group(group_name) => delete_group(backend_handler, group_name).await, + err => Err(err.into_ldap_error( + &request, + format!( + r#""uid=id,ou=people,{}" or "uid=id,ou=groups,{}""#, + base_dn_str, base_dn_str + ), + )), + } +} + +#[instrument(skip_all, level = "debug")] +async fn delete_user( + backend_handler: &impl AdminBackendHandler, + user_id: UserId, +) -> LdapResult> { + backend_handler + .get_user_details(&user_id) + .await + .map_err(|err| match err { + DomainError::EntityNotFound(_) => LdapError { + code: LdapResultCode::NoSuchObject, + message: "Could not find user".to_string(), + }, + e => LdapError { + code: LdapResultCode::OperationsError, + message: format!("Error while finding user: {:?}", e), + }, + })?; + backend_handler + .delete_user(&user_id) + .await + .map_err(|e| LdapError { + code: LdapResultCode::OperationsError, + message: format!("Error while deleting user: {:?}", e), + })?; + Ok(vec![make_del_response( + LdapResultCode::Success, + String::new(), + )]) +} + +#[instrument(skip_all, level = "debug")] +async fn delete_group( + backend_handler: &impl AdminBackendHandler, + group_name: GroupName, +) -> LdapResult> { + let groups = backend_handler + .list_groups(Some(GroupRequestFilter::DisplayName(group_name.clone()))) + .await + .map_err(|e| LdapError { + code: LdapResultCode::OperationsError, + message: format!("Error while finding group: {:?}", e), + })?; + let group_id = groups + .iter() + .find(|g| g.display_name == group_name) + .map(|g| g.id) + .ok_or_else(|| LdapError { + code: LdapResultCode::NoSuchObject, + message: "Could not find group".to_string(), + })?; + backend_handler + .delete_group(group_id) + .await + .map_err(|e| LdapError { + code: LdapResultCode::OperationsError, + message: format!("Error while deleting group: {:?}", e), + })?; + Ok(vec![make_del_response( + LdapResultCode::Success, + String::new(), + )]) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::infra::{ + ldap::handler::tests::setup_bound_admin_handler, test_utils::MockTestBackendHandler, + }; + use chrono::TimeZone; + use lldap_domain::{ + types::{Group, GroupId, User}, + uuid, + }; + use lldap_domain_model::error::DomainError; + use mockall::predicate::eq; + use pretty_assertions::assert_eq; + use tokio; + + #[tokio::test] + async fn test_delete_user() { + let mut mock = MockTestBackendHandler::new(); + mock.expect_get_user_details() + .with(eq(UserId::new("bob"))) + .return_once(|_| { + Ok(User { + user_id: UserId::new("bob"), + ..Default::default() + }) + }); + mock.expect_delete_user() + .with(eq(UserId::new("bob"))) + .times(1) + .return_once(|_| Ok(())); + let mut ldap_handler = setup_bound_admin_handler(mock).await; + let request = LdapOp::DelRequest("uid=bob,ou=people,dc=example,dc=com".to_owned()); + assert_eq!( + ldap_handler.handle_ldap_message(request).await, + Some(vec![make_del_response( + LdapResultCode::Success, + String::new() + )]) + ); + } + + #[tokio::test] + async fn test_delete_group() { + let mut mock = MockTestBackendHandler::new(); + mock.expect_list_groups() + .with(eq(Some(GroupRequestFilter::DisplayName(GroupName::from( + "bob", + ))))) + .return_once(|_| { + Ok(vec![Group { + id: GroupId(34), + display_name: GroupName::from("bob"), + creation_date: chrono::Utc.timestamp_opt(42, 42).unwrap().naive_utc(), + uuid: uuid!("a1a2a3a4b1b2c1c2d1d2d3d4d5d6d7d8"), + users: Vec::new(), + attributes: Vec::new(), + }]) + }); + mock.expect_delete_group() + .with(eq(GroupId(34))) + .times(1) + .return_once(|_| Ok(())); + let mut ldap_handler = setup_bound_admin_handler(mock).await; + let request = LdapOp::DelRequest("uid=bob,ou=groups,dc=example,dc=com".to_owned()); + assert_eq!( + ldap_handler.handle_ldap_message(request).await, + Some(vec![make_del_response( + LdapResultCode::Success, + String::new() + )]) + ); + } + + #[tokio::test] + async fn test_delete_user_not_found() { + let mut mock = MockTestBackendHandler::new(); + mock.expect_get_user_details() + .with(eq(UserId::new("bob"))) + .return_once(|_| Err(DomainError::EntityNotFound("No such user".to_string()))); + let mut ldap_handler = setup_bound_admin_handler(mock).await; + let request = LdapOp::DelRequest("uid=bob,ou=people,dc=example,dc=com".to_owned()); + assert_eq!( + ldap_handler.handle_ldap_message(request).await, + Some(vec![make_del_response( + LdapResultCode::NoSuchObject, + "Could not find user".to_string() + )]) + ); + } + + #[tokio::test] + async fn test_delete_user_lookup_error() { + let mut mock = MockTestBackendHandler::new(); + mock.expect_get_user_details() + .with(eq(UserId::new("bob"))) + .return_once(|_| Err(DomainError::InternalError("WTF?".to_string()))); + let mut ldap_handler = setup_bound_admin_handler(mock).await; + let request = LdapOp::DelRequest("uid=bob,ou=people,dc=example,dc=com".to_owned()); + assert_eq!( + ldap_handler.handle_ldap_message(request).await, + Some(vec![make_del_response( + LdapResultCode::OperationsError, + r#"Error while finding user: InternalError("WTF?")"#.to_string() + )]) + ); + } + + #[tokio::test] + async fn test_delete_user_deletion_error() { + let mut mock = MockTestBackendHandler::new(); + mock.expect_get_user_details() + .with(eq(UserId::new("bob"))) + .return_once(|_| { + Ok(User { + user_id: UserId::new("bob"), + ..Default::default() + }) + }); + mock.expect_delete_user() + .with(eq(UserId::new("bob"))) + .times(1) + .return_once(|_| Err(DomainError::InternalError("WTF?".to_string()))); + let mut ldap_handler = setup_bound_admin_handler(mock).await; + let request = LdapOp::DelRequest("uid=bob,ou=people,dc=example,dc=com".to_owned()); + assert_eq!( + ldap_handler.handle_ldap_message(request).await, + Some(vec![make_del_response( + LdapResultCode::OperationsError, + r#"Error while deleting user: InternalError("WTF?")"#.to_string() + )]) + ); + } + + #[tokio::test] + async fn test_delete_group_not_found() { + let mut mock = MockTestBackendHandler::new(); + mock.expect_list_groups() + .with(eq(Some(GroupRequestFilter::DisplayName(GroupName::from( + "bob", + ))))) + .return_once(|_| Ok(vec![])); + let mut ldap_handler = setup_bound_admin_handler(mock).await; + let request = LdapOp::DelRequest("uid=bob,ou=groups,dc=example,dc=com".to_owned()); + assert_eq!( + ldap_handler.handle_ldap_message(request).await, + Some(vec![make_del_response( + LdapResultCode::NoSuchObject, + "Could not find group".to_string() + )]) + ); + } + + #[tokio::test] + async fn test_delete_group_lookup_error() { + let mut mock = MockTestBackendHandler::new(); + mock.expect_list_groups() + .with(eq(Some(GroupRequestFilter::DisplayName(GroupName::from( + "bob", + ))))) + .return_once(|_| Err(DomainError::InternalError("WTF?".to_string()))); + let mut ldap_handler = setup_bound_admin_handler(mock).await; + let request = LdapOp::DelRequest("uid=bob,ou=groups,dc=example,dc=com".to_owned()); + assert_eq!( + ldap_handler.handle_ldap_message(request).await, + Some(vec![make_del_response( + LdapResultCode::OperationsError, + r#"Error while finding group: InternalError("WTF?")"#.to_string() + )]) + ); + } + + #[tokio::test] + async fn test_delete_group_deletion_error() { + let mut mock = MockTestBackendHandler::new(); + mock.expect_list_groups() + .with(eq(Some(GroupRequestFilter::DisplayName(GroupName::from( + "bob", + ))))) + .return_once(|_| { + Ok(vec![Group { + id: GroupId(34), + display_name: GroupName::from("bob"), + creation_date: chrono::Utc.timestamp_opt(42, 42).unwrap().naive_utc(), + uuid: uuid!("a1a2a3a4b1b2c1c2d1d2d3d4d5d6d7d8"), + users: Vec::new(), + attributes: Vec::new(), + }]) + }); + mock.expect_delete_group() + .with(eq(GroupId(34))) + .times(1) + .return_once(|_| Err(DomainError::InternalError("WTF?".to_string()))); + let mut ldap_handler = setup_bound_admin_handler(mock).await; + let request = LdapOp::DelRequest("uid=bob,ou=groups,dc=example,dc=com".to_owned()); + assert_eq!( + ldap_handler.handle_ldap_message(request).await, + Some(vec![make_del_response( + LdapResultCode::OperationsError, + r#"Error while deleting group: InternalError("WTF?")"#.to_string() + )]) + ); + } +} diff --git a/server/src/infra/ldap/handler.rs b/server/src/infra/ldap/handler.rs index 18600ca..78628a4 100644 --- a/server/src/infra/ldap/handler.rs +++ b/server/src/infra/ldap/handler.rs @@ -9,7 +9,7 @@ use crate::{ infra::{ access_control::AccessControlledBackendHandler, ldap::{ - compare, create, modify, + compare, create, delete, modify, password::{self, do_password_modification}, search::{ self, is_root_dse_request, make_search_error, make_search_request, @@ -28,7 +28,9 @@ use lldap_domain::types::AttributeName; use lldap_domain_handlers::handler::{BackendHandler, LoginHandler}; use tracing::{debug, instrument}; -pub(crate) fn make_add_error(code: LdapResultCode, message: String) -> LdapOp { +use super::delete::make_del_response; + +pub(crate) fn make_add_response(code: LdapResultCode, message: String) -> LdapOp { LdapOp::AddResponse(LdapResultOp { code, matcheddn: "".to_string(), @@ -267,6 +269,19 @@ impl LdapHandler LdapResult> { + 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(&mut self, request: LdapCompareRequest) -> LdapResult> { let req = make_search_request::( @@ -305,7 +320,11 @@ impl LdapHandler self .create_user_or_group(request) .await - .unwrap_or_else(|e: LdapError| vec![make_add_error(e.code, e.message)]), + .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 diff --git a/server/src/infra/ldap/mod.rs b/server/src/infra/ldap/mod.rs index b3e321e..a84fe66 100644 --- a/server/src/infra/ldap/mod.rs +++ b/server/src/infra/ldap/mod.rs @@ -1,5 +1,6 @@ pub mod compare; pub mod create; +pub mod delete; pub mod handler; pub mod modify; pub mod password;