diff --git a/server/src/infra/ldap/compare.rs b/server/src/infra/ldap/compare.rs index ed1756e..3859cd5 100644 --- a/server/src/infra/ldap/compare.rs +++ b/server/src/infra/ldap/compare.rs @@ -56,8 +56,11 @@ mod tests { ldap::handler::tests::setup_bound_admin_handler, test_utils::MockTestBackendHandler, }; use chrono::TimeZone; - use lldap_domain::{types::*, uuid}; - use lldap_domain_handlers::handler::*; + use lldap_domain::{ + types::{Group, GroupId, User, UserAndGroups, UserId}, + uuid, + }; + use lldap_domain_handlers::handler::{GroupRequestFilter, UserRequestFilter}; use pretty_assertions::assert_eq; use tokio; diff --git a/server/src/infra/ldap/handler.rs b/server/src/infra/ldap/handler.rs index c5c0b0a..18600ca 100644 --- a/server/src/infra/ldap/handler.rs +++ b/server/src/infra/ldap/handler.rs @@ -24,7 +24,7 @@ use ldap3_proto::proto::{ LdapResult as LdapResultOp, LdapResultCode, LdapSearchRequest, OID_PASSWORD_MODIFY, OID_WHOAMI, }; use lldap_auth::access_control::ValidationResults; -use lldap_domain::types::{AttributeName, UserId}; +use lldap_domain::types::AttributeName; use lldap_domain_handlers::handler::{BackendHandler, LoginHandler}; use tracing::{debug, instrument}; @@ -235,7 +235,7 @@ impl LdapHandler Vec { + pub async fn do_modify_request(&mut self, request: &LdapModifyRequest) -> Vec { let credentials = match self.get_credentials() { Credentials::Bound(cred) => cred, Credentials::Unbound(err) => return err, @@ -245,7 +245,6 @@ impl LdapHandler( opaque_handler: &impl OpaqueHandler, - get_readable_handler: impl FnOnce(&'cred ValidationResults, UserId) -> &'cred UserBackendHandler, + get_readable_handler: impl FnOnce( + &'cred ValidationResults, + UserId, + ) -> Option<&'cred UserBackendHandler>, ldap_info: &LdapInfo, credentials: &'cred ValidationResults, request: &LdapModifyRequest, @@ -85,6 +88,14 @@ where ) { Ok(uid) => { let user_is_admin = get_readable_handler(credentials, uid.clone()) + .ok_or_else(|| LdapError { + code: LdapResultCode::InsufficentAccessRights, + message: format!( + "User `{}` cannot modify user `{}`", + credentials.user.as_str(), + uid.as_str() + ), + })? .get_user_groups(&uid) .await .map_err(|e| LdapError { @@ -114,3 +125,191 @@ where }), } } + +#[cfg(test)] +mod tests { + use std::collections::HashSet; + + use super::*; + use crate::infra::{ + ldap::{ + handler::tests::{ + setup_bound_admin_handler, setup_bound_handler_with_group, + setup_bound_password_manager_handler, + }, + password::tests::expect_password_change, + }, + test_utils::MockTestBackendHandler, + }; + use chrono::TimeZone; + use ldap3_proto::proto::LdapResult as LdapResultOp; + use lldap_domain::{ + types::{GroupDetails, GroupId, GroupName, UserId}, + uuid, + }; + use mockall::predicate::eq; + use pretty_assertions::assert_eq; + use tokio; + + fn setup_target_user_groups( + mock: &mut MockTestBackendHandler, + target_user: &str, + groups: Vec<&'static str>, + ) { + mock.expect_get_user_groups() + .times(1) + .with(eq(UserId::from(target_user))) + .return_once(move |_| { + let mut g = HashSet::::new(); + for group in groups { + g.insert(GroupDetails { + group_id: GroupId(42), + display_name: GroupName::from(group), + creation_date: chrono::Utc.timestamp_opt(42, 42).unwrap().naive_utc(), + uuid: uuid!("a1a2a3a4b1b2c1c2d1d2d3d4d5d6d7d8"), + attributes: Vec::new(), + }); + } + Ok(g) + }); + } + + fn make_password_modify_request(target_user: &str) -> LdapModifyRequest { + LdapModifyRequest { + dn: format!("uid={},ou=people,dc=example,dc=com", target_user), + changes: vec![LdapModify { + operation: LdapModifyType::Replace, + modification: ldap3_proto::LdapPartialAttribute { + atype: "userPassword".to_string(), + vals: vec![b"tommy".to_vec()], + }, + }], + } + } + + fn make_modify_success_response() -> Vec { + vec![LdapOp::ModifyResponse(LdapResultOp { + code: LdapResultCode::Success, + matcheddn: "".to_string(), + message: "".to_string(), + referral: vec![], + })] + } + + fn make_modify_failure_response(code: LdapResultCode, message: &str) -> Vec { + vec![LdapOp::ModifyResponse(LdapResultOp { + code, + matcheddn: "".to_string(), + message: message.to_string(), + referral: vec![], + })] + } + + #[tokio::test] + async fn test_modify_password_of_regular_as_admin() { + let mut mock = MockTestBackendHandler::new(); + setup_target_user_groups(&mut mock, "bob", Vec::new()); + expect_password_change(&mut mock, "bob"); + let mut ldap_handler = setup_bound_admin_handler(mock).await; + let request = make_password_modify_request("bob"); + assert_eq!( + ldap_handler.do_modify_request(&request).await, + make_modify_success_response() + ); + } + + #[tokio::test] + async fn test_modify_password_of_regular_as_regular() { + let mut mock = MockTestBackendHandler::new(); + setup_target_user_groups(&mut mock, "test", Vec::new()); + expect_password_change(&mut mock, "test"); + let mut ldap_handler = setup_bound_handler_with_group(mock, "regular").await; + let request = make_password_modify_request("test"); + assert_eq!( + ldap_handler.do_modify_request(&request).await, + make_modify_success_response() + ); + } + + #[tokio::test] + async fn test_modify_password_of_regular_as_password_manager() { + let mut mock = MockTestBackendHandler::new(); + setup_target_user_groups(&mut mock, "bob", Vec::new()); + expect_password_change(&mut mock, "bob"); + let mut ldap_handler = setup_bound_password_manager_handler(mock).await; + let request = make_password_modify_request("bob"); + assert_eq!( + ldap_handler.do_modify_request(&request).await, + make_modify_success_response() + ); + } + + #[tokio::test] + async fn test_modify_password_of_admin_as_password_manager() { + let mut mock = MockTestBackendHandler::new(); + setup_target_user_groups(&mut mock, "bob", vec!["lldap_admin"]); + let mut ldap_handler = setup_bound_password_manager_handler(mock).await; + let request = make_password_modify_request("bob"); + assert_eq!( + ldap_handler.do_modify_request(&request).await, + make_modify_failure_response( + LdapResultCode::InsufficentAccessRights, + "User `test` cannot modify the password of user `bob`" + ) + ); + } + + #[tokio::test] + async fn test_modify_password_of_other_regular_as_regular() { + let mut ldap_handler = + setup_bound_handler_with_group(MockTestBackendHandler::new(), "regular").await; + let request = make_password_modify_request("bob"); + assert_eq!( + ldap_handler.do_modify_request(&request).await, + make_modify_failure_response( + LdapResultCode::InsufficentAccessRights, + "User `test` cannot modify user `bob`" + ) + ); + } + + #[tokio::test] + async fn test_modify_password_of_admin_as_admin() { + let mut mock = MockTestBackendHandler::new(); + setup_target_user_groups(&mut mock, "test", vec!["lldap_admin"]); + expect_password_change(&mut mock, "test"); + let mut ldap_handler = setup_bound_admin_handler(mock).await; + let request = make_password_modify_request("test"); + assert_eq!( + ldap_handler.do_modify_request(&request).await, + make_modify_success_response() + ); + } + + #[tokio::test] + async fn test_modify_password_invalid_number_of_values() { + let mut mock = MockTestBackendHandler::new(); + setup_target_user_groups(&mut mock, "bob", Vec::new()); + let mut ldap_handler = setup_bound_admin_handler(mock).await; + let request = { + let target_user = "bob"; + LdapModifyRequest { + dn: format!("uid={},ou=people,dc=example,dc=com", target_user), + changes: vec![LdapModify { + operation: LdapModifyType::Replace, + modification: ldap3_proto::LdapPartialAttribute { + atype: "userPassword".to_string(), + vals: vec![b"tommy".to_vec(), b"other_value".to_vec()], + }, + }], + } + }; + assert_eq!( + ldap_handler.do_modify_request(&request).await, + make_modify_failure_response( + LdapResultCode::InvalidAttributeSyntax, + "Wrong number of values for password attribute: 2" + ) + ); + } +} diff --git a/server/src/infra/ldap/password.rs b/server/src/infra/ldap/password.rs index 1c8bb19..6d9e681 100644 --- a/server/src/infra/ldap/password.rs +++ b/server/src/infra/ldap/password.rs @@ -198,6 +198,33 @@ pub mod tests { make_bind_result(LdapResultCode::Success, "") } + pub fn expect_password_change(mock: &mut MockTestBackendHandler, user: &str) { + use lldap_auth::{opaque, registration}; + let mut rng = rand::rngs::OsRng; + let registration_start_request = + opaque::client::registration::start_registration("password".as_bytes(), &mut rng) + .unwrap(); + let request = registration::ClientRegistrationStartRequest { + username: user.into(), + registration_start_request: registration_start_request.message, + }; + let start_response = opaque::server::registration::start_registration( + &opaque::server::ServerSetup::new(&mut rng), + request.registration_start_request, + &request.username, + ) + .unwrap(); + mock.expect_registration_start().times(1).return_once(|_| { + Ok(registration::ServerRegistrationStartResponse { + server_data: "".to_string(), + registration_response: start_response.message, + }) + }); + mock.expect_registration_finish() + .times(1) + .return_once(|_| Ok(())); + } + #[tokio::test] async fn test_bind() { let mut mock = MockTestBackendHandler::new(); @@ -323,30 +350,7 @@ pub mod tests { mock.expect_get_user_groups() .with(eq(UserId::new("bob"))) .returning(|_| Ok(HashSet::new())); - use lldap_auth::*; - let mut rng = rand::rngs::OsRng; - let registration_start_request = - opaque::client::registration::start_registration("password".as_bytes(), &mut rng) - .unwrap(); - let request = registration::ClientRegistrationStartRequest { - username: "bob".into(), - registration_start_request: registration_start_request.message, - }; - let start_response = opaque::server::registration::start_registration( - &opaque::server::ServerSetup::new(&mut rng), - request.registration_start_request, - &request.username, - ) - .unwrap(); - mock.expect_registration_start().times(1).return_once(|_| { - Ok(registration::ServerRegistrationStartResponse { - server_data: "".to_string(), - registration_response: start_response.message, - }) - }); - mock.expect_registration_finish() - .times(1) - .return_once(|_| Ok(())); + expect_password_change(&mut mock, "bob"); let mut ldap_handler = setup_bound_admin_handler(mock).await; let request = LdapOp::ExtendedRequest( LdapPasswordModifyRequest {