lldap/server/src/infra/graphql/mutation.rs

630 lines
21 KiB
Rust

use std::sync::Arc;
use crate::{
domain::{
deserialize::deserialize_attribute_value,
handler::{
AttributeList, BackendHandler, CreateAttributeRequest, CreateGroupRequest,
CreateUserRequest, UpdateGroupRequest, UpdateUserRequest,
},
types::{
AttributeName, AttributeType, AttributeValue as DomainAttributeValue, 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<Handler: BackendHandler> {
_phantom: std::marker::PhantomData<Box<Handler>>,
}
impl<Handler: BackendHandler> Mutation<Handler> {
pub fn new() -> Self {
Self {
_phantom: std::marker::PhantomData,
}
}
}
#[derive(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<String>,
}
#[derive(PartialEq, Eq, Debug, GraphQLInputObject)]
/// The details required to create a user.
pub struct CreateUserInput {
id: String,
email: String,
display_name: Option<String>,
first_name: Option<String>,
last_name: Option<String>,
/// Base64 encoded JpegPhoto.
avatar: Option<String>,
/// User-defined attributes.
attributes: Option<Vec<AttributeValue>>,
}
#[derive(PartialEq, Eq, Debug, GraphQLInputObject)]
/// The details required to create a group.
pub struct CreateGroupInput {
display_name: String,
/// User-defined attributes.
attributes: Option<Vec<AttributeValue>>,
}
#[derive(PartialEq, Eq, Debug, GraphQLInputObject)]
/// The fields that can be updated for a user.
pub struct UpdateUserInput {
id: String,
email: Option<String>,
display_name: Option<String>,
first_name: Option<String>,
last_name: Option<String>,
/// Base64 encoded JpegPhoto.
avatar: Option<String>,
/// Attribute names to remove.
/// They are processed before insertions.
remove_attributes: Option<Vec<String>>,
/// Inserts or updates the given attributes.
/// For lists, the entire list must be provided.
insert_attributes: Option<Vec<AttributeValue>>,
}
#[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<String>,
/// Attribute names to remove.
/// They are processed before insertions.
remove_attributes: Option<Vec<String>>,
/// Inserts or updates the given attributes.
/// For lists, the entire list must be provided.
insert_attributes: Option<Vec<AttributeValue>>,
}
#[derive(PartialEq, Eq, Debug, GraphQLObject)]
pub struct Success {
ok: bool,
}
impl Success {
fn new() -> Self {
Self { ok: true }
}
}
#[graphql_object(context = Context<Handler>)]
impl<Handler: BackendHandler> Mutation<Handler> {
async fn create_user(
context: &Context<Handler>,
user: CreateUserInput,
) -> FieldResult<super::query::User<Handler>> {
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 attributes = user
.attributes
.unwrap_or_default()
.into_iter()
.map(|attr| deserialize_attribute(&schema.get_schema().user_attributes, attr, true))
.collect::<Result<Vec<_>, _>>()?;
handler
.create_user(CreateUserRequest {
user_id: user_id.clone(),
email: user.email.into(),
display_name: user.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::<Handler>::from_user(user_details, Arc::new(schema))
}
async fn create_group(
context: &Context<Handler>,
name: String,
) -> FieldResult<super::query::Group<Handler>> {
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<Handler>,
request: CreateGroupInput,
) -> FieldResult<super::query::Group<Handler>> {
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<Handler>,
user: UpdateUserInput,
) -> FieldResult<Success> {
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 insert_attributes = user
.insert_attributes
.unwrap_or_default()
.into_iter()
.map(|attr| deserialize_attribute(&schema.get_schema().user_attributes, attr, is_admin))
.collect::<Result<Vec<_>, _>>()?;
handler
.update_user(UpdateUserRequest {
user_id,
email: user.email.map(Into::into),
display_name: user.display_name,
first_name: user.first_name,
last_name: user.last_name,
avatar,
delete_attributes: user
.remove_attributes
.unwrap_or_default()
.into_iter()
.map(Into::into)
.collect(),
insert_attributes,
})
.instrument(span)
.await?;
Ok(Success::new())
}
async fn update_group(
context: &Context<Handler>,
group: UpdateGroupInput,
) -> FieldResult<Success> {
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"))?;
if group.id == 1 && group.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()
.map(|attr| deserialize_attribute(&schema.get_schema().group_attributes, attr, true))
.collect::<Result<Vec<_>, _>>()?;
handler
.update_group(UpdateGroupRequest {
group_id: GroupId(group.id),
display_name: group.display_name.map(Into::into),
delete_attributes: group
.remove_attributes
.unwrap_or_default()
.into_iter()
.map(Into::into)
.collect(),
insert_attributes,
})
.instrument(span)
.await?;
Ok(Success::new())
}
async fn add_user_to_group(
context: &Context<Handler>,
user_id: String,
group_id: i32,
) -> FieldResult<Success> {
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<Handler>,
user_id: String,
group_id: i32,
) -> FieldResult<Success> {
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<Handler>, user_id: String) -> FieldResult<Success> {
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<Handler>, group_id: i32) -> FieldResult<Success> {
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<Handler>,
name: String,
attribute_type: AttributeType,
is_list: bool,
is_visible: bool,
is_editable: bool,
) -> FieldResult<Success> {
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<Handler>,
name: String,
attribute_type: AttributeType,
is_list: bool,
is_visible: bool,
is_editable: bool,
) -> FieldResult<Success> {
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<Handler>,
name: String,
) -> FieldResult<Success> {
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<Handler>,
name: String,
) -> FieldResult<Success> {
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<Handler>,
name: String,
) -> FieldResult<Success> {
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<Handler>,
name: String,
) -> FieldResult<Success> {
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<Handler>,
name: String,
) -> FieldResult<Success> {
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<Handler>,
name: String,
) -> FieldResult<Success> {
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<Handler: BackendHandler>(
context: &Context<Handler>,
request: CreateGroupInput,
span: Span,
) -> FieldResult<super::query::Group<Handler>> {
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::<Result<Vec<_>, _>>()?;
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::<Handler>::from_group_details(group_details, Arc::new(schema))
}
fn deserialize_attribute(
attribute_schema: &AttributeList,
attribute: AttributeValue,
is_admin: bool,
) -> FieldResult<DomainAttributeValue> {
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 !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,
})
}