ldap: add support for creating groups

This commit is contained in:
Valentin Tolmer
2025-04-04 10:43:38 -05:00
committed by nitnelave
parent 31364da6d4
commit b8f114bd43
2 changed files with 131 additions and 58 deletions
+55 -34
View File
@@ -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<Vec<(String, String)>> {
.collect()
}
fn get_id_from_distinguished_name(
dn: &str,
base_tree: &[(String, String)],
base_dn_str: &str,
is_group: bool,
) -> LdapResult<String> {
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<UserId> {
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<GroupName> {
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 {
+76 -24
View File
@@ -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<Backend: BackendHandler + LoginHandler + OpaqueHandler> LdapHandler<Backend
}
#[instrument(skip_all, level = "debug")]
async fn do_create_user(&self, request: LdapAddRequest) -> LdapResult<Vec<LdapOp>> {
async fn do_create_user_or_group(&self, request: LdapAddRequest) -> LdapResult<Vec<LdapOp>> {
let backend_handler = self
.user_info
.as_ref()
@@ -730,11 +733,33 @@ impl<Backend: BackendHandler + LoginHandler + OpaqueHandler> LdapHandler<Backend
code: LdapResultCode::InsufficentAccessRights,
message: "Unauthorized write".to_string(),
})?;
let user_id = get_user_id_from_distinguished_name(
&request.dn,
&self.ldap_info.base_dn,
&self.ldap_info.base_dn_str,
)?;
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<LdapAttribute>,
) -> LdapResult<Vec<LdapOp>> {
fn parse_attribute(mut attr: LdapPartialAttribute) -> LdapResult<(String, Vec<u8>)> {
if attr.vals.len() > 1 {
Err(LdapError {
@@ -752,8 +777,7 @@ impl<Backend: BackendHandler + LoginHandler + OpaqueHandler> LdapHandler<Backend
}
}
}
let attributes: HashMap<String, Vec<u8>> = request
.attributes
let attributes: HashMap<String, Vec<u8>> = attributes
.into_iter()
.filter(|a| !a.atype.eq_ignore_ascii_case("objectclass"))
.map(parse_attribute)
@@ -828,6 +852,26 @@ impl<Backend: BackendHandler + LoginHandler + OpaqueHandler> LdapHandler<Backend
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<LdapAttribute>,
) -> LdapResult<Vec<LdapOp>> {
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<Vec<LdapOp>> {
let req = make_search_request::<String>(
@@ -911,7 +955,7 @@ impl<Backend: BackendHandler + LoginHandler + OpaqueHandler> LdapHandler<Backend
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(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())])
);
}