diff --git a/schema.graphql b/schema.graphql index a691c86..2824491 100644 --- a/schema.graphql +++ b/schema.graphql @@ -1,6 +1,7 @@ type AttributeValue { name: String! value: [String!]! + schema: AttributeSchema! } type Mutation { @@ -152,10 +153,6 @@ type User { groups: [Group!]! } -type AttributeList { - attributes: [AttributeSchema!]! -} - enum AttributeType { STRING INTEGER @@ -163,6 +160,10 @@ enum AttributeType { DATE_TIME } +type AttributeList { + attributes: [AttributeSchema!]! +} + type Success { ok: Boolean! } diff --git a/server/src/domain/schema.rs b/server/src/domain/schema.rs index dcd845b..665bc87 100644 --- a/server/src/domain/schema.rs +++ b/server/src/domain/schema.rs @@ -4,7 +4,7 @@ use crate::domain::{ }; use serde::{Deserialize, Serialize}; -#[derive(PartialEq, Eq, Debug, Serialize, Deserialize)] +#[derive(PartialEq, Eq, Debug, Serialize, Deserialize, Clone)] pub struct PublicSchema(Schema); impl PublicSchema { diff --git a/server/src/infra/graphql/mutation.rs b/server/src/infra/graphql/mutation.rs index 4d88613..b7d28b2 100644 --- a/server/src/infra/graphql/mutation.rs +++ b/server/src/infra/graphql/mutation.rs @@ -1,3 +1,5 @@ +use std::sync::Arc; + use crate::{ domain::{ deserialize::deserialize_attribute_value, @@ -159,11 +161,8 @@ impl Mutation { }) .instrument(span.clone()) .await?; - Ok(handler - .get_user_details(&user_id) - .instrument(span) - .await - .map(Into::into)?) + let user_details = handler.get_user_details(&user_id).instrument(span).await?; + super::query::User::::from_user(user_details, Arc::new(schema)) } async fn create_group( @@ -513,11 +512,8 @@ async fn create_group_with_details( attributes, }; let group_id = handler.create_group(request).await?; - Ok(handler - .get_group_details(group_id) - .instrument(span) - .await - .map(Into::into)?) + let group_details = handler.get_group_details(group_id).instrument(span).await?; + super::query::Group::::from_group_details(group_details, Arc::new(schema)) } fn deserialize_attribute( diff --git a/server/src/infra/graphql/query.rs b/server/src/infra/graphql/query.rs index de6bffc..ae4e7d9 100644 --- a/server/src/infra/graphql/query.rs +++ b/server/src/infra/graphql/query.rs @@ -1,13 +1,12 @@ +use std::sync::Arc; + use crate::{ domain::{ deserialize::deserialize_attribute_value, handler::{BackendHandler, ReadSchemaBackendHandler}, ldap::utils::{map_user_field, UserFieldType}, model::UserColumn, - schema::{ - PublicSchema, SchemaAttributeExtractor, SchemaGroupAttributeExtractor, - SchemaUserAttributeExtractor, - }, + schema::PublicSchema, types::{AttributeType, GroupDetails, GroupId, JpegPhoto, UserId}, }, infra::{ @@ -143,11 +142,9 @@ impl Query { &span, "Unauthorized access to user data", ))?; - Ok(handler - .get_user_details(&user_id) - .instrument(span) - .await - .map(Into::into)?) + 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( @@ -164,8 +161,8 @@ impl Query { &span, "Unauthorized access to user list", ))?; - let schema = self.get_schema(context, span.clone()).await?; - Ok(handler + 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)) @@ -173,8 +170,11 @@ impl Query { false, ) .instrument(span) - .await - .map(|v| v.into_iter().map(Into::into).collect())?) + .await?; + users + .into_iter() + .map(|u| User::::from_user_and_groups(u, schema.clone())) + .collect() } async fn groups(context: &Context) -> FieldResult>> { @@ -185,11 +185,12 @@ impl Query { &span, "Unauthorized access to group list", ))?; - Ok(handler - .list_groups(None) - .instrument(span) - .await - .map(|v| v.into_iter().map(Into::into).collect())?) + 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> { @@ -203,11 +204,12 @@ impl Query { &span, "Unauthorized access to group data", ))?; - Ok(handler + let schema = Arc::new(self.get_schema(context, span.clone()).await?); + let group_details = handler .get_group_details(GroupId(group_id)) .instrument(span) - .await - .map(Into::into)?) + .await?; + Group::::from_group_details(group_details, schema.clone()) } async fn schema(context: &Context) -> FieldResult> { @@ -237,16 +239,45 @@ impl Query { /// Represents a single user. pub struct User { user: DomainUser, + attributes: Vec>, + schema: Arc, + groups: Option>>, _phantom: std::marker::PhantomData>, } -#[cfg(test)] -impl Default for User { - fn default() -> Self { - Self { - user: DomainUser::default(), +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) } } @@ -299,17 +330,15 @@ impl User { } /// User-defined attributes. - fn attributes(&self) -> Vec> { - self.user - .attributes - .clone() - .into_iter() - .map(Into::into) - .collect() + 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); @@ -317,36 +346,16 @@ impl User { let handler = context .get_readable_handler(&self.user.user_id) .expect("We shouldn't be able to get there without readable permission"); - Ok(handler + let domain_groups = handler .get_user_groups(&self.user.user_id) .instrument(span) - .await - .map(|set| { - let mut groups = set - .into_iter() - .map(Into::into) - .collect::>>(); - groups.sort_by(|g1, g2| g1.display_name.cmp(&g2.display_name)); - groups - })?) - } -} - -impl From for User { - fn from(user: DomainUser) -> Self { - Self { - user, - _phantom: std::marker::PhantomData, - } - } -} - -impl From for User { - fn from(user: DomainUserAndGroups) -> Self { - Self { - user: user.user, - _phantom: std::marker::PhantomData, - } + .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) } } @@ -357,11 +366,69 @@ pub struct Group { display_name: String, creation_date: chrono::NaiveDateTime, uuid: String, - attributes: Vec, - members: Option>, + 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 { @@ -378,12 +445,8 @@ impl Group { } /// User-defined attributes. - fn attributes(&self) -> Vec> { - self.attributes - .clone() - .into_iter() - .map(Into::into) - .collect() + fn attributes(&self) -> &[AttributeValue] { + &self.attributes } /// The groups to which this user belongs. @@ -398,42 +461,17 @@ impl Group { &span, "Unauthorized access to group data", ))?; - Ok(handler + let domain_users = handler .list_users( Some(DomainRequestFilter::MemberOfId(GroupId(self.group_id))), false, ) .instrument(span) - .await - .map(|v| v.into_iter().map(Into::into).collect())?) - } -} - -impl From for Group { - fn from(group_details: GroupDetails) -> Self { - 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, - members: None, - _phantom: std::marker::PhantomData, - } - } -} - -impl From for Group { - fn from(group: DomainGroup) -> Self { - 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, - members: Some(group.users.into_iter().map(UserId::into_string).collect()), - _phantom: std::marker::PhantomData, - } + .await?; + domain_users + .into_iter() + .map(|u| User::::from_user_and_groups(u, self.schema.clone())) + .collect() } } @@ -465,6 +503,15 @@ impl AttributeSchema { } } +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 { @@ -527,88 +574,92 @@ impl From for Schema { } #[derive(PartialEq, Eq, Debug, Serialize, Deserialize)] -pub struct AttributeValue { +pub struct AttributeValue { attribute: DomainAttributeValue, + schema: AttributeSchema, _phantom: std::marker::PhantomData>, - _phantom_extractor: std::marker::PhantomData, } #[graphql_object(context = Context)] -impl - AttributeValue -{ +impl AttributeValue { fn name(&self) -> &str { self.attribute.name.as_str() } - async fn value(&self, context: &Context) -> FieldResult> { - let handler = context - .handler - .get_user_restricted_lister_handler(&context.validation_result); - serialize_attribute( - &self.attribute, - Extractor::get_attributes(&PublicSchema::from(handler.get_schema().await?)), - ) + + 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, - attributes: &DomainAttributeList, -) -> FieldResult> { + attribute_schema: &DomainAttributeSchema, +) -> Vec { let convert_date = |date| chrono::Utc.from_utc_datetime(&date).to_rfc3339(); - attributes - .get_attribute_type(&attribute.name) - .map(|attribute_type| { - match attribute_type { - (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(), - } - }) - .ok_or_else(|| FieldError::from(anyhow::anyhow!("Unknown attribute: {}", &attribute.name))) + 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 From - for AttributeValue -{ - fn from(value: DomainAttributeValue) -> Self { - Self { - attribute: value, - _phantom: std::marker::PhantomData, - _phantom_extractor: std::marker::PhantomData, +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))), } } }