use std::sync::Arc; use crate::{ domain::{ deserialize::deserialize_attribute_value, handler::{BackendHandler, ReadSchemaBackendHandler}, ldap::utils::{map_user_field, UserFieldType}, model::UserColumn, schema::PublicSchema, types::{AttributeType, GroupDetails, GroupId, JpegPhoto, LdapObjectClass, 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, Span}; type DomainRequestFilter = crate::domain::handler::UserRequestFilter; type DomainUser = crate::domain::types::User; type DomainGroup = crate::domain::types::Group; type DomainUserAndGroups = crate::domain::types::UserAndGroups; type DomainAttributeList = crate::domain::handler::AttributeList; type DomainAttributeSchema = crate::domain::handler::AttributeSchema; type DomainAttributeValue = crate::domain::types::AttributeValue; #[derive(PartialEq, Eq, Debug, GraphQLInputObject)] /// A filter for requests, specifying a boolean expression based on field constraints. Only one of /// the fields can be set at a time. pub struct RequestFilter { any: Option>, all: Option>, not: Option>, eq: Option, member_of: Option, member_of_id: Option, } impl RequestFilter { fn try_into_domain_filter(self, schema: &PublicSchema) -> FieldResult { match ( self.eq, self.any, self.all, self.not, self.member_of, self.member_of_id, ) { (Some(eq), None, None, None, None, None) => { 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(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()) } 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( any.into_iter() .map(|f| f.try_into_domain_filter(schema)) .collect::>>()?, )), (None, None, Some(all), None, None, None) => Ok(DomainRequestFilter::And( all.into_iter() .map(|f| f.try_into_domain_filter(schema)) .collect::>>()?, )), (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())) } (None, None, None, None, None, Some(group_id)) => { Ok(DomainRequestFilter::MemberOfId(GroupId(group_id))) } (None, None, None, None, None, None) => { Err("No field specified in request filter".into()) } _ => Err("Multiple fields specified in request filter".into()), } } } #[derive(PartialEq, Eq, Debug, GraphQLInputObject)] pub struct EqualityConstraint { field: String, value: String, } #[derive(PartialEq, Eq, Debug)] /// The top-level GraphQL query type. pub struct Query { _phantom: std::marker::PhantomData>, } impl Query { pub fn new() -> Self { Self { _phantom: std::marker::PhantomData, } } } #[graphql_object(context = Context)] impl Query { fn api_version() -> &'static str { "1.0" } pub async fn user(context: &Context, user_id: String) -> FieldResult> { use anyhow::Context; let span = debug_span!("[GraphQL query] user"); span.in_scope(|| { debug!(?user_id); }); let user_id = urlencoding::decode(&user_id).context("Invalid user parameter")?; let user_id = UserId::new(&user_id); let handler = context .get_readable_handler(&user_id) .ok_or_else(field_error_callback( &span, "Unauthorized access to user data", ))?; let schema = Arc::new(self.get_schema(context, span.clone()).await?); let user = handler.get_user_details(&user_id).instrument(span).await?; User::::from_user(user, schema) } async fn users( context: &Context, #[graphql(name = "where")] filters: Option, ) -> FieldResult>> { let span = debug_span!("[GraphQL query] users"); span.in_scope(|| { debug!(?filters); }); let handler = context .get_readonly_handler() .ok_or_else(field_error_callback( &span, "Unauthorized access to user list", ))?; let schema = Arc::new(self.get_schema(context, span.clone()).await?); let users = handler .list_users( filters .map(|f| f.try_into_domain_filter(&schema)) .transpose()?, false, ) .instrument(span) .await?; users .into_iter() .map(|u| User::::from_user_and_groups(u, schema.clone())) .collect() } async fn groups(context: &Context) -> FieldResult>> { let span = debug_span!("[GraphQL query] groups"); let handler = context .get_readonly_handler() .ok_or_else(field_error_callback( &span, "Unauthorized access to group list", ))?; let schema = Arc::new(self.get_schema(context, span.clone()).await?); let domain_groups = handler.list_groups(None).instrument(span).await?; domain_groups .into_iter() .map(|g| Group::::from_group(g, schema.clone())) .collect() } async fn group(context: &Context, group_id: i32) -> FieldResult> { let span = debug_span!("[GraphQL query] group"); span.in_scope(|| { debug!(?group_id); }); let handler = context .get_readonly_handler() .ok_or_else(field_error_callback( &span, "Unauthorized access to group data", ))?; let schema = Arc::new(self.get_schema(context, span.clone()).await?); let group_details = handler .get_group_details(GroupId(group_id)) .instrument(span) .await?; Group::::from_group_details(group_details, schema.clone()) } 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); Ok(handler .get_schema() .instrument(span) .await .map(Into::::into)?) } } #[derive(PartialEq, Eq, Debug, Serialize, Deserialize)] /// Represents a single user. pub struct User { user: DomainUser, attributes: Vec>, schema: Arc, groups: Option>>, _phantom: std::marker::PhantomData>, } impl User { pub fn from_user(mut user: DomainUser, schema: Arc) -> FieldResult { let attributes = std::mem::take(&mut user.attributes); Ok(Self { user, attributes: attributes .into_iter() .map(|a| { AttributeValue::::from_schema(a, &schema.get_schema().user_attributes) }) .collect::>>()?, schema, groups: None, _phantom: std::marker::PhantomData, }) } } impl User { pub fn from_user_and_groups( DomainUserAndGroups { user, groups }: DomainUserAndGroups, schema: Arc, ) -> FieldResult { let mut user = Self::from_user(user, schema.clone())?; if let Some(groups) = groups { user.groups = Some( groups .into_iter() .map(|g| Group::::from_group_details(g, schema.clone())) .collect::>>()?, ); } Ok(user) } } #[graphql_object(context = Context)] impl User { fn id(&self) -> &str { self.user.user_id.as_str() } fn email(&self) -> &str { self.user.email.as_str() } fn display_name(&self) -> &str { self.user.display_name.as_deref().unwrap_or("") } fn first_name(&self) -> &str { self.attributes .iter() .find(|a| a.attribute.name.as_str() == "first_name") .map(|a| a.attribute.value.unwrap()) .unwrap_or("") } fn last_name(&self) -> &str { self.attributes .iter() .find(|a| a.attribute.name.as_str() == "last_name") .map(|a| a.attribute.value.unwrap()) .unwrap_or("") } fn avatar(&self) -> Option { self.attributes .iter() .find(|a| a.attribute.name.as_str() == "avatar") .map(|a| String::from(&a.attribute.value.unwrap::())) } fn creation_date(&self) -> chrono::DateTime { chrono::Utc.from_utc_datetime(&self.user.creation_date) } fn uuid(&self) -> &str { self.user.uuid.as_str() } /// User-defined attributes. fn attributes(&self) -> &[AttributeValue] { &self.attributes } /// The groups to which this user belongs. async fn groups(&self, context: &Context) -> FieldResult>> { if let Some(groups) = &self.groups { return Ok(groups.clone()); } let span = debug_span!("[GraphQL query] user::groups"); span.in_scope(|| { debug!(user_id = ?self.user.user_id); }); let handler = context .get_readable_handler(&self.user.user_id) .expect("We shouldn't be able to get there without readable permission"); let domain_groups = handler .get_user_groups(&self.user.user_id) .instrument(span) .await?; let mut groups = domain_groups .into_iter() .map(|g| Group::::from_group_details(g, self.schema.clone())) .collect::>>>()?; groups.sort_by(|g1, g2| g1.display_name.cmp(&g2.display_name)); Ok(groups) } } #[derive(PartialEq, Eq, Debug, Serialize, Deserialize)] /// Represents a single group. pub struct Group { group_id: i32, display_name: String, creation_date: chrono::NaiveDateTime, uuid: String, attributes: Vec>, schema: Arc, _phantom: std::marker::PhantomData>, } impl Group { pub fn from_group( group: DomainGroup, schema: Arc, ) -> FieldResult> { Ok(Self { group_id: group.id.0, display_name: group.display_name.to_string(), creation_date: group.creation_date, uuid: group.uuid.into_string(), attributes: group .attributes .into_iter() .map(|a| { AttributeValue::::from_schema(a, &schema.get_schema().group_attributes) }) .collect::>>()?, schema, _phantom: std::marker::PhantomData, }) } pub fn from_group_details( group_details: GroupDetails, schema: Arc, ) -> FieldResult> { Ok(Self { group_id: group_details.group_id.0, display_name: group_details.display_name.to_string(), creation_date: group_details.creation_date, uuid: group_details.uuid.into_string(), attributes: group_details .attributes .into_iter() .map(|a| { AttributeValue::::from_schema(a, &schema.get_schema().group_attributes) }) .collect::>>()?, schema, _phantom: std::marker::PhantomData, }) } } impl Clone for Group { fn clone(&self) -> Self { Self { group_id: self.group_id, display_name: self.display_name.clone(), creation_date: self.creation_date, uuid: self.uuid.clone(), attributes: self.attributes.clone(), schema: self.schema.clone(), _phantom: std::marker::PhantomData, } } } #[graphql_object(context = Context)] impl Group { fn id(&self) -> i32 { self.group_id } fn display_name(&self) -> String { self.display_name.clone() } fn creation_date(&self) -> chrono::DateTime { chrono::Utc.from_utc_datetime(&self.creation_date) } fn uuid(&self) -> String { self.uuid.clone() } /// User-defined attributes. fn attributes(&self) -> &[AttributeValue] { &self.attributes } /// The groups to which this user belongs. async fn users(&self, context: &Context) -> FieldResult>> { let span = debug_span!("[GraphQL query] group::users"); span.in_scope(|| { debug!(name = %self.display_name); }); let handler = context .get_readonly_handler() .ok_or_else(field_error_callback( &span, "Unauthorized access to group data", ))?; let domain_users = handler .list_users( Some(DomainRequestFilter::MemberOfId(GroupId(self.group_id))), false, ) .instrument(span) .await?; domain_users .into_iter() .map(|u| User::::from_user_and_groups(u, self.schema.clone())) .collect() } } #[derive(PartialEq, Eq, Debug, Serialize, Deserialize)] pub struct AttributeSchema { schema: DomainAttributeSchema, _phantom: std::marker::PhantomData>, } #[graphql_object(context = Context)] impl AttributeSchema { fn name(&self) -> String { self.schema.name.to_string() } fn attribute_type(&self) -> AttributeType { self.schema.attribute_type } fn is_list(&self) -> bool { self.schema.is_list } fn is_visible(&self) -> bool { self.schema.is_visible } fn is_editable(&self) -> bool { self.schema.is_editable } fn is_hardcoded(&self) -> bool { self.schema.is_hardcoded } fn is_readonly(&self) -> bool { self.schema.is_readonly } } impl Clone for AttributeSchema { fn clone(&self) -> Self { Self { schema: self.schema.clone(), _phantom: std::marker::PhantomData, } } } impl From for AttributeSchema { fn from(value: DomainAttributeSchema) -> Self { Self { schema: value, _phantom: std::marker::PhantomData, } } } #[derive(PartialEq, Eq, Debug, Serialize, Deserialize)] pub struct AttributeList { attributes: DomainAttributeList, extra_classes: Vec, _phantom: std::marker::PhantomData>, } #[graphql_object(context = Context)] impl AttributeList { fn attributes(&self) -> Vec> { self.attributes .attributes .clone() .into_iter() .map(Into::into) .collect() } fn extra_ldap_object_classes(&self) -> Vec { self.extra_classes.iter().map(|c| c.to_string()).collect() } } impl AttributeList { fn new(attributes: DomainAttributeList, extra_classes: Vec) -> Self { Self { attributes, extra_classes, _phantom: std::marker::PhantomData, } } } #[derive(PartialEq, Eq, Debug, Serialize, Deserialize)] pub struct Schema { schema: PublicSchema, _phantom: std::marker::PhantomData>, } #[graphql_object(context = Context)] impl Schema { fn user_schema(&self) -> AttributeList { AttributeList::::new( self.schema.get_schema().user_attributes.clone(), self.schema.get_schema().extra_user_object_classes.clone(), ) } fn group_schema(&self) -> AttributeList { AttributeList::::new( self.schema.get_schema().group_attributes.clone(), self.schema.get_schema().extra_group_object_classes.clone(), ) } } impl From for Schema { fn from(value: PublicSchema) -> Self { Self { schema: value, _phantom: std::marker::PhantomData, } } } #[derive(PartialEq, Eq, Debug, Serialize, Deserialize)] pub struct AttributeValue { attribute: DomainAttributeValue, schema: AttributeSchema, _phantom: std::marker::PhantomData>, } #[graphql_object(context = Context)] impl AttributeValue { fn name(&self) -> &str { self.attribute.name.as_str() } fn value(&self) -> FieldResult> { Ok(serialize_attribute(&self.attribute, &self.schema.schema)) } fn schema(&self) -> &AttributeSchema { &self.schema } } impl Clone for AttributeValue { fn clone(&self) -> Self { Self { attribute: self.attribute.clone(), schema: self.schema.clone(), _phantom: std::marker::PhantomData, } } } pub fn serialize_attribute( attribute: &DomainAttributeValue, attribute_schema: &DomainAttributeSchema, ) -> Vec { let convert_date = |date| chrono::Utc.from_utc_datetime(&date).to_rfc3339(); match (attribute_schema.attribute_type, attribute_schema.is_list) { (AttributeType::String, false) => vec![attribute.value.unwrap::()], (AttributeType::Integer, false) => { // LDAP integers are encoded as strings. vec![attribute.value.unwrap::().to_string()] } (AttributeType::JpegPhoto, false) => { vec![String::from(&attribute.value.unwrap::())] } (AttributeType::DateTime, false) => { vec![convert_date(attribute.value.unwrap::())] } (AttributeType::String, true) => attribute .value .unwrap::>() .into_iter() .collect(), (AttributeType::Integer, true) => attribute .value .unwrap::>() .into_iter() .map(|i| i.to_string()) .collect(), (AttributeType::JpegPhoto, true) => attribute .value .unwrap::>() .iter() .map(String::from) .collect(), (AttributeType::DateTime, true) => attribute .value .unwrap::>() .into_iter() .map(convert_date) .collect(), } } impl AttributeValue { fn from_schema(a: DomainAttributeValue, schema: &DomainAttributeList) -> FieldResult { match schema.get_attribute_schema(&a.name) { Some(s) => Ok(AttributeValue:: { attribute: a, schema: AttributeSchema:: { schema: s.clone(), _phantom: std::marker::PhantomData, }, _phantom: std::marker::PhantomData, }), None => Err(FieldError::from(format!("Unknown attribute {}", &a.name))), } } } #[cfg(test)] mod tests { use super::*; use crate::{ domain::{ handler::AttributeList, types::{AttributeName, AttributeType, LdapObjectClass, Serialized}, }, infra::{ access_control::{Permission, ValidationResults}, test_utils::{setup_default_schema, MockTestBackendHandler}, }, }; use chrono::TimeZone; use juniper::{ execute, graphql_value, DefaultScalarValue, EmptyMutation, EmptySubscription, GraphQLType, RootNode, Variables, }; use mockall::predicate::eq; use pretty_assertions::assert_eq; use std::collections::HashSet; fn schema<'q, C, Q>(query_root: Q) -> RootNode<'q, Q, EmptyMutation, EmptySubscription> where Q: GraphQLType + 'q, { RootNode::new( query_root, EmptyMutation::::new(), EmptySubscription::::new(), ) } #[tokio::test] async fn get_user_by_id() { const QUERY: &str = r#"{ user(userId: "bob") { id email creationDate firstName lastName uuid attributes { name value } groups { id displayName creationDate uuid attributes { name value } } } }"#; let mut mock = MockTestBackendHandler::new(); mock.expect_get_schema().returning(|| { Ok(crate::domain::handler::Schema { user_attributes: DomainAttributeList { attributes: vec![ DomainAttributeSchema { name: "first_name".into(), attribute_type: AttributeType::String, is_list: false, is_visible: true, is_editable: true, is_hardcoded: true, is_readonly: false, }, DomainAttributeSchema { name: "last_name".into(), attribute_type: AttributeType::String, is_list: false, is_visible: true, is_editable: true, is_hardcoded: true, is_readonly: false, }, ], }, group_attributes: DomainAttributeList { attributes: vec![DomainAttributeSchema { name: "club_name".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![ LdapObjectClass::from("customUserClass"), LdapObjectClass::from("myUserClass"), ], extra_group_object_classes: vec![LdapObjectClass::from("customGroupClass")], }) }); mock.expect_get_user_details() .with(eq(UserId::new("bob"))) .return_once(|_| { Ok(DomainUser { user_id: UserId::new("bob"), email: "bob@bobbers.on".into(), creation_date: chrono::Utc.timestamp_millis_opt(42).unwrap().naive_utc(), uuid: crate::uuid!("b1a2a3a4b1b2c1c2d1d2d3d4d5d6d7d8"), attributes: vec![ DomainAttributeValue { name: "first_name".into(), value: Serialized::from("Bob"), }, DomainAttributeValue { name: "last_name".into(), value: Serialized::from("Bobberson"), }, ], ..Default::default() }) }); let mut groups = HashSet::new(); groups.insert(GroupDetails { group_id: GroupId(3), display_name: "Bobbersons".into(), creation_date: chrono::Utc.timestamp_nanos(42).naive_utc(), uuid: crate::uuid!("a1a2a3a4b1b2c1c2d1d2d3d4d5d6d7d8"), attributes: vec![DomainAttributeValue { name: "club_name".into(), value: Serialized::from("Gang of Four"), }], }); groups.insert(GroupDetails { group_id: GroupId(7), display_name: "Jefferees".into(), creation_date: chrono::Utc.timestamp_nanos(12).naive_utc(), uuid: crate::uuid!("b1a2a3a4b1b2c1c2d1d2d3d4d5d6d7d8"), attributes: Vec::new(), }); mock.expect_get_user_groups() .with(eq(UserId::new("bob"))) .return_once(|_| Ok(groups)); let context = Context::::new_for_tests(mock, ValidationResults::admin()); let schema = schema(Query::::new()); assert_eq!( execute(QUERY, None, &schema, &Variables::new(), &context).await, Ok(( graphql_value!( { "user": { "id": "bob", "email": "bob@bobbers.on", "creationDate": "1970-01-01T00:00:00.042+00:00", "uuid": "b1a2a3a4-b1b2-c1c2-d1d2-d3d4d5d6d7d8", "firstName": "Bob", "lastName": "Bobberson", "attributes": [{ "name": "first_name", "value": ["Bob"], }, { "name": "last_name", "value": ["Bobberson"], }], "groups": [{ "id": 3, "displayName": "Bobbersons", "creationDate": "1970-01-01T00:00:00.000000042+00:00", "uuid": "a1a2a3a4-b1b2-c1c2-d1d2-d3d4d5d6d7d8", "attributes": [{ "name": "club_name", "value": ["Gang of Four"], }, ], }, { "id": 7, "displayName": "Jefferees", "creationDate": "1970-01-01T00:00:00.000000012+00:00", "uuid": "b1a2a3a4-b1b2-c1c2-d1d2-d3d4d5d6d7d8", "attributes": [], }] } }), vec![] )) ); } #[tokio::test] async fn list_users() { const QUERY: &str = r#"{ users(filters: { any: [ {eq: { field: "id" value: "bob" }}, {eq: { field: "email" value: "robert@bobbers.on" }}, {eq: { field: "firstName" value: "robert" }} ]}) { id email } }"#; let mut mock = MockTestBackendHandler::new(); setup_default_schema(&mut mock); mock.expect_list_users() .with( eq(Some(DomainRequestFilter::Or(vec![ DomainRequestFilter::UserId(UserId::new("bob")), DomainRequestFilter::Equality( UserColumn::Email, "robert@bobbers.on".to_owned(), ), DomainRequestFilter::AttributeEquality( AttributeName::from("first_name"), Serialized::from("robert"), ), ]))), eq(false), ) .return_once(|_, _| { Ok(vec![ DomainUserAndGroups { user: DomainUser { user_id: UserId::new("bob"), email: "bob@bobbers.on".into(), ..Default::default() }, groups: None, }, DomainUserAndGroups { user: DomainUser { user_id: UserId::new("robert"), email: "robert@bobbers.on".into(), ..Default::default() }, groups: None, }, ]) }); let context = Context::::new_for_tests(mock, ValidationResults::admin()); let schema = schema(Query::::new()); assert_eq!( execute(QUERY, None, &schema, &Variables::new(), &context).await, Ok(( graphql_value!( { "users": [ { "id": "bob", "email": "bob@bobbers.on" }, { "id": "robert", "email": "robert@bobbers.on" }, ] }), vec![] )) ); } #[tokio::test] async fn get_schema() { const QUERY: &str = r#"{ schema { userSchema { attributes { name attributeType isList isVisible isEditable isHardcoded } extraLdapObjectClasses } groupSchema { attributes { name attributeType isList isVisible isEditable isHardcoded } extraLdapObjectClasses } } }"#; let mut mock = MockTestBackendHandler::new(); setup_default_schema(&mut mock); let context = Context::::new_for_tests(mock, ValidationResults::admin()); let schema = schema(Query::::new()); assert_eq!( execute(QUERY, None, &schema, &Variables::new(), &context).await, Ok(( graphql_value!( { "schema": { "userSchema": { "attributes": [ { "name": "avatar", "attributeType": "JPEG_PHOTO", "isList": false, "isVisible": true, "isEditable": true, "isHardcoded": true, }, { "name": "creation_date", "attributeType": "DATE_TIME", "isList": false, "isVisible": true, "isEditable": false, "isHardcoded": true, }, { "name": "display_name", "attributeType": "STRING", "isList": false, "isVisible": true, "isEditable": true, "isHardcoded": true, }, { "name": "first_name", "attributeType": "STRING", "isList": false, "isVisible": true, "isEditable": true, "isHardcoded": true, }, { "name": "last_name", "attributeType": "STRING", "isList": false, "isVisible": true, "isEditable": true, "isHardcoded": true, }, { "name": "mail", "attributeType": "STRING", "isList": false, "isVisible": true, "isEditable": true, "isHardcoded": true, }, { "name": "user_id", "attributeType": "STRING", "isList": false, "isVisible": true, "isEditable": false, "isHardcoded": true, }, { "name": "uuid", "attributeType": "STRING", "isList": false, "isVisible": true, "isEditable": false, "isHardcoded": true, }, ], "extraLdapObjectClasses": ["customUserClass"], }, "groupSchema": { "attributes": [ { "name": "creation_date", "attributeType": "DATE_TIME", "isList": false, "isVisible": true, "isEditable": false, "isHardcoded": true, }, { "name": "display_name", "attributeType": "STRING", "isList": false, "isVisible": true, "isEditable": true, "isHardcoded": true, }, { "name": "group_id", "attributeType": "INTEGER", "isList": false, "isVisible": true, "isEditable": false, "isHardcoded": true, }, { "name": "uuid", "attributeType": "STRING", "isList": false, "isVisible": true, "isEditable": false, "isHardcoded": true, }, ], "extraLdapObjectClasses": [], } } }), vec![] )) ); } #[tokio::test] async fn regular_user_doesnt_see_non_visible_attributes() { const QUERY: &str = r#"{ schema { userSchema { attributes { name } extraLdapObjectClasses } } }"#; let mut mock = MockTestBackendHandler::new(); mock.expect_get_schema().times(1).return_once(|| { Ok(crate::domain::handler::Schema { user_attributes: AttributeList { attributes: vec![DomainAttributeSchema { name: "invisible".into(), attribute_type: AttributeType::JpegPhoto, is_list: false, is_visible: false, is_editable: true, is_hardcoded: true, is_readonly: false, }], }, group_attributes: AttributeList { attributes: Vec::new(), }, extra_user_object_classes: vec![LdapObjectClass::from("customUserClass")], extra_group_object_classes: Vec::new(), }) }); let context = Context::::new_for_tests( mock, ValidationResults { user: UserId::new("bob"), permission: Permission::Regular, }, ); let schema = schema(Query::::new()); assert_eq!( execute(QUERY, None, &schema, &Variables::new(), &context).await, Ok(( graphql_value!( { "schema": { "userSchema": { "attributes": [ {"name": "creation_date"}, {"name": "display_name"}, {"name": "mail"}, {"name": "user_id"}, {"name": "uuid"}, ], "extraLdapObjectClasses": ["customUserClass"], } } } ), vec![] )) ); } }