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.
This commit is contained in:
Simon Broeng Jensen
2025-01-22 10:37:04 +01:00
committed by GitHub
parent f5fbb31e6e
commit 0799b6bc26
3 changed files with 132 additions and 34 deletions

View File

@@ -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::<i32>()
.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,

View File

@@ -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)
}))
}

View File

@@ -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(),