From c4be7f5b6fcbab01a5abdd7199d06c8cda8cb9c3 Mon Sep 17 00:00:00 2001 From: Valentin Tolmer Date: Sun, 7 Jan 2024 13:04:30 +0100 Subject: [PATCH] server: Serialize attribute values when searching This should fix #763 and allow filtering by custom attribute values. --- server/src/domain/deserialize.rs | 50 ++++++++++++ server/src/domain/handler.rs | 4 +- server/src/domain/ldap/group.rs | 22 +++--- server/src/domain/ldap/user.rs | 43 ++++++++--- server/src/domain/ldap/utils.rs | 58 ++++++++++---- server/src/domain/mod.rs | 1 + server/src/domain/sql_user_backend_handler.rs | 6 +- server/src/infra/graphql/mutation.rs | 57 ++------------ server/src/infra/graphql/query.rs | 76 ++++++++++++------- server/src/infra/ldap_handler.rs | 17 ++++- 10 files changed, 216 insertions(+), 118 deletions(-) create mode 100644 server/src/domain/deserialize.rs diff --git a/server/src/domain/deserialize.rs b/server/src/domain/deserialize.rs new file mode 100644 index 0000000..834b0fd --- /dev/null +++ b/server/src/domain/deserialize.rs @@ -0,0 +1,50 @@ +use crate::domain::types::{AttributeType, JpegPhoto, Serialized}; +use anyhow::{bail, Context as AnyhowContext}; + +pub fn deserialize_attribute_value( + value: &[String], + typ: AttributeType, + is_list: bool, +) -> anyhow::Result { + if !is_list && value.len() != 1 { + bail!("Attribute is not a list, but multiple values were provided",); + } + let parse_int = |value: &String| -> anyhow::Result { + value + .parse::() + .with_context(|| format!("Invalid integer value {}", value)) + }; + let parse_date = |value: &String| -> anyhow::Result { + Ok(chrono::DateTime::parse_from_rfc3339(value) + .with_context(|| format!("Invalid date value {}", value))? + .naive_utc()) + }; + let parse_photo = |value: &String| -> anyhow::Result { + JpegPhoto::try_from(value.as_str()).context("Provided image is not a valid JPEG") + }; + Ok(match (typ, is_list) { + (AttributeType::String, false) => Serialized::from(&value[0]), + (AttributeType::String, true) => Serialized::from(&value), + (AttributeType::Integer, false) => Serialized::from(&parse_int(&value[0])?), + (AttributeType::Integer, true) => Serialized::from( + &value + .iter() + .map(parse_int) + .collect::>>()?, + ), + (AttributeType::DateTime, false) => Serialized::from(&parse_date(&value[0])?), + (AttributeType::DateTime, true) => Serialized::from( + &value + .iter() + .map(parse_date) + .collect::>>()?, + ), + (AttributeType::JpegPhoto, false) => Serialized::from(&parse_photo(&value[0])?), + (AttributeType::JpegPhoto, true) => Serialized::from( + &value + .iter() + .map(parse_photo) + .collect::>>()?, + ), + }) +} diff --git a/server/src/domain/handler.rs b/server/src/domain/handler.rs index 8515885..0b4550c 100644 --- a/server/src/domain/handler.rs +++ b/server/src/domain/handler.rs @@ -2,7 +2,7 @@ use crate::domain::{ error::Result, types::{ AttributeName, AttributeType, AttributeValue, Email, Group, GroupDetails, GroupId, - GroupName, JpegPhoto, User, UserAndGroups, UserColumn, UserId, Uuid, + GroupName, JpegPhoto, Serialized, User, UserAndGroups, UserColumn, UserId, Uuid, }, }; use async_trait::async_trait; @@ -54,7 +54,7 @@ pub enum UserRequestFilter { UserId(UserId), UserIdSubString(SubStringFilter), Equality(UserColumn, String), - AttributeEquality(AttributeName, String), + AttributeEquality(AttributeName, Serialized), SubString(UserColumn, SubStringFilter), // Check if a user belongs to a group identified by name. MemberOf(GroupName), diff --git a/server/src/domain/ldap/group.rs b/server/src/domain/ldap/group.rs index 9fe15de..c769e6c 100644 --- a/server/src/domain/ldap/group.rs +++ b/server/src/domain/ldap/group.rs @@ -14,7 +14,7 @@ use super::{ error::LdapResult, utils::{ expand_attribute_wildcards, get_custom_attribute, get_group_id_from_distinguished_name, - get_user_id_from_distinguished_name, map_group_field, LdapInfo, + get_user_id_from_distinguished_name, map_group_field, GroupFieldType, LdapInfo, }, }; @@ -124,8 +124,9 @@ fn make_ldap_search_group_result_entry( fn convert_group_filter( ldap_info: &LdapInfo, filter: &LdapFilter, + schema: &PublicSchema, ) -> LdapResult { - let rec = |f| convert_group_filter(ldap_info, f); + let rec = |f| convert_group_filter(ldap_info, f, schema); match filter { LdapFilter::Equality(field, value) => { let field = AttributeName::from(field.as_str()); @@ -153,9 +154,11 @@ fn convert_group_filter( warn!("Invalid dn filter on group: {}", value); GroupRequestFilter::from(false) })), - _ => match map_group_field(&field) { - Some("display_name") => Ok(GroupRequestFilter::DisplayName(value.into())), - Some("uuid") => Ok(GroupRequestFilter::Uuid( + _ => match map_group_field(&field, schema) { + 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), @@ -187,13 +190,13 @@ fn convert_group_filter( field.as_str() == "objectclass" || field.as_str() == "dn" || field.as_str() == "distinguishedname" - || map_group_field(&field).is_some(), + || !matches!(map_group_field(&field, schema), GroupFieldType::NoMatch), )) } LdapFilter::Substring(field, substring_filter) => { let field = AttributeName::from(field.as_str()); - match map_group_field(&field) { - Some("display_name") => Ok(GroupRequestFilter::DisplayNameSubString( + match map_group_field(&field, schema) { + GroupFieldType::DisplayName => Ok(GroupRequestFilter::DisplayNameSubString( substring_filter.clone().into(), )), _ => Err(LdapError { @@ -218,8 +221,9 @@ pub async fn get_groups_list( ldap_filter: &LdapFilter, base: &str, backend: &Backend, + schema: &PublicSchema, ) -> LdapResult> { - let filters = convert_group_filter(ldap_info, ldap_filter)?; + let filters = convert_group_filter(ldap_info, ldap_filter, schema)?; debug!(?filters); backend .list_groups(Some(filters)) diff --git a/server/src/domain/ldap/user.rs b/server/src/domain/ldap/user.rs index 1ffce50..23b92a1 100644 --- a/server/src/domain/ldap/user.rs +++ b/server/src/domain/ldap/user.rs @@ -5,6 +5,7 @@ use ldap3_proto::{ use tracing::{debug, instrument, warn}; use crate::domain::{ + deserialize::deserialize_attribute_value, handler::{UserListerBackendHandler, UserRequestFilter}, ldap::{ error::{LdapError, LdapResult}, @@ -14,7 +15,7 @@ use crate::domain::{ }, }, schema::{PublicSchema, SchemaUserAttributeExtractor}, - types::{AttributeName, GroupDetails, User, UserAndGroups, UserColumn, UserId}, + types::{AttributeName, AttributeType, GroupDetails, User, UserAndGroups, UserColumn, UserId}, }; pub fn get_user_attribute( @@ -150,8 +151,26 @@ fn make_ldap_search_user_result_entry( } } -fn convert_user_filter(ldap_info: &LdapInfo, filter: &LdapFilter) -> LdapResult { - let rec = |f| convert_user_filter(ldap_info, f); +fn get_user_attribute_equality_filter( + field: &AttributeName, + typ: AttributeType, + is_list: bool, + value: &str, +) -> LdapResult { + 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| UserRequestFilter::AttributeEquality(field.clone(), v)) +} + +fn convert_user_filter( + ldap_info: &LdapInfo, + filter: &LdapFilter, + schema: &PublicSchema, +) -> LdapResult { + let rec = |f| convert_user_filter(ldap_info, f, schema); match filter { LdapFilter::And(filters) => Ok(UserRequestFilter::And( filters.iter().map(rec).collect::>()?, @@ -184,17 +203,16 @@ fn convert_user_filter(ldap_info: &LdapInfo, filter: &LdapFilter) -> LdapResult< warn!("Invalid dn filter on user: {}", value); UserRequestFilter::from(false) })), - _ => match map_user_field(&field) { + _ => 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) => Ok(UserRequestFilter::AttributeEquality( - AttributeName::from(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!( @@ -215,17 +233,17 @@ fn convert_user_filter(ldap_info: &LdapInfo, filter: &LdapFilter) -> LdapResult< field.as_str() == "objectclass" || field.as_str() == "dn" || field.as_str() == "distinguishedname" - || !matches!(map_user_field(&field), UserFieldType::NoMatch), + || !matches!(map_user_field(&field, schema), UserFieldType::NoMatch), )) } LdapFilter::Substring(field, substring_filter) => { let field = AttributeName::from(field.as_str()); - match map_user_field(&field) { + match map_user_field(&field, schema) { UserFieldType::PrimaryField(UserColumn::UserId) => Ok( UserRequestFilter::UserIdSubString(substring_filter.clone().into()), ), UserFieldType::NoMatch - | UserFieldType::Attribute(_) + | UserFieldType::Attribute(_, _, _) | UserFieldType::PrimaryField(UserColumn::CreationDate) | UserFieldType::PrimaryField(UserColumn::Uuid) => Err(LdapError { code: LdapResultCode::UnwillingToPerform, @@ -258,8 +276,9 @@ pub async fn get_user_list( request_groups: bool, base: &str, backend: &Backend, + schema: &PublicSchema, ) -> LdapResult> { - let filters = convert_user_filter(ldap_info, ldap_filter)?; + let filters = convert_user_filter(ldap_info, ldap_filter, schema)?; debug!(?filters); backend .list_users(Some(filters), request_groups) diff --git a/server/src/domain/ldap/utils.rs b/server/src/domain/ldap/utils.rs index f057957..ad61225 100644 --- a/server/src/domain/ldap/utils.rs +++ b/server/src/domain/ldap/utils.rs @@ -159,34 +159,66 @@ pub fn is_subtree(subtree: &[(String, String)], base_tree: &[(String, String)]) pub enum UserFieldType { NoMatch, PrimaryField(UserColumn), - Attribute(&'static str), + Attribute(AttributeName, AttributeType, bool), } -pub fn map_user_field(field: &AttributeName) -> UserFieldType { +pub fn map_user_field(field: &AttributeName, schema: &PublicSchema) -> UserFieldType { match field.as_str() { "uid" | "user_id" | "id" => UserFieldType::PrimaryField(UserColumn::UserId), "mail" | "email" => UserFieldType::PrimaryField(UserColumn::Email), "cn" | "displayname" | "display_name" => { UserFieldType::PrimaryField(UserColumn::DisplayName) } - "givenname" | "first_name" | "firstname" => UserFieldType::Attribute("first_name"), - "sn" | "last_name" | "lastname" => UserFieldType::Attribute("last_name"), - "avatar" | "jpegphoto" => UserFieldType::Attribute("avatar"), + "givenname" | "first_name" | "firstname" => UserFieldType::Attribute( + AttributeName::from("first_name"), + AttributeType::String, + false, + ), + "sn" | "last_name" | "lastname" => UserFieldType::Attribute( + AttributeName::from("last_name"), + AttributeType::String, + false, + ), + "avatar" | "jpegphoto" => UserFieldType::Attribute( + AttributeName::from("avatar"), + AttributeType::JpegPhoto, + false, + ), "creationdate" | "createtimestamp" | "modifytimestamp" | "creation_date" => { UserFieldType::PrimaryField(UserColumn::CreationDate) } "entryuuid" | "uuid" => UserFieldType::PrimaryField(UserColumn::Uuid), - _ => UserFieldType::NoMatch, + _ => schema + .get_schema() + .user_attributes + .get_attribute_type(field) + .map(|(t, is_list)| UserFieldType::Attribute(field.clone(), t, is_list)) + .unwrap_or(UserFieldType::NoMatch), } } -pub fn map_group_field(field: &AttributeName) -> Option<&'static str> { - Some(match field.as_str() { - "cn" | "displayname" | "uid" | "display_name" => "display_name", - "creationdate" | "createtimestamp" | "modifytimestamp" | "creation_date" => "creation_date", - "entryuuid" | "uuid" => "uuid", - _ => return None, - }) +pub enum GroupFieldType { + NoMatch, + DisplayName, + CreationDate, + Uuid, + Attribute(AttributeName, AttributeType, bool), +} + +pub fn map_group_field(field: &AttributeName, schema: &PublicSchema) -> GroupFieldType { + match field.as_str() { + "cn" | "displayname" | "uid" | "display_name" => GroupFieldType::DisplayName, + "creationdate" | "createtimestamp" | "modifytimestamp" | "creation_date" => { + GroupFieldType::CreationDate + } + "entryuuid" | "uuid" => GroupFieldType::Uuid, + _ => schema + .get_schema() + .group_attributes + .get_attribute_type(field) + .map(|(t, is_list)| GroupFieldType::Attribute(field.clone(), t, is_list)) + .unwrap_or(GroupFieldType::NoMatch), + } } pub struct LdapInfo { diff --git a/server/src/domain/mod.rs b/server/src/domain/mod.rs index c1bb524..9475249 100644 --- a/server/src/domain/mod.rs +++ b/server/src/domain/mod.rs @@ -1,3 +1,4 @@ +pub mod deserialize; pub mod error; pub mod handler; pub mod ldap; diff --git a/server/src/domain/sql_user_backend_handler.rs b/server/src/domain/sql_user_backend_handler.rs index 02b3e69..d52418a 100644 --- a/server/src/domain/sql_user_backend_handler.rs +++ b/server/src/domain/sql_user_backend_handler.rs @@ -22,14 +22,14 @@ use sea_orm::{ use std::collections::HashSet; use tracing::instrument; -fn attribute_condition(name: AttributeName, value: String) -> Cond { +fn attribute_condition(name: AttributeName, value: Serialized) -> Cond { Expr::in_subquery( Expr::col(UserColumn::UserId.as_column_ref()), model::UserAttributes::find() .select_only() .column(model::UserAttributesColumn::UserId) .filter(model::UserAttributesColumn::AttributeName.eq(name)) - .filter(model::UserAttributesColumn::Value.eq(Serialized::from(&value))) + .filter(model::UserAttributesColumn::Value.eq(value)) .into_query(), ) .into_condition() @@ -463,7 +463,7 @@ mod tests { &fixture.handler, Some(UserRequestFilter::AttributeEquality( AttributeName::from("first_name"), - "first bob".to_string(), + Serialized::from("first bob"), )), ) .await; diff --git a/server/src/infra/graphql/mutation.rs b/server/src/infra/graphql/mutation.rs index 893e96b..4d88613 100644 --- a/server/src/infra/graphql/mutation.rs +++ b/server/src/infra/graphql/mutation.rs @@ -1,12 +1,13 @@ use crate::{ domain::{ + deserialize::deserialize_attribute_value, handler::{ AttributeList, BackendHandler, CreateAttributeRequest, CreateGroupRequest, CreateUserRequest, UpdateGroupRequest, UpdateUserRequest, }, types::{ AttributeName, AttributeType, AttributeValue as DomainAttributeValue, GroupId, - JpegPhoto, Serialized, UserId, + JpegPhoto, UserId, }, }, infra::{ @@ -535,54 +536,12 @@ fn deserialize_attribute( ) .into()); } - if !attribute_schema.is_list && attribute.value.len() != 1 { - return Err(anyhow!( - "Attribute {} is not a list, but multiple values were provided", - attribute.name - ) - .into()); - } - let parse_int = |value: &String| -> FieldResult { - Ok(value - .parse::() - .with_context(|| format!("Invalid integer value {}", value))?) - }; - let parse_date = |value: &String| -> FieldResult { - Ok(chrono::DateTime::parse_from_rfc3339(value) - .with_context(|| format!("Invalid date value {}", value))? - .naive_utc()) - }; - let parse_photo = |value: &String| -> FieldResult { - Ok(JpegPhoto::try_from(value.as_str()).context("Provided image is not a valid JPEG")?) - }; - let deserialized_values = match (attribute_schema.attribute_type, attribute_schema.is_list) { - (AttributeType::String, false) => Serialized::from(&attribute.value[0]), - (AttributeType::String, true) => Serialized::from(&attribute.value), - (AttributeType::Integer, false) => Serialized::from(&parse_int(&attribute.value[0])?), - (AttributeType::Integer, true) => Serialized::from( - &attribute - .value - .iter() - .map(parse_int) - .collect::>>()?, - ), - (AttributeType::DateTime, false) => Serialized::from(&parse_date(&attribute.value[0])?), - (AttributeType::DateTime, true) => Serialized::from( - &attribute - .value - .iter() - .map(parse_date) - .collect::>>()?, - ), - (AttributeType::JpegPhoto, false) => Serialized::from(&parse_photo(&attribute.value[0])?), - (AttributeType::JpegPhoto, true) => Serialized::from( - &attribute - .value - .iter() - .map(parse_photo) - .collect::>>()?, - ), - }; + let deserialized_values = deserialize_attribute_value( + &attribute.value, + attribute_schema.attribute_type, + attribute_schema.is_list, + ) + .context(format!("While deserializing attribute {}", attribute.name))?; Ok(DomainAttributeValue { name: attribute_name, value: deserialized_values, diff --git a/server/src/infra/graphql/query.rs b/server/src/infra/graphql/query.rs index ce2c5c0..df2da9c 100644 --- a/server/src/infra/graphql/query.rs +++ b/server/src/infra/graphql/query.rs @@ -1,24 +1,25 @@ use crate::{ domain::{ + deserialize::deserialize_attribute_value, handler::{BackendHandler, ReadSchemaBackendHandler}, ldap::utils::{map_user_field, UserFieldType}, + model::UserColumn, schema::{ PublicSchema, SchemaAttributeExtractor, SchemaGroupAttributeExtractor, SchemaUserAttributeExtractor, }, - types::{ - AttributeName, AttributeType, GroupDetails, GroupId, JpegPhoto, UserColumn, UserId, - }, + types::{AttributeType, GroupDetails, GroupId, JpegPhoto, UserId}, }, infra::{ access_control::{ReadonlyBackendHandler, UserReadableBackendHandler}, graphql::api::{field_error_callback, Context}, }, }; +use anyhow::Context as AnyhowContext; use chrono::{NaiveDateTime, TimeZone}; use juniper::{graphql_object, FieldError, FieldResult, GraphQLInputObject}; use serde::{Deserialize, Serialize}; -use tracing::{debug, debug_span, Instrument}; +use tracing::{debug, debug_span, Instrument, Span}; type DomainRequestFilter = crate::domain::handler::UserRequestFilter; type DomainUser = crate::domain::types::User; @@ -40,9 +41,8 @@ pub struct RequestFilter { member_of_id: Option, } -impl TryInto for RequestFilter { - type Error = String; - fn try_into(self) -> Result { +impl RequestFilter { + fn try_into_domain_filter(self, schema: &PublicSchema) -> FieldResult { match ( self.eq, self.any, @@ -52,33 +52,39 @@ impl TryInto for RequestFilter { self.member_of_id, ) { (Some(eq), None, None, None, None, None) => { - match map_user_field(&eq.field.as_str().into()) { - UserFieldType::NoMatch => Err(format!("Unknown request filter: {}", &eq.field)), + match map_user_field(&eq.field.as_str().into(), schema) { + UserFieldType::NoMatch => { + Err(format!("Unknown request filter: {}", &eq.field).into()) + } UserFieldType::PrimaryField(UserColumn::UserId) => { Ok(DomainRequestFilter::UserId(UserId::new(&eq.value))) } UserFieldType::PrimaryField(column) => { Ok(DomainRequestFilter::Equality(column, eq.value)) } - UserFieldType::Attribute(column) => Ok(DomainRequestFilter::AttributeEquality( - AttributeName::from(column), - eq.value, - )), + UserFieldType::Attribute(name, typ, false) => { + let value = deserialize_attribute_value(&[eq.value], typ, false) + .context(format!("While deserializing attribute {}", &name))?; + Ok(DomainRequestFilter::AttributeEquality(name, value)) + } + UserFieldType::Attribute(_, _, true) => { + Err("Equality not supported for list fields".into()) + } } } (None, Some(any), None, None, None, None) => Ok(DomainRequestFilter::Or( any.into_iter() - .map(TryInto::try_into) - .collect::, String>>()?, + .map(|f| f.try_into_domain_filter(schema)) + .collect::>>()?, )), (None, None, Some(all), None, None, None) => Ok(DomainRequestFilter::And( all.into_iter() - .map(TryInto::try_into) - .collect::, String>>()?, + .map(|f| f.try_into_domain_filter(schema)) + .collect::>>()?, )), - (None, None, None, Some(not), None, None) => { - Ok(DomainRequestFilter::Not(Box::new((*not).try_into()?))) - } + (None, None, None, Some(not), None, None) => Ok(DomainRequestFilter::Not(Box::new( + (*not).try_into_domain_filter(schema)?, + ))), (None, None, None, None, Some(group), None) => { Ok(DomainRequestFilter::MemberOf(group.into())) } @@ -86,9 +92,9 @@ impl TryInto for RequestFilter { Ok(DomainRequestFilter::MemberOfId(GroupId(group_id))) } (None, None, None, None, None, None) => { - Err("No field specified in request filter".to_string()) + Err("No field specified in request filter".into()) } - _ => Err("Multiple fields specified in request filter".to_string()), + _ => Err("Multiple fields specified in request filter".into()), } } } @@ -154,8 +160,14 @@ impl Query { &span, "Unauthorized access to user list", ))?; + let schema = self.get_schema(context, span.clone()).await?; Ok(handler - .list_users(filters.map(TryInto::try_into).transpose()?, false) + .list_users( + filters + .map(|f| f.try_into_domain_filter(&schema)) + .transpose()?, + false, + ) .instrument(span) .await .map(|v| v.into_iter().map(Into::into).collect())?) @@ -196,6 +208,16 @@ impl Query { async fn schema(context: &Context) -> FieldResult> { let span = debug_span!("[GraphQL query] get_schema"); + self.get_schema(context, span).await.map(Into::into) + } +} + +impl Query { + async fn get_schema( + &self, + context: &Context, + span: Span, + ) -> FieldResult { let handler = context .handler .get_user_restricted_lister_handler(&context.validation_result); @@ -203,8 +225,7 @@ impl Query { .get_schema() .instrument(span) .await - .map(Into::::into) - .map(Into::into)?) + .map(Into::::into)?) } } @@ -594,7 +615,7 @@ mod tests { use crate::{ domain::{ handler::AttributeList, - types::{AttributeType, Serialized}, + types::{AttributeName, AttributeType, Serialized}, }, infra::{ access_control::{Permission, ValidationResults}, @@ -795,6 +816,7 @@ mod tests { }"#; let mut mock = MockTestBackendHandler::new(); + setup_default_schema(&mut mock); mock.expect_list_users() .with( eq(Some(DomainRequestFilter::Or(vec![ @@ -805,7 +827,7 @@ mod tests { ), DomainRequestFilter::AttributeEquality( AttributeName::from("first_name"), - "robert".to_owned(), + Serialized::from("robert"), ), ]))), eq(false), diff --git a/server/src/infra/ldap_handler.rs b/server/src/infra/ldap_handler.rs index 902c4f7..02b901c 100644 --- a/server/src/infra/ldap_handler.rs +++ b/server/src/infra/ldap_handler.rs @@ -531,6 +531,7 @@ impl LdapHandler LdapResult { let dn_parts = parse_distinguished_name(&request.base.to_ascii_lowercase())?; let scope = get_search_scope(&self.ldap_info.base_dn, &dn_parts, &request.scope); @@ -554,11 +555,19 @@ impl LdapHandler InternalSearchResults::UsersAndGroups( @@ -617,13 +626,15 @@ impl LdapHandler { convert_users_to_ldap_op(users, &request.attrs, &self.ldap_info, &schema) @@ -1686,7 +1697,7 @@ mod tests { false.into(), UserRequestFilter::AttributeEquality( AttributeName::from("first_name"), - "firstname".to_owned(), + Serialized::from("firstname"), ), false.into(), UserRequestFilter::UserIdSubString(SubStringFilter {