From 0799b6bc26521c3d747bcd3e7775fb631c47a13f Mon Sep 17 00:00:00 2001 From: Simon Broeng Jensen Date: Wed, 22 Jan 2025 10:37:04 +0100 Subject: [PATCH] server: include preserved case in user attribute value search Extends the generated UserRequestFilter with an OR'ed clause for the attribute value in both it's original case and lowercased. --- server/src/domain/ldap/group.rs | 41 ++++++++++------ server/src/domain/ldap/user.rs | 41 ++++++++++------ server/src/infra/ldap_handler.rs | 84 ++++++++++++++++++++++++++++++-- 3 files changed, 132 insertions(+), 34 deletions(-) diff --git a/server/src/domain/ldap/group.rs b/server/src/domain/ldap/group.rs index faba140..d102025 100644 --- a/server/src/domain/ldap/group.rs +++ b/server/src/domain/ldap/group.rs @@ -154,12 +154,23 @@ fn get_group_attribute_equality_filter( is_list: bool, value: &str, ) -> GroupRequestFilter { - deserialize_attribute_value(&[value.to_owned()], typ, is_list) - .map(|v| GroupRequestFilter::AttributeEquality(field.clone(), v)) - .unwrap_or_else(|e| { + let value_lc = value.to_ascii_lowercase(); + let serialized_value = deserialize_attribute_value(&[value.to_owned()], typ, is_list); + let serialized_value_lc = deserialize_attribute_value(&[value_lc.to_owned()], typ, is_list); + match (serialized_value, serialized_value_lc) { + (Ok(v), Ok(v_lc)) => GroupRequestFilter::Or(vec![ + GroupRequestFilter::AttributeEquality(field.clone(), v), + GroupRequestFilter::AttributeEquality(field.clone(), v_lc), + ]), + (Ok(_), Err(e)) => { + warn!("Invalid value for attribute {} (lowercased): {}", field, e); + GroupRequestFilter::from(false) + } + (Err(e), _) => { warn!("Invalid value for attribute {}: {}", field, e); GroupRequestFilter::from(false) - }) + } + } } fn convert_group_filter( @@ -171,24 +182,24 @@ fn convert_group_filter( match filter { LdapFilter::Equality(field, value) => { let field = AttributeName::from(field.as_str()); - let value = value.to_ascii_lowercase(); + let value_lc = value.to_ascii_lowercase(); match map_group_field(&field, schema) { - GroupFieldType::GroupId => Ok(value + GroupFieldType::GroupId => Ok(value_lc .parse::() .map(|id| GroupRequestFilter::GroupId(GroupId(id))) .unwrap_or_else(|_| { - warn!("Given group id is not a valid integer: {}", value); + warn!("Given group id is not a valid integer: {}", value_lc); GroupRequestFilter::from(false) })), - GroupFieldType::DisplayName => Ok(GroupRequestFilter::DisplayName(value.into())), - GroupFieldType::Uuid => Uuid::try_from(value.as_str()) + GroupFieldType::DisplayName => Ok(GroupRequestFilter::DisplayName(value_lc.into())), + GroupFieldType::Uuid => Uuid::try_from(value_lc.as_str()) .map(GroupRequestFilter::Uuid) .map_err(|e| LdapError { code: LdapResultCode::Other, message: format!("Invalid UUID: {:#}", e), }), GroupFieldType::Member => Ok(get_user_id_from_distinguished_name_or_plain_name( - &value, + &value_lc, &ldap_info.base_dn, &ldap_info.base_dn_str, ) @@ -198,21 +209,21 @@ fn convert_group_filter( GroupRequestFilter::from(false) })), GroupFieldType::ObjectClass => Ok(GroupRequestFilter::from( - matches!(value.as_str(), "groupofuniquenames" | "groupofnames") + matches!(value_lc.as_str(), "groupofuniquenames" | "groupofnames") || schema .get_schema() .extra_group_object_classes - .contains(&LdapObjectClass::from(value)), + .contains(&LdapObjectClass::from(value_lc)), )), GroupFieldType::Dn | GroupFieldType::EntryDn => { Ok(get_group_id_from_distinguished_name_or_plain_name( - value.as_str(), + value_lc.as_str(), &ldap_info.base_dn, &ldap_info.base_dn_str, ) .map(GroupRequestFilter::DisplayName) .unwrap_or_else(|_| { - warn!("Invalid dn filter on group: {}", value); + warn!("Invalid dn filter on group: {}", value_lc); GroupRequestFilter::from(false) })) } @@ -227,7 +238,7 @@ fn convert_group_filter( Ok(GroupRequestFilter::from(false)) } GroupFieldType::Attribute(field, typ, is_list) => Ok( - get_group_attribute_equality_filter(&field, typ, is_list, &value), + get_group_attribute_equality_filter(&field, typ, is_list, value), ), GroupFieldType::CreationDate => Err(LdapError { code: LdapResultCode::UnwillingToPerform, diff --git a/server/src/domain/ldap/user.rs b/server/src/domain/ldap/user.rs index 9f1d985..9786b6e 100644 --- a/server/src/domain/ldap/user.rs +++ b/server/src/domain/ldap/user.rs @@ -174,12 +174,23 @@ fn get_user_attribute_equality_filter( is_list: bool, value: &str, ) -> UserRequestFilter { - deserialize_attribute_value(&[value.to_owned()], typ, is_list) - .map(|v| UserRequestFilter::AttributeEquality(field.clone(), v)) - .unwrap_or_else(|e| { + let value_lc = value.to_ascii_lowercase(); + let serialized_value = deserialize_attribute_value(&[value.to_owned()], typ, is_list); + let serialized_value_lc = deserialize_attribute_value(&[value_lc.to_owned()], typ, is_list); + match (serialized_value, serialized_value_lc) { + (Ok(v), Ok(v_lc)) => UserRequestFilter::Or(vec![ + UserRequestFilter::AttributeEquality(field.clone(), v), + UserRequestFilter::AttributeEquality(field.clone(), v_lc), + ]), + (Ok(_), Err(e)) => { + warn!("Invalid value for attribute {} (lowercased): {}", field, e); + UserRequestFilter::from(false) + } + (Err(e), _) => { warn!("Invalid value for attribute {}: {}", field, e); UserRequestFilter::from(false) - }) + } + } } fn convert_user_filter( @@ -198,18 +209,20 @@ fn convert_user_filter( LdapFilter::Not(filter) => Ok(UserRequestFilter::Not(Box::new(rec(filter)?))), LdapFilter::Equality(field, value) => { let field = AttributeName::from(field.as_str()); - let value = value.to_ascii_lowercase(); + let value_lc = value.to_ascii_lowercase(); match map_user_field(&field, schema) { UserFieldType::PrimaryField(UserColumn::UserId) => { - Ok(UserRequestFilter::UserId(UserId::new(&value))) + Ok(UserRequestFilter::UserId(UserId::new(&value_lc))) } UserFieldType::PrimaryField(UserColumn::Email) => Ok(UserRequestFilter::Equality( UserColumn::LowercaseEmail, - value, + value_lc, )), - UserFieldType::PrimaryField(field) => Ok(UserRequestFilter::Equality(field, value)), + UserFieldType::PrimaryField(field) => { + Ok(UserRequestFilter::Equality(field, value_lc)) + } UserFieldType::Attribute(field, typ, is_list) => Ok( - get_user_attribute_equality_filter(&field, typ, is_list, &value), + get_user_attribute_equality_filter(&field, typ, is_list, value), ), UserFieldType::NoMatch => { if !ldap_info.ignored_user_attributes.contains(&field) { @@ -223,15 +236,15 @@ fn convert_user_filter( } UserFieldType::ObjectClass => Ok(UserRequestFilter::from( matches!( - value.as_str(), + value_lc.as_str(), "person" | "inetorgperson" | "posixaccount" | "mailaccount" ) || schema .get_schema() .extra_user_object_classes - .contains(&LdapObjectClass::from(value)), + .contains(&LdapObjectClass::from(value_lc)), )), UserFieldType::MemberOf => Ok(get_group_id_from_distinguished_name_or_plain_name( - &value, + &value_lc, &ldap_info.base_dn, &ldap_info.base_dn_str, ) @@ -242,13 +255,13 @@ fn convert_user_filter( })), UserFieldType::EntryDn | UserFieldType::Dn => { Ok(get_user_id_from_distinguished_name_or_plain_name( - value.as_str(), + value_lc.as_str(), &ldap_info.base_dn, &ldap_info.base_dn_str, ) .map(UserRequestFilter::UserId) .unwrap_or_else(|_| { - warn!("Invalid dn filter on user: {}", value); + warn!("Invalid dn filter on user: {}", value_lc); UserRequestFilter::from(false) })) } diff --git a/server/src/infra/ldap_handler.rs b/server/src/infra/ldap_handler.rs index 42e3806..7da24e7 100644 --- a/server/src/infra/ldap_handler.rs +++ b/server/src/infra/ldap_handler.rs @@ -1659,6 +1659,74 @@ mod tests { ); } + #[tokio::test] + async fn test_search_groups_filter_3() { + let mut mock = MockTestBackendHandler::new(); + mock.expect_list_groups() + .with(eq(Some(GroupRequestFilter::Or(vec![ + GroupRequestFilter::AttributeEquality( + AttributeName::from("attr"), + Serialized::from("TEST"), + ), + GroupRequestFilter::AttributeEquality( + AttributeName::from("attr"), + Serialized::from("test"), + ), + ])))) + .times(1) + .return_once(|_| { + Ok(vec![Group { + display_name: "group_1".into(), + id: GroupId(1), + creation_date: chrono::Utc.timestamp_opt(42, 42).unwrap().naive_utc(), + users: vec![], + uuid: uuid!("04ac75e0-2900-3e21-926c-2f732c26b3fc"), + attributes: vec![AttributeValue { + name: "Attr".into(), + value: Serialized::from("TEST"), + }], + }]) + }); + mock.expect_get_schema().returning(|| { + Ok(crate::domain::handler::Schema { + user_attributes: AttributeList { + attributes: Vec::new(), + }, + group_attributes: AttributeList { + attributes: vec![AttributeSchema { + name: "Attr".into(), + attribute_type: AttributeType::String, + is_list: false, + is_visible: true, + is_editable: true, + is_hardcoded: false, + is_readonly: false, + }], + }, + extra_user_object_classes: Vec::new(), + extra_group_object_classes: Vec::new(), + }) + }); + let mut ldap_handler = setup_bound_admin_handler(mock).await; + let request = make_group_search_request( + LdapFilter::Equality("Attr".to_string(), "TEST".to_string()), + vec!["cn"], + ); + assert_eq!( + ldap_handler.do_search_or_dse(&request).await, + Ok(vec![ + LdapOp::SearchResultEntry(LdapSearchResultEntry { + dn: "cn=group_1,ou=groups,dc=example,dc=com".to_string(), + attributes: vec![LdapPartialAttribute { + atype: "cn".to_string(), + vals: vec![b"group_1".to_vec()] + },], + }), + make_search_success(), + ]) + ); + } + #[tokio::test] async fn test_search_group_as_scope() { let mut mock = MockTestBackendHandler::new(); @@ -1789,10 +1857,16 @@ mod tests { true.into(), true.into(), false.into(), - UserRequestFilter::AttributeEquality( - AttributeName::from("first_name"), - Serialized::from("firstname"), - ), + UserRequestFilter::Or(vec![ + UserRequestFilter::AttributeEquality( + AttributeName::from("first_name"), + Serialized::from("FirstName"), + ), + UserRequestFilter::AttributeEquality( + AttributeName::from("first_name"), + Serialized::from("firstname"), + ), + ]), false.into(), UserRequestFilter::UserIdSubString(SubStringFilter { initial: Some("iNIt".to_owned()), @@ -1833,7 +1907,7 @@ mod tests { LdapFilter::Present("objectClass".to_string()), LdapFilter::Present("uid".to_string()), LdapFilter::Present("unknown".to_string()), - LdapFilter::Equality("givenname".to_string(), "firstname".to_string()), + LdapFilter::Equality("givenname".to_string(), "FirstName".to_string()), LdapFilter::Equality("unknown_attribute".to_string(), "randomValue".to_string()), LdapFilter::Substring( "uid".to_owned(),