server: clean up the attributes, relax the substring filter conditions

This consolidates both user and group attributes in their map_{user,group}_attribute as the only point of parsing. It adds support for custom attribute filters for groups, and makes a SubString filter on an unknown attribute resolve to just false.
This commit is contained in:
Valentin Tolmer
2024-01-17 23:29:19 +01:00
committed by nitnelave
parent 4adb636d53
commit bd0a58b476
7 changed files with 283 additions and 163 deletions
+1
View File
@@ -83,6 +83,7 @@ pub enum GroupRequestFilter {
GroupId(GroupId), GroupId(GroupId),
// Check if the group contains a user identified by uid. // Check if the group contains a user identified by uid.
Member(UserId), Member(UserId),
AttributeEquality(AttributeName, Serialized),
} }
impl From<bool> for GroupRequestFilter { impl From<bool> for GroupRequestFilter {
+96 -70
View File
@@ -1,13 +1,15 @@
use chrono::TimeZone;
use ldap3_proto::{ use ldap3_proto::{
proto::LdapOp, LdapFilter, LdapPartialAttribute, LdapResultCode, LdapSearchResultEntry, proto::LdapOp, LdapFilter, LdapPartialAttribute, LdapResultCode, LdapSearchResultEntry,
}; };
use tracing::{debug, instrument, warn}; use tracing::{debug, instrument, warn};
use crate::domain::{ use crate::domain::{
deserialize::deserialize_attribute_value,
handler::{GroupListerBackendHandler, GroupRequestFilter}, handler::{GroupListerBackendHandler, GroupRequestFilter},
ldap::error::LdapError, ldap::error::LdapError,
schema::{PublicSchema, SchemaGroupAttributeExtractor}, schema::{PublicSchema, SchemaGroupAttributeExtractor},
types::{AttributeName, Group, UserId, Uuid}, types::{AttributeName, AttributeType, Group, UserId, Uuid},
}; };
use super::{ use super::{
@@ -27,47 +29,53 @@ pub fn get_group_attribute(
schema: &PublicSchema, schema: &PublicSchema,
) -> Option<Vec<Vec<u8>>> { ) -> Option<Vec<Vec<u8>>> {
let attribute = AttributeName::from(attribute); let attribute = AttributeName::from(attribute);
let attribute_values = match attribute.as_str() { let attribute_values = match map_group_field(&attribute, schema) {
"objectclass" => vec![b"groupOfUniqueNames".to_vec()], GroupFieldType::ObjectClass => vec![b"groupOfUniqueNames".to_vec()],
// Always returned as part of the base response. // Always returned as part of the base response.
"dn" | "distinguishedname" => return None, GroupFieldType::Dn => return None,
"entrydn" => { GroupFieldType::EntryDn => {
vec![format!("uid={},ou=groups,{}", group.display_name, base_dn_str).into_bytes()] vec![format!("uid={},ou=groups,{}", group.display_name, base_dn_str).into_bytes()]
} }
"cn" | "uid" | "id" => vec![group.display_name.to_string().into_bytes()], GroupFieldType::DisplayName => vec![group.display_name.to_string().into_bytes()],
"entryuuid" | "uuid" => vec![group.uuid.to_string().into_bytes()], GroupFieldType::CreationDate => vec![chrono::Utc
"member" | "uniquemember" => group .from_utc_datetime(&group.creation_date)
.to_rfc3339()
.into_bytes()],
GroupFieldType::Member => group
.users .users
.iter() .iter()
.filter(|u| user_filter.as_ref().map(|f| *u == f).unwrap_or(true)) .filter(|u| user_filter.as_ref().map(|f| *u == f).unwrap_or(true))
.map(|u| format!("uid={},ou=people,{}", u, base_dn_str).into_bytes()) .map(|u| format!("uid={},ou=people,{}", u, base_dn_str).into_bytes())
.collect(), .collect(),
"1.1" => return None, GroupFieldType::Uuid => vec![group.uuid.to_string().into_bytes()],
// We ignore the operational attribute wildcard GroupFieldType::Attribute(attr, _, _) => {
"+" => return None, get_custom_attribute::<SchemaGroupAttributeExtractor>(&group.attributes, &attr, schema)?
"*" => {
panic!(
"Matched {}, * should have been expanded into attribute list and * removed",
attribute
)
} }
_ => { GroupFieldType::NoMatch => match attribute.as_str() {
if !ignored_group_attributes.contains(&attribute) { "1.1" => return None,
match get_custom_attribute::<SchemaGroupAttributeExtractor>( // We ignore the operational attribute wildcard
&group.attributes, "+" => return None,
&attribute, "*" => {
schema, panic!(
) { "Matched {}, * should have been expanded into attribute list and * removed",
Some(v) => return Some(v), attribute
None => warn!( )
r#"Ignoring unrecognized group attribute: {}\n\
To disable this warning, add it to "ignored_group_attributes" in the config."#,
attribute
),
};
} }
return None; _ => {
} if ignored_group_attributes.contains(&attribute) {
return None;
}
get_custom_attribute::<SchemaGroupAttributeExtractor>(
&group.attributes,
&attribute,
schema,
).or_else(||{warn!(
r#"Ignoring unrecognized group attribute: {}\n\
To disable this warning, add it to "ignored_group_attributes" in the config."#,
attribute
);None})?
}
},
}; };
if attribute_values.len() == 1 && attribute_values[0].is_empty() { if attribute_values.len() == 1 && attribute_values[0].is_empty() {
None None
@@ -121,6 +129,20 @@ fn make_ldap_search_group_result_entry(
} }
} }
fn get_group_attribute_equality_filter(
field: &AttributeName,
typ: AttributeType,
is_list: bool,
value: &str,
) -> LdapResult<GroupRequestFilter> {
deserialize_attribute_value(&[value.to_owned()], typ, is_list)
.map_err(|e| LdapError {
code: LdapResultCode::Other,
message: format!("Invalid value for attribute {}: {}", field, e),
})
.map(|v| GroupRequestFilter::AttributeEquality(field.clone(), v))
}
fn convert_group_filter( fn convert_group_filter(
ldap_info: &LdapInfo, ldap_info: &LdapInfo,
filter: &LdapFilter, filter: &LdapFilter,
@@ -131,8 +153,15 @@ fn convert_group_filter(
LdapFilter::Equality(field, value) => { LdapFilter::Equality(field, value) => {
let field = AttributeName::from(field.as_str()); let field = AttributeName::from(field.as_str());
let value = value.to_ascii_lowercase(); let value = value.to_ascii_lowercase();
match field.as_str() { match map_group_field(&field, schema) {
"member" | "uniquemember" => { GroupFieldType::DisplayName => Ok(GroupRequestFilter::DisplayName(value.into())),
GroupFieldType::Uuid => Ok(GroupRequestFilter::Uuid(
Uuid::try_from(value.as_str()).map_err(|e| LdapError {
code: LdapResultCode::InappropriateMatching,
message: format!("Invalid UUID: {:#}", e),
})?,
)),
GroupFieldType::Member => {
let user_name = get_user_id_from_distinguished_name( let user_name = get_user_id_from_distinguished_name(
&value, &value,
&ldap_info.base_dn, &ldap_info.base_dn,
@@ -140,41 +169,39 @@ fn convert_group_filter(
)?; )?;
Ok(GroupRequestFilter::Member(user_name)) Ok(GroupRequestFilter::Member(user_name))
} }
"objectclass" => Ok(GroupRequestFilter::from(matches!( GroupFieldType::ObjectClass => Ok(GroupRequestFilter::from(matches!(
value.as_str(), value.as_str(),
"groupofuniquenames" | "groupofnames" "groupofuniquenames" | "groupofnames"
))), ))),
"dn" => Ok(get_group_id_from_distinguished_name( GroupFieldType::Dn | GroupFieldType::EntryDn => {
value.to_ascii_lowercase().as_str(), Ok(get_group_id_from_distinguished_name(
&ldap_info.base_dn, value.as_str(),
&ldap_info.base_dn_str, &ldap_info.base_dn,
) &ldap_info.base_dn_str,
.map(GroupRequestFilter::DisplayName) )
.unwrap_or_else(|_| { .map(GroupRequestFilter::DisplayName)
warn!("Invalid dn filter on group: {}", value); .unwrap_or_else(|_| {
GroupRequestFilter::from(false) warn!("Invalid dn filter on group: {}", value);
})), GroupRequestFilter::from(false)
_ => match map_group_field(&field, schema) { }))
GroupFieldType::DisplayName => { }
Ok(GroupRequestFilter::DisplayName(value.into())) GroupFieldType::NoMatch => {
} if !ldap_info.ignored_group_attributes.contains(&field) {
GroupFieldType::Uuid => Ok(GroupRequestFilter::Uuid( warn!(
Uuid::try_from(value.as_str()).map_err(|e| LdapError { r#"Ignoring unknown group attribute "{}" in filter.\n\
code: LdapResultCode::InappropriateMatching,
message: format!("Invalid UUID: {:#}", e),
})?,
)),
_ => {
if !ldap_info.ignored_group_attributes.contains(&field) {
warn!(
r#"Ignoring unknown group attribute "{}" in filter.\n\
To disable this warning, add it to "ignored_group_attributes" in the config."#, To disable this warning, add it to "ignored_group_attributes" in the config."#,
field field
); );
}
Ok(GroupRequestFilter::from(false))
} }
}, Ok(GroupRequestFilter::from(false))
}
GroupFieldType::Attribute(field, typ, is_list) => {
get_group_attribute_equality_filter(&field, typ, is_list, &value)
}
GroupFieldType::CreationDate => Err(LdapError {
code: LdapResultCode::UnwillingToPerform,
message: "Creation date filter for groups not supported".to_owned(),
}),
} }
} }
LdapFilter::And(filters) => Ok(GroupRequestFilter::And( LdapFilter::And(filters) => Ok(GroupRequestFilter::And(
@@ -186,12 +213,10 @@ fn convert_group_filter(
LdapFilter::Not(filter) => Ok(GroupRequestFilter::Not(Box::new(rec(filter)?))), LdapFilter::Not(filter) => Ok(GroupRequestFilter::Not(Box::new(rec(filter)?))),
LdapFilter::Present(field) => { LdapFilter::Present(field) => {
let field = AttributeName::from(field.as_str()); let field = AttributeName::from(field.as_str());
Ok(GroupRequestFilter::from( Ok(GroupRequestFilter::from(!matches!(
field.as_str() == "objectclass" map_group_field(&field, schema),
|| field.as_str() == "dn" GroupFieldType::NoMatch
|| field.as_str() == "distinguishedname" )))
|| !matches!(map_group_field(&field, schema), GroupFieldType::NoMatch),
))
} }
LdapFilter::Substring(field, substring_filter) => { LdapFilter::Substring(field, substring_filter) => {
let field = AttributeName::from(field.as_str()); let field = AttributeName::from(field.as_str());
@@ -199,6 +224,7 @@ fn convert_group_filter(
GroupFieldType::DisplayName => Ok(GroupRequestFilter::DisplayNameSubString( GroupFieldType::DisplayName => Ok(GroupRequestFilter::DisplayNameSubString(
substring_filter.clone().into(), substring_filter.clone().into(),
)), )),
GroupFieldType::NoMatch => Ok(GroupRequestFilter::from(false)),
_ => Err(LdapError { _ => Err(LdapError {
code: LdapResultCode::UnwillingToPerform, code: LdapResultCode::UnwillingToPerform,
message: format!( message: format!(
+90 -90
View File
@@ -27,78 +27,75 @@ pub fn get_user_attribute(
schema: &PublicSchema, schema: &PublicSchema,
) -> Option<Vec<Vec<u8>>> { ) -> Option<Vec<Vec<u8>>> {
let attribute = AttributeName::from(attribute); let attribute = AttributeName::from(attribute);
let attribute_values = match attribute.as_str() { let attribute_values = match map_user_field(&attribute, schema) {
"objectclass" => vec![ UserFieldType::ObjectClass => vec![
b"inetOrgPerson".to_vec(), b"inetOrgPerson".to_vec(),
b"posixAccount".to_vec(), b"posixAccount".to_vec(),
b"mailAccount".to_vec(), b"mailAccount".to_vec(),
b"person".to_vec(), b"person".to_vec(),
], ],
// dn is always returned as part of the base response. // dn is always returned as part of the base response.
"dn" | "distinguishedname" => return None, UserFieldType::Dn => return None,
"entrydn" => { UserFieldType::EntryDn => {
vec![format!("uid={},ou=people,{}", &user.user_id, base_dn_str).into_bytes()] vec![format!("uid={},ou=people,{}", &user.user_id, base_dn_str).into_bytes()]
} }
"uid" | "user_id" | "id" => vec![user.user_id.to_string().into_bytes()], UserFieldType::MemberOf => groups
"entryuuid" | "uuid" => vec![user.uuid.to_string().into_bytes()],
"mail" | "email" => vec![user.email.to_string().into_bytes()],
"givenname" | "first_name" | "firstname" => {
get_custom_attribute::<SchemaUserAttributeExtractor>(
&user.attributes,
&"first_name".into(),
schema,
)?
}
"sn" | "last_name" | "lastname" => get_custom_attribute::<SchemaUserAttributeExtractor>(
&user.attributes,
&"last_name".into(),
schema,
)?,
"jpegphoto" | "avatar" => get_custom_attribute::<SchemaUserAttributeExtractor>(
&user.attributes,
&"avatar".into(),
schema,
)?,
"memberof" => groups
.into_iter() .into_iter()
.flatten() .flatten()
.map(|id_and_name| { .map(|id_and_name| {
format!("cn={},ou=groups,{}", &id_and_name.display_name, base_dn_str).into_bytes() format!("cn={},ou=groups,{}", &id_and_name.display_name, base_dn_str).into_bytes()
}) })
.collect(), .collect(),
"cn" | "displayname" => vec![user.display_name.clone()?.into_bytes()], UserFieldType::PrimaryField(UserColumn::UserId) => {
"creationdate" | "creation_date" | "createtimestamp" | "modifytimestamp" => { vec![user.user_id.to_string().into_bytes()]
vec![chrono::Utc
.from_utc_datetime(&user.creation_date)
.to_rfc3339()
.into_bytes()]
} }
"1.1" => return None, UserFieldType::PrimaryField(UserColumn::Email) => vec![user.email.to_string().into_bytes()],
// We ignore the operational attribute wildcard. UserFieldType::PrimaryField(
"+" => return None, UserColumn::LowercaseEmail
"*" => { | UserColumn::PasswordHash
panic!( | UserColumn::TotpSecret
"Matched {}, * should have been expanded into attribute list and * removed", | UserColumn::MfaType,
attribute ) => panic!("Should not get here"),
) UserFieldType::PrimaryField(UserColumn::Uuid) => vec![user.uuid.to_string().into_bytes()],
UserFieldType::PrimaryField(UserColumn::DisplayName) => {
vec![user.display_name.clone()?.into_bytes()]
} }
attr => { UserFieldType::PrimaryField(UserColumn::CreationDate) => vec![chrono::Utc
if !ignored_user_attributes.contains(&attribute) { .from_utc_datetime(&user.creation_date)
match get_custom_attribute::<SchemaUserAttributeExtractor>( .to_rfc3339()
.into_bytes()],
UserFieldType::Attribute(attr, _, _) => {
get_custom_attribute::<SchemaUserAttributeExtractor>(&user.attributes, &attr, schema)?
}
UserFieldType::NoMatch => match attribute.as_str() {
"1.1" => return None,
// We ignore the operational attribute wildcard.
"+" => return None,
"*" => {
panic!(
"Matched {}, * should have been expanded into attribute list and * removed",
attribute
)
}
_ => {
if ignored_user_attributes.contains(&attribute) {
return None;
}
get_custom_attribute::<SchemaUserAttributeExtractor>(
&user.attributes, &user.attributes,
&attribute, &attribute,
schema, schema,
) { )
Some(v) => return Some(v), .or_else(|| {
None => warn!( warn!(
r#"Ignoring unrecognized group attribute: {}\n\ r#"Ignoring unrecognized group attribute: {}\n\
To disable this warning, add it to "ignored_user_attributes" in the config."#, To disable this warning, add it to "ignored_user_attributes" in the config."#,
attr attribute
), );
}; None
})?
} }
return None; },
}
}; };
if attribute_values.len() == 1 && attribute_values[0].is_empty() { if attribute_values.len() == 1 && attribute_values[0].is_empty() {
None None
@@ -181,49 +178,48 @@ fn convert_user_filter(
LdapFilter::Not(filter) => Ok(UserRequestFilter::Not(Box::new(rec(filter)?))), LdapFilter::Not(filter) => Ok(UserRequestFilter::Not(Box::new(rec(filter)?))),
LdapFilter::Equality(field, value) => { LdapFilter::Equality(field, value) => {
let field = AttributeName::from(field.as_str()); let field = AttributeName::from(field.as_str());
match field.as_str() { let value = value.to_ascii_lowercase();
"memberof" => Ok(UserRequestFilter::MemberOf( match map_user_field(&field, schema) {
UserFieldType::PrimaryField(UserColumn::UserId) => {
Ok(UserRequestFilter::UserId(UserId::new(&value)))
}
UserFieldType::PrimaryField(field) => Ok(UserRequestFilter::Equality(field, value)),
UserFieldType::Attribute(field, typ, is_list) => {
get_user_attribute_equality_filter(&field, typ, is_list, &value)
}
UserFieldType::NoMatch => {
if !ldap_info.ignored_user_attributes.contains(&field) {
warn!(
r#"Ignoring unknown user attribute "{}" in filter.\n\
To disable this warning, add it to "ignored_user_attributes" in the config"#,
field
);
}
Ok(UserRequestFilter::from(false))
}
UserFieldType::ObjectClass => Ok(UserRequestFilter::from(matches!(
value.as_str(),
"person" | "inetorgperson" | "posixaccount" | "mailaccount"
))),
UserFieldType::MemberOf => Ok(UserRequestFilter::MemberOf(
get_group_id_from_distinguished_name( get_group_id_from_distinguished_name(
&value.to_ascii_lowercase(), &value,
&ldap_info.base_dn, &ldap_info.base_dn,
&ldap_info.base_dn_str, &ldap_info.base_dn_str,
)?, )?,
)), )),
"objectclass" => Ok(UserRequestFilter::from(matches!( UserFieldType::EntryDn | UserFieldType::Dn => {
value.to_ascii_lowercase().as_str(), Ok(get_user_id_from_distinguished_name(
"person" | "inetorgperson" | "posixaccount" | "mailaccount" value.as_str(),
))), &ldap_info.base_dn,
"dn" => Ok(get_user_id_from_distinguished_name( &ldap_info.base_dn_str,
value.to_ascii_lowercase().as_str(), )
&ldap_info.base_dn, .map(UserRequestFilter::UserId)
&ldap_info.base_dn_str, .unwrap_or_else(|_| {
) warn!("Invalid dn filter on user: {}", value);
.map(UserRequestFilter::UserId) UserRequestFilter::from(false)
.unwrap_or_else(|_| { }))
warn!("Invalid dn filter on user: {}", value); }
UserRequestFilter::from(false)
})),
_ => match map_user_field(&field, schema) {
UserFieldType::PrimaryField(UserColumn::UserId) => {
Ok(UserRequestFilter::UserId(UserId::new(value)))
}
UserFieldType::PrimaryField(field) => {
Ok(UserRequestFilter::Equality(field, value.clone()))
}
UserFieldType::Attribute(field, typ, is_list) => {
get_user_attribute_equality_filter(&field, typ, is_list, value)
}
UserFieldType::NoMatch => {
if !ldap_info.ignored_user_attributes.contains(&field) {
warn!(
r#"Ignoring unknown user attribute "{}" in filter.\n\
To disable this warning, add it to "ignored_user_attributes" in the config"#,
field
);
}
Ok(UserRequestFilter::from(false))
}
},
} }
} }
LdapFilter::Present(field) => { LdapFilter::Present(field) => {
@@ -242,8 +238,11 @@ fn convert_user_filter(
UserFieldType::PrimaryField(UserColumn::UserId) => Ok( UserFieldType::PrimaryField(UserColumn::UserId) => Ok(
UserRequestFilter::UserIdSubString(substring_filter.clone().into()), UserRequestFilter::UserIdSubString(substring_filter.clone().into()),
), ),
UserFieldType::NoMatch UserFieldType::Attribute(_, _, _)
| UserFieldType::Attribute(_, _, _) | UserFieldType::ObjectClass
| UserFieldType::MemberOf
| UserFieldType::Dn
| UserFieldType::EntryDn
| UserFieldType::PrimaryField(UserColumn::CreationDate) | UserFieldType::PrimaryField(UserColumn::CreationDate)
| UserFieldType::PrimaryField(UserColumn::Uuid) => Err(LdapError { | UserFieldType::PrimaryField(UserColumn::Uuid) => Err(LdapError {
code: LdapResultCode::UnwillingToPerform, code: LdapResultCode::UnwillingToPerform,
@@ -252,6 +251,7 @@ fn convert_user_filter(
field field
), ),
}), }),
UserFieldType::NoMatch => Ok(UserRequestFilter::from(false)),
UserFieldType::PrimaryField(field) => Ok(UserRequestFilter::SubString( UserFieldType::PrimaryField(field) => Ok(UserRequestFilter::SubString(
field, field,
substring_filter.clone().into(), substring_filter.clone().into(),
+18 -1
View File
@@ -158,12 +158,20 @@ pub fn is_subtree(subtree: &[(String, String)], base_tree: &[(String, String)])
pub enum UserFieldType { pub enum UserFieldType {
NoMatch, NoMatch,
ObjectClass,
MemberOf,
Dn,
EntryDn,
PrimaryField(UserColumn), PrimaryField(UserColumn),
Attribute(AttributeName, AttributeType, bool), Attribute(AttributeName, AttributeType, bool),
} }
pub fn map_user_field(field: &AttributeName, schema: &PublicSchema) -> UserFieldType { pub fn map_user_field(field: &AttributeName, schema: &PublicSchema) -> UserFieldType {
match field.as_str() { match field.as_str() {
"memberof" | "ismemberof" => UserFieldType::MemberOf,
"objectclass" => UserFieldType::ObjectClass,
"dn" | "distinguishedname" => UserFieldType::Dn,
"entrydn" => UserFieldType::EntryDn,
"uid" | "user_id" | "id" => UserFieldType::PrimaryField(UserColumn::UserId), "uid" | "user_id" | "id" => UserFieldType::PrimaryField(UserColumn::UserId),
"mail" | "email" => UserFieldType::PrimaryField(UserColumn::Email), "mail" | "email" => UserFieldType::PrimaryField(UserColumn::Email),
"cn" | "displayname" | "display_name" => { "cn" | "displayname" | "display_name" => {
@@ -201,16 +209,25 @@ pub enum GroupFieldType {
NoMatch, NoMatch,
DisplayName, DisplayName,
CreationDate, CreationDate,
ObjectClass,
Dn,
// Like Dn, but returned as part of the attributes.
EntryDn,
Member,
Uuid, Uuid,
Attribute(AttributeName, AttributeType, bool), Attribute(AttributeName, AttributeType, bool),
} }
pub fn map_group_field(field: &AttributeName, schema: &PublicSchema) -> GroupFieldType { pub fn map_group_field(field: &AttributeName, schema: &PublicSchema) -> GroupFieldType {
match field.as_str() { match field.as_str() {
"cn" | "displayname" | "uid" | "display_name" => GroupFieldType::DisplayName, "dn" | "distinguishedname" => GroupFieldType::Dn,
"entrydn" => GroupFieldType::EntryDn,
"objectclass" => GroupFieldType::ObjectClass,
"cn" | "displayname" | "uid" | "display_name" | "id" => GroupFieldType::DisplayName,
"creationdate" | "createtimestamp" | "modifytimestamp" | "creation_date" => { "creationdate" | "createtimestamp" | "modifytimestamp" | "creation_date" => {
GroupFieldType::CreationDate GroupFieldType::CreationDate
} }
"member" | "uniquemember" => GroupFieldType::Member,
"entryuuid" | "uuid" => GroupFieldType::Uuid, "entryuuid" | "uuid" => GroupFieldType::Uuid,
_ => schema _ => schema
.get_schema() .get_schema()
+55 -1
View File
@@ -6,7 +6,7 @@ use crate::domain::{
}, },
model::{self, GroupColumn, MembershipColumn}, model::{self, GroupColumn, MembershipColumn},
sql_backend_handler::SqlBackendHandler, sql_backend_handler::SqlBackendHandler,
types::{AttributeValue, Group, GroupDetails, GroupId, Uuid}, types::{AttributeName, AttributeValue, Group, GroupDetails, GroupId, Serialized, Uuid},
}; };
use async_trait::async_trait; use async_trait::async_trait;
use sea_orm::{ use sea_orm::{
@@ -16,6 +16,19 @@ use sea_orm::{
}; };
use tracing::instrument; use tracing::instrument;
fn attribute_condition(name: AttributeName, value: Serialized) -> Cond {
Expr::in_subquery(
Expr::col(GroupColumn::GroupId.as_column_ref()),
model::GroupAttributes::find()
.select_only()
.column(model::GroupAttributesColumn::GroupId)
.filter(model::GroupAttributesColumn::AttributeName.eq(name))
.filter(model::GroupAttributesColumn::Value.eq(value))
.into_query(),
)
.into_condition()
}
fn get_group_filter_expr(filter: GroupRequestFilter) -> Cond { fn get_group_filter_expr(filter: GroupRequestFilter) -> Cond {
use GroupRequestFilter::*; use GroupRequestFilter::*;
let group_table = Alias::new("groups"); let group_table = Alias::new("groups");
@@ -58,6 +71,7 @@ fn get_group_filter_expr(filter: GroupRequestFilter) -> Cond {
)))) ))))
.like(filter.to_sql_filter()) .like(filter.to_sql_filter())
.into_condition(), .into_condition(),
AttributeEquality(name, value) => attribute_condition(name, value),
} }
} }
@@ -405,6 +419,46 @@ mod tests {
); );
} }
#[tokio::test]
async fn test_list_groups_other_filter() {
let fixture = TestFixture::new().await;
fixture
.handler
.add_group_attribute(CreateAttributeRequest {
name: "gid".into(),
attribute_type: AttributeType::Integer,
is_list: false,
is_visible: true,
is_editable: true,
})
.await
.unwrap();
fixture
.handler
.update_group(UpdateGroupRequest {
group_id: fixture.groups[0],
display_name: None,
delete_attributes: Vec::new(),
insert_attributes: vec![AttributeValue {
name: "gid".into(),
value: Serialized::from(&512),
}],
})
.await
.unwrap();
assert_eq!(
get_group_ids(
&fixture.handler,
Some(GroupRequestFilter::AttributeEquality(
AttributeName::from("gid"),
Serialized::from(&512),
)),
)
.await,
vec![fixture.groups[0]]
);
}
#[tokio::test] #[tokio::test]
async fn test_get_group_details() { async fn test_get_group_details() {
let fixture = TestFixture::new().await; let fixture = TestFixture::new().await;
+4
View File
@@ -70,6 +70,10 @@ impl RequestFilter {
UserFieldType::Attribute(_, _, true) => { UserFieldType::Attribute(_, _, true) => {
Err("Equality not supported for list fields".into()) Err("Equality not supported for list fields".into())
} }
UserFieldType::MemberOf => Ok(DomainRequestFilter::MemberOf(eq.value.into())),
UserFieldType::ObjectClass | UserFieldType::Dn | UserFieldType::EntryDn => {
Err("Ldap fields not supported in request filter".into())
}
} }
} }
(None, Some(any), None, None, None, None) => Ok(DomainRequestFilter::Or( (None, Some(any), None, None, None, None) => Ok(DomainRequestFilter::Or(
+19 -1
View File
@@ -1613,7 +1613,7 @@ mod tests {
#[tokio::test] #[tokio::test]
async fn test_search_groups_unsupported_substring() { async fn test_search_groups_unsupported_substring() {
let mut ldap_handler = setup_bound_admin_handler(MockTestBackendHandler::new()).await; let mut ldap_handler = setup_bound_readonly_handler(MockTestBackendHandler::new()).await;
let request = make_group_search_request( let request = make_group_search_request(
LdapFilter::Substring("member".to_owned(), LdapSubstringFilter::default()), LdapFilter::Substring("member".to_owned(), LdapSubstringFilter::default()),
vec!["cn"], vec!["cn"],
@@ -1627,6 +1627,24 @@ mod tests {
); );
} }
#[tokio::test]
async fn test_search_groups_missing_attribute_substring() {
let request = make_group_search_request(
LdapFilter::Substring("nonexistent".to_owned(), LdapSubstringFilter::default()),
vec!["cn"],
);
let mut mock = MockTestBackendHandler::new();
mock.expect_list_groups()
.with(eq(Some(false.into())))
.times(1)
.return_once(|_| Ok(vec![]));
let mut ldap_handler = setup_bound_readonly_handler(mock).await;
assert_eq!(
ldap_handler.do_search_or_dse(&request).await,
Ok(vec![make_search_success()]),
);
}
#[tokio::test] #[tokio::test]
async fn test_search_groups_error() { async fn test_search_groups_error() {
let mut mock = MockTestBackendHandler::new(); let mut mock = MockTestBackendHandler::new();