use std::sync::Arc; use crate::{ domain::{ deserialize::deserialize_attribute_value, handler::{ AttributeList, BackendHandler, CreateAttributeRequest, CreateGroupRequest, CreateUserRequest, UpdateGroupRequest, UpdateUserRequest, }, schema::PublicSchema, types::{ AttributeName, AttributeType, AttributeValue as DomainAttributeValue, Email, GroupId, JpegPhoto, LdapObjectClass, UserId, }, }, infra::{ access_control::{ AdminBackendHandler, ReadonlyBackendHandler, UserReadableBackendHandler, UserWriteableBackendHandler, }, graphql::api::{field_error_callback, Context}, }, }; use anyhow::{anyhow, Context as AnyhowContext}; use base64::Engine; use juniper::{graphql_object, FieldResult, GraphQLInputObject, GraphQLObject}; use tracing::{debug, debug_span, Instrument, Span}; #[derive(PartialEq, Eq, Debug)] /// The top-level GraphQL mutation type. pub struct Mutation { _phantom: std::marker::PhantomData>, } impl Mutation { pub fn new() -> Self { Self { _phantom: std::marker::PhantomData, } } } #[derive(Clone, PartialEq, Eq, Debug, GraphQLInputObject)] // This conflicts with the attribute values returned by the user/group queries. #[graphql(name = "AttributeValueInput")] struct AttributeValue { /// The name of the attribute. It must be present in the schema, and the type informs how /// to interpret the values. name: String, /// The values of the attribute. /// If the attribute is not a list, the vector must contain exactly one element. /// Integers (signed 64 bits) are represented as strings. /// Dates are represented as strings in RFC3339 format, e.g. "2019-10-12T07:20:50.52Z". /// JpegPhotos are represented as base64 encoded strings. They must be valid JPEGs. value: Vec, } #[derive(PartialEq, Eq, Debug, GraphQLInputObject)] /// The details required to create a user. pub struct CreateUserInput { id: String, // The email can be specified as an attribute, but one of the two is required. email: Option, display_name: Option, first_name: Option, last_name: Option, /// Base64 encoded JpegPhoto. avatar: Option, /// User-defined attributes. attributes: Option>, } #[derive(PartialEq, Eq, Debug, GraphQLInputObject)] /// The details required to create a group. pub struct CreateGroupInput { display_name: String, /// User-defined attributes. attributes: Option>, } #[derive(PartialEq, Eq, Debug, GraphQLInputObject)] /// The fields that can be updated for a user. pub struct UpdateUserInput { id: String, email: Option, display_name: Option, first_name: Option, last_name: Option, /// Base64 encoded JpegPhoto. avatar: Option, /// Attribute names to remove. /// They are processed before insertions. remove_attributes: Option>, /// Inserts or updates the given attributes. /// For lists, the entire list must be provided. insert_attributes: Option>, } #[derive(PartialEq, Eq, Debug, GraphQLInputObject)] /// The fields that can be updated for a group. pub struct UpdateGroupInput { /// The group ID. id: i32, /// The new display name. display_name: Option, /// Attribute names to remove. /// They are processed before insertions. remove_attributes: Option>, /// Inserts or updates the given attributes. /// For lists, the entire list must be provided. insert_attributes: Option>, } #[derive(PartialEq, Eq, Debug, GraphQLObject)] pub struct Success { ok: bool, } impl Success { fn new() -> Self { Self { ok: true } } } struct UnpackedAttributes { email: Option, display_name: Option, attributes: Vec, } fn unpack_attributes( attributes: Vec, schema: &PublicSchema, is_admin: bool, ) -> FieldResult { let email = attributes .iter() .find(|attr| attr.name == "mail") .cloned() .map(|attr| deserialize_attribute(&schema.get_schema().user_attributes, attr, is_admin)) .transpose()? .map(|attr| attr.value.unwrap::()) .map(Email::from); let display_name = attributes .iter() .find(|attr| attr.name == "display_name") .cloned() .map(|attr| deserialize_attribute(&schema.get_schema().user_attributes, attr, is_admin)) .transpose()? .map(|attr| attr.value.unwrap::()); let attributes = attributes .into_iter() .filter(|attr| attr.name != "mail" && attr.name != "display_name") .map(|attr| deserialize_attribute(&schema.get_schema().user_attributes, attr, is_admin)) .collect::, _>>()?; Ok(UnpackedAttributes { email, display_name, attributes, }) } #[graphql_object(context = Context)] impl Mutation { async fn create_user( context: &Context, user: CreateUserInput, ) -> FieldResult> { let span = debug_span!("[GraphQL mutation] create_user"); span.in_scope(|| { debug!("{:?}", &user.id); }); let handler = context .get_admin_handler() .ok_or_else(field_error_callback(&span, "Unauthorized user creation"))?; let user_id = UserId::new(&user.id); let avatar = user .avatar .map(|bytes| base64::engine::general_purpose::STANDARD.decode(bytes)) .transpose() .context("Invalid base64 image")? .map(JpegPhoto::try_from) .transpose() .context("Provided image is not a valid JPEG")?; let schema = handler.get_schema().await?; let UnpackedAttributes { email, display_name, attributes, } = unpack_attributes(user.attributes.unwrap_or_default(), &schema, true)?; handler .create_user(CreateUserRequest { user_id: user_id.clone(), email: user .email .map(Email::from) .or(email) .ok_or_else(|| anyhow!("Email is required when creating a new user"))?, display_name: user.display_name.or(display_name), first_name: user.first_name, last_name: user.last_name, avatar, attributes, }) .instrument(span.clone()) .await?; 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( context: &Context, name: String, ) -> FieldResult> { let span = debug_span!("[GraphQL mutation] create_group"); span.in_scope(|| { debug!(?name); }); create_group_with_details( context, CreateGroupInput { display_name: name, attributes: Some(Vec::new()), }, span, ) .await } async fn create_group_with_details( context: &Context, request: CreateGroupInput, ) -> FieldResult> { let span = debug_span!("[GraphQL mutation] create_group_with_details"); span.in_scope(|| { debug!(?request); }); create_group_with_details(context, request, span).await } async fn update_user( context: &Context, user: UpdateUserInput, ) -> FieldResult { let span = debug_span!("[GraphQL mutation] update_user"); span.in_scope(|| { debug!(?user.id); }); let user_id = UserId::new(&user.id); let handler = context .get_writeable_handler(&user_id) .ok_or_else(field_error_callback(&span, "Unauthorized user update"))?; let is_admin = context.validation_result.is_admin(); let avatar = user .avatar .map(|bytes| base64::engine::general_purpose::STANDARD.decode(bytes)) .transpose() .context("Invalid base64 image")? .map(JpegPhoto::try_from) .transpose() .context("Provided image is not a valid JPEG")?; let schema = handler.get_schema().await?; let user_insert_attributes = user.insert_attributes.unwrap_or_default(); let UnpackedAttributes { email, display_name, attributes: insert_attributes, } = unpack_attributes(user_insert_attributes, &schema, is_admin)?; let display_name = display_name.or_else(|| { // If the display name is not inserted, but removed, reset it. user.remove_attributes .iter() .flatten() .find(|attr| *attr == "display_name") .map(|_| String::new()) }); handler .update_user(UpdateUserRequest { user_id, email: user.email.map(Into::into).or(email), display_name: user.display_name.or(display_name), first_name: user.first_name, last_name: user.last_name, avatar, delete_attributes: user .remove_attributes .unwrap_or_default() .into_iter() .filter(|attr| attr != "mail" && attr != "display_name") .map(Into::into) .collect(), insert_attributes, }) .instrument(span) .await?; Ok(Success::new()) } async fn update_group( context: &Context, group: UpdateGroupInput, ) -> FieldResult { let span = debug_span!("[GraphQL mutation] update_group"); span.in_scope(|| { debug!(?group.id); }); let handler = context .get_admin_handler() .ok_or_else(field_error_callback(&span, "Unauthorized group update"))?; let new_display_name = group.display_name.clone().or_else(|| { group.insert_attributes.as_ref().and_then(|a| { a.iter() .find(|attr| attr.name == "display_name") .map(|attr| attr.value[0].clone()) }) }); if group.id == 1 && new_display_name.is_some() { span.in_scope(|| debug!("Cannot change lldap_admin group name")); return Err("Cannot change lldap_admin group name".into()); } let schema = handler.get_schema().await?; let insert_attributes = group .insert_attributes .unwrap_or_default() .into_iter() .filter(|attr| attr.name != "display_name") .map(|attr| deserialize_attribute(&schema.get_schema().group_attributes, attr, true)) .collect::, _>>()?; handler .update_group(UpdateGroupRequest { group_id: GroupId(group.id), display_name: new_display_name.map(|s| s.as_str().into()), delete_attributes: group .remove_attributes .unwrap_or_default() .into_iter() .filter(|attr| attr != "display_name") .map(Into::into) .collect(), insert_attributes, }) .instrument(span) .await?; Ok(Success::new()) } async fn add_user_to_group( context: &Context, user_id: String, group_id: i32, ) -> FieldResult { let span = debug_span!("[GraphQL mutation] add_user_to_group"); span.in_scope(|| { debug!(?user_id, ?group_id); }); let handler = context .get_admin_handler() .ok_or_else(field_error_callback( &span, "Unauthorized group membership modification", ))?; handler .add_user_to_group(&UserId::new(&user_id), GroupId(group_id)) .instrument(span) .await?; Ok(Success::new()) } async fn remove_user_from_group( context: &Context, user_id: String, group_id: i32, ) -> FieldResult { let span = debug_span!("[GraphQL mutation] remove_user_from_group"); span.in_scope(|| { debug!(?user_id, ?group_id); }); let handler = context .get_admin_handler() .ok_or_else(field_error_callback( &span, "Unauthorized group membership modification", ))?; let user_id = UserId::new(&user_id); if context.validation_result.user == user_id && group_id == 1 { span.in_scope(|| debug!("Cannot remove admin rights for current user")); return Err("Cannot remove admin rights for current user".into()); } handler .remove_user_from_group(&user_id, GroupId(group_id)) .instrument(span) .await?; Ok(Success::new()) } async fn delete_user(context: &Context, user_id: String) -> FieldResult { let span = debug_span!("[GraphQL mutation] delete_user"); span.in_scope(|| { debug!(?user_id); }); let user_id = UserId::new(&user_id); let handler = context .get_admin_handler() .ok_or_else(field_error_callback(&span, "Unauthorized user deletion"))?; if context.validation_result.user == user_id { span.in_scope(|| debug!("Cannot delete current user")); return Err("Cannot delete current user".into()); } handler.delete_user(&user_id).instrument(span).await?; Ok(Success::new()) } async fn delete_group(context: &Context, group_id: i32) -> FieldResult { let span = debug_span!("[GraphQL mutation] delete_group"); span.in_scope(|| { debug!(?group_id); }); let handler = context .get_admin_handler() .ok_or_else(field_error_callback(&span, "Unauthorized group deletion"))?; if group_id == 1 { span.in_scope(|| debug!("Cannot delete admin group")); return Err("Cannot delete admin group".into()); } handler .delete_group(GroupId(group_id)) .instrument(span) .await?; Ok(Success::new()) } async fn add_user_attribute( context: &Context, name: String, attribute_type: AttributeType, is_list: bool, is_visible: bool, is_editable: bool, ) -> FieldResult { let span = debug_span!("[GraphQL mutation] add_user_attribute"); span.in_scope(|| { debug!(?name, ?attribute_type, is_list, is_visible, is_editable); }); let handler = context .get_admin_handler() .ok_or_else(field_error_callback( &span, "Unauthorized attribute creation", ))?; handler .add_user_attribute(CreateAttributeRequest { name: name.into(), attribute_type, is_list, is_visible, is_editable, }) .instrument(span) .await?; Ok(Success::new()) } async fn add_group_attribute( context: &Context, name: String, attribute_type: AttributeType, is_list: bool, is_visible: bool, is_editable: bool, ) -> FieldResult { let span = debug_span!("[GraphQL mutation] add_group_attribute"); span.in_scope(|| { debug!(?name, ?attribute_type, is_list, is_visible, is_editable); }); let handler = context .get_admin_handler() .ok_or_else(field_error_callback( &span, "Unauthorized attribute creation", ))?; handler .add_group_attribute(CreateAttributeRequest { name: name.into(), attribute_type, is_list, is_visible, is_editable, }) .instrument(span) .await?; Ok(Success::new()) } async fn delete_user_attribute( context: &Context, name: String, ) -> FieldResult { let span = debug_span!("[GraphQL mutation] delete_user_attribute"); let name = AttributeName::from(name); span.in_scope(|| { debug!(?name); }); let handler = context .get_admin_handler() .ok_or_else(field_error_callback( &span, "Unauthorized attribute deletion", ))?; let schema = handler.get_schema().await?; let attribute_schema = schema .get_schema() .user_attributes .get_attribute_schema(&name) .ok_or_else(|| anyhow!("Attribute {} is not defined in the schema", &name))?; if attribute_schema.is_hardcoded { return Err(anyhow!("Permission denied: Attribute {} cannot be deleted", &name).into()); } handler .delete_user_attribute(&name) .instrument(span) .await?; Ok(Success::new()) } async fn delete_group_attribute( context: &Context, name: String, ) -> FieldResult { let span = debug_span!("[GraphQL mutation] delete_group_attribute"); let name = AttributeName::from(name); span.in_scope(|| { debug!(?name); }); let handler = context .get_admin_handler() .ok_or_else(field_error_callback( &span, "Unauthorized attribute deletion", ))?; let schema = handler.get_schema().await?; let attribute_schema = schema .get_schema() .group_attributes .get_attribute_schema(&name) .ok_or_else(|| anyhow!("Attribute {} is not defined in the schema", &name))?; if attribute_schema.is_hardcoded { return Err(anyhow!("Permission denied: Attribute {} cannot be deleted", &name).into()); } handler .delete_group_attribute(&name) .instrument(span) .await?; Ok(Success::new()) } async fn add_user_object_class( context: &Context, name: String, ) -> FieldResult { let span = debug_span!("[GraphQL mutation] add_user_object_class"); span.in_scope(|| { debug!(?name); }); let handler = context .get_admin_handler() .ok_or_else(field_error_callback( &span, "Unauthorized object class addition", ))?; handler .add_user_object_class(&LdapObjectClass::from(name)) .instrument(span) .await?; Ok(Success::new()) } async fn add_group_object_class( context: &Context, name: String, ) -> FieldResult { let span = debug_span!("[GraphQL mutation] add_group_object_class"); span.in_scope(|| { debug!(?name); }); let handler = context .get_admin_handler() .ok_or_else(field_error_callback( &span, "Unauthorized object class addition", ))?; handler .add_group_object_class(&LdapObjectClass::from(name)) .instrument(span) .await?; Ok(Success::new()) } async fn delete_user_object_class( context: &Context, name: String, ) -> FieldResult { let span = debug_span!("[GraphQL mutation] delete_user_object_class"); span.in_scope(|| { debug!(?name); }); let handler = context .get_admin_handler() .ok_or_else(field_error_callback( &span, "Unauthorized object class deletion", ))?; handler .delete_user_object_class(&LdapObjectClass::from(name)) .instrument(span) .await?; Ok(Success::new()) } async fn delete_group_object_class( context: &Context, name: String, ) -> FieldResult { let span = debug_span!("[GraphQL mutation] delete_group_object_class"); span.in_scope(|| { debug!(?name); }); let handler = context .get_admin_handler() .ok_or_else(field_error_callback( &span, "Unauthorized object class deletion", ))?; handler .delete_group_object_class(&LdapObjectClass::from(name)) .instrument(span) .await?; Ok(Success::new()) } } async fn create_group_with_details( context: &Context, request: CreateGroupInput, span: Span, ) -> FieldResult> { let handler = context .get_admin_handler() .ok_or_else(field_error_callback(&span, "Unauthorized group creation"))?; let schema = handler.get_schema().await?; let attributes = request .attributes .unwrap_or_default() .into_iter() .map(|attr| deserialize_attribute(&schema.get_schema().group_attributes, attr, true)) .collect::, _>>()?; let request = CreateGroupRequest { display_name: request.display_name.into(), attributes, }; let group_id = handler.create_group(request).await?; 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( attribute_schema: &AttributeList, attribute: AttributeValue, is_admin: bool, ) -> FieldResult { let attribute_name = AttributeName::from(attribute.name.as_str()); let attribute_schema = attribute_schema .get_attribute_schema(&attribute_name) .ok_or_else(|| anyhow!("Attribute {} is not defined in the schema", attribute.name))?; if attribute_schema.is_readonly { return Err(anyhow!( "Permission denied: Attribute {} is read-only", attribute.name ) .into()); } if !is_admin && !attribute_schema.is_editable { return Err(anyhow!( "Permission denied: Attribute {} is not editable by regular users", attribute.name ) .into()); } 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, }) }