mirror of
https://github.com/lldap/lldap.git
synced 2024-11-25 09:06:03 +00:00
server: Add graphql support for setting attributes
This commit is contained in:
parent
9e88bfe6b4
commit
c6ecf8d58a
9 changed files with 244 additions and 38 deletions
|
@ -90,6 +90,7 @@ impl CommonComponent<CreateUserForm> for CreateUserForm {
|
|||
firstName: to_option(model.first_name),
|
||||
lastName: to_option(model.last_name),
|
||||
avatar: None,
|
||||
attributes: None,
|
||||
},
|
||||
};
|
||||
self.common.call_graphql::<CreateUser, _>(
|
||||
|
|
|
@ -391,6 +391,8 @@ impl UserDetailsForm {
|
|||
firstName: None,
|
||||
lastName: None,
|
||||
avatar: None,
|
||||
removeAttributes: None,
|
||||
insertAttributes: None,
|
||||
};
|
||||
let default_user_input = user_input.clone();
|
||||
let model = self.form.model();
|
||||
|
|
|
@ -18,7 +18,7 @@ js = []
|
|||
rust-argon2 = "0.8"
|
||||
curve25519-dalek = "3"
|
||||
digest = "0.9"
|
||||
generic-array = "*"
|
||||
generic-array = "0.14"
|
||||
rand = "0.8"
|
||||
serde = "*"
|
||||
sha2 = "0.9"
|
||||
|
|
|
@ -194,6 +194,7 @@ impl TryFrom<ResultEntry> for User {
|
|||
first_name,
|
||||
last_name,
|
||||
avatar: avatar.map(base64::encode),
|
||||
attributes: None,
|
||||
},
|
||||
password,
|
||||
entry.dn,
|
||||
|
|
46
schema.graphql
generated
46
schema.graphql
generated
|
@ -6,6 +6,7 @@ type AttributeValue {
|
|||
type Mutation {
|
||||
createUser(user: CreateUserInput!): User!
|
||||
createGroup(name: String!): Group!
|
||||
createGroupWithDetails(request: CreateGroupInput!): Group!
|
||||
updateUser(user: UpdateUserInput!): Success!
|
||||
updateGroup(group: UpdateGroupInput!): Success!
|
||||
addUserToGroup(userId: String!, groupId: Int!): Success!
|
||||
|
@ -61,7 +62,8 @@ input CreateUserInput {
|
|||
displayName: String
|
||||
firstName: String
|
||||
lastName: String
|
||||
avatar: String
|
||||
"Base64 encoded JpegPhoto." avatar: String
|
||||
"User-defined attributes." attributes: [AttributeValueInput!]
|
||||
}
|
||||
|
||||
type AttributeSchema {
|
||||
|
@ -80,7 +82,15 @@ input UpdateUserInput {
|
|||
displayName: String
|
||||
firstName: String
|
||||
lastName: String
|
||||
avatar: String
|
||||
"Base64 encoded JpegPhoto." avatar: String
|
||||
"""
|
||||
Attribute names to remove.
|
||||
They are processed before insertions.
|
||||
""" removeAttributes: [String!]
|
||||
"""
|
||||
Inserts or updates the given attributes.
|
||||
For lists, the entire list must be provided.
|
||||
""" insertAttributes: [AttributeValueInput!]
|
||||
}
|
||||
|
||||
input EqualityConstraint {
|
||||
|
@ -95,8 +105,36 @@ type Schema {
|
|||
|
||||
"The fields that can be updated for a group."
|
||||
input UpdateGroupInput {
|
||||
id: Int!
|
||||
displayName: String
|
||||
"The group ID." id: Int!
|
||||
"The new display name." displayName: String
|
||||
"""
|
||||
Attribute names to remove.
|
||||
They are processed before insertions.
|
||||
""" removeAttributes: [String!]
|
||||
"""
|
||||
Inserts or updates the given attributes.
|
||||
For lists, the entire list must be provided.
|
||||
""" insertAttributes: [AttributeValueInput!]
|
||||
}
|
||||
|
||||
input AttributeValueInput {
|
||||
"""
|
||||
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: [String!]!
|
||||
}
|
||||
|
||||
"The details required to create a group."
|
||||
input CreateGroupInput {
|
||||
displayName: String!
|
||||
"User-defined attributes." attributes: [AttributeValueInput!]
|
||||
}
|
||||
|
||||
type User {
|
||||
|
|
|
@ -205,13 +205,11 @@ impl TryFrom<Vec<u8>> for JpegPhoto {
|
|||
}
|
||||
}
|
||||
|
||||
impl TryFrom<String> for JpegPhoto {
|
||||
impl TryFrom<&str> for JpegPhoto {
|
||||
type Error = anyhow::Error;
|
||||
fn try_from(string: String) -> anyhow::Result<Self> {
|
||||
fn try_from(string: &str) -> anyhow::Result<Self> {
|
||||
// The String format is in base64.
|
||||
<Self as TryFrom<_>>::try_from(
|
||||
base64::engine::general_purpose::STANDARD.decode(string.as_str())?,
|
||||
)
|
||||
<Self as TryFrom<_>>::try_from(base64::engine::general_purpose::STANDARD.decode(string)?)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -11,6 +11,7 @@ use crate::domain::{
|
|||
ReadSchemaBackendHandler, Schema, SchemaBackendHandler, UpdateGroupRequest,
|
||||
UpdateUserRequest, UserBackendHandler, UserListerBackendHandler, UserRequestFilter,
|
||||
},
|
||||
schema::PublicSchema,
|
||||
types::{Group, GroupDetails, GroupId, User, UserAndGroups, UserId},
|
||||
};
|
||||
|
||||
|
@ -71,9 +72,10 @@ impl ValidationResults {
|
|||
}
|
||||
|
||||
#[async_trait]
|
||||
pub trait UserReadableBackendHandler {
|
||||
pub trait UserReadableBackendHandler: ReadSchemaBackendHandler {
|
||||
async fn get_user_details(&self, user_id: &UserId) -> Result<User>;
|
||||
async fn get_user_groups(&self, user_id: &UserId) -> Result<HashSet<GroupDetails>>;
|
||||
async fn get_schema(&self) -> Result<PublicSchema>;
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
|
@ -120,6 +122,11 @@ impl<Handler: BackendHandler> UserReadableBackendHandler for Handler {
|
|||
async fn get_user_groups(&self, user_id: &UserId) -> Result<HashSet<GroupDetails>> {
|
||||
<Handler as UserBackendHandler>::get_user_groups(self, user_id).await
|
||||
}
|
||||
async fn get_schema(&self) -> Result<PublicSchema> {
|
||||
Ok(PublicSchema::from(
|
||||
<Handler as ReadSchemaBackendHandler>::get_schema(self).await?,
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
|
|
|
@ -1,10 +1,13 @@
|
|||
use crate::{
|
||||
domain::{
|
||||
handler::{
|
||||
BackendHandler, CreateAttributeRequest, CreateGroupRequest, CreateUserRequest,
|
||||
UpdateGroupRequest, UpdateUserRequest,
|
||||
AttributeList, BackendHandler, CreateAttributeRequest, CreateGroupRequest,
|
||||
CreateUserRequest, UpdateGroupRequest, UpdateUserRequest,
|
||||
},
|
||||
types::{
|
||||
AttributeType, AttributeValue as DomainAttributeValue, GroupId, JpegPhoto, Serialized,
|
||||
UserId,
|
||||
},
|
||||
types::{AttributeType, GroupId, JpegPhoto, UserId},
|
||||
},
|
||||
infra::{
|
||||
access_control::{
|
||||
|
@ -14,10 +17,10 @@ use crate::{
|
|||
graphql::api::{field_error_callback, Context},
|
||||
},
|
||||
};
|
||||
use anyhow::Context as AnyhowContext;
|
||||
use anyhow::{anyhow, Context as AnyhowContext};
|
||||
use base64::Engine;
|
||||
use juniper::{graphql_object, FieldResult, GraphQLInputObject, GraphQLObject};
|
||||
use tracing::{debug, debug_span, Instrument};
|
||||
use tracing::{debug, debug_span, Instrument, Span};
|
||||
|
||||
#[derive(PartialEq, Eq, Debug)]
|
||||
/// The top-level GraphQL mutation type.
|
||||
|
@ -33,6 +36,21 @@ impl<Handler: BackendHandler> Mutation<Handler> {
|
|||
}
|
||||
}
|
||||
|
||||
#[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 {
|
||||
|
@ -41,8 +59,18 @@ pub struct CreateUserInput {
|
|||
display_name: Option<String>,
|
||||
first_name: Option<String>,
|
||||
last_name: Option<String>,
|
||||
// Base64 encoded JpegPhoto.
|
||||
/// 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)]
|
||||
|
@ -53,15 +81,29 @@ pub struct UpdateUserInput {
|
|||
display_name: Option<String>,
|
||||
first_name: Option<String>,
|
||||
last_name: Option<String>,
|
||||
// Base64 encoded JpegPhoto.
|
||||
/// 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)]
|
||||
|
@ -97,6 +139,13 @@ impl<Handler: BackendHandler> Mutation<Handler> {
|
|||
.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))
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
handler
|
||||
.create_user(CreateUserRequest {
|
||||
user_id: user_id.clone(),
|
||||
|
@ -105,7 +154,7 @@ impl<Handler: BackendHandler> Mutation<Handler> {
|
|||
first_name: user.first_name,
|
||||
last_name: user.last_name,
|
||||
avatar,
|
||||
..Default::default()
|
||||
attributes,
|
||||
})
|
||||
.instrument(span.clone())
|
||||
.await?;
|
||||
|
@ -124,19 +173,25 @@ impl<Handler: BackendHandler> Mutation<Handler> {
|
|||
span.in_scope(|| {
|
||||
debug!(?name);
|
||||
});
|
||||
let handler = context
|
||||
.get_admin_handler()
|
||||
.ok_or_else(field_error_callback(&span, "Unauthorized group creation"))?;
|
||||
let request = CreateGroupRequest {
|
||||
display_name: name,
|
||||
..Default::default()
|
||||
};
|
||||
let group_id = handler.create_group(request).await?;
|
||||
Ok(handler
|
||||
.get_group_details(group_id)
|
||||
.instrument(span)
|
||||
.await
|
||||
.map(Into::into)?)
|
||||
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(
|
||||
|
@ -159,6 +214,13 @@ impl<Handler: BackendHandler> Mutation<Handler> {
|
|||
.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))
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
handler
|
||||
.update_user(UpdateUserRequest {
|
||||
user_id,
|
||||
|
@ -167,7 +229,8 @@ impl<Handler: BackendHandler> Mutation<Handler> {
|
|||
first_name: user.first_name,
|
||||
last_name: user.last_name,
|
||||
avatar,
|
||||
..Default::default()
|
||||
delete_attributes: user.remove_attributes.unwrap_or_default(),
|
||||
insert_attributes,
|
||||
})
|
||||
.instrument(span)
|
||||
.await?;
|
||||
|
@ -185,16 +248,23 @@ impl<Handler: BackendHandler> Mutation<Handler> {
|
|||
let handler = context
|
||||
.get_admin_handler()
|
||||
.ok_or_else(field_error_callback(&span, "Unauthorized group update"))?;
|
||||
if group.id == 1 {
|
||||
span.in_scope(|| debug!("Cannot change admin group details"));
|
||||
return Err("Cannot change admin group details".into());
|
||||
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))
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
handler
|
||||
.update_group(UpdateGroupRequest {
|
||||
group_id: GroupId(group.id),
|
||||
display_name: group.display_name,
|
||||
delete_attributes: Vec::new(),
|
||||
insert_attributes: Vec::new(),
|
||||
delete_attributes: group.remove_attributes.unwrap_or_default(),
|
||||
insert_attributes,
|
||||
})
|
||||
.instrument(span)
|
||||
.await?;
|
||||
|
@ -390,3 +460,91 @@ impl<Handler: BackendHandler> Mutation<Handler> {
|
|||
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))
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
let request = CreateGroupRequest {
|
||||
display_name: request.display_name,
|
||||
attributes,
|
||||
};
|
||||
let group_id = handler.create_group(request).await?;
|
||||
Ok(handler
|
||||
.get_group_details(group_id)
|
||||
.instrument(span)
|
||||
.await
|
||||
.map(Into::into)?)
|
||||
}
|
||||
|
||||
fn deserialize_attribute(
|
||||
attribute_schema: &AttributeList,
|
||||
attribute: AttributeValue,
|
||||
) -> FieldResult<DomainAttributeValue> {
|
||||
let attribute_type = attribute_schema
|
||||
.get_attribute_type(&attribute.name)
|
||||
.ok_or_else(|| anyhow!("Attribute {} is not defined in the schema", attribute.name))?;
|
||||
if !attribute_type.1 && attribute.value.len() != 1 {
|
||||
return Err(anyhow!(
|
||||
"Attribute {} is not a list, but multiple values were provided",
|
||||
attribute.name
|
||||
)
|
||||
.into());
|
||||
}
|
||||
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_type {
|
||||
(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 {
|
||||
name: attribute.name,
|
||||
value: deserialized_values,
|
||||
})
|
||||
}
|
||||
|
|
|
@ -108,6 +108,7 @@ impl LLDAPFixture {
|
|||
display_name: None,
|
||||
first_name: None,
|
||||
last_name: None,
|
||||
attributes: None,
|
||||
},
|
||||
},
|
||||
)
|
||||
|
|
Loading…
Reference in a new issue