server: Serialize attribute values when searching

This should fix #763 and allow filtering by custom attribute values.
This commit is contained in:
Valentin Tolmer
2024-01-07 13:04:30 +01:00
committed by nitnelave
parent 337101edea
commit c4be7f5b6f
10 changed files with 216 additions and 118 deletions
+50
View File
@@ -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<Serialized> {
if !is_list && value.len() != 1 {
bail!("Attribute is not a list, but multiple values were provided",);
}
let parse_int = |value: &String| -> anyhow::Result<i64> {
value
.parse::<i64>()
.with_context(|| format!("Invalid integer value {}", value))
};
let parse_date = |value: &String| -> anyhow::Result<chrono::NaiveDateTime> {
Ok(chrono::DateTime::parse_from_rfc3339(value)
.with_context(|| format!("Invalid date value {}", value))?
.naive_utc())
};
let parse_photo = |value: &String| -> anyhow::Result<JpegPhoto> {
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::<anyhow::Result<Vec<_>>>()?,
),
(AttributeType::DateTime, false) => Serialized::from(&parse_date(&value[0])?),
(AttributeType::DateTime, true) => Serialized::from(
&value
.iter()
.map(parse_date)
.collect::<anyhow::Result<Vec<_>>>()?,
),
(AttributeType::JpegPhoto, false) => Serialized::from(&parse_photo(&value[0])?),
(AttributeType::JpegPhoto, true) => Serialized::from(
&value
.iter()
.map(parse_photo)
.collect::<anyhow::Result<Vec<_>>>()?,
),
})
}
+2 -2
View File
@@ -2,7 +2,7 @@ use crate::domain::{
error::Result, error::Result,
types::{ types::{
AttributeName, AttributeType, AttributeValue, Email, Group, GroupDetails, GroupId, 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; use async_trait::async_trait;
@@ -54,7 +54,7 @@ pub enum UserRequestFilter {
UserId(UserId), UserId(UserId),
UserIdSubString(SubStringFilter), UserIdSubString(SubStringFilter),
Equality(UserColumn, String), Equality(UserColumn, String),
AttributeEquality(AttributeName, String), AttributeEquality(AttributeName, Serialized),
SubString(UserColumn, SubStringFilter), SubString(UserColumn, SubStringFilter),
// Check if a user belongs to a group identified by name. // Check if a user belongs to a group identified by name.
MemberOf(GroupName), MemberOf(GroupName),
+13 -9
View File
@@ -14,7 +14,7 @@ use super::{
error::LdapResult, error::LdapResult,
utils::{ utils::{
expand_attribute_wildcards, get_custom_attribute, get_group_id_from_distinguished_name, 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( fn convert_group_filter(
ldap_info: &LdapInfo, ldap_info: &LdapInfo,
filter: &LdapFilter, filter: &LdapFilter,
schema: &PublicSchema,
) -> LdapResult<GroupRequestFilter> { ) -> LdapResult<GroupRequestFilter> {
let rec = |f| convert_group_filter(ldap_info, f); let rec = |f| convert_group_filter(ldap_info, f, schema);
match filter { match filter {
LdapFilter::Equality(field, value) => { LdapFilter::Equality(field, value) => {
let field = AttributeName::from(field.as_str()); let field = AttributeName::from(field.as_str());
@@ -153,9 +154,11 @@ fn convert_group_filter(
warn!("Invalid dn filter on group: {}", value); warn!("Invalid dn filter on group: {}", value);
GroupRequestFilter::from(false) GroupRequestFilter::from(false)
})), })),
_ => match map_group_field(&field) { _ => match map_group_field(&field, schema) {
Some("display_name") => Ok(GroupRequestFilter::DisplayName(value.into())), GroupFieldType::DisplayName => {
Some("uuid") => Ok(GroupRequestFilter::Uuid( Ok(GroupRequestFilter::DisplayName(value.into()))
}
GroupFieldType::Uuid => Ok(GroupRequestFilter::Uuid(
Uuid::try_from(value.as_str()).map_err(|e| LdapError { Uuid::try_from(value.as_str()).map_err(|e| LdapError {
code: LdapResultCode::InappropriateMatching, code: LdapResultCode::InappropriateMatching,
message: format!("Invalid UUID: {:#}", e), message: format!("Invalid UUID: {:#}", e),
@@ -187,13 +190,13 @@ fn convert_group_filter(
field.as_str() == "objectclass" field.as_str() == "objectclass"
|| field.as_str() == "dn" || field.as_str() == "dn"
|| field.as_str() == "distinguishedname" || field.as_str() == "distinguishedname"
|| map_group_field(&field).is_some(), || !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());
match map_group_field(&field) { match map_group_field(&field, schema) {
Some("display_name") => Ok(GroupRequestFilter::DisplayNameSubString( GroupFieldType::DisplayName => Ok(GroupRequestFilter::DisplayNameSubString(
substring_filter.clone().into(), substring_filter.clone().into(),
)), )),
_ => Err(LdapError { _ => Err(LdapError {
@@ -218,8 +221,9 @@ pub async fn get_groups_list<Backend: GroupListerBackendHandler>(
ldap_filter: &LdapFilter, ldap_filter: &LdapFilter,
base: &str, base: &str,
backend: &Backend, backend: &Backend,
schema: &PublicSchema,
) -> LdapResult<Vec<Group>> { ) -> LdapResult<Vec<Group>> {
let filters = convert_group_filter(ldap_info, ldap_filter)?; let filters = convert_group_filter(ldap_info, ldap_filter, schema)?;
debug!(?filters); debug!(?filters);
backend backend
.list_groups(Some(filters)) .list_groups(Some(filters))
+31 -12
View File
@@ -5,6 +5,7 @@ use ldap3_proto::{
use tracing::{debug, instrument, warn}; use tracing::{debug, instrument, warn};
use crate::domain::{ use crate::domain::{
deserialize::deserialize_attribute_value,
handler::{UserListerBackendHandler, UserRequestFilter}, handler::{UserListerBackendHandler, UserRequestFilter},
ldap::{ ldap::{
error::{LdapError, LdapResult}, error::{LdapError, LdapResult},
@@ -14,7 +15,7 @@ use crate::domain::{
}, },
}, },
schema::{PublicSchema, SchemaUserAttributeExtractor}, schema::{PublicSchema, SchemaUserAttributeExtractor},
types::{AttributeName, GroupDetails, User, UserAndGroups, UserColumn, UserId}, types::{AttributeName, AttributeType, GroupDetails, User, UserAndGroups, UserColumn, UserId},
}; };
pub fn get_user_attribute( 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<UserRequestFilter> { fn get_user_attribute_equality_filter(
let rec = |f| convert_user_filter(ldap_info, f); field: &AttributeName,
typ: AttributeType,
is_list: bool,
value: &str,
) -> LdapResult<UserRequestFilter> {
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<UserRequestFilter> {
let rec = |f| convert_user_filter(ldap_info, f, schema);
match filter { match filter {
LdapFilter::And(filters) => Ok(UserRequestFilter::And( LdapFilter::And(filters) => Ok(UserRequestFilter::And(
filters.iter().map(rec).collect::<LdapResult<_>>()?, filters.iter().map(rec).collect::<LdapResult<_>>()?,
@@ -184,17 +203,16 @@ fn convert_user_filter(ldap_info: &LdapInfo, filter: &LdapFilter) -> LdapResult<
warn!("Invalid dn filter on user: {}", value); warn!("Invalid dn filter on user: {}", value);
UserRequestFilter::from(false) UserRequestFilter::from(false)
})), })),
_ => match map_user_field(&field) { _ => match map_user_field(&field, schema) {
UserFieldType::PrimaryField(UserColumn::UserId) => { UserFieldType::PrimaryField(UserColumn::UserId) => {
Ok(UserRequestFilter::UserId(UserId::new(value))) Ok(UserRequestFilter::UserId(UserId::new(value)))
} }
UserFieldType::PrimaryField(field) => { UserFieldType::PrimaryField(field) => {
Ok(UserRequestFilter::Equality(field, value.clone())) Ok(UserRequestFilter::Equality(field, value.clone()))
} }
UserFieldType::Attribute(field) => Ok(UserRequestFilter::AttributeEquality( UserFieldType::Attribute(field, typ, is_list) => {
AttributeName::from(field), get_user_attribute_equality_filter(&field, typ, is_list, value)
value.clone(), }
)),
UserFieldType::NoMatch => { UserFieldType::NoMatch => {
if !ldap_info.ignored_user_attributes.contains(&field) { if !ldap_info.ignored_user_attributes.contains(&field) {
warn!( warn!(
@@ -215,17 +233,17 @@ fn convert_user_filter(ldap_info: &LdapInfo, filter: &LdapFilter) -> LdapResult<
field.as_str() == "objectclass" field.as_str() == "objectclass"
|| field.as_str() == "dn" || field.as_str() == "dn"
|| field.as_str() == "distinguishedname" || field.as_str() == "distinguishedname"
|| !matches!(map_user_field(&field), UserFieldType::NoMatch), || !matches!(map_user_field(&field, schema), UserFieldType::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());
match map_user_field(&field) { match map_user_field(&field, schema) {
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::NoMatch
| UserFieldType::Attribute(_) | UserFieldType::Attribute(_, _, _)
| 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,
@@ -258,8 +276,9 @@ pub async fn get_user_list<Backend: UserListerBackendHandler>(
request_groups: bool, request_groups: bool,
base: &str, base: &str,
backend: &Backend, backend: &Backend,
schema: &PublicSchema,
) -> LdapResult<Vec<UserAndGroups>> { ) -> LdapResult<Vec<UserAndGroups>> {
let filters = convert_user_filter(ldap_info, ldap_filter)?; let filters = convert_user_filter(ldap_info, ldap_filter, schema)?;
debug!(?filters); debug!(?filters);
backend backend
.list_users(Some(filters), request_groups) .list_users(Some(filters), request_groups)
+45 -13
View File
@@ -159,34 +159,66 @@ pub fn is_subtree(subtree: &[(String, String)], base_tree: &[(String, String)])
pub enum UserFieldType { pub enum UserFieldType {
NoMatch, NoMatch,
PrimaryField(UserColumn), 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() { match field.as_str() {
"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" => {
UserFieldType::PrimaryField(UserColumn::DisplayName) UserFieldType::PrimaryField(UserColumn::DisplayName)
} }
"givenname" | "first_name" | "firstname" => UserFieldType::Attribute("first_name"), "givenname" | "first_name" | "firstname" => UserFieldType::Attribute(
"sn" | "last_name" | "lastname" => UserFieldType::Attribute("last_name"), AttributeName::from("first_name"),
"avatar" | "jpegphoto" => UserFieldType::Attribute("avatar"), 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" => { "creationdate" | "createtimestamp" | "modifytimestamp" | "creation_date" => {
UserFieldType::PrimaryField(UserColumn::CreationDate) UserFieldType::PrimaryField(UserColumn::CreationDate)
} }
"entryuuid" | "uuid" => UserFieldType::PrimaryField(UserColumn::Uuid), "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> { pub enum GroupFieldType {
Some(match field.as_str() { NoMatch,
"cn" | "displayname" | "uid" | "display_name" => "display_name", DisplayName,
"creationdate" | "createtimestamp" | "modifytimestamp" | "creation_date" => "creation_date", CreationDate,
"entryuuid" | "uuid" => "uuid", Uuid,
_ => return None, 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 { pub struct LdapInfo {
+1
View File
@@ -1,3 +1,4 @@
pub mod deserialize;
pub mod error; pub mod error;
pub mod handler; pub mod handler;
pub mod ldap; pub mod ldap;
@@ -22,14 +22,14 @@ use sea_orm::{
use std::collections::HashSet; use std::collections::HashSet;
use tracing::instrument; use tracing::instrument;
fn attribute_condition(name: AttributeName, value: String) -> Cond { fn attribute_condition(name: AttributeName, value: Serialized) -> Cond {
Expr::in_subquery( Expr::in_subquery(
Expr::col(UserColumn::UserId.as_column_ref()), Expr::col(UserColumn::UserId.as_column_ref()),
model::UserAttributes::find() model::UserAttributes::find()
.select_only() .select_only()
.column(model::UserAttributesColumn::UserId) .column(model::UserAttributesColumn::UserId)
.filter(model::UserAttributesColumn::AttributeName.eq(name)) .filter(model::UserAttributesColumn::AttributeName.eq(name))
.filter(model::UserAttributesColumn::Value.eq(Serialized::from(&value))) .filter(model::UserAttributesColumn::Value.eq(value))
.into_query(), .into_query(),
) )
.into_condition() .into_condition()
@@ -463,7 +463,7 @@ mod tests {
&fixture.handler, &fixture.handler,
Some(UserRequestFilter::AttributeEquality( Some(UserRequestFilter::AttributeEquality(
AttributeName::from("first_name"), AttributeName::from("first_name"),
"first bob".to_string(), Serialized::from("first bob"),
)), )),
) )
.await; .await;
+8 -49
View File
@@ -1,12 +1,13 @@
use crate::{ use crate::{
domain::{ domain::{
deserialize::deserialize_attribute_value,
handler::{ handler::{
AttributeList, BackendHandler, CreateAttributeRequest, CreateGroupRequest, AttributeList, BackendHandler, CreateAttributeRequest, CreateGroupRequest,
CreateUserRequest, UpdateGroupRequest, UpdateUserRequest, CreateUserRequest, UpdateGroupRequest, UpdateUserRequest,
}, },
types::{ types::{
AttributeName, AttributeType, AttributeValue as DomainAttributeValue, GroupId, AttributeName, AttributeType, AttributeValue as DomainAttributeValue, GroupId,
JpegPhoto, Serialized, UserId, JpegPhoto, UserId,
}, },
}, },
infra::{ infra::{
@@ -535,54 +536,12 @@ fn deserialize_attribute(
) )
.into()); .into());
} }
if !attribute_schema.is_list && attribute.value.len() != 1 { let deserialized_values = deserialize_attribute_value(
return Err(anyhow!( &attribute.value,
"Attribute {} is not a list, but multiple values were provided", attribute_schema.attribute_type,
attribute.name attribute_schema.is_list,
) )
.into()); .context(format!("While deserializing attribute {}", attribute.name))?;
}
let parse_int = |value: &String| -> FieldResult<i64> {
Ok(value
.parse::<i64>()
.with_context(|| format!("Invalid integer value {}", value))?)
};
let parse_date = |value: &String| -> FieldResult<chrono::NaiveDateTime> {
Ok(chrono::DateTime::parse_from_rfc3339(value)
.with_context(|| format!("Invalid date value {}", value))?
.naive_utc())
};
let parse_photo = |value: &String| -> FieldResult<JpegPhoto> {
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::<FieldResult<Vec<_>>>()?,
),
(AttributeType::DateTime, false) => Serialized::from(&parse_date(&attribute.value[0])?),
(AttributeType::DateTime, true) => Serialized::from(
&attribute
.value
.iter()
.map(parse_date)
.collect::<FieldResult<Vec<_>>>()?,
),
(AttributeType::JpegPhoto, false) => Serialized::from(&parse_photo(&attribute.value[0])?),
(AttributeType::JpegPhoto, true) => Serialized::from(
&attribute
.value
.iter()
.map(parse_photo)
.collect::<FieldResult<Vec<_>>>()?,
),
};
Ok(DomainAttributeValue { Ok(DomainAttributeValue {
name: attribute_name, name: attribute_name,
value: deserialized_values, value: deserialized_values,
+49 -27
View File
@@ -1,24 +1,25 @@
use crate::{ use crate::{
domain::{ domain::{
deserialize::deserialize_attribute_value,
handler::{BackendHandler, ReadSchemaBackendHandler}, handler::{BackendHandler, ReadSchemaBackendHandler},
ldap::utils::{map_user_field, UserFieldType}, ldap::utils::{map_user_field, UserFieldType},
model::UserColumn,
schema::{ schema::{
PublicSchema, SchemaAttributeExtractor, SchemaGroupAttributeExtractor, PublicSchema, SchemaAttributeExtractor, SchemaGroupAttributeExtractor,
SchemaUserAttributeExtractor, SchemaUserAttributeExtractor,
}, },
types::{ types::{AttributeType, GroupDetails, GroupId, JpegPhoto, UserId},
AttributeName, AttributeType, GroupDetails, GroupId, JpegPhoto, UserColumn, UserId,
},
}, },
infra::{ infra::{
access_control::{ReadonlyBackendHandler, UserReadableBackendHandler}, access_control::{ReadonlyBackendHandler, UserReadableBackendHandler},
graphql::api::{field_error_callback, Context}, graphql::api::{field_error_callback, Context},
}, },
}; };
use anyhow::Context as AnyhowContext;
use chrono::{NaiveDateTime, TimeZone}; use chrono::{NaiveDateTime, TimeZone};
use juniper::{graphql_object, FieldError, FieldResult, GraphQLInputObject}; use juniper::{graphql_object, FieldError, FieldResult, GraphQLInputObject};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use tracing::{debug, debug_span, Instrument}; use tracing::{debug, debug_span, Instrument, Span};
type DomainRequestFilter = crate::domain::handler::UserRequestFilter; type DomainRequestFilter = crate::domain::handler::UserRequestFilter;
type DomainUser = crate::domain::types::User; type DomainUser = crate::domain::types::User;
@@ -40,9 +41,8 @@ pub struct RequestFilter {
member_of_id: Option<i32>, member_of_id: Option<i32>,
} }
impl TryInto<DomainRequestFilter> for RequestFilter { impl RequestFilter {
type Error = String; fn try_into_domain_filter(self, schema: &PublicSchema) -> FieldResult<DomainRequestFilter> {
fn try_into(self) -> Result<DomainRequestFilter, Self::Error> {
match ( match (
self.eq, self.eq,
self.any, self.any,
@@ -52,33 +52,39 @@ impl TryInto<DomainRequestFilter> for RequestFilter {
self.member_of_id, self.member_of_id,
) { ) {
(Some(eq), None, None, None, None, None) => { (Some(eq), None, None, None, None, None) => {
match map_user_field(&eq.field.as_str().into()) { match map_user_field(&eq.field.as_str().into(), schema) {
UserFieldType::NoMatch => Err(format!("Unknown request filter: {}", &eq.field)), UserFieldType::NoMatch => {
Err(format!("Unknown request filter: {}", &eq.field).into())
}
UserFieldType::PrimaryField(UserColumn::UserId) => { UserFieldType::PrimaryField(UserColumn::UserId) => {
Ok(DomainRequestFilter::UserId(UserId::new(&eq.value))) Ok(DomainRequestFilter::UserId(UserId::new(&eq.value)))
} }
UserFieldType::PrimaryField(column) => { UserFieldType::PrimaryField(column) => {
Ok(DomainRequestFilter::Equality(column, eq.value)) Ok(DomainRequestFilter::Equality(column, eq.value))
} }
UserFieldType::Attribute(column) => Ok(DomainRequestFilter::AttributeEquality( UserFieldType::Attribute(name, typ, false) => {
AttributeName::from(column), let value = deserialize_attribute_value(&[eq.value], typ, false)
eq.value, .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( (None, Some(any), None, None, None, None) => Ok(DomainRequestFilter::Or(
any.into_iter() any.into_iter()
.map(TryInto::try_into) .map(|f| f.try_into_domain_filter(schema))
.collect::<Result<Vec<_>, String>>()?, .collect::<FieldResult<Vec<_>>>()?,
)), )),
(None, None, Some(all), None, None, None) => Ok(DomainRequestFilter::And( (None, None, Some(all), None, None, None) => Ok(DomainRequestFilter::And(
all.into_iter() all.into_iter()
.map(TryInto::try_into) .map(|f| f.try_into_domain_filter(schema))
.collect::<Result<Vec<_>, String>>()?, .collect::<FieldResult<Vec<_>>>()?,
)), )),
(None, None, None, Some(not), None, None) => { (None, None, None, Some(not), None, None) => Ok(DomainRequestFilter::Not(Box::new(
Ok(DomainRequestFilter::Not(Box::new((*not).try_into()?))) (*not).try_into_domain_filter(schema)?,
} ))),
(None, None, None, None, Some(group), None) => { (None, None, None, None, Some(group), None) => {
Ok(DomainRequestFilter::MemberOf(group.into())) Ok(DomainRequestFilter::MemberOf(group.into()))
} }
@@ -86,9 +92,9 @@ impl TryInto<DomainRequestFilter> for RequestFilter {
Ok(DomainRequestFilter::MemberOfId(GroupId(group_id))) Ok(DomainRequestFilter::MemberOfId(GroupId(group_id)))
} }
(None, None, None, None, None, None) => { (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<Handler: BackendHandler> Query<Handler> {
&span, &span,
"Unauthorized access to user list", "Unauthorized access to user list",
))?; ))?;
let schema = self.get_schema(context, span.clone()).await?;
Ok(handler 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) .instrument(span)
.await .await
.map(|v| v.into_iter().map(Into::into).collect())?) .map(|v| v.into_iter().map(Into::into).collect())?)
@@ -196,6 +208,16 @@ impl<Handler: BackendHandler> Query<Handler> {
async fn schema(context: &Context<Handler>) -> FieldResult<Schema<Handler>> { async fn schema(context: &Context<Handler>) -> FieldResult<Schema<Handler>> {
let span = debug_span!("[GraphQL query] get_schema"); let span = debug_span!("[GraphQL query] get_schema");
self.get_schema(context, span).await.map(Into::into)
}
}
impl<Handler: BackendHandler> Query<Handler> {
async fn get_schema(
&self,
context: &Context<Handler>,
span: Span,
) -> FieldResult<PublicSchema> {
let handler = context let handler = context
.handler .handler
.get_user_restricted_lister_handler(&context.validation_result); .get_user_restricted_lister_handler(&context.validation_result);
@@ -203,8 +225,7 @@ impl<Handler: BackendHandler> Query<Handler> {
.get_schema() .get_schema()
.instrument(span) .instrument(span)
.await .await
.map(Into::<PublicSchema>::into) .map(Into::<PublicSchema>::into)?)
.map(Into::into)?)
} }
} }
@@ -594,7 +615,7 @@ mod tests {
use crate::{ use crate::{
domain::{ domain::{
handler::AttributeList, handler::AttributeList,
types::{AttributeType, Serialized}, types::{AttributeName, AttributeType, Serialized},
}, },
infra::{ infra::{
access_control::{Permission, ValidationResults}, access_control::{Permission, ValidationResults},
@@ -795,6 +816,7 @@ mod tests {
}"#; }"#;
let mut mock = MockTestBackendHandler::new(); let mut mock = MockTestBackendHandler::new();
setup_default_schema(&mut mock);
mock.expect_list_users() mock.expect_list_users()
.with( .with(
eq(Some(DomainRequestFilter::Or(vec![ eq(Some(DomainRequestFilter::Or(vec![
@@ -805,7 +827,7 @@ mod tests {
), ),
DomainRequestFilter::AttributeEquality( DomainRequestFilter::AttributeEquality(
AttributeName::from("first_name"), AttributeName::from("first_name"),
"robert".to_owned(), Serialized::from("robert"),
), ),
]))), ]))),
eq(false), eq(false),
+14 -3
View File
@@ -531,6 +531,7 @@ impl<Backend: BackendHandler + LoginHandler + OpaqueHandler> LdapHandler<Backend
&self, &self,
backend_handler: &impl UserAndGroupListerBackendHandler, backend_handler: &impl UserAndGroupListerBackendHandler,
request: &LdapSearchRequest, request: &LdapSearchRequest,
schema: &PublicSchema,
) -> LdapResult<InternalSearchResults> { ) -> LdapResult<InternalSearchResults> {
let dn_parts = parse_distinguished_name(&request.base.to_ascii_lowercase())?; 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); let scope = get_search_scope(&self.ldap_info.base_dn, &dn_parts, &request.scope);
@@ -554,11 +555,19 @@ impl<Backend: BackendHandler + LoginHandler + OpaqueHandler> LdapHandler<Backend
need_groups, need_groups,
&request.base, &request.base,
backend_handler, backend_handler,
schema,
) )
.await .await
}); });
let get_group_list = cast(|filter: &LdapFilter| async { let get_group_list = cast(|filter: &LdapFilter| async {
get_groups_list(&self.ldap_info, filter, &request.base, backend_handler).await get_groups_list(
&self.ldap_info,
filter,
&request.base,
backend_handler,
schema,
)
.await
}); });
Ok(match scope { Ok(match scope {
SearchScope::Global => InternalSearchResults::UsersAndGroups( SearchScope::Global => InternalSearchResults::UsersAndGroups(
@@ -617,13 +626,15 @@ impl<Backend: BackendHandler + LoginHandler + OpaqueHandler> LdapHandler<Backend
let backend_handler = self let backend_handler = self
.backend_handler .backend_handler
.get_user_restricted_lister_handler(user_info); .get_user_restricted_lister_handler(user_info);
let search_results = self.do_search_internal(&backend_handler, request).await?;
let schema = let schema =
PublicSchema::from(backend_handler.get_schema().await.map_err(|e| LdapError { PublicSchema::from(backend_handler.get_schema().await.map_err(|e| LdapError {
code: LdapResultCode::OperationsError, code: LdapResultCode::OperationsError,
message: format!("Unable to get schema: {:#}", e), message: format!("Unable to get schema: {:#}", e),
})?); })?);
let search_results = self
.do_search_internal(&backend_handler, request, &schema)
.await?;
let mut results = match search_results { let mut results = match search_results {
InternalSearchResults::UsersAndGroups(users, groups) => { InternalSearchResults::UsersAndGroups(users, groups) => {
convert_users_to_ldap_op(users, &request.attrs, &self.ldap_info, &schema) convert_users_to_ldap_op(users, &request.attrs, &self.ldap_info, &schema)
@@ -1686,7 +1697,7 @@ mod tests {
false.into(), false.into(),
UserRequestFilter::AttributeEquality( UserRequestFilter::AttributeEquality(
AttributeName::from("first_name"), AttributeName::from("first_name"),
"firstname".to_owned(), Serialized::from("firstname"),
), ),
false.into(), false.into(),
UserRequestFilter::UserIdSubString(SubStringFilter { UserRequestFilter::UserIdSubString(SubStringFilter {