From c4aca0dad7cd72d139722e37e2ca752e0b47df4b Mon Sep 17 00:00:00 2001 From: Valentin Tolmer Date: Fri, 4 Apr 2025 11:27:08 -0500 Subject: [PATCH] server: split off ldap/search from ldap_handler --- server/src/domain/ldap/utils.rs | 38 + server/src/infra/access_control.rs | 6 +- server/src/infra/ldap/handler.rs | 1421 +++++++++++++ server/src/infra/ldap/mod.rs | 2 + .../infra/{ldap_handler.rs => ldap/search.rs} | 1759 ++--------------- server/src/infra/ldap_server.rs | 2 +- server/src/infra/mod.rs | 2 +- 7 files changed, 1642 insertions(+), 1588 deletions(-) create mode 100644 server/src/infra/ldap/handler.rs create mode 100644 server/src/infra/ldap/mod.rs rename server/src/infra/{ldap_handler.rs => ldap/search.rs} (51%) diff --git a/server/src/domain/ldap/utils.rs b/server/src/domain/ldap/utils.rs index c26185e..14d408f 100644 --- a/server/src/domain/ldap/utils.rs +++ b/server/src/domain/ldap/utils.rs @@ -331,3 +331,41 @@ pub fn get_custom_attribute( } }) } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_is_subtree() { + let subtree1 = &[ + ("ou".to_string(), "people".to_string()), + ("dc".to_string(), "example".to_string()), + ("dc".to_string(), "com".to_string()), + ]; + let root = &[ + ("dc".to_string(), "example".to_string()), + ("dc".to_string(), "com".to_string()), + ]; + assert!(is_subtree(subtree1, root)); + assert!(!is_subtree(&[], root)); + } + + #[test] + fn test_parse_distinguished_name() { + let parsed_dn = &[ + ("ou".to_string(), "people".to_string()), + ("dc".to_string(), "example".to_string()), + ("dc".to_string(), "com".to_string()), + ]; + assert_eq!( + parse_distinguished_name("ou=people,dc=example,dc=com").expect("parsing failed"), + parsed_dn + ); + assert_eq!( + parse_distinguished_name(" ou = people , dc = example , dc = com ") + .expect("parsing failed"), + parsed_dn + ); + } +} diff --git a/server/src/infra/access_control.rs b/server/src/infra/access_control.rs index 41be90b..6bf5e0f 100644 --- a/server/src/infra/access_control.rs +++ b/server/src/infra/access_control.rs @@ -259,7 +259,7 @@ impl AccessControlledBackendHandler { pub struct UserRestrictedListerBackendHandler<'a, Handler> { handler: &'a Handler, - pub user_filter: Option, + user_filter: Option, } #[async_trait] @@ -325,10 +325,14 @@ impl GroupListerBackendHandler pub trait UserAndGroupListerBackendHandler: UserListerBackendHandler + GroupListerBackendHandler { + fn user_filter(&self) -> &Option; } #[async_trait] impl UserAndGroupListerBackendHandler for UserRestrictedListerBackendHandler<'_, Handler> { + fn user_filter(&self) -> &Option { + &self.user_filter + } } diff --git a/server/src/infra/ldap/handler.rs b/server/src/infra/ldap/handler.rs new file mode 100644 index 0000000..b6693e8 --- /dev/null +++ b/server/src/infra/ldap/handler.rs @@ -0,0 +1,1421 @@ +use crate::{ + domain::{ + deserialize, + ldap::{ + error::{LdapError, LdapResult}, + utils::{ + LdapInfo, UserOrGroupName, get_user_id_from_distinguished_name, + get_user_or_group_id_from_distinguished_name, parse_distinguished_name, + }, + }, + opaque_handler::OpaqueHandler, + }, + infra::{ + access_control::{ + AccessControlledBackendHandler, AdminBackendHandler, UserReadableBackendHandler, + }, + ldap::search::{ + self, make_search_error, make_search_request, make_search_success, root_dse_response, is_root_dse_request + }, + }, +}; +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, + }; +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 std::collections::HashMap; +use tracing::{debug, instrument}; + +fn make_add_error(code: LdapResultCode, message: String) -> LdapOp { + LdapOp::AddResponse(LdapResultOp { + code, + matcheddn: "".to_string(), + message, + referral: vec![], + }) +} + +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, + }) +} + +fn make_modify_response(code: LdapResultCode, message: String) -> LdapOp { + LdapOp::ModifyResponse(LdapResultOp { + code, + matcheddn: "".to_string(), + message, + referral: vec![], + }) +} + +pub struct LdapHandler { + user_info: Option, + backend_handler: AccessControlledBackendHandler, + ldap_info: LdapInfo, + session_uuid: uuid::Uuid, +} + +impl LdapHandler { + pub fn session_uuid(&self) -> &uuid::Uuid { + &self.session_uuid + } +} + +impl LdapHandler { + pub fn get_login_handler(&self) -> &(impl LoginHandler + use) { + self.backend_handler.unsafe_get_handler() + } +} + +impl LdapHandler { + pub fn get_opaque_handler(&self) -> &(impl OpaqueHandler + use) { + self.backend_handler.unsafe_get_handler() + } +} + +impl LdapHandler { + pub fn new( + backend_handler: AccessControlledBackendHandler, + mut ldap_base_dn: String, + ignored_user_attributes: Vec, + ignored_group_attributes: Vec, + session_uuid: uuid::Uuid, + ) -> Self { + ldap_base_dn.make_ascii_lowercase(); + Self { + user_info: None, + backend_handler, + ldap_info: LdapInfo { + base_dn: parse_distinguished_name(&ldap_base_dn).unwrap_or_else(|_| { + panic!( + "Invalid value for ldap_base_dn in configuration: {}", + ldap_base_dn + ) + }), + base_dn_str: ldap_base_dn, + ignored_user_attributes, + ignored_group_attributes, + }, + session_uuid, + } + } + + #[cfg(test)] + pub fn new_for_tests(backend_handler: Backend, ldap_base_dn: &str) -> Self { + Self::new( + AccessControlledBackendHandler::new(backend_handler), + ldap_base_dn.to_string(), + vec![], + vec![], + uuid::Uuid::parse_str("550e8400-e29b-41d4-a716-446655440000").unwrap(), + ) + } + + pub async fn do_search_or_dse( + &mut self, + request: &LdapSearchRequest, + ) -> LdapResult> { + if is_root_dse_request(request) { + debug!("rootDSE request"); + return Ok(vec![ + root_dse_response(&self.ldap_info.base_dn_str), + make_search_success(), + ]); + } + self.do_search(request).await + } + + #[instrument(skip_all, level = "debug")] + async fn do_search(&self, request: &LdapSearchRequest) -> LdapResult> { + 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) -> (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()), + } + } + + async fn change_password( + &self, + backend_handler: &B, + user: UserId, + password: &[u8], + ) -> Result<()> { + use lldap_auth::*; + let mut rng = rand::rngs::OsRng; + let registration_start_request = + opaque::client::registration::start_registration(password, &mut rng)?; + let req = registration::ClientRegistrationStartRequest { + username: user.clone(), + registration_start_request: registration_start_request.message, + }; + let registration_start_response = backend_handler.registration_start(req).await?; + let registration_finish = opaque::client::registration::finish_registration( + registration_start_request.state, + registration_start_response.registration_response, + &mut rng, + )?; + let req = registration::ClientRegistrationFinishRequest { + server_data: registration_start_response.server_data, + registration_upload: registration_finish.message, + }; + backend_handler.registration_finish(req).await?; + Ok(()) + } + + async fn do_password_modification( + &mut self, + request: &LdapPasswordModifyRequest, + ) -> LdapResult> { + let credentials = self.user_info.as_ref().ok_or_else(|| LdapError { + code: LdapResultCode::InsufficentAccessRights, + message: "No user currently bound".to_string(), + })?; + match (&request.user_identity, &request.new_password) { + (Some(user), Some(password)) => { + match get_user_id_from_distinguished_name( + &user.to_ascii_lowercase(), + &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()); + if !credentials.can_change_password(&uid, user_is_admin) { + Err(LdapError { + code: LdapResultCode::InsufficentAccessRights, + message: format!( + r#"User `{}` cannot modify the password of user `{}`"#, + &credentials.user, &uid + ), + }) + } else if let Err(e) = self + .change_password(self.get_opaque_handler(), uid, password.as_bytes()) + .await + { + Err(LdapError { + code: LdapResultCode::Other, + message: format!("Error while changing the password: {:#?}", e), + }) + } else { + Ok(vec![make_extended_response( + LdapResultCode::Success, + "".to_string(), + )]) + } + } + Err(e) => Err(LdapError { + code: LdapResultCode::InvalidDNSyntax, + message: format!("Invalid username: {}", e), + }), + } + } + _ => Err(LdapError { + code: LdapResultCode::ConstraintViolation, + message: "Missing either user_id or password".to_string(), + }), + } + } + + #[instrument(skip_all, level = "debug")] + async fn do_extended_request(&mut self, request: &LdapExtendedRequest) -> Vec { + match request.name.as_str() { + OID_PASSWORD_MODIFY => match LdapPasswordModifyRequest::try_from(request) { + Ok(password_request) => self + .do_password_modification(&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), + )], + } + } + + async fn handle_modify_change( + &mut self, + 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() { + self.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)]) + } + + #[instrument(skip_all, level = "debug")] + async fn do_create_user_or_group(&self, request: LdapAddRequest) -> 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(), + })?; + let base_dn_str = &self.ldap_info.base_dn_str; + match get_user_or_group_id_from_distinguished_name(&request.dn, &self.ldap_info.base_dn) { + UserOrGroupName::User(user_id) => { + self.do_create_user(backend_handler, user_id, request.attributes) + .await + } + UserOrGroupName::Group(group_name) => { + self.do_create_group(backend_handler, group_name, request.attributes) + .await + } + err => Err(err.into_ldap_error( + &request.dn, + format!( + r#""uid=id,ou=people,{}" or "uid=id,ou=groups,{}""#, + base_dn_str, base_dn_str + ), + )), + } + } + + #[instrument(skip_all, level = "debug")] + async fn do_create_user( + &self, + backend_handler: &impl AdminBackendHandler, + user_id: UserId, + attributes: Vec, + ) -> LdapResult> { + fn parse_attribute(mut attr: LdapPartialAttribute) -> LdapResult<(String, Vec)> { + if attr.vals.len() > 1 { + Err(LdapError { + code: LdapResultCode::ConstraintViolation, + message: format!("Expected a single value for attribute {}", attr.atype), + }) + } else { + attr.atype.make_ascii_lowercase(); + match attr.vals.pop() { + Some(val) => Ok((attr.atype, val)), + None => Err(LdapError { + code: LdapResultCode::ConstraintViolation, + message: format!("Missing value for attribute {}", attr.atype), + }), + } + } + } + let attributes: HashMap> = attributes + .into_iter() + .filter(|a| !a.atype.eq_ignore_ascii_case("objectclass")) + .map(parse_attribute) + .collect::>()?; + fn decode_attribute_value(val: &[u8]) -> LdapResult { + std::str::from_utf8(val) + .map_err(|e| LdapError { + code: LdapResultCode::ConstraintViolation, + message: format!( + "Attribute value is invalid UTF-8: {:#?} (value {:?})", + e, val + ), + }) + .map(str::to_owned) + } + let get_attribute = |name| { + attributes + .get(name) + .map(Vec::as_slice) + .map(decode_attribute_value) + }; + let make_encoded_attribute = |name: &str, typ: AttributeType, value: String| { + Ok(Attribute { + name: AttributeName::from(name), + value: deserialize::deserialize_attribute_value(&[value], typ, false).map_err( + |e| LdapError { + code: LdapResultCode::ConstraintViolation, + message: format!("Invalid attribute value: {}", e), + }, + )?, + }) + }; + let mut new_user_attributes: Vec = Vec::new(); + if let Some(first_name) = get_attribute("givenname").transpose()? { + new_user_attributes.push(make_encoded_attribute( + "first_name", + AttributeType::String, + first_name, + )?); + } + if let Some(last_name) = get_attribute("sn").transpose()? { + new_user_attributes.push(make_encoded_attribute( + "last_name", + AttributeType::String, + last_name, + )?); + } + if let Some(avatar) = get_attribute("avatar").transpose()? { + new_user_attributes.push(make_encoded_attribute( + "avatar", + AttributeType::JpegPhoto, + avatar, + )?); + } + backend_handler + .create_user(CreateUserRequest { + user_id, + email: Email::from( + get_attribute("mail") + .or_else(|| get_attribute("email")) + .transpose()? + .unwrap_or_default(), + ), + display_name: get_attribute("cn").transpose()?, + attributes: new_user_attributes, + }) + .await + .map_err(|e| LdapError { + code: LdapResultCode::OperationsError, + message: format!("Could not create user: {:#?}", e), + })?; + Ok(vec![make_add_error(LdapResultCode::Success, String::new())]) + } + + #[instrument(skip_all, level = "debug")] + async fn do_create_group( + &self, + backend_handler: &impl AdminBackendHandler, + group_name: GroupName, + _attributes: Vec, + ) -> LdapResult> { + backend_handler + .create_group(CreateGroupRequest { + display_name: group_name, + attributes: Vec::new(), + }) + .await + .map_err(|e| LdapError { + code: LdapResultCode::OperationsError, + message: format!("Could not create group: {:#?}", e), + })?; + Ok(vec![make_add_error(LdapResultCode::Success, String::new())]) + } + + #[instrument(skip_all, level = "debug")] + pub async fn do_compare(&mut self, request: LdapCompareRequest) -> LdapResult> { + let req = make_search_request::( + &self.ldap_info.base_dn_str, + LdapFilter::Equality("dn".to_string(), request.dn.to_string()), + vec![request.atype.clone()], + ); + let entries = self.do_search(&req).await?; + if entries.len() > 2 { + // SearchResultEntry + SearchResultDone + return Err(LdapError { + code: LdapResultCode::OperationsError, + message: "Too many search results".to_string(), + }); + } + let requested_attribute = AttributeName::from(&request.atype); + match entries.first() { + Some(LdapOp::SearchResultEntry(entry)) => { + let available = entry.attributes.iter().any(|attr| { + AttributeName::from(&attr.atype) == requested_attribute + && attr.vals.contains(&request.val) + }); + Ok(vec![LdapOp::CompareResult(LdapResultOp { + code: if available { + LdapResultCode::CompareTrue + } else { + LdapResultCode::CompareFalse + }, + matcheddn: request.dn, + message: "".to_string(), + referral: vec![], + })]) + } + Some(LdapOp::SearchResultDone(_)) => Ok(vec![LdapOp::CompareResult(LdapResultOp { + code: LdapResultCode::NoSuchObject, + matcheddn: self.ldap_info.base_dn_str.clone(), + message: "".to_string(), + referral: vec![], + })]), + None => Err(LdapError { + code: LdapResultCode::OperationsError, + message: "Search request returned nothing".to_string(), + }), + _ => Err(LdapError { + code: LdapResultCode::OperationsError, + message: "Unexpected results from search".to_string(), + }), + } + } + + pub async fn handle_ldap_message(&mut self, ldap_op: LdapOp) -> Option> { + 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::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(""), + ); + 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 + .do_create_user_or_group(request) + .await + .unwrap_or_else(|e: LdapError| vec![make_add_error(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::infra::test_utils::{MockTestBackendHandler, setup_default_schema}; + use chrono::TimeZone; + use ldap3_proto::proto::LdapWhoamiRequest; + use lldap_domain::{types::*, uuid}; + use lldap_domain_handlers::handler::*; + use mockall::predicate::eq; + use pretty_assertions::assert_eq; + use std::collections::HashSet; + use tokio; + + pub fn make_user_search_request>( + filter: LdapFilter, + attrs: Vec, + ) -> LdapSearchRequest { + make_search_request::("ou=people,Dc=example,dc=com", filter, attrs) + } + + pub fn make_group_search_request>( + filter: LdapFilter, + attrs: Vec, + ) -> LdapSearchRequest { + make_search_request::("ou=groups,dc=example,dc=com", filter, attrs) + } + + pub async fn setup_bound_handler_with_group( + mut mock: MockTestBackendHandler, + group: &str, + ) -> LdapHandler { + 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(), + }); + 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.0, + LdapResultCode::Success + ); + ldap_handler + } + + pub async fn setup_bound_readonly_handler( + mock: MockTestBackendHandler, + ) -> LdapHandler { + setup_bound_handler_with_group(mock, "lldap_strict_readonly").await + } + + pub async fn setup_bound_password_manager_handler( + mock: MockTestBackendHandler, + ) -> LdapHandler { + setup_bound_handler_with_group(mock, "lldap_password_manager").await + } + + pub async fn setup_bound_admin_handler( + mock: MockTestBackendHandler, + ) -> LdapHandler { + 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(); + 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(())); + let mut ldap_handler = setup_bound_admin_handler(mock).await; + let request = LdapOp::ExtendedRequest( + LdapPasswordModifyRequest { + user_identity: Some("uid=bob,ou=people,dc=example,dc=com".to_string()), + old_password: None, + new_password: Some("password".to_string()), + } + .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_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(), + )]) + ); + } + + #[tokio::test] + async fn test_password_change_modify_request() { + let mut mock = MockTestBackendHandler::new(); + 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(())); + let mut ldap_handler = setup_bound_admin_handler(mock).await; + let request = LdapOp::ModifyRequest(LdapModifyRequest { + dn: "uid=bob,ou=people,dc=example,dc=com".to_string(), + changes: vec![LdapModify { + operation: LdapModifyType::Replace, + modification: LdapPartialAttribute { + atype: "userPassword".to_owned(), + vals: vec!["password".as_bytes().to_vec()], + }, + }], + }); + assert_eq!( + ldap_handler.handle_ldap_message(request).await, + Some(vec![make_modify_response( + LdapResultCode::Success, + "".to_string(), + )]) + ); + } + + #[tokio::test] + async fn test_password_change_password_manager() { + let mut mock = MockTestBackendHandler::new(); + 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(())); + let mut ldap_handler = setup_bound_password_manager_handler(mock).await; + let request = LdapOp::ExtendedRequest( + LdapPasswordModifyRequest { + user_identity: Some("uid=bob,ou=people,dc=example,dc=com".to_string()), + old_password: None, + new_password: Some("password".to_string()), + } + .into(), + ); + assert_eq!( + ldap_handler.handle_ldap_message(request).await, + Some(vec![make_extended_response( + LdapResultCode::Success, + "".to_string(), + )]) + ); + } + + #[tokio::test] + async fn test_password_change_errors() { + let mut mock = MockTestBackendHandler::new(); + mock.expect_get_user_groups() + .with(eq(UserId::new("bob"))) + .returning(|_| Ok(HashSet::new())); + let mut ldap_handler = setup_bound_admin_handler(mock).await; + let request = LdapOp::ExtendedRequest( + LdapPasswordModifyRequest { + user_identity: None, + old_password: None, + new_password: None, + } + .into(), + ); + assert_eq!( + ldap_handler.handle_ldap_message(request).await, + Some(vec![make_extended_response( + LdapResultCode::ConstraintViolation, + "Missing either user_id or password".to_string(), + )]) + ); + let request = LdapOp::ExtendedRequest( + LdapPasswordModifyRequest { + user_identity: Some("uid=bob,ou=groups,ou=people,dc=example,dc=com".to_string()), + old_password: None, + new_password: Some("password".to_string()), + } + .into(), + ); + assert_eq!( + ldap_handler.handle_ldap_message(request).await, + Some(vec![make_extended_response( + LdapResultCode::InvalidDNSyntax, + r#"Invalid username: Unexpected DN format. Got "uid=bob,ou=groups,ou=people,dc=example,dc=com", expected: "uid=id,ou=people,dc=example,dc=com""#.to_string(), + )]) + ); + let request = LdapOp::ExtendedRequest(LdapExtendedRequest { + name: "test".to_string(), + value: None, + }); + assert_eq!( + ldap_handler.handle_ldap_message(request).await, + Some(vec![make_extended_response( + LdapResultCode::UnwillingToPerform, + "Unsupported extended operation: test".to_string(), + )]) + ); + } + + #[tokio::test] + async fn test_password_change_unauthorized_password_manager() { + let mut mock = MockTestBackendHandler::new(); + let mut groups = HashSet::new(); + groups.insert(GroupDetails { + group_id: GroupId(0), + display_name: "lldap_admin".into(), + creation_date: chrono::Utc.timestamp_opt(42, 42).unwrap().naive_utc(), + uuid: uuid!("a1a2a3a4b1b2c1c2d1d2d3d4d5d6d7d8"), + attributes: Vec::new(), + }); + mock.expect_get_user_groups() + .with(eq(UserId::new("bob"))) + .times(1) + .return_once(|_| Ok(groups)); + let mut ldap_handler = setup_bound_password_manager_handler(mock).await; + let request = LdapOp::ExtendedRequest( + LdapPasswordModifyRequest { + user_identity: Some("uid=bob,ou=people,dc=example,dc=com".to_string()), + old_password: Some("pass".to_string()), + new_password: Some("password".to_string()), + } + .into(), + ); + assert_eq!( + ldap_handler.handle_ldap_message(request).await, + Some(vec![make_extended_response( + LdapResultCode::InsufficentAccessRights, + "User `test` cannot modify the password of user `bob`".to_string(), + )]) + ); + } + + #[tokio::test] + async fn test_password_change_unauthorized_readonly() { + let mut mock = MockTestBackendHandler::new(); + mock.expect_get_user_groups() + .with(eq(UserId::new("bob"))) + .times(1) + .return_once(|_| Ok(HashSet::new())); + let mut ldap_handler = setup_bound_readonly_handler(mock).await; + let request = LdapOp::ExtendedRequest( + LdapPasswordModifyRequest { + user_identity: Some("uid=bob,ou=people,dc=example,dc=com".to_string()), + old_password: Some("pass".to_string()), + new_password: Some("password".to_string()), + } + .into(), + ); + assert_eq!( + ldap_handler.handle_ldap_message(request).await, + Some(vec![make_extended_response( + LdapResultCode::InsufficentAccessRights, + "User `test` cannot modify the password of user `bob`".to_string(), + )]) + ); + } + + #[tokio::test] + async fn test_create_user() { + let mut mock = MockTestBackendHandler::new(); + mock.expect_create_user() + .with(eq(CreateUserRequest { + user_id: UserId::new("bob"), + email: "".into(), + display_name: Some("Bob".to_string()), + ..Default::default() + })) + .times(1) + .return_once(|_| Ok(())); + let ldap_handler = setup_bound_admin_handler(mock).await; + let request = LdapAddRequest { + dn: "uid=bob,ou=people,dc=example,dc=com".to_owned(), + attributes: vec![LdapPartialAttribute { + atype: "cn".to_owned(), + vals: vec![b"Bob".to_vec()], + }], + }; + assert_eq!( + ldap_handler.do_create_user_or_group(request).await, + Ok(vec![make_add_error(LdapResultCode::Success, String::new())]) + ); + } + + #[tokio::test] + async fn test_create_group() { + let mut mock = MockTestBackendHandler::new(); + mock.expect_create_group() + .with(eq(CreateGroupRequest { + display_name: GroupName::new("bob"), + ..Default::default() + })) + .times(1) + .return_once(|_| Ok(GroupId(5))); + let ldap_handler = setup_bound_admin_handler(mock).await; + let request = LdapAddRequest { + dn: "uid=bob,ou=groups,dc=example,dc=com".to_owned(), + attributes: vec![LdapPartialAttribute { + atype: "cn".to_owned(), + vals: vec![b"Bobby".to_vec()], + }], + }; + assert_eq!( + ldap_handler.do_create_user_or_group(request).await, + Ok(vec![make_add_error(LdapResultCode::Success, String::new())]) + ); + } + + #[tokio::test] + async fn test_create_user_multiple_object_class() { + let mut mock = MockTestBackendHandler::new(); + mock.expect_create_user() + .with(eq(CreateUserRequest { + user_id: UserId::new("bob"), + email: "".into(), + display_name: Some("Bob".to_string()), + ..Default::default() + })) + .times(1) + .return_once(|_| Ok(())); + let ldap_handler = setup_bound_admin_handler(mock).await; + let request = LdapAddRequest { + dn: "uid=bob,ou=people,dc=example,dc=com".to_owned(), + attributes: vec![ + LdapPartialAttribute { + atype: "cn".to_owned(), + vals: vec![b"Bob".to_vec()], + }, + LdapPartialAttribute { + atype: "objectClass".to_owned(), + vals: vec![ + b"top".to_vec(), + b"person".to_vec(), + b"inetOrgPerson".to_vec(), + ], + }, + ], + }; + assert_eq!( + ldap_handler.do_create_user_or_group(request).await, + Ok(vec![make_add_error(LdapResultCode::Success, String::new())]) + ); + } + + #[tokio::test] + async fn test_compare_user() { + let mut mock = MockTestBackendHandler::new(); + mock.expect_list_users().returning(|f, g| { + assert_eq!(f, Some(UserRequestFilter::UserId(UserId::new("bob")))); + assert!(!g); + Ok(vec![UserAndGroups { + user: User { + user_id: UserId::new("bob"), + email: "bob@bobmail.bob".into(), + ..Default::default() + }, + groups: None, + }]) + }); + mock.expect_list_groups().returning(|_| Ok(vec![])); + let mut ldap_handler = setup_bound_admin_handler(mock).await; + let dn = "uid=bob,ou=people,dc=example,dc=com"; + let request = LdapCompareRequest { + dn: dn.to_string(), + atype: "uid".to_owned(), + val: b"bob".to_vec(), + }; + assert_eq!( + ldap_handler.do_compare(request).await, + Ok(vec![LdapOp::CompareResult(LdapResultOp { + code: LdapResultCode::CompareTrue, + matcheddn: dn.to_string(), + message: "".to_string(), + referral: vec![], + })]) + ); + // Non-canonical attribute. + let request = LdapCompareRequest { + dn: dn.to_string(), + atype: "eMail".to_owned(), + val: b"bob@bobmail.bob".to_vec(), + }; + assert_eq!( + ldap_handler.do_compare(request).await, + Ok(vec![LdapOp::CompareResult(LdapResultOp { + code: LdapResultCode::CompareTrue, + matcheddn: dn.to_string(), + message: "".to_string(), + referral: vec![], + })]) + ); + } + + #[tokio::test] + async fn test_compare_group() { + let mut mock = MockTestBackendHandler::new(); + mock.expect_list_users().returning(|_, _| Ok(vec![])); + mock.expect_list_groups().returning(|f| { + assert_eq!(f, Some(GroupRequestFilter::DisplayName("group".into()))); + Ok(vec![Group { + id: GroupId(1), + display_name: "group".into(), + creation_date: chrono::Utc.timestamp_opt(42, 42).unwrap().naive_utc(), + users: vec![UserId::new("bob")], + uuid: uuid!("04ac75e0-2900-3e21-926c-2f732c26b3fc"), + attributes: Vec::new(), + }]) + }); + let mut ldap_handler = setup_bound_admin_handler(mock).await; + let dn = "uid=group,ou=groups,dc=example,dc=com"; + let request = LdapCompareRequest { + dn: dn.to_string(), + atype: "uid".to_owned(), + val: b"group".to_vec(), + }; + assert_eq!( + ldap_handler.do_compare(request).await, + Ok(vec![LdapOp::CompareResult(LdapResultOp { + code: LdapResultCode::CompareTrue, + matcheddn: dn.to_string(), + message: "".to_string(), + referral: vec![], + })]) + ); + } + + #[tokio::test] + async fn test_compare_not_found() { + let mut mock = MockTestBackendHandler::new(); + mock.expect_list_users().returning(|f, g| { + assert_eq!(f, Some(UserRequestFilter::UserId(UserId::new("bob")))); + assert!(!g); + Ok(vec![]) + }); + mock.expect_list_groups().returning(|_| Ok(vec![])); + let mut ldap_handler = setup_bound_admin_handler(mock).await; + let dn = "uid=bob,ou=people,dc=example,dc=com"; + let request = LdapCompareRequest { + dn: dn.to_string(), + atype: "uid".to_owned(), + val: b"bob".to_vec(), + }; + assert_eq!( + ldap_handler.do_compare(request).await, + Ok(vec![LdapOp::CompareResult(LdapResultOp { + code: LdapResultCode::NoSuchObject, + matcheddn: "dc=example,dc=com".to_owned(), + message: "".to_string(), + referral: vec![], + })]) + ); + } + + #[tokio::test] + async fn test_compare_no_match() { + let mut mock = MockTestBackendHandler::new(); + mock.expect_list_users().returning(|f, g| { + assert_eq!(f, Some(UserRequestFilter::UserId(UserId::new("bob")))); + assert!(!g); + Ok(vec![UserAndGroups { + user: User { + user_id: UserId::new("bob"), + email: "bob@bobmail.bob".into(), + ..Default::default() + }, + groups: None, + }]) + }); + mock.expect_list_groups().returning(|_| Ok(vec![])); + let mut ldap_handler = setup_bound_admin_handler(mock).await; + let dn = "uid=bob,ou=people,dc=example,dc=com"; + let request = LdapCompareRequest { + dn: dn.to_string(), + atype: "mail".to_owned(), + val: b"bob@bob".to_vec(), + }; + assert_eq!( + ldap_handler.do_compare(request).await, + Ok(vec![LdapOp::CompareResult(LdapResultOp { + code: LdapResultCode::CompareFalse, + matcheddn: dn.to_string(), + message: "".to_string(), + referral: vec![], + })]) + ); + } + + #[tokio::test] + async fn test_compare_group_member() { + let mut mock = MockTestBackendHandler::new(); + mock.expect_list_users().returning(|_, _| Ok(vec![])); + mock.expect_list_groups().returning(|f| { + assert_eq!(f, Some(GroupRequestFilter::DisplayName("group".into()))); + Ok(vec![Group { + id: GroupId(1), + display_name: "group".into(), + creation_date: chrono::Utc.timestamp_opt(42, 42).unwrap().naive_utc(), + users: vec![UserId::new("bob")], + uuid: uuid!("04ac75e0-2900-3e21-926c-2f732c26b3fc"), + attributes: Vec::new(), + }]) + }); + let mut ldap_handler = setup_bound_admin_handler(mock).await; + let dn = "uid=group,ou=groups,dc=example,dc=com"; + let request = LdapCompareRequest { + dn: dn.to_string(), + atype: "uniqueMember".to_owned(), + val: b"uid=bob,ou=people,dc=example,dc=com".to_vec(), + }; + assert_eq!( + ldap_handler.do_compare(request).await, + Ok(vec![LdapOp::CompareResult(LdapResultOp { + code: LdapResultCode::CompareTrue, + matcheddn: dn.to_owned(), + message: "".to_string(), + referral: vec![], + })]) + ); + } +} diff --git a/server/src/infra/ldap/mod.rs b/server/src/infra/ldap/mod.rs new file mode 100644 index 0000000..2cc8e62 --- /dev/null +++ b/server/src/infra/ldap/mod.rs @@ -0,0 +1,2 @@ +pub mod handler; +pub mod search; diff --git a/server/src/infra/ldap_handler.rs b/server/src/infra/ldap/search.rs similarity index 51% rename from server/src/infra/ldap_handler.rs rename to server/src/infra/ldap/search.rs index 555c71a..30203c6 100644 --- a/server/src/infra/ldap_handler.rs +++ b/server/src/infra/ldap/search.rs @@ -1,43 +1,25 @@ +use ldap3_proto::{ + LdapFilter, LdapPartialAttribute, LdapResultCode, LdapSearchResultEntry, LdapSearchScope, + proto::{ + LdapDerefAliases, LdapOp, LdapResult as LdapResultOp, LdapSearchRequest, + OID_PASSWORD_MODIFY, OID_WHOAMI, + }, +}; +use lldap_domain::types::{Group, UserAndGroups}; +use tracing::{debug, instrument, warn}; + use crate::{ domain::{ - deserialize, ldap::{ error::{LdapError, LdapResult}, group::{convert_groups_to_ldap_op, get_groups_list}, user::{convert_users_to_ldap_op, get_user_list}, - utils::{ - LdapInfo, UserOrGroupName, get_user_id_from_distinguished_name, - get_user_or_group_id_from_distinguished_name, is_subtree, parse_distinguished_name, - }, + utils::{LdapInfo, is_subtree, parse_distinguished_name}, }, - opaque_handler::OpaqueHandler, schema::PublicSchema, }, - infra::access_control::{ - AccessControlledBackendHandler, AdminBackendHandler, UserAndGroupListerBackendHandler, - UserReadableBackendHandler, - }, + infra::access_control::UserAndGroupListerBackendHandler, }; -use anyhow::Result; -use ldap3_proto::proto::{ - LdapAddRequest, LdapAttribute, LdapBindCred, LdapBindRequest, LdapBindResponse, - LdapCompareRequest, LdapDerefAliases, LdapExtendedRequest, LdapExtendedResponse, LdapFilter, - LdapModify, LdapModifyRequest, LdapModifyType, LdapOp, LdapPartialAttribute, - LdapPasswordModifyRequest, LdapResult as LdapResultOp, LdapResultCode, LdapSearchRequest, - LdapSearchResultEntry, LdapSearchScope, OID_PASSWORD_MODIFY, OID_WHOAMI, -}; -use lldap_auth::access_control::ValidationResults; -use lldap_domain::{ - requests::{CreateGroupRequest, CreateUserRequest}, - types::{ - Attribute, AttributeName, AttributeType, Email, Group, GroupName, UserAndGroups, UserId, - }, -}; -use lldap_domain_handlers::handler::{ - BackendHandler, BindRequest, LoginHandler, ReadSchemaBackendHandler, -}; -use std::collections::HashMap; -use tracing::{debug, instrument, warn}; #[derive(Debug)] enum SearchScope { @@ -103,7 +85,7 @@ fn get_search_scope( } } -fn make_search_request>( +pub(crate) fn make_search_request>( base: &str, filter: LdapFilter, attrs: Vec, @@ -120,11 +102,11 @@ fn make_search_request>( } } -fn make_search_success() -> LdapOp { +pub(crate) fn make_search_success() -> LdapOp { make_search_error(LdapResultCode::Success, "".to_string()) } -fn make_search_error(code: LdapResultCode, message: String) -> LdapOp { +pub(crate) fn make_search_error(code: LdapResultCode, message: String) -> LdapOp { LdapOp::SearchResultDone(LdapResultOp { code, matcheddn: "".to_string(), @@ -133,38 +115,7 @@ fn make_search_error(code: LdapResultCode, message: String) -> LdapOp { }) } -fn make_add_error(code: LdapResultCode, message: String) -> LdapOp { - LdapOp::AddResponse(LdapResultOp { - code, - matcheddn: "".to_string(), - message, - referral: vec![], - }) -} - -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, - }) -} - -fn make_modify_response(code: LdapResultCode, message: String) -> LdapOp { - LdapOp::ModifyResponse(LdapResultOp { - code, - matcheddn: "".to_string(), - message, - referral: vec![], - }) -} - -fn root_dse_response(base_dn: &str) -> LdapOp { +pub(crate) fn root_dse_response(base_dn: &str) -> LdapOp { LdapOp::SearchResultEntry(LdapSearchResultEntry { dn: "".to_string(), attributes: vec![ @@ -220,910 +171,194 @@ fn root_dse_response(base_dn: &str) -> LdapOp { }) } -pub struct LdapHandler { - user_info: Option, - backend_handler: AccessControlledBackendHandler, - ldap_info: LdapInfo, - session_uuid: uuid::Uuid, -} - -impl LdapHandler { - pub fn session_uuid(&self) -> &uuid::Uuid { - &self.session_uuid +pub(crate) fn is_root_dse_request(request: &LdapSearchRequest) -> bool { + if request.base.is_empty() && request.scope == LdapSearchScope::Base { + if let LdapFilter::Present(attribute) = &request.filter { + if attribute.eq_ignore_ascii_case("objectclass") { + return true; + } + } } + false } -impl LdapHandler { - pub fn get_login_handler(&self) -> &(impl LoginHandler + use) { - self.backend_handler.unsafe_get_handler() +async fn do_search_internal( + ldap_info: &LdapInfo, + backend_handler: &impl UserAndGroupListerBackendHandler, + request: &LdapSearchRequest, + schema: &PublicSchema, +) -> LdapResult { + let dn_parts = parse_distinguished_name(&request.base.to_ascii_lowercase())?; + let scope = get_search_scope(&ldap_info.base_dn, &dn_parts, &request.scope); + debug!(?request.base, ?scope); + // Disambiguate the lifetimes. + fn cast<'a, T, R>(x: T) -> T + where + T: Fn(&'a LdapFilter) -> R + 'a, + { + x } -} -impl LdapHandler { - pub fn get_opaque_handler(&self) -> &(impl OpaqueHandler + use) { - self.backend_handler.unsafe_get_handler() - } -} - -impl LdapHandler { - pub fn new( - backend_handler: AccessControlledBackendHandler, - mut ldap_base_dn: String, - ignored_user_attributes: Vec, - ignored_group_attributes: Vec, - session_uuid: uuid::Uuid, - ) -> Self { - ldap_base_dn.make_ascii_lowercase(); - Self { - user_info: None, + let get_user_list = cast(async |filter: &LdapFilter| { + let need_groups = request + .attrs + .iter() + .any(|s| s.eq_ignore_ascii_case("memberof")); + get_user_list( + ldap_info, + filter, + need_groups, + &request.base, backend_handler, - ldap_info: LdapInfo { - base_dn: parse_distinguished_name(&ldap_base_dn).unwrap_or_else(|_| { - panic!( - "Invalid value for ldap_base_dn in configuration: {}", - ldap_base_dn - ) - }), - base_dn_str: ldap_base_dn, - ignored_user_attributes, - ignored_group_attributes, - }, - session_uuid, - } - } - - #[cfg(test)] - pub fn new_for_tests(backend_handler: Backend, ldap_base_dn: &str) -> Self { - Self::new( - AccessControlledBackendHandler::new(backend_handler), - ldap_base_dn.to_string(), - vec![], - vec![], - uuid::Uuid::parse_str("550e8400-e29b-41d4-a716-446655440000").unwrap(), + schema, ) - } - - #[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(), + .await + }); + let get_group_list = cast(|filter: &LdapFilter| async { + get_groups_list(ldap_info, filter, &request.base, backend_handler, schema).await + }); + Ok(match scope { + SearchScope::Global => { + let users = get_user_list(&request.filter).await; + let groups = get_group_list(&request.filter).await; + match (users, groups) { + (Ok(users), Err(e)) => { + warn!("Error while getting groups: {:#}", e); + InternalSearchResults::UsersAndGroups(users, Vec::new()) + } + (Err(e), Ok(groups)) => { + warn!("Error while getting users: {:#}", e); + InternalSearchResults::UsersAndGroups(Vec::new(), groups) + } + (Err(user_error), Err(_)) => InternalSearchResults::Raw(vec![make_search_error( + user_error.code, + user_error.message, + )]), + (Ok(users), Ok(groups)) => InternalSearchResults::UsersAndGroups(users, groups), + } + } + SearchScope::Users => { + InternalSearchResults::UsersAndGroups(get_user_list(&request.filter).await?, Vec::new()) + } + SearchScope::Groups => InternalSearchResults::UsersAndGroups( + Vec::new(), + get_group_list(&request.filter).await?, + ), + SearchScope::User(filter) => { + let filter = LdapFilter::And(vec![request.filter.clone(), filter]); + InternalSearchResults::UsersAndGroups(get_user_list(&filter).await?, Vec::new()) + } + SearchScope::Group(filter) => { + let filter = LdapFilter::And(vec![request.filter.clone(), filter]); + InternalSearchResults::UsersAndGroups(Vec::new(), get_group_list(&filter).await?) + } + SearchScope::UserOuOnly | SearchScope::GroupOuOnly => { + InternalSearchResults::Raw(vec![LdapOp::SearchResultEntry(LdapSearchResultEntry { + dn: request.base.clone(), + attributes: vec![LdapPartialAttribute { + atype: "objectClass".to_owned(), + vals: vec![b"top".to_vec(), b"organizationalUnit".to_vec()], + }], + })]) + } + SearchScope::Unknown => { + warn!( + r#"The requested search tree "{}" matches neither the user subtree "ou=people,{}" nor the group subtree "ou=groups,{}""#, + &request.base, &ldap_info.base_dn_str, &ldap_info.base_dn_str ); + InternalSearchResults::Empty } - 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(), + SearchScope::Invalid => { + // Search path is not in our tree, just return an empty success. + warn!( + "The specified search tree {:?} is not under the common subtree {:?}", + &dn_parts, &ldap_info.base_dn ); - }; - 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()), + InternalSearchResults::Empty } + }) +} + +#[instrument(skip_all, level = "debug")] +pub async fn do_search( + backend_handler: &impl UserAndGroupListerBackendHandler, + ldap_info: &LdapInfo, + request: &LdapSearchRequest, +) -> LdapResult> { + let schema = PublicSchema::from(backend_handler.get_schema().await.map_err(|e| LdapError { + code: LdapResultCode::OperationsError, + message: format!("Unable to get schema: {:#}", e), + })?); + let search_results = do_search_internal(ldap_info, backend_handler, request, &schema).await?; + let mut results = match search_results { + InternalSearchResults::UsersAndGroups(users, groups) => { + convert_users_to_ldap_op(users, &request.attrs, ldap_info, &schema) + .chain(convert_groups_to_ldap_op( + groups, + &request.attrs, + ldap_info, + backend_handler.user_filter(), + &schema, + )) + .collect() + } + InternalSearchResults::Raw(raw_results) => raw_results, + InternalSearchResults::Empty => Vec::new(), + }; + if !matches!(results.last(), Some(LdapOp::SearchResultDone(_))) { + results.push(make_search_success()); } - - async fn change_password( - &self, - backend_handler: &B, - user: UserId, - password: &[u8], - ) -> Result<()> { - use lldap_auth::*; - let mut rng = rand::rngs::OsRng; - let registration_start_request = - opaque::client::registration::start_registration(password, &mut rng)?; - let req = registration::ClientRegistrationStartRequest { - username: user.clone(), - registration_start_request: registration_start_request.message, - }; - let registration_start_response = backend_handler.registration_start(req).await?; - let registration_finish = opaque::client::registration::finish_registration( - registration_start_request.state, - registration_start_response.registration_response, - &mut rng, - )?; - let req = registration::ClientRegistrationFinishRequest { - server_data: registration_start_response.server_data, - registration_upload: registration_finish.message, - }; - backend_handler.registration_finish(req).await?; - Ok(()) - } - - async fn do_password_modification( - &mut self, - request: &LdapPasswordModifyRequest, - ) -> LdapResult> { - let credentials = self.user_info.as_ref().ok_or_else(|| LdapError { - code: LdapResultCode::InsufficentAccessRights, - message: "No user currently bound".to_string(), - })?; - match (&request.user_identity, &request.new_password) { - (Some(user), Some(password)) => { - match get_user_id_from_distinguished_name( - &user.to_ascii_lowercase(), - &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()); - if !credentials.can_change_password(&uid, user_is_admin) { - Err(LdapError { - code: LdapResultCode::InsufficentAccessRights, - message: format!( - r#"User `{}` cannot modify the password of user `{}`"#, - &credentials.user, &uid - ), - }) - } else if let Err(e) = self - .change_password(self.get_opaque_handler(), uid, password.as_bytes()) - .await - { - Err(LdapError { - code: LdapResultCode::Other, - message: format!("Error while changing the password: {:#?}", e), - }) - } else { - Ok(vec![make_extended_response( - LdapResultCode::Success, - "".to_string(), - )]) - } - } - Err(e) => Err(LdapError { - code: LdapResultCode::InvalidDNSyntax, - message: format!("Invalid username: {}", e), - }), - } - } - _ => Err(LdapError { - code: LdapResultCode::ConstraintViolation, - message: "Missing either user_id or password".to_string(), - }), - } - } - - #[instrument(skip_all, level = "debug")] - async fn do_extended_request(&mut self, request: &LdapExtendedRequest) -> Vec { - match request.name.as_str() { - OID_PASSWORD_MODIFY => match LdapPasswordModifyRequest::try_from(request) { - Ok(password_request) => self - .do_password_modification(&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), - )], - } - } - - async fn handle_modify_change( - &mut self, - 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() { - self.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)]) - } - - pub async fn do_search_or_dse( - &mut self, - request: &LdapSearchRequest, - ) -> LdapResult> { - if request.base.is_empty() && request.scope == LdapSearchScope::Base { - if let LdapFilter::Present(attribute) = &request.filter { - if attribute.eq_ignore_ascii_case("objectclass") { - debug!("rootDSE request"); - return Ok(vec![ - root_dse_response(&self.ldap_info.base_dn_str), - make_search_success(), - ]); - } - } - } - self.do_search(request).await - } - - async fn do_search_internal( - &self, - backend_handler: &impl UserAndGroupListerBackendHandler, - request: &LdapSearchRequest, - schema: &PublicSchema, - ) -> LdapResult { - let dn_parts = parse_distinguished_name(&request.base.to_ascii_lowercase())?; - let scope = get_search_scope(&self.ldap_info.base_dn, &dn_parts, &request.scope); - debug!(?request.base, ?scope); - // Disambiguate the lifetimes. - fn cast<'a, T, R>(x: T) -> T - where - T: Fn(&'a LdapFilter) -> R + 'a, - { - x - } - - let get_user_list = cast(async |filter: &LdapFilter| { - let need_groups = request - .attrs - .iter() - .any(|s| s.eq_ignore_ascii_case("memberof")); - get_user_list( - &self.ldap_info, - filter, - need_groups, - &request.base, - backend_handler, - schema, - ) - .await - }); - let get_group_list = cast(|filter: &LdapFilter| async { - get_groups_list( - &self.ldap_info, - filter, - &request.base, - backend_handler, - schema, - ) - .await - }); - Ok(match scope { - SearchScope::Global => { - let users = get_user_list(&request.filter).await; - let groups = get_group_list(&request.filter).await; - match (users, groups) { - (Ok(users), Err(e)) => { - warn!("Error while getting groups: {:#}", e); - InternalSearchResults::UsersAndGroups(users, Vec::new()) - } - (Err(e), Ok(groups)) => { - warn!("Error while getting users: {:#}", e); - InternalSearchResults::UsersAndGroups(Vec::new(), groups) - } - (Err(user_error), Err(_)) => { - InternalSearchResults::Raw(vec![make_search_error( - user_error.code, - user_error.message, - )]) - } - (Ok(users), Ok(groups)) => InternalSearchResults::UsersAndGroups(users, groups), - } - } - SearchScope::Users => InternalSearchResults::UsersAndGroups( - get_user_list(&request.filter).await?, - Vec::new(), - ), - SearchScope::Groups => InternalSearchResults::UsersAndGroups( - Vec::new(), - get_group_list(&request.filter).await?, - ), - SearchScope::User(filter) => { - let filter = LdapFilter::And(vec![request.filter.clone(), filter]); - InternalSearchResults::UsersAndGroups(get_user_list(&filter).await?, Vec::new()) - } - SearchScope::Group(filter) => { - let filter = LdapFilter::And(vec![request.filter.clone(), filter]); - InternalSearchResults::UsersAndGroups(Vec::new(), get_group_list(&filter).await?) - } - SearchScope::UserOuOnly | SearchScope::GroupOuOnly => { - InternalSearchResults::Raw(vec![LdapOp::SearchResultEntry(LdapSearchResultEntry { - dn: request.base.clone(), - attributes: vec![LdapPartialAttribute { - atype: "objectClass".to_owned(), - vals: vec![b"top".to_vec(), b"organizationalUnit".to_vec()], - }], - })]) - } - SearchScope::Unknown => { - warn!( - r#"The requested search tree "{}" matches neither the user subtree "ou=people,{}" nor the group subtree "ou=groups,{}""#, - &request.base, &self.ldap_info.base_dn_str, &self.ldap_info.base_dn_str - ); - InternalSearchResults::Empty - } - SearchScope::Invalid => { - // Search path is not in our tree, just return an empty success. - warn!( - "The specified search tree {:?} is not under the common subtree {:?}", - &dn_parts, &self.ldap_info.base_dn - ); - InternalSearchResults::Empty - } - }) - } - - #[instrument(skip_all, level = "debug")] - pub async fn do_search(&self, request: &LdapSearchRequest) -> LdapResult> { - 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); - - let schema = - PublicSchema::from(backend_handler.get_schema().await.map_err(|e| LdapError { - code: LdapResultCode::OperationsError, - message: format!("Unable to get schema: {:#}", e), - })?); - let search_results = self - .do_search_internal(&backend_handler, request, &schema) - .await?; - let mut results = match search_results { - InternalSearchResults::UsersAndGroups(users, groups) => { - convert_users_to_ldap_op(users, &request.attrs, &self.ldap_info, &schema) - .chain(convert_groups_to_ldap_op( - groups, - &request.attrs, - &self.ldap_info, - &backend_handler.user_filter, - &schema, - )) - .collect() - } - InternalSearchResults::Raw(raw_results) => raw_results, - InternalSearchResults::Empty => Vec::new(), - }; - if !matches!(results.last(), Some(LdapOp::SearchResultDone(_))) { - results.push(make_search_success()); - } - Ok(results) - } - - #[instrument(skip_all, level = "debug")] - async fn do_create_user_or_group(&self, request: LdapAddRequest) -> 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(), - })?; - let base_dn_str = &self.ldap_info.base_dn_str; - match get_user_or_group_id_from_distinguished_name(&request.dn, &self.ldap_info.base_dn) { - UserOrGroupName::User(user_id) => { - self.do_create_user(backend_handler, user_id, request.attributes) - .await - } - UserOrGroupName::Group(group_name) => { - self.do_create_group(backend_handler, group_name, request.attributes) - .await - } - err => Err(err.into_ldap_error( - &request.dn, - format!( - r#""uid=id,ou=people,{}" or "uid=id,ou=groups,{}""#, - base_dn_str, base_dn_str - ), - )), - } - } - - #[instrument(skip_all, level = "debug")] - async fn do_create_user( - &self, - backend_handler: &impl AdminBackendHandler, - user_id: UserId, - attributes: Vec, - ) -> LdapResult> { - fn parse_attribute(mut attr: LdapPartialAttribute) -> LdapResult<(String, Vec)> { - if attr.vals.len() > 1 { - Err(LdapError { - code: LdapResultCode::ConstraintViolation, - message: format!("Expected a single value for attribute {}", attr.atype), - }) - } else { - attr.atype.make_ascii_lowercase(); - match attr.vals.pop() { - Some(val) => Ok((attr.atype, val)), - None => Err(LdapError { - code: LdapResultCode::ConstraintViolation, - message: format!("Missing value for attribute {}", attr.atype), - }), - } - } - } - let attributes: HashMap> = attributes - .into_iter() - .filter(|a| !a.atype.eq_ignore_ascii_case("objectclass")) - .map(parse_attribute) - .collect::>()?; - fn decode_attribute_value(val: &[u8]) -> LdapResult { - std::str::from_utf8(val) - .map_err(|e| LdapError { - code: LdapResultCode::ConstraintViolation, - message: format!( - "Attribute value is invalid UTF-8: {:#?} (value {:?})", - e, val - ), - }) - .map(str::to_owned) - } - let get_attribute = |name| { - attributes - .get(name) - .map(Vec::as_slice) - .map(decode_attribute_value) - }; - let make_encoded_attribute = |name: &str, typ: AttributeType, value: String| { - Ok(Attribute { - name: AttributeName::from(name), - value: deserialize::deserialize_attribute_value(&[value], typ, false).map_err( - |e| LdapError { - code: LdapResultCode::ConstraintViolation, - message: format!("Invalid attribute value: {}", e), - }, - )?, - }) - }; - let mut new_user_attributes: Vec = Vec::new(); - if let Some(first_name) = get_attribute("givenname").transpose()? { - new_user_attributes.push(make_encoded_attribute( - "first_name", - AttributeType::String, - first_name, - )?); - } - if let Some(last_name) = get_attribute("sn").transpose()? { - new_user_attributes.push(make_encoded_attribute( - "last_name", - AttributeType::String, - last_name, - )?); - } - if let Some(avatar) = get_attribute("avatar").transpose()? { - new_user_attributes.push(make_encoded_attribute( - "avatar", - AttributeType::JpegPhoto, - avatar, - )?); - } - backend_handler - .create_user(CreateUserRequest { - user_id, - email: Email::from( - get_attribute("mail") - .or_else(|| get_attribute("email")) - .transpose()? - .unwrap_or_default(), - ), - display_name: get_attribute("cn").transpose()?, - attributes: new_user_attributes, - }) - .await - .map_err(|e| LdapError { - code: LdapResultCode::OperationsError, - message: format!("Could not create user: {:#?}", e), - })?; - Ok(vec![make_add_error(LdapResultCode::Success, String::new())]) - } - - #[instrument(skip_all, level = "debug")] - async fn do_create_group( - &self, - backend_handler: &impl AdminBackendHandler, - group_name: GroupName, - _attributes: Vec, - ) -> LdapResult> { - backend_handler - .create_group(CreateGroupRequest { - display_name: group_name, - attributes: Vec::new(), - }) - .await - .map_err(|e| LdapError { - code: LdapResultCode::OperationsError, - message: format!("Could not create group: {:#?}", e), - })?; - Ok(vec![make_add_error(LdapResultCode::Success, String::new())]) - } - - #[instrument(skip_all, level = "debug")] - pub async fn do_compare(&mut self, request: LdapCompareRequest) -> LdapResult> { - let req = make_search_request::( - &self.ldap_info.base_dn_str, - LdapFilter::Equality("dn".to_string(), request.dn.to_string()), - vec![request.atype.clone()], - ); - let entries = self.do_search(&req).await?; - if entries.len() > 2 { - // SearchResultEntry + SearchResultDone - return Err(LdapError { - code: LdapResultCode::OperationsError, - message: "Too many search results".to_string(), - }); - } - let requested_attribute = AttributeName::from(&request.atype); - match entries.first() { - Some(LdapOp::SearchResultEntry(entry)) => { - let available = entry.attributes.iter().any(|attr| { - AttributeName::from(&attr.atype) == requested_attribute - && attr.vals.contains(&request.val) - }); - Ok(vec![LdapOp::CompareResult(LdapResultOp { - code: if available { - LdapResultCode::CompareTrue - } else { - LdapResultCode::CompareFalse - }, - matcheddn: request.dn, - message: "".to_string(), - referral: vec![], - })]) - } - Some(LdapOp::SearchResultDone(_)) => Ok(vec![LdapOp::CompareResult(LdapResultOp { - code: LdapResultCode::NoSuchObject, - matcheddn: self.ldap_info.base_dn_str.clone(), - message: "".to_string(), - referral: vec![], - })]), - None => Err(LdapError { - code: LdapResultCode::OperationsError, - message: "Search request returned nothing".to_string(), - }), - _ => Err(LdapError { - code: LdapResultCode::OperationsError, - message: "Unexpected results from search".to_string(), - }), - } - } - - pub async fn handle_ldap_message(&mut self, ldap_op: LdapOp) -> Option> { - 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::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(""), - ); - 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 - .do_create_user_or_group(request) - .await - .unwrap_or_else(|e: LdapError| vec![make_add_error(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), - )], - }) - } + Ok(results) } #[cfg(test)] mod tests { use super::*; - use crate::infra::test_utils::{MockTestBackendHandler, setup_default_schema}; - use chrono::TimeZone; - use ldap3_proto::proto::{ - LdapDerefAliases, LdapSearchScope, LdapSubstringFilter, LdapWhoamiRequest, + use crate::{ + domain::ldap::error::LdapError, + infra::{ + ldap::handler::tests::{ + make_group_search_request, make_user_search_request, setup_bound_admin_handler, + setup_bound_handler_with_group, setup_bound_readonly_handler, + }, + test_utils::MockTestBackendHandler, + }, }; + use chrono::TimeZone; + use ldap3_proto::proto::{LdapDerefAliases, LdapSearchScope, LdapSubstringFilter}; use lldap_domain::{ schema::{AttributeList, AttributeSchema, Schema}, - types::*, + types::{ + Attribute, AttributeName, AttributeType, GroupDetails, GroupId, JpegPhoto, + LdapObjectClass, User, UserId, + }, uuid, }; use lldap_domain_handlers::handler::*; use lldap_domain_model::model::UserColumn; use mockall::predicate::eq; use pretty_assertions::assert_eq; - use std::collections::HashSet; use tokio; - fn make_user_search_request>( - filter: LdapFilter, - attrs: Vec, - ) -> LdapSearchRequest { - make_search_request::("ou=people,Dc=example,dc=com", filter, attrs) - } - - fn make_group_search_request>( - filter: LdapFilter, - attrs: Vec, - ) -> LdapSearchRequest { - make_search_request::("ou=groups,dc=example,dc=com", filter, attrs) - } - - async fn setup_bound_handler_with_group( - mut mock: MockTestBackendHandler, - group: &str, - ) -> LdapHandler { - 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(), - }); - 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()), + #[tokio::test] + async fn test_search_root_dse() { + let mut ldap_handler = setup_bound_admin_handler(MockTestBackendHandler::new()).await; + let request = LdapSearchRequest { + base: "".to_string(), + scope: LdapSearchScope::Base, + aliases: LdapDerefAliases::Never, + sizelimit: 0, + timelimit: 0, + typesonly: false, + filter: LdapFilter::Present("objectClass".to_string()), + attrs: vec!["supportedExtension".to_string()], }; assert_eq!( - ldap_handler.do_bind(&request).await.0, - LdapResultCode::Success - ); - ldap_handler - } - - async fn setup_bound_readonly_handler( - mock: MockTestBackendHandler, - ) -> LdapHandler { - setup_bound_handler_with_group(mock, "lldap_strict_readonly").await - } - - async fn setup_bound_password_manager_handler( - mock: MockTestBackendHandler, - ) -> LdapHandler { - setup_bound_handler_with_group(mock, "lldap_password_manager").await - } - - async fn setup_bound_admin_handler( - mock: MockTestBackendHandler, - ) -> LdapHandler { - 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 + ldap_handler.do_search_or_dse(&request).await, + Ok(vec![ + root_dse_response("dc=example,dc=com"), + make_search_success() + ]) ); } @@ -1254,86 +489,6 @@ mod tests { ); } - #[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, - ); - } - - #[test] - fn test_is_subtree() { - let subtree1 = &[ - ("ou".to_string(), "people".to_string()), - ("dc".to_string(), "example".to_string()), - ("dc".to_string(), "com".to_string()), - ]; - let root = &[ - ("dc".to_string(), "example".to_string()), - ("dc".to_string(), "com".to_string()), - ]; - assert!(is_subtree(subtree1, root)); - assert!(!is_subtree(&[], root)); - } - - #[test] - fn test_parse_distinguished_name() { - let parsed_dn = &[ - ("ou".to_string(), "people".to_string()), - ("dc".to_string(), "example".to_string()), - ("dc".to_string(), "com".to_string()), - ]; - assert_eq!( - parse_distinguished_name("ou=people,dc=example,dc=com").expect("parsing failed"), - parsed_dn - ); - assert_eq!( - parse_distinguished_name(" ou = people , dc = example , dc = com ") - .expect("parsing failed"), - parsed_dn - ); - } - #[tokio::test] async fn test_search_users() { use chrono::prelude::*; @@ -2516,396 +1671,6 @@ mod tests { ); } - #[tokio::test] - async fn test_password_change() { - let mut mock = MockTestBackendHandler::new(); - 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(())); - let mut ldap_handler = setup_bound_admin_handler(mock).await; - let request = LdapOp::ExtendedRequest( - LdapPasswordModifyRequest { - user_identity: Some("uid=bob,ou=people,dc=example,dc=com".to_string()), - old_password: None, - new_password: Some("password".to_string()), - } - .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_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(), - )]) - ); - } - - #[tokio::test] - async fn test_password_change_modify_request() { - let mut mock = MockTestBackendHandler::new(); - 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(())); - let mut ldap_handler = setup_bound_admin_handler(mock).await; - let request = LdapOp::ModifyRequest(LdapModifyRequest { - dn: "uid=bob,ou=people,dc=example,dc=com".to_string(), - changes: vec![LdapModify { - operation: LdapModifyType::Replace, - modification: LdapPartialAttribute { - atype: "userPassword".to_owned(), - vals: vec!["password".as_bytes().to_vec()], - }, - }], - }); - assert_eq!( - ldap_handler.handle_ldap_message(request).await, - Some(vec![make_modify_response( - LdapResultCode::Success, - "".to_string(), - )]) - ); - } - - #[tokio::test] - async fn test_password_change_password_manager() { - let mut mock = MockTestBackendHandler::new(); - 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(())); - let mut ldap_handler = setup_bound_password_manager_handler(mock).await; - let request = LdapOp::ExtendedRequest( - LdapPasswordModifyRequest { - user_identity: Some("uid=bob,ou=people,dc=example,dc=com".to_string()), - old_password: None, - new_password: Some("password".to_string()), - } - .into(), - ); - assert_eq!( - ldap_handler.handle_ldap_message(request).await, - Some(vec![make_extended_response( - LdapResultCode::Success, - "".to_string(), - )]) - ); - } - - #[tokio::test] - async fn test_password_change_errors() { - let mut mock = MockTestBackendHandler::new(); - mock.expect_get_user_groups() - .with(eq(UserId::new("bob"))) - .returning(|_| Ok(HashSet::new())); - let mut ldap_handler = setup_bound_admin_handler(mock).await; - let request = LdapOp::ExtendedRequest( - LdapPasswordModifyRequest { - user_identity: None, - old_password: None, - new_password: None, - } - .into(), - ); - assert_eq!( - ldap_handler.handle_ldap_message(request).await, - Some(vec![make_extended_response( - LdapResultCode::ConstraintViolation, - "Missing either user_id or password".to_string(), - )]) - ); - let request = LdapOp::ExtendedRequest( - LdapPasswordModifyRequest { - user_identity: Some("uid=bob,ou=groups,ou=people,dc=example,dc=com".to_string()), - old_password: None, - new_password: Some("password".to_string()), - } - .into(), - ); - assert_eq!( - ldap_handler.handle_ldap_message(request).await, - Some(vec![make_extended_response( - LdapResultCode::InvalidDNSyntax, - r#"Invalid username: Unexpected DN format. Got "uid=bob,ou=groups,ou=people,dc=example,dc=com", expected: "uid=id,ou=people,dc=example,dc=com""#.to_string(), - )]) - ); - let request = LdapOp::ExtendedRequest(LdapExtendedRequest { - name: "test".to_string(), - value: None, - }); - assert_eq!( - ldap_handler.handle_ldap_message(request).await, - Some(vec![make_extended_response( - LdapResultCode::UnwillingToPerform, - "Unsupported extended operation: test".to_string(), - )]) - ); - } - - #[tokio::test] - async fn test_password_change_unauthorized_password_manager() { - let mut mock = MockTestBackendHandler::new(); - let mut groups = HashSet::new(); - groups.insert(GroupDetails { - group_id: GroupId(0), - display_name: "lldap_admin".into(), - creation_date: chrono::Utc.timestamp_opt(42, 42).unwrap().naive_utc(), - uuid: uuid!("a1a2a3a4b1b2c1c2d1d2d3d4d5d6d7d8"), - attributes: Vec::new(), - }); - mock.expect_get_user_groups() - .with(eq(UserId::new("bob"))) - .times(1) - .return_once(|_| Ok(groups)); - let mut ldap_handler = setup_bound_password_manager_handler(mock).await; - let request = LdapOp::ExtendedRequest( - LdapPasswordModifyRequest { - user_identity: Some("uid=bob,ou=people,dc=example,dc=com".to_string()), - old_password: Some("pass".to_string()), - new_password: Some("password".to_string()), - } - .into(), - ); - assert_eq!( - ldap_handler.handle_ldap_message(request).await, - Some(vec![make_extended_response( - LdapResultCode::InsufficentAccessRights, - "User `test` cannot modify the password of user `bob`".to_string(), - )]) - ); - } - - #[tokio::test] - async fn test_password_change_unauthorized_readonly() { - let mut mock = MockTestBackendHandler::new(); - mock.expect_get_user_groups() - .with(eq(UserId::new("bob"))) - .times(1) - .return_once(|_| Ok(HashSet::new())); - let mut ldap_handler = setup_bound_readonly_handler(mock).await; - let request = LdapOp::ExtendedRequest( - LdapPasswordModifyRequest { - user_identity: Some("uid=bob,ou=people,dc=example,dc=com".to_string()), - old_password: Some("pass".to_string()), - new_password: Some("password".to_string()), - } - .into(), - ); - assert_eq!( - ldap_handler.handle_ldap_message(request).await, - Some(vec![make_extended_response( - LdapResultCode::InsufficentAccessRights, - "User `test` cannot modify the password of user `bob`".to_string(), - )]) - ); - } - - #[tokio::test] - async fn test_search_root_dse() { - let mut ldap_handler = setup_bound_admin_handler(MockTestBackendHandler::new()).await; - let request = LdapSearchRequest { - base: "".to_string(), - scope: LdapSearchScope::Base, - aliases: LdapDerefAliases::Never, - sizelimit: 0, - timelimit: 0, - typesonly: false, - filter: LdapFilter::Present("objectClass".to_string()), - attrs: vec!["supportedExtension".to_string()], - }; - assert_eq!( - ldap_handler.do_search_or_dse(&request).await, - Ok(vec![ - root_dse_response("dc=example,dc=com"), - make_search_success() - ]) - ); - } - - #[tokio::test] - async fn test_create_user() { - let mut mock = MockTestBackendHandler::new(); - mock.expect_create_user() - .with(eq(CreateUserRequest { - user_id: UserId::new("bob"), - email: "".into(), - display_name: Some("Bob".to_string()), - ..Default::default() - })) - .times(1) - .return_once(|_| Ok(())); - let ldap_handler = setup_bound_admin_handler(mock).await; - let request = LdapAddRequest { - dn: "uid=bob,ou=people,dc=example,dc=com".to_owned(), - attributes: vec![LdapPartialAttribute { - atype: "cn".to_owned(), - vals: vec![b"Bob".to_vec()], - }], - }; - assert_eq!( - ldap_handler.do_create_user_or_group(request).await, - Ok(vec![make_add_error(LdapResultCode::Success, String::new())]) - ); - } - - #[tokio::test] - async fn test_create_group() { - let mut mock = MockTestBackendHandler::new(); - mock.expect_create_group() - .with(eq(CreateGroupRequest { - display_name: GroupName::new("bob"), - ..Default::default() - })) - .times(1) - .return_once(|_| Ok(GroupId(5))); - let ldap_handler = setup_bound_admin_handler(mock).await; - let request = LdapAddRequest { - dn: "uid=bob,ou=groups,dc=example,dc=com".to_owned(), - attributes: vec![LdapPartialAttribute { - atype: "cn".to_owned(), - vals: vec![b"Bobby".to_vec()], - }], - }; - assert_eq!( - ldap_handler.do_create_user_or_group(request).await, - Ok(vec![make_add_error(LdapResultCode::Success, String::new())]) - ); - } - - #[tokio::test] - async fn test_create_user_multiple_object_class() { - let mut mock = MockTestBackendHandler::new(); - mock.expect_create_user() - .with(eq(CreateUserRequest { - user_id: UserId::new("bob"), - email: "".into(), - display_name: Some("Bob".to_string()), - ..Default::default() - })) - .times(1) - .return_once(|_| Ok(())); - let ldap_handler = setup_bound_admin_handler(mock).await; - let request = LdapAddRequest { - dn: "uid=bob,ou=people,dc=example,dc=com".to_owned(), - attributes: vec![ - LdapPartialAttribute { - atype: "cn".to_owned(), - vals: vec![b"Bob".to_vec()], - }, - LdapPartialAttribute { - atype: "objectClass".to_owned(), - vals: vec![ - b"top".to_vec(), - b"person".to_vec(), - b"inetOrgPerson".to_vec(), - ], - }, - ], - }; - assert_eq!( - ldap_handler.do_create_user_or_group(request).await, - Ok(vec![make_add_error(LdapResultCode::Success, String::new())]) - ); - } - #[tokio::test] async fn test_search_filter_non_attribute() { let mut mock = MockTestBackendHandler::new(); @@ -2924,182 +1689,6 @@ mod tests { ); } - #[tokio::test] - async fn test_compare_user() { - let mut mock = MockTestBackendHandler::new(); - mock.expect_list_users().returning(|f, g| { - assert_eq!(f, Some(UserRequestFilter::UserId(UserId::new("bob")))); - assert!(!g); - Ok(vec![UserAndGroups { - user: User { - user_id: UserId::new("bob"), - email: "bob@bobmail.bob".into(), - ..Default::default() - }, - groups: None, - }]) - }); - mock.expect_list_groups().returning(|_| Ok(vec![])); - let mut ldap_handler = setup_bound_admin_handler(mock).await; - let dn = "uid=bob,ou=people,dc=example,dc=com"; - let request = LdapCompareRequest { - dn: dn.to_string(), - atype: "uid".to_owned(), - val: b"bob".to_vec(), - }; - assert_eq!( - ldap_handler.do_compare(request).await, - Ok(vec![LdapOp::CompareResult(LdapResultOp { - code: LdapResultCode::CompareTrue, - matcheddn: dn.to_string(), - message: "".to_string(), - referral: vec![], - })]) - ); - // Non-canonical attribute. - let request = LdapCompareRequest { - dn: dn.to_string(), - atype: "eMail".to_owned(), - val: b"bob@bobmail.bob".to_vec(), - }; - assert_eq!( - ldap_handler.do_compare(request).await, - Ok(vec![LdapOp::CompareResult(LdapResultOp { - code: LdapResultCode::CompareTrue, - matcheddn: dn.to_string(), - message: "".to_string(), - referral: vec![], - })]) - ); - } - - #[tokio::test] - async fn test_compare_group() { - let mut mock = MockTestBackendHandler::new(); - mock.expect_list_users().returning(|_, _| Ok(vec![])); - mock.expect_list_groups().returning(|f| { - assert_eq!(f, Some(GroupRequestFilter::DisplayName("group".into()))); - Ok(vec![Group { - id: GroupId(1), - display_name: "group".into(), - creation_date: chrono::Utc.timestamp_opt(42, 42).unwrap().naive_utc(), - users: vec![UserId::new("bob")], - uuid: uuid!("04ac75e0-2900-3e21-926c-2f732c26b3fc"), - attributes: Vec::new(), - }]) - }); - let mut ldap_handler = setup_bound_admin_handler(mock).await; - let dn = "uid=group,ou=groups,dc=example,dc=com"; - let request = LdapCompareRequest { - dn: dn.to_string(), - atype: "uid".to_owned(), - val: b"group".to_vec(), - }; - assert_eq!( - ldap_handler.do_compare(request).await, - Ok(vec![LdapOp::CompareResult(LdapResultOp { - code: LdapResultCode::CompareTrue, - matcheddn: dn.to_string(), - message: "".to_string(), - referral: vec![], - })]) - ); - } - - #[tokio::test] - async fn test_compare_not_found() { - let mut mock = MockTestBackendHandler::new(); - mock.expect_list_users().returning(|f, g| { - assert_eq!(f, Some(UserRequestFilter::UserId(UserId::new("bob")))); - assert!(!g); - Ok(vec![]) - }); - mock.expect_list_groups().returning(|_| Ok(vec![])); - let mut ldap_handler = setup_bound_admin_handler(mock).await; - let dn = "uid=bob,ou=people,dc=example,dc=com"; - let request = LdapCompareRequest { - dn: dn.to_string(), - atype: "uid".to_owned(), - val: b"bob".to_vec(), - }; - assert_eq!( - ldap_handler.do_compare(request).await, - Ok(vec![LdapOp::CompareResult(LdapResultOp { - code: LdapResultCode::NoSuchObject, - matcheddn: "dc=example,dc=com".to_owned(), - message: "".to_string(), - referral: vec![], - })]) - ); - } - - #[tokio::test] - async fn test_compare_no_match() { - let mut mock = MockTestBackendHandler::new(); - mock.expect_list_users().returning(|f, g| { - assert_eq!(f, Some(UserRequestFilter::UserId(UserId::new("bob")))); - assert!(!g); - Ok(vec![UserAndGroups { - user: User { - user_id: UserId::new("bob"), - email: "bob@bobmail.bob".into(), - ..Default::default() - }, - groups: None, - }]) - }); - mock.expect_list_groups().returning(|_| Ok(vec![])); - let mut ldap_handler = setup_bound_admin_handler(mock).await; - let dn = "uid=bob,ou=people,dc=example,dc=com"; - let request = LdapCompareRequest { - dn: dn.to_string(), - atype: "mail".to_owned(), - val: b"bob@bob".to_vec(), - }; - assert_eq!( - ldap_handler.do_compare(request).await, - Ok(vec![LdapOp::CompareResult(LdapResultOp { - code: LdapResultCode::CompareFalse, - matcheddn: dn.to_string(), - message: "".to_string(), - referral: vec![], - })]) - ); - } - - #[tokio::test] - async fn test_compare_group_member() { - let mut mock = MockTestBackendHandler::new(); - mock.expect_list_users().returning(|_, _| Ok(vec![])); - mock.expect_list_groups().returning(|f| { - assert_eq!(f, Some(GroupRequestFilter::DisplayName("group".into()))); - Ok(vec![Group { - id: GroupId(1), - display_name: "group".into(), - creation_date: chrono::Utc.timestamp_opt(42, 42).unwrap().naive_utc(), - users: vec![UserId::new("bob")], - uuid: uuid!("04ac75e0-2900-3e21-926c-2f732c26b3fc"), - attributes: Vec::new(), - }]) - }); - let mut ldap_handler = setup_bound_admin_handler(mock).await; - let dn = "uid=group,ou=groups,dc=example,dc=com"; - let request = LdapCompareRequest { - dn: dn.to_string(), - atype: "uniqueMember".to_owned(), - val: b"uid=bob,ou=people,dc=example,dc=com".to_vec(), - }; - assert_eq!( - ldap_handler.do_compare(request).await, - Ok(vec![LdapOp::CompareResult(LdapResultOp { - code: LdapResultCode::CompareTrue, - matcheddn: dn.to_owned(), - message: "".to_string(), - referral: vec![], - })]) - ); - } - #[tokio::test] async fn test_user_ou_search() { let mut ldap_handler = setup_bound_readonly_handler(MockTestBackendHandler::new()).await; diff --git a/server/src/infra/ldap_server.rs b/server/src/infra/ldap_server.rs index 6581376..7d05e33 100644 --- a/server/src/infra/ldap_server.rs +++ b/server/src/infra/ldap_server.rs @@ -3,7 +3,7 @@ use crate::{ infra::{ access_control::AccessControlledBackendHandler, configuration::{Configuration, LdapsOptions}, - ldap_handler::LdapHandler, + ldap::handler::LdapHandler, }, }; use actix_rt::net::TcpStream; diff --git a/server/src/infra/mod.rs b/server/src/infra/mod.rs index 38301ad..e5e286a 100644 --- a/server/src/infra/mod.rs +++ b/server/src/infra/mod.rs @@ -7,7 +7,7 @@ pub mod db_cleaner; pub mod graphql; pub mod healthcheck; pub mod jwt_sql_tables; -pub mod ldap_handler; +pub mod ldap; pub mod ldap_server; pub mod logging; pub mod mail;