diff --git a/server/src/infra/ldap/handler.rs b/server/src/infra/ldap/handler.rs index 0d71a81..c5c0b0a 100644 --- a/server/src/infra/ldap/handler.rs +++ b/server/src/infra/ldap/handler.rs @@ -2,31 +2,25 @@ use crate::{ domain::{ ldap::{ error::{LdapError, LdapResult}, - utils::{ - LdapInfo, get_user_id_from_distinguished_name, parse_distinguished_name, - }, + utils::{LdapInfo, parse_distinguished_name}, }, opaque_handler::OpaqueHandler, }, infra::{ - access_control::{ - AccessControlledBackendHandler, UserReadableBackendHandler, - }, + access_control::AccessControlledBackendHandler, ldap::{ + compare, create, modify, password::{self, do_password_modification}, search::{ self, is_root_dse_request, make_search_error, make_search_request, make_search_success, root_dse_response, }, - compare, - create, }, }, }; use ldap3_proto::proto::{ - LdapAddRequest, LdapBindRequest, LdapBindResponse, LdapCompareRequest, - LdapExtendedRequest, LdapExtendedResponse, LdapFilter, LdapModify, LdapModifyRequest, - LdapModifyType, LdapOp, LdapPasswordModifyRequest, + LdapAddRequest, LdapBindRequest, LdapBindResponse, LdapCompareRequest, LdapExtendedRequest, + LdapExtendedResponse, LdapFilter, LdapModifyRequest, LdapOp, LdapPasswordModifyRequest, LdapResult as LdapResultOp, LdapResultCode, LdapSearchRequest, OID_PASSWORD_MODIFY, OID_WHOAMI, }; use lldap_auth::access_control::ValidationResults; @@ -90,6 +84,11 @@ impl LdapHandler { } } +enum Credentials<'s> { + Bound(&'s ValidationResults), + Unbound(Vec), +} + impl LdapHandler { pub fn new( backend_handler: AccessControlledBackendHandler, @@ -128,6 +127,16 @@ impl LdapHandler 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( &mut self, request: &LdapSearchRequest, @@ -185,14 +194,9 @@ impl LdapHandler match LdapPasswordModifyRequest::try_from(request) { Ok(password_request) => { - let credentials = match self.user_info.as_ref() { - Some(user_id) => user_id, - None => { - return vec![make_extended_response( - LdapResultCode::InsufficentAccessRights, - "No user currently bound".to_string(), - )]; - } + let credentials = match self.get_credentials() { + Credentials::Bound(cred) => cred, + Credentials::Unbound(err) => return err, }; do_password_modification( credentials, @@ -230,106 +234,25 @@ impl LdapHandler LdapResult<()> { - if !change - .modification - .atype - .eq_ignore_ascii_case("userpassword") - || change.operation != LdapModifyType::Replace - { - return Err(LdapError { - code: LdapResultCode::UnwillingToPerform, - message: format!( - r#"Unsupported operation: `{:?}` for `{}`"#, - change.operation, change.modification.atype - ), - }); - } - if !credentials.can_change_password(&user_id, user_is_admin) { - return Err(LdapError { - code: LdapResultCode::InsufficentAccessRights, - message: format!( - r#"User `{}` cannot modify the password of user `{}`"#, - &credentials.user, &user_id - ), - }); - } - if let [value] = &change.modification.vals.as_slice() { - password::change_password(self.get_opaque_handler(), user_id, value) - .await - .map_err(|e| LdapError { - code: LdapResultCode::Other, - message: format!("Error while changing the password: {:#?}", e), - })?; - } else { - return Err(LdapError { - code: LdapResultCode::InvalidAttributeSyntax, - message: format!( - r#"Wrong number of values for password attribute: {}"#, - change.modification.vals.len() - ), - }); - } - Ok(()) - } - - async fn handle_modify_request( - &mut self, - request: &LdapModifyRequest, - ) -> LdapResult> { - let credentials = self - .user_info - .as_ref() - .ok_or_else(|| LdapError { - code: LdapResultCode::InsufficentAccessRights, - message: "No user currently bound".to_string(), - })? - .clone(); - match get_user_id_from_distinguished_name( - &request.dn, - &self.ldap_info.base_dn, - &self.ldap_info.base_dn_str, - ) { - Ok(uid) => { - let user_is_admin = self - .backend_handler - .get_readable_handler(&credentials, &uid) - .expect("Unexpected permission error") - .get_user_groups(&uid) - .await - .map_err(|e| LdapError { - code: LdapResultCode::OperationsError, - message: format!("Internal error while requesting user's groups: {:#?}", e), - })? - .iter() - .any(|g| g.display_name == "lldap_admin".into()); - for change in &request.changes { - self.handle_modify_change(uid.clone(), &credentials, user_is_admin, change) - .await? - } - Ok(vec![make_modify_response( - LdapResultCode::Success, - String::new(), - )]) - } - Err(e) => Err(LdapError { - code: LdapResultCode::InvalidDNSyntax, - message: format!("Invalid username: {}", e), - }), - } - } - #[instrument(skip_all, level = "debug", fields(dn = %request.dn))] async fn do_modify_request(&mut self, request: &LdapModifyRequest) -> Vec { - self.handle_modify_request(request) - .await - .unwrap_or_else(|e: LdapError| vec![make_modify_response(e.code, e.message)]) + 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) + .expect("Unexpected permission error") + }, + &self.ldap_info, + credentials, + request, + ) + .await + .unwrap_or_else(|e: LdapError| vec![make_modify_response(e.code, e.message)]) } #[instrument(skip_all, level = "debug")] @@ -352,7 +275,11 @@ impl LdapHandler Option> { diff --git a/server/src/infra/ldap/mod.rs b/server/src/infra/ldap/mod.rs index d99478c..b3e321e 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 handler; +pub mod modify; pub mod password; pub mod search; diff --git a/server/src/infra/ldap/modify.rs b/server/src/infra/ldap/modify.rs new file mode 100644 index 0000000..8fcbbb1 --- /dev/null +++ b/server/src/infra/ldap/modify.rs @@ -0,0 +1,116 @@ +use crate::{ + domain::{ + ldap::{ + error::{LdapError, LdapResult}, + utils::{LdapInfo, get_user_id_from_distinguished_name}, + }, + opaque_handler::OpaqueHandler, + }, + infra::{ + access_control::UserReadableBackendHandler, + ldap::{ + handler::make_modify_response, + password::{self}, + }, + }, +}; +use ldap3_proto::proto::{LdapModify, LdapModifyRequest, LdapModifyType, LdapOp, LdapResultCode}; +use lldap_auth::access_control::ValidationResults; +use lldap_domain::types::UserId; + +async fn handle_modify_change( + opaque_handler: &impl OpaqueHandler, + user_id: UserId, + credentials: &ValidationResults, + user_is_admin: bool, + change: &LdapModify, +) -> LdapResult<()> { + if !change + .modification + .atype + .eq_ignore_ascii_case("userpassword") + || change.operation != LdapModifyType::Replace + { + return Err(LdapError { + code: LdapResultCode::UnwillingToPerform, + message: format!( + r#"Unsupported operation: `{:?}` for `{}`"#, + change.operation, change.modification.atype + ), + }); + } + if !credentials.can_change_password(&user_id, user_is_admin) { + return Err(LdapError { + code: LdapResultCode::InsufficentAccessRights, + message: format!( + r#"User `{}` cannot modify the password of user `{}`"#, + &credentials.user, &user_id + ), + }); + } + if let [value] = &change.modification.vals.as_slice() { + password::change_password(opaque_handler, user_id, value) + .await + .map_err(|e| LdapError { + code: LdapResultCode::Other, + message: format!("Error while changing the password: {:#?}", e), + })?; + } else { + return Err(LdapError { + code: LdapResultCode::InvalidAttributeSyntax, + message: format!( + r#"Wrong number of values for password attribute: {}"#, + change.modification.vals.len() + ), + }); + } + Ok(()) +} + +pub(crate) async fn handle_modify_request<'cred, UserBackendHandler>( + opaque_handler: &impl OpaqueHandler, + get_readable_handler: impl FnOnce(&'cred ValidationResults, UserId) -> &'cred UserBackendHandler, + ldap_info: &LdapInfo, + credentials: &'cred ValidationResults, + request: &LdapModifyRequest, +) -> LdapResult> +where + // Note: ideally, get_readable_handler would take UserId by reference, but I couldn't make the lifetimes work. + UserBackendHandler: UserReadableBackendHandler + 'cred, +{ + match get_user_id_from_distinguished_name( + &request.dn, + &ldap_info.base_dn, + &ldap_info.base_dn_str, + ) { + Ok(uid) => { + let user_is_admin = get_readable_handler(credentials, uid.clone()) + .get_user_groups(&uid) + .await + .map_err(|e| LdapError { + code: LdapResultCode::OperationsError, + message: format!("Internal error while requesting user's groups: {:#?}", e), + })? + .iter() + .any(|g| g.display_name == "lldap_admin".into()); + for change in &request.changes { + handle_modify_change( + opaque_handler, + uid.clone(), + credentials, + user_is_admin, + change, + ) + .await? + } + Ok(vec![make_modify_response( + LdapResultCode::Success, + String::new(), + )]) + } + Err(e) => Err(LdapError { + code: LdapResultCode::InvalidDNSyntax, + message: format!("Invalid username: {}", e), + }), + } +}