diff --git a/server/src/domain/ldap/group.rs b/server/src/domain/ldap/group.rs index a3584ea..21f47c5 100644 --- a/server/src/domain/ldap/group.rs +++ b/server/src/domain/ldap/group.rs @@ -7,31 +7,28 @@ use tracing::{debug, instrument, warn}; use crate::domain::{ deserialize::deserialize_attribute_value, handler::{GroupListerBackendHandler, GroupRequestFilter}, - ldap::error::LdapError, + ldap::{ + error::{LdapError, LdapResult}, + utils::{ + expand_attribute_wildcards, get_custom_attribute, + get_group_id_from_distinguished_name_or_plain_name, + get_user_id_from_distinguished_name_or_plain_name, map_group_field, ExpandedAttributes, + GroupFieldType, LdapInfo, + }, + }, schema::{PublicSchema, SchemaGroupAttributeExtractor}, types::{AttributeName, AttributeType, Group, LdapObjectClass, UserId, Uuid}, }; -use super::{ - error::LdapResult, - utils::{ - expand_attribute_wildcards, get_custom_attribute, - get_group_id_from_distinguished_name_or_plain_name, - get_user_id_from_distinguished_name_or_plain_name, map_group_field, GroupFieldType, - LdapInfo, - }, -}; - pub fn get_group_attribute( group: &Group, base_dn_str: &str, - attribute: &str, + attribute: &AttributeName, user_filter: &Option, ignored_group_attributes: &[AttributeName], schema: &PublicSchema, ) -> Option>> { - let attribute = AttributeName::from(attribute); - let attribute_values = match map_group_field(&attribute, schema) { + let attribute_values = match map_group_field(attribute, schema) { GroupFieldType::ObjectClass => { let mut classes = vec![b"groupOfUniqueNames".to_vec()]; classes.extend( @@ -74,12 +71,12 @@ pub fn get_group_attribute( ) } _ => { - if ignored_group_attributes.contains(&attribute) { + if ignored_group_attributes.contains(attribute) { return None; } get_custom_attribute::( &group.attributes, - &attribute, + attribute, schema, ).or_else(||{warn!( r#"Ignoring unrecognized group attribute: {}\n\ @@ -105,33 +102,42 @@ const ALL_GROUP_ATTRIBUTE_KEYS: &[&str] = &[ "entryuuid", ]; -fn expand_group_attribute_wildcards(attributes: &[String]) -> Vec<&str> { +fn expand_group_attribute_wildcards(attributes: &[String]) -> ExpandedAttributes { expand_attribute_wildcards(attributes, ALL_GROUP_ATTRIBUTE_KEYS) } fn make_ldap_search_group_result_entry( group: Group, base_dn_str: &str, - expanded_attributes: &[&str], + mut expanded_attributes: ExpandedAttributes, user_filter: &Option, ignored_group_attributes: &[AttributeName], schema: &PublicSchema, ) -> LdapSearchResultEntry { + if expanded_attributes.include_custom_attributes { + expanded_attributes.attribute_keys.extend( + group + .attributes + .iter() + .map(|a| (a.name.clone(), a.name.to_string())), + ); + } LdapSearchResultEntry { dn: format!("cn={},ou=groups,{}", group.display_name, base_dn_str), attributes: expanded_attributes - .iter() - .filter_map(|a| { + .attribute_keys + .into_iter() + .filter_map(|(attribute, name)| { let values = get_group_attribute( &group, base_dn_str, - a, + &attribute, user_filter, ignored_group_attributes, schema, )?; Some(LdapPartialAttribute { - atype: a.to_string(), + atype: name, vals: values, }) }) @@ -295,7 +301,7 @@ pub fn convert_groups_to_ldap_op<'a>( LdapOp::SearchResultEntry(make_ldap_search_group_result_entry( g, &ldap_info.base_dn_str, - expanded_attributes.as_ref().unwrap(), + expanded_attributes.clone().unwrap(), user_filter, &ldap_info.ignored_group_attributes, schema, diff --git a/server/src/domain/ldap/user.rs b/server/src/domain/ldap/user.rs index 325200b..9f1d985 100644 --- a/server/src/domain/ldap/user.rs +++ b/server/src/domain/ldap/user.rs @@ -12,8 +12,8 @@ use crate::domain::{ utils::{ expand_attribute_wildcards, get_custom_attribute, get_group_id_from_distinguished_name_or_plain_name, - get_user_id_from_distinguished_name_or_plain_name, map_user_field, LdapInfo, - UserFieldType, + get_user_id_from_distinguished_name_or_plain_name, map_user_field, ExpandedAttributes, + LdapInfo, UserFieldType, }, }, schema::{PublicSchema, SchemaUserAttributeExtractor}, @@ -25,14 +25,13 @@ use crate::domain::{ pub fn get_user_attribute( user: &User, - attribute: &str, + attribute: &AttributeName, base_dn_str: &str, groups: Option<&[GroupDetails]>, ignored_user_attributes: &[AttributeName], schema: &PublicSchema, ) -> Option>> { - let attribute = AttributeName::from(attribute); - let attribute_values = match map_user_field(&attribute, schema) { + let attribute_values = match map_user_field(attribute, schema) { UserFieldType::ObjectClass => { let mut classes = vec![ b"inetOrgPerson".to_vec(), @@ -93,12 +92,12 @@ pub fn get_user_attribute( ) } _ => { - if ignored_user_attributes.contains(&attribute) { + if ignored_user_attributes.contains(attribute) { return None; } get_custom_attribute::( &user.attributes, - &attribute, + attribute, schema, ) .or_else(|| { @@ -134,27 +133,34 @@ const ALL_USER_ATTRIBUTE_KEYS: &[&str] = &[ fn make_ldap_search_user_result_entry( user: User, base_dn_str: &str, - expanded_attributes: &[&str], + mut expanded_attributes: ExpandedAttributes, groups: Option<&[GroupDetails]>, ignored_user_attributes: &[AttributeName], schema: &PublicSchema, ) -> LdapSearchResultEntry { - let dn = format!("uid={},ou=people,{}", user.user_id.as_str(), base_dn_str); + if expanded_attributes.include_custom_attributes { + expanded_attributes.attribute_keys.extend( + user.attributes + .iter() + .map(|a| (a.name.clone(), a.name.to_string())), + ); + } LdapSearchResultEntry { - dn, + dn: format!("uid={},ou=people,{}", user.user_id.as_str(), base_dn_str), attributes: expanded_attributes - .iter() - .filter_map(|a| { + .attribute_keys + .into_iter() + .filter_map(|(attribute, name)| { let values = get_user_attribute( &user, - a, + &attribute, base_dn_str, groups, ignored_user_attributes, schema, )?; Some(LdapPartialAttribute { - atype: a.to_string(), + atype: name, vals: values, }) }) @@ -295,7 +301,7 @@ fn convert_user_filter( } } -fn expand_user_attribute_wildcards(attributes: &[String]) -> Vec<&str> { +fn expand_user_attribute_wildcards(attributes: &[String]) -> ExpandedAttributes { expand_attribute_wildcards(attributes, ALL_USER_ATTRIBUTE_KEYS) } @@ -334,7 +340,7 @@ pub fn convert_users_to_ldap_op<'a>( LdapOp::SearchResultEntry(make_ldap_search_user_result_entry( u.user, &ldap_info.base_dn_str, - expanded_attributes.as_ref().unwrap(), + expanded_attributes.clone().unwrap(), u.groups.as_deref(), &ldap_info.ignored_user_attributes, schema, diff --git a/server/src/domain/ldap/utils.rs b/server/src/domain/ldap/utils.rs index 835213c..4ce055f 100644 --- a/server/src/domain/ldap/utils.rs +++ b/server/src/domain/ldap/utils.rs @@ -1,5 +1,6 @@ +use std::collections::BTreeMap; + use chrono::{NaiveDateTime, TimeZone}; -use itertools::Itertools; use ldap3_proto::{proto::LdapSubstringFilter, LdapResultCode}; use tracing::{debug, instrument, warn}; @@ -137,30 +138,39 @@ pub fn get_group_id_from_distinguished_name_or_plain_name( } } +#[derive(Clone)] +pub struct ExpandedAttributes { + // Lowercase name to original name. + pub attribute_keys: BTreeMap, + pub include_custom_attributes: bool, +} + #[instrument(skip(all_attribute_keys), level = "debug")] -pub fn expand_attribute_wildcards<'a>( - ldap_attributes: &'a [String], - all_attribute_keys: &'a [&'static str], -) -> Vec<&'a str> { - let extra_attributes = +pub fn expand_attribute_wildcards( + ldap_attributes: &[String], + all_attribute_keys: &[&'static str], +) -> ExpandedAttributes { + let mut include_custom_attributes = false; + let mut attributes_out: BTreeMap<_, _> = ldap_attributes + .iter() + .filter(|&s| s != "*" && s != "+" && s != "1.1") + .map(|s| (AttributeName::from(s), s.to_string())) + .collect(); + attributes_out.extend( if ldap_attributes.iter().any(|x| x == "*") || ldap_attributes.is_empty() { + include_custom_attributes = true; all_attribute_keys } else { &[] } .iter() - .copied(); - let attributes_out = ldap_attributes - .iter() - .map(|s| s.as_str()) - .filter(|&s| s != "*" && s != "+" && s != "1.1"); - - // Deduplicate, preserving order - let resolved_attributes = itertools::chain(attributes_out, extra_attributes) - .unique_by(|a| a.to_ascii_lowercase()) - .collect_vec(); - debug!(?resolved_attributes); - resolved_attributes + .map(|&s| (AttributeName::from(s), s.to_string())), + ); + debug!(?attributes_out); + ExpandedAttributes { + attribute_keys: attributes_out, + include_custom_attributes, + } } pub fn is_subtree(subtree: &[(String, String)], base_tree: &[(String, String)]) -> bool { diff --git a/server/src/infra/ldap_handler.rs b/server/src/infra/ldap_handler.rs index f9f2ea1..a381737 100644 --- a/server/src/infra/ldap_handler.rs +++ b/server/src/infra/ldap_handler.rs @@ -775,13 +775,13 @@ impl LdapHandler { - let available = entry - .attributes - .iter() - .any(|attr| attr.atype == request.atype && attr.vals.contains(&request.val)); + 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 @@ -1287,32 +1287,6 @@ mod tests { LdapOp::SearchResultEntry(LdapSearchResultEntry { dn: "uid=bob_1,ou=people,dc=example,dc=com".to_string(), attributes: vec![ - LdapPartialAttribute { - atype: "objectClass".to_string(), - vals: vec![ - b"inetOrgPerson".to_vec(), - b"posixAccount".to_vec(), - b"mailAccount".to_vec(), - b"person".to_vec(), - b"customUserClass".to_vec(), - ] - }, - LdapPartialAttribute { - atype: "uid".to_string(), - vals: vec![b"bob_1".to_vec()] - }, - LdapPartialAttribute { - atype: "mail".to_string(), - vals: vec![b"bob@bobmail.bob".to_vec()] - }, - LdapPartialAttribute { - atype: "givenName".to_string(), - vals: vec!["Bôb".to_string().into_bytes()] - }, - LdapPartialAttribute { - atype: "sn".to_string(), - vals: vec!["Böbberson".to_string().into_bytes()] - }, LdapPartialAttribute { atype: "cn".to_string(), vals: vec!["Bôb Böbberson".to_string().into_bytes()] @@ -1325,11 +1299,14 @@ mod tests { atype: "entryUuid".to_string(), vals: vec![b"698e1d5f-7a40-3151-8745-b9b8a37839da".to_vec()] }, - ], - }), - LdapOp::SearchResultEntry(LdapSearchResultEntry { - dn: "uid=jim,ou=people,dc=example,dc=com".to_string(), - attributes: vec![ + LdapPartialAttribute { + atype: "givenName".to_string(), + vals: vec!["Bôb".to_string().into_bytes()] + }, + LdapPartialAttribute { + atype: "mail".to_string(), + vals: vec![b"bob@bobmail.bob".to_vec()] + }, LdapPartialAttribute { atype: "objectClass".to_string(), vals: vec![ @@ -1340,22 +1317,19 @@ mod tests { b"customUserClass".to_vec(), ] }, - LdapPartialAttribute { - atype: "uid".to_string(), - vals: vec![b"jim".to_vec()] - }, - LdapPartialAttribute { - atype: "mail".to_string(), - vals: vec![b"jim@cricket.jim".to_vec()] - }, - LdapPartialAttribute { - atype: "givenName".to_string(), - vals: vec![b"Jim".to_vec()] - }, LdapPartialAttribute { atype: "sn".to_string(), - vals: vec![b"Cricket".to_vec()] + vals: vec!["Böbberson".to_string().into_bytes()] }, + LdapPartialAttribute { + atype: "uid".to_string(), + vals: vec![b"bob_1".to_vec()] + }, + ], + }), + LdapOp::SearchResultEntry(LdapSearchResultEntry { + dn: "uid=jim,ou=people,dc=example,dc=com".to_string(), + attributes: vec![ LdapPartialAttribute { atype: "cn".to_string(), vals: vec![b"Jimminy Cricket".to_vec()] @@ -1368,10 +1342,36 @@ mod tests { atype: "entryUuid".to_string(), vals: vec![b"04ac75e0-2900-3e21-926c-2f732c26b3fc".to_vec()] }, + LdapPartialAttribute { + atype: "givenName".to_string(), + vals: vec![b"Jim".to_vec()] + }, LdapPartialAttribute { atype: "jpegPhoto".to_string(), vals: vec![JpegPhoto::for_tests().into_bytes()] }, + LdapPartialAttribute { + atype: "mail".to_string(), + vals: vec![b"jim@cricket.jim".to_vec()] + }, + LdapPartialAttribute { + atype: "objectClass".to_string(), + vals: vec![ + b"inetOrgPerson".to_vec(), + b"posixAccount".to_vec(), + b"mailAccount".to_vec(), + b"person".to_vec(), + b"customUserClass".to_vec(), + ] + }, + LdapPartialAttribute { + atype: "sn".to_string(), + vals: vec![b"Cricket".to_vec()] + }, + LdapPartialAttribute { + atype: "uid".to_string(), + vals: vec![b"jim".to_vec()] + }, ], }), make_search_success(), @@ -1423,14 +1423,22 @@ mod tests { LdapOp::SearchResultEntry(LdapSearchResultEntry { dn: "cn=group_1,ou=groups,dc=example,dc=com".to_string(), attributes: vec![ - LdapPartialAttribute { - atype: "objectClass".to_string(), - vals: vec![b"groupOfUniqueNames".to_vec(),] - }, LdapPartialAttribute { atype: "cn".to_string(), vals: vec![b"group_1".to_vec()] }, + LdapPartialAttribute { + atype: "entryDN".to_string(), + vals: vec![b"uid=group_1,ou=groups,dc=example,dc=com".to_vec()], + }, + LdapPartialAttribute { + atype: "entryUuid".to_string(), + vals: vec![b"04ac75e0-2900-3e21-926c-2f732c26b3fc".to_vec()], + }, + LdapPartialAttribute { + atype: "objectClass".to_string(), + vals: vec![b"groupOfUniqueNames".to_vec(),] + }, LdapPartialAttribute { atype: "uniqueMember".to_string(), vals: vec![ @@ -1438,38 +1446,30 @@ mod tests { b"uid=john,ou=people,dc=example,dc=com".to_vec(), ] }, - LdapPartialAttribute { - atype: "entryUuid".to_string(), - vals: vec![b"04ac75e0-2900-3e21-926c-2f732c26b3fc".to_vec()], - }, - LdapPartialAttribute { - atype: "entryDN".to_string(), - vals: vec![b"uid=group_1,ou=groups,dc=example,dc=com".to_vec()], - }, ], }), LdapOp::SearchResultEntry(LdapSearchResultEntry { dn: "cn=BestGroup,ou=groups,dc=example,dc=com".to_string(), attributes: vec![ - LdapPartialAttribute { - atype: "objectClass".to_string(), - vals: vec![b"groupOfUniqueNames".to_vec(),] - }, LdapPartialAttribute { atype: "cn".to_string(), vals: vec![b"BestGroup".to_vec()] }, LdapPartialAttribute { - atype: "uniqueMember".to_string(), - vals: vec![b"uid=john,ou=people,dc=example,dc=com".to_vec()] + atype: "entryDN".to_string(), + vals: vec![b"uid=BestGroup,ou=groups,dc=example,dc=com".to_vec()], }, LdapPartialAttribute { atype: "entryUuid".to_string(), vals: vec![b"04ac75e0-2900-3e21-926c-2f732c26b3fc".to_vec()], }, LdapPartialAttribute { - atype: "entryDN".to_string(), - vals: vec![b"uid=BestGroup,ou=groups,dc=example,dc=com".to_vec()], + atype: "objectClass".to_string(), + vals: vec![b"groupOfUniqueNames".to_vec(),] + }, + LdapPartialAttribute { + atype: "uniqueMember".to_string(), + vals: vec![b"uid=john,ou=people,dc=example,dc=com".to_vec()] }, ], }), @@ -2028,6 +2028,10 @@ mod tests { LdapOp::SearchResultEntry(LdapSearchResultEntry { dn: "uid=bob_1,ou=people,dc=example,dc=com".to_string(), attributes: vec![ + LdapPartialAttribute { + atype: "cn".to_string(), + vals: vec!["Bôb Böbberson".to_string().into_bytes()] + }, LdapPartialAttribute { atype: "objectClass".to_string(), vals: vec![ @@ -2036,25 +2040,21 @@ mod tests { b"mailAccount".to_vec(), b"person".to_vec(), b"customUserClass".to_vec(), - ] - }, - LdapPartialAttribute { - atype: "cn".to_string(), - vals: vec!["Bôb Böbberson".to_string().into_bytes()] + ], }, ], }), LdapOp::SearchResultEntry(LdapSearchResultEntry { dn: "cn=group_1,ou=groups,dc=example,dc=com".to_string(), attributes: vec![ - LdapPartialAttribute { - atype: "objectClass".to_string(), - vals: vec![b"groupOfUniqueNames".to_vec(),] - }, LdapPartialAttribute { atype: "cn".to_string(), vals: vec![b"group_1".to_vec()] }, + LdapPartialAttribute { + atype: "objectClass".to_string(), + vals: vec![b"groupOfUniqueNames".to_vec(),] + }, ], }), make_search_success(), @@ -2114,35 +2114,13 @@ mod tests { dn: "uid=bob_1,ou=people,dc=example,dc=com".to_string(), attributes: vec![ LdapPartialAttribute { - atype: "objectclass".to_string(), - vals: vec![ - b"inetOrgPerson".to_vec(), - b"posixAccount".to_vec(), - b"mailAccount".to_vec(), - b"person".to_vec(), - b"customUserClass".to_vec(), - ], - }, - LdapPartialAttribute { - atype: "uid".to_string(), - vals: vec![b"bob_1".to_vec()], - }, - LdapPartialAttribute { - atype: "mail".to_string(), - vals: vec![b"bob@bobmail.bob".to_vec()], - }, - LdapPartialAttribute { - atype: "sn".to_string(), - vals: vec!["Böbberson".to_string().into_bytes()], + atype: "avatar".to_string(), + vals: vec![JpegPhoto::for_tests().into_bytes()], }, LdapPartialAttribute { atype: "cn".to_string(), vals: vec!["Bôb Böbberson".to_string().into_bytes()], }, - LdapPartialAttribute { - atype: "jpegPhoto".to_string(), - vals: vec![JpegPhoto::for_tests().into_bytes()], - }, LdapPartialAttribute { atype: "createtimestamp".to_string(), vals: vec![chrono::Utc @@ -2155,25 +2133,50 @@ mod tests { atype: "entryuuid".to_string(), vals: vec![b"b4ac75e0-2900-3e21-926c-2f732c26b3fc".to_vec()], }, + LdapPartialAttribute { + atype: "jpegPhoto".to_string(), + vals: vec![JpegPhoto::for_tests().into_bytes()], + }, + LdapPartialAttribute { + atype: "last_name".to_string(), + vals: vec!["Böbberson".to_string().into_bytes()], + }, + LdapPartialAttribute { + atype: "mail".to_string(), + vals: vec![b"bob@bobmail.bob".to_vec()], + }, + LdapPartialAttribute { + atype: "objectclass".to_string(), + vals: vec![ + b"inetOrgPerson".to_vec(), + b"posixAccount".to_vec(), + b"mailAccount".to_vec(), + b"person".to_vec(), + b"customUserClass".to_vec(), + ], + }, + LdapPartialAttribute { + atype: "sn".to_string(), + vals: vec!["Böbberson".to_string().into_bytes()], + }, + LdapPartialAttribute { + atype: "uid".to_string(), + vals: vec![b"bob_1".to_vec()], + }, ], }), // "objectclass", "dn", "uid", "cn", "member", "uniquemember" LdapOp::SearchResultEntry(LdapSearchResultEntry { dn: "cn=group_1,ou=groups,dc=example,dc=com".to_string(), attributes: vec![ - LdapPartialAttribute { - atype: "objectclass".to_string(), - vals: vec![b"groupOfUniqueNames".to_vec()], - }, - // UID - LdapPartialAttribute { - atype: "uid".to_string(), - vals: vec![b"group_1".to_vec()], - }, LdapPartialAttribute { atype: "cn".to_string(), vals: vec![b"group_1".to_vec()], }, + LdapPartialAttribute { + atype: "entryuuid".to_string(), + vals: vec![b"04ac75e0-2900-3e21-926c-2f732c26b3fc".to_vec()], + }, //member / uniquemember : "uid={},ou=people,{}" LdapPartialAttribute { atype: "member".to_string(), @@ -2182,6 +2185,15 @@ mod tests { b"uid=john,ou=people,dc=example,dc=com".to_vec(), ], }, + LdapPartialAttribute { + atype: "objectclass".to_string(), + vals: vec![b"groupOfUniqueNames".to_vec()], + }, + // UID + LdapPartialAttribute { + atype: "uid".to_string(), + vals: vec![b"group_1".to_vec()], + }, LdapPartialAttribute { atype: "uniquemember".to_string(), vals: vec![ @@ -2189,10 +2201,6 @@ mod tests { b"uid=john,ou=people,dc=example,dc=com".to_vec(), ], }, - LdapPartialAttribute { - atype: "entryuuid".to_string(), - vals: vec![b"04ac75e0-2900-3e21-926c-2f732c26b3fc".to_vec()], - }, ], }), make_search_success(), @@ -2924,27 +2932,27 @@ mod tests { LdapOp::SearchResultEntry(LdapSearchResultEntry { dn: "uid=test,ou=people,dc=example,dc=com".to_string(), attributes: vec![ - LdapPartialAttribute { - atype: "uid".to_owned(), - vals: vec![b"test".to_vec()], - }, LdapPartialAttribute { atype: "nickname".to_owned(), vals: vec![b"Bob the Builder".to_vec()], }, + LdapPartialAttribute { + atype: "uid".to_owned(), + vals: vec![b"test".to_vec()], + }, ], }), LdapOp::SearchResultEntry(LdapSearchResultEntry { dn: "cn=group,ou=groups,dc=example,dc=com".to_owned(), attributes: vec![ - LdapPartialAttribute { - atype: "uid".to_owned(), - vals: vec![b"group".to_vec()], - }, LdapPartialAttribute { atype: "club_name".to_owned(), vals: vec![b"Breakfast Club".to_vec()], }, + LdapPartialAttribute { + atype: "uid".to_owned(), + vals: vec![b"group".to_vec()], + }, ], }), make_search_success()