From b8f114bd43a24376a59a30f4a8e238b88509b927 Mon Sep 17 00:00:00 2001 From: Valentin Tolmer Date: Fri, 4 Apr 2025 10:43:38 -0500 Subject: [PATCH] ldap: add support for creating groups --- server/src/domain/ldap/utils.rs | 89 ++++++++++++++++----------- server/src/infra/ldap_handler.rs | 100 +++++++++++++++++++++++-------- 2 files changed, 131 insertions(+), 58 deletions(-) diff --git a/server/src/domain/ldap/utils.rs b/server/src/domain/ldap/utils.rs index b32ff3b..c26185e 100644 --- a/server/src/domain/ldap/utils.rs +++ b/server/src/domain/ldap/utils.rs @@ -31,9 +31,9 @@ where Ok(pair) } })() - .map_err(|s| LdapError { + .map_err(|e| LdapError { code: LdapResultCode::InvalidDNSyntax, - message: s, + message: e, }) } @@ -44,38 +44,53 @@ pub fn parse_distinguished_name(dn: &str) -> LdapResult> { .collect() } -fn get_id_from_distinguished_name( - dn: &str, - base_tree: &[(String, String)], - base_dn_str: &str, - is_group: bool, -) -> LdapResult { - let parts = parse_distinguished_name(dn)?; - { - let ou = if is_group { "groups" } else { "people" }; - if !is_subtree(&parts, base_tree) { - Err("Not a subtree of the base tree".to_string()) - } else if parts.len() == base_tree.len() + 2 { - if parts[1].0 != "ou" || parts[1].1 != ou || (parts[0].0 != "cn" && parts[0].0 != "uid") - { - Err(format!( - r#"Unexpected DN format. Got "{}", expected: "uid=id,ou={},{}""#, - dn, ou, base_dn_str - )) - } else { - Ok(parts[0].1.to_string()) - } - } else { - Err(format!( - r#"Unexpected DN format. Got "{}", expected: "uid=id,ou={},{}""#, - dn, ou, base_dn_str - )) +pub enum UserOrGroupName { + User(UserId), + Group(GroupName), + BadSubStree, + UnexpectedFormat, + InvalidSyntax(LdapError), +} + +impl UserOrGroupName { + pub fn into_ldap_error(self, input: &str, expected_format: String) -> LdapError { + LdapError { + code: LdapResultCode::InvalidDNSyntax, + message: match self { + UserOrGroupName::BadSubStree => "Not a subtree of the base tree".to_string(), + UserOrGroupName::InvalidSyntax(err) => return err, + UserOrGroupName::UnexpectedFormat + | UserOrGroupName::User(_) + | UserOrGroupName::Group(_) => format!( + r#"Unexpected DN format. Got "{}", expected: {}"#, + input, expected_format + ), + }, } } - .map_err(|s| LdapError { - code: LdapResultCode::InvalidDNSyntax, - message: s, - }) +} + +pub fn get_user_or_group_id_from_distinguished_name( + dn: &str, + base_tree: &[(String, String)], +) -> UserOrGroupName { + let parts = match parse_distinguished_name(dn) { + Ok(p) => p, + Err(e) => return UserOrGroupName::InvalidSyntax(e), + }; + if !is_subtree(&parts, base_tree) { + return UserOrGroupName::BadSubStree; + } else if parts.len() == base_tree.len() + 2 + && parts[1].0 == "ou" + && (parts[0].0 == "cn" || parts[0].0 == "uid") + { + if parts[1].1 == "groups" { + return UserOrGroupName::Group(GroupName::from(parts[0].1.clone())); + } else if parts[1].1 == "people" { + return UserOrGroupName::User(UserId::from(parts[0].1.clone())); + } + } + UserOrGroupName::UnexpectedFormat } pub fn get_user_id_from_distinguished_name( @@ -83,7 +98,10 @@ pub fn get_user_id_from_distinguished_name( base_tree: &[(String, String)], base_dn_str: &str, ) -> LdapResult { - get_id_from_distinguished_name(dn, base_tree, base_dn_str, false).map(UserId::from) + match get_user_or_group_id_from_distinguished_name(dn, base_tree) { + UserOrGroupName::User(user_id) => Ok(user_id), + err => Err(err.into_ldap_error(dn, format!(r#""uid=id,ou=people,{}""#, base_dn_str))), + } } pub fn get_group_id_from_distinguished_name( @@ -91,7 +109,10 @@ pub fn get_group_id_from_distinguished_name( base_tree: &[(String, String)], base_dn_str: &str, ) -> LdapResult { - get_id_from_distinguished_name(dn, base_tree, base_dn_str, true).map(GroupName::from) + match get_user_or_group_id_from_distinguished_name(dn, base_tree) { + UserOrGroupName::Group(group_name) => Ok(group_name), + err => Err(err.into_ldap_error(dn, format!(r#""uid=id,ou=groups,{}""#, base_dn_str))), + } } fn looks_like_distinguished_name(dn: &str) -> bool { diff --git a/server/src/infra/ldap_handler.rs b/server/src/infra/ldap_handler.rs index 004970a..555c71a 100644 --- a/server/src/infra/ldap_handler.rs +++ b/server/src/infra/ldap_handler.rs @@ -6,7 +6,8 @@ use crate::{ group::{convert_groups_to_ldap_op, get_groups_list}, user::{convert_users_to_ldap_op, get_user_list}, utils::{ - LdapInfo, get_user_id_from_distinguished_name, is_subtree, parse_distinguished_name, + LdapInfo, UserOrGroupName, get_user_id_from_distinguished_name, + get_user_or_group_id_from_distinguished_name, is_subtree, parse_distinguished_name, }, }, opaque_handler::OpaqueHandler, @@ -19,16 +20,18 @@ use crate::{ }; use anyhow::Result; use ldap3_proto::proto::{ - LdapAddRequest, 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, + 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::CreateUserRequest, - types::{Attribute, AttributeName, AttributeType, Email, Group, UserAndGroups, UserId}, + requests::{CreateGroupRequest, CreateUserRequest}, + types::{ + Attribute, AttributeName, AttributeType, Email, Group, GroupName, UserAndGroups, UserId, + }, }; use lldap_domain_handlers::handler::{ BackendHandler, BindRequest, LoginHandler, ReadSchemaBackendHandler, @@ -721,7 +724,7 @@ impl LdapHandler LdapResult> { + async fn do_create_user_or_group(&self, request: LdapAddRequest) -> LdapResult> { let backend_handler = self .user_info .as_ref() @@ -730,11 +733,33 @@ impl LdapHandler { + 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 { @@ -752,8 +777,7 @@ impl LdapHandler> = request - .attributes + let attributes: HashMap> = attributes .into_iter() .filter(|a| !a.atype.eq_ignore_ascii_case("objectclass")) .map(parse_attribute) @@ -828,6 +852,26 @@ impl LdapHandler, + ) -> 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::( @@ -911,7 +955,7 @@ impl LdapHandler self.do_modify_request(&request).await, LdapOp::ExtendedRequest(request) => self.do_extended_request(&request).await, LdapOp::AddRequest(request) => self - .do_create_user(request) + .do_create_user_or_group(request) .await .unwrap_or_else(|e: LdapError| vec![make_add_error(e.code, e.message)]), LdapOp::CompareRequest(request) => self @@ -2797,24 +2841,32 @@ mod tests { }], }; assert_eq!( - ldap_handler.do_create_user(request).await, + 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_wrong_ou() { - let ldap_handler = setup_bound_admin_handler(MockTestBackendHandler::new()).await; + 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"Bob".to_vec()], + vals: vec![b"Bobby".to_vec()], }], }; assert_eq!( - ldap_handler.do_create_user(request).await, - Err(LdapError{ code: LdapResultCode::InvalidDNSyntax, message: r#"Unexpected DN format. Got "uid=bob,ou=groups,dc=example,dc=com", expected: "uid=id,ou=people,dc=example,dc=com""#.to_string() }) + ldap_handler.do_create_user_or_group(request).await, + Ok(vec![make_add_error(LdapResultCode::Success, String::new())]) ); } @@ -2849,7 +2901,7 @@ mod tests { ], }; assert_eq!( - ldap_handler.do_create_user(request).await, + ldap_handler.do_create_user_or_group(request).await, Ok(vec![make_add_error(LdapResultCode::Success, String::new())]) ); }