mirror of
https://github.com/lldap/lldap.git
synced 2026-03-31 15:07:48 +01:00
graphql: split query.rs and mutation.rs into modular structures (#1311)
This commit is contained in:
@@ -0,0 +1,160 @@
|
||||
use anyhow::{Context as AnyhowContext, anyhow};
|
||||
use juniper::FieldResult;
|
||||
use lldap_access_control::{AdminBackendHandler, ReadonlyBackendHandler};
|
||||
use lldap_domain::{
|
||||
deserialize::deserialize_attribute_value,
|
||||
public_schema::PublicSchema,
|
||||
requests::CreateGroupRequest,
|
||||
schema::AttributeList,
|
||||
types::{Attribute as DomainAttribute, AttributeName, Email},
|
||||
};
|
||||
use lldap_domain_handlers::handler::{BackendHandler, ReadSchemaBackendHandler};
|
||||
use std::{collections::BTreeMap, sync::Arc};
|
||||
use tracing::{Instrument, Span};
|
||||
|
||||
use super::inputs::AttributeValue;
|
||||
use crate::api::{Context, field_error_callback};
|
||||
|
||||
pub struct UnpackedAttributes {
|
||||
pub email: Option<Email>,
|
||||
pub display_name: Option<String>,
|
||||
pub attributes: Vec<DomainAttribute>,
|
||||
}
|
||||
|
||||
pub fn unpack_attributes(
|
||||
attributes: Vec<AttributeValue>,
|
||||
schema: &PublicSchema,
|
||||
is_admin: bool,
|
||||
) -> FieldResult<UnpackedAttributes> {
|
||||
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.into_string().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.into_string().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::<Result<Vec<_>, _>>()?;
|
||||
Ok(UnpackedAttributes {
|
||||
email,
|
||||
display_name,
|
||||
attributes,
|
||||
})
|
||||
}
|
||||
|
||||
/// Consolidates caller supplied user fields and attributes into a list of attributes.
|
||||
///
|
||||
/// A number of user fields are internally represented as attributes, but are still also
|
||||
/// available as fields on user objects. This function consolidates these fields and the
|
||||
/// given attributes into a resulting attribute list. If a value is supplied for both a
|
||||
/// field and the corresponding attribute, the attribute will take precedence.
|
||||
pub fn consolidate_attributes(
|
||||
attributes: Vec<AttributeValue>,
|
||||
first_name: Option<String>,
|
||||
last_name: Option<String>,
|
||||
avatar: Option<String>,
|
||||
) -> Vec<AttributeValue> {
|
||||
// Prepare map of the client provided attributes
|
||||
let mut provided_attributes: BTreeMap<AttributeName, AttributeValue> = attributes
|
||||
.into_iter()
|
||||
.map(|x| {
|
||||
(
|
||||
x.name.clone().into(),
|
||||
AttributeValue {
|
||||
name: x.name.to_ascii_lowercase(),
|
||||
value: x.value,
|
||||
},
|
||||
)
|
||||
})
|
||||
.collect::<BTreeMap<_, _>>();
|
||||
// Prepare list of fallback attribute values
|
||||
let field_attrs = [
|
||||
("first_name", first_name),
|
||||
("last_name", last_name),
|
||||
("avatar", avatar),
|
||||
];
|
||||
for (name, value) in field_attrs.into_iter() {
|
||||
if let Some(val) = value {
|
||||
let attr_name: AttributeName = name.into();
|
||||
provided_attributes
|
||||
.entry(attr_name)
|
||||
.or_insert_with(|| AttributeValue {
|
||||
name: name.to_string(),
|
||||
value: vec![val],
|
||||
});
|
||||
}
|
||||
}
|
||||
// Return the values of the resulting map
|
||||
provided_attributes.into_values().collect()
|
||||
}
|
||||
|
||||
pub async fn create_group_with_details<Handler: BackendHandler>(
|
||||
context: &Context<Handler>,
|
||||
request: super::inputs::CreateGroupInput,
|
||||
span: Span,
|
||||
) -> FieldResult<crate::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 public_schema: PublicSchema = schema.into();
|
||||
let attributes = request
|
||||
.attributes
|
||||
.unwrap_or_default()
|
||||
.into_iter()
|
||||
.map(|attr| deserialize_attribute(&public_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?;
|
||||
crate::query::Group::<Handler>::from_group_details(group_details, Arc::new(public_schema))
|
||||
}
|
||||
|
||||
pub fn deserialize_attribute(
|
||||
attribute_schema: &AttributeList,
|
||||
attribute: AttributeValue,
|
||||
is_admin: bool,
|
||||
) -> FieldResult<DomainAttribute> {
|
||||
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(DomainAttribute {
|
||||
name: attribute_name,
|
||||
value: deserialized_values,
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
use juniper::{GraphQLInputObject, GraphQLObject};
|
||||
|
||||
#[derive(Clone, PartialEq, Eq, Debug, GraphQLInputObject)]
|
||||
// This conflicts with the attribute values returned by the user/group queries.
|
||||
#[graphql(name = "AttributeValueInput")]
|
||||
pub struct AttributeValue {
|
||||
/// The name of the attribute. It must be present in the schema, and the type informs how
|
||||
/// to interpret the values.
|
||||
pub 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.
|
||||
pub value: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Eq, Debug, GraphQLInputObject)]
|
||||
/// The details required to create a user.
|
||||
pub struct CreateUserInput {
|
||||
pub id: String,
|
||||
// The email can be specified as an attribute, but one of the two is required.
|
||||
pub email: Option<String>,
|
||||
pub display_name: Option<String>,
|
||||
/// First name of user. Deprecated: use attribute instead.
|
||||
/// If both field and corresponding attribute is supplied, the attribute will take precedence.
|
||||
pub first_name: Option<String>,
|
||||
/// Last name of user. Deprecated: use attribute instead.
|
||||
/// If both field and corresponding attribute is supplied, the attribute will take precedence.
|
||||
pub last_name: Option<String>,
|
||||
/// Base64 encoded JpegPhoto. Deprecated: use attribute instead.
|
||||
/// If both field and corresponding attribute is supplied, the attribute will take precedence.
|
||||
pub avatar: Option<String>,
|
||||
/// Attributes.
|
||||
pub attributes: Option<Vec<AttributeValue>>,
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Eq, Debug, GraphQLInputObject)]
|
||||
/// The details required to create a group.
|
||||
pub struct CreateGroupInput {
|
||||
pub display_name: String,
|
||||
/// User-defined attributes.
|
||||
pub attributes: Option<Vec<AttributeValue>>,
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Eq, Debug, GraphQLInputObject)]
|
||||
/// The fields that can be updated for a user.
|
||||
pub struct UpdateUserInput {
|
||||
pub id: String,
|
||||
pub email: Option<String>,
|
||||
pub display_name: Option<String>,
|
||||
/// First name of user. Deprecated: use attribute instead.
|
||||
/// If both field and corresponding attribute is supplied, the attribute will take precedence.
|
||||
pub first_name: Option<String>,
|
||||
/// Last name of user. Deprecated: use attribute instead.
|
||||
/// If both field and corresponding attribute is supplied, the attribute will take precedence.
|
||||
pub last_name: Option<String>,
|
||||
/// Base64 encoded JpegPhoto. Deprecated: use attribute instead.
|
||||
/// If both field and corresponding attribute is supplied, the attribute will take precedence.
|
||||
pub avatar: Option<String>,
|
||||
/// Attribute names to remove.
|
||||
/// They are processed before insertions.
|
||||
pub remove_attributes: Option<Vec<String>>,
|
||||
/// Inserts or updates the given attributes.
|
||||
/// For lists, the entire list must be provided.
|
||||
pub 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.
|
||||
pub id: i32,
|
||||
/// The new display name.
|
||||
pub display_name: Option<String>,
|
||||
/// Attribute names to remove.
|
||||
/// They are processed before insertions.
|
||||
pub remove_attributes: Option<Vec<String>>,
|
||||
/// Inserts or updates the given attributes.
|
||||
/// For lists, the entire list must be provided.
|
||||
pub insert_attributes: Option<Vec<AttributeValue>>,
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Eq, Debug, GraphQLObject)]
|
||||
pub struct Success {
|
||||
ok: bool,
|
||||
}
|
||||
|
||||
impl Success {
|
||||
pub fn new() -> Self {
|
||||
Self { ok: true }
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Success {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
+20
-254
@@ -1,27 +1,30 @@
|
||||
pub mod helpers;
|
||||
pub mod inputs;
|
||||
|
||||
// Re-export public types
|
||||
pub use inputs::{
|
||||
AttributeValue, CreateGroupInput, CreateUserInput, Success, UpdateGroupInput, UpdateUserInput,
|
||||
};
|
||||
|
||||
use crate::api::{Context, field_error_callback};
|
||||
use anyhow::{Context as AnyhowContext, anyhow};
|
||||
use juniper::{FieldError, FieldResult, GraphQLInputObject, GraphQLObject, graphql_object};
|
||||
use anyhow::anyhow;
|
||||
use juniper::{FieldError, FieldResult, graphql_object};
|
||||
use lldap_access_control::{
|
||||
AdminBackendHandler, ReadonlyBackendHandler, UserReadableBackendHandler,
|
||||
UserWriteableBackendHandler,
|
||||
AdminBackendHandler, UserReadableBackendHandler, UserWriteableBackendHandler,
|
||||
};
|
||||
use lldap_domain::{
|
||||
deserialize::deserialize_attribute_value,
|
||||
public_schema::PublicSchema,
|
||||
requests::{
|
||||
CreateAttributeRequest, CreateGroupRequest, CreateUserRequest, UpdateGroupRequest,
|
||||
UpdateUserRequest,
|
||||
},
|
||||
schema::AttributeList,
|
||||
types::{
|
||||
Attribute as DomainAttribute, AttributeName, AttributeType, Email, GroupId,
|
||||
LdapObjectClass, UserId,
|
||||
},
|
||||
requests::{CreateAttributeRequest, CreateUserRequest, UpdateGroupRequest, UpdateUserRequest},
|
||||
types::{AttributeName, AttributeType, Email, GroupId, LdapObjectClass, UserId},
|
||||
};
|
||||
use lldap_domain_handlers::handler::BackendHandler;
|
||||
use lldap_validation::attributes::{ALLOWED_CHARACTERS_DESCRIPTION, validate_attribute_name};
|
||||
use std::{collections::BTreeMap, sync::Arc};
|
||||
use tracing::{Instrument, Span, debug, debug_span};
|
||||
use std::sync::Arc;
|
||||
use tracing::{Instrument, debug, debug_span};
|
||||
|
||||
use helpers::{
|
||||
UnpackedAttributes, consolidate_attributes, create_group_with_details, deserialize_attribute,
|
||||
unpack_attributes,
|
||||
};
|
||||
|
||||
#[derive(PartialEq, Eq, Debug)]
|
||||
/// The top-level GraphQL mutation type.
|
||||
@@ -42,183 +45,6 @@ impl<Handler: BackendHandler> Mutation<Handler> {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[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<String>,
|
||||
}
|
||||
|
||||
#[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<String>,
|
||||
display_name: Option<String>,
|
||||
/// First name of user. Deprecated: use attribute instead.
|
||||
/// If both field and corresponding attribute is supplied, the attribute will take precedence.
|
||||
first_name: Option<String>,
|
||||
/// Last name of user. Deprecated: use attribute instead.
|
||||
/// If both field and corresponding attribute is supplied, the attribute will take precedence.
|
||||
last_name: Option<String>,
|
||||
/// Base64 encoded JpegPhoto. Deprecated: use attribute instead.
|
||||
/// If both field and corresponding attribute is supplied, the attribute will take precedence.
|
||||
avatar: Option<String>,
|
||||
/// 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 of user. Deprecated: use attribute instead.
|
||||
/// If both field and corresponding attribute is supplied, the attribute will take precedence.
|
||||
first_name: Option<String>,
|
||||
/// Last name of user. Deprecated: use attribute instead.
|
||||
/// If both field and corresponding attribute is supplied, the attribute will take precedence.
|
||||
last_name: Option<String>,
|
||||
/// Base64 encoded JpegPhoto. Deprecated: use attribute instead.
|
||||
/// If both field and corresponding attribute is supplied, the attribute will take precedence.
|
||||
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 }
|
||||
}
|
||||
}
|
||||
|
||||
struct UnpackedAttributes {
|
||||
email: Option<Email>,
|
||||
display_name: Option<String>,
|
||||
attributes: Vec<DomainAttribute>,
|
||||
}
|
||||
|
||||
fn unpack_attributes(
|
||||
attributes: Vec<AttributeValue>,
|
||||
schema: &PublicSchema,
|
||||
is_admin: bool,
|
||||
) -> FieldResult<UnpackedAttributes> {
|
||||
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.into_string().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.into_string().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::<Result<Vec<_>, _>>()?;
|
||||
Ok(UnpackedAttributes {
|
||||
email,
|
||||
display_name,
|
||||
attributes,
|
||||
})
|
||||
}
|
||||
|
||||
/// Consolidates caller supplied user fields and attributes into a list of attributes.
|
||||
///
|
||||
/// A number of user fields are internally represented as attributes, but are still also
|
||||
/// available as fields on user objects. This function consolidates these fields and the
|
||||
/// given attributes into a resulting attribute list. If a value is supplied for both a
|
||||
/// field and the corresponding attribute, the attribute will take precedence.
|
||||
fn consolidate_attributes(
|
||||
attributes: Vec<AttributeValue>,
|
||||
first_name: Option<String>,
|
||||
last_name: Option<String>,
|
||||
avatar: Option<String>,
|
||||
) -> Vec<AttributeValue> {
|
||||
// Prepare map of the client provided attributes
|
||||
let mut provided_attributes: BTreeMap<AttributeName, AttributeValue> = attributes
|
||||
.into_iter()
|
||||
.map(|x| {
|
||||
(
|
||||
x.name.clone().into(),
|
||||
AttributeValue {
|
||||
name: x.name.to_ascii_lowercase(),
|
||||
value: x.value,
|
||||
},
|
||||
)
|
||||
})
|
||||
.collect::<BTreeMap<_, _>>();
|
||||
// Prepare list of fallback attribute values
|
||||
let field_attrs = [
|
||||
("first_name", first_name),
|
||||
("last_name", last_name),
|
||||
("avatar", avatar),
|
||||
];
|
||||
for (name, value) in field_attrs.into_iter() {
|
||||
if let Some(val) = value {
|
||||
let attr_name: AttributeName = name.into();
|
||||
provided_attributes
|
||||
.entry(attr_name)
|
||||
.or_insert_with(|| AttributeValue {
|
||||
name: name.to_string(),
|
||||
value: vec![val],
|
||||
});
|
||||
}
|
||||
}
|
||||
// Return the values of the resulting map
|
||||
provided_attributes.into_values().collect()
|
||||
}
|
||||
|
||||
#[graphql_object(context = Context<Handler>)]
|
||||
impl<Handler: BackendHandler> Mutation<Handler> {
|
||||
async fn create_user(
|
||||
@@ -721,66 +547,6 @@ 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, 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<DomainAttribute> {
|
||||
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(DomainAttribute {
|
||||
name: attribute_name,
|
||||
value: deserialized_values,
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,267 @@
|
||||
use chrono::TimeZone;
|
||||
use juniper::{FieldResult, graphql_object};
|
||||
use lldap_domain::public_schema::PublicSchema;
|
||||
use lldap_domain::schema::AttributeList as DomainAttributeList;
|
||||
use lldap_domain::schema::AttributeSchema as DomainAttributeSchema;
|
||||
use lldap_domain::types::{Attribute as DomainAttribute, AttributeValue as DomainAttributeValue};
|
||||
use lldap_domain::types::{Cardinality, Group as DomainGroup, GroupDetails, User as DomainUser};
|
||||
use lldap_domain_handlers::handler::BackendHandler;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::api::Context;
|
||||
|
||||
#[derive(PartialEq, Eq, Debug, Serialize, Deserialize)]
|
||||
pub struct AttributeSchema<Handler: BackendHandler> {
|
||||
schema: DomainAttributeSchema,
|
||||
_phantom: std::marker::PhantomData<Box<Handler>>,
|
||||
}
|
||||
|
||||
#[graphql_object(context = Context<Handler>)]
|
||||
impl<Handler: BackendHandler> AttributeSchema<Handler> {
|
||||
fn name(&self) -> String {
|
||||
self.schema.name.to_string()
|
||||
}
|
||||
fn attribute_type(&self) -> lldap_domain::types::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<Handler: BackendHandler> Clone for AttributeSchema<Handler> {
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
schema: self.schema.clone(),
|
||||
_phantom: std::marker::PhantomData,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<Handler: BackendHandler> From<DomainAttributeSchema> for AttributeSchema<Handler> {
|
||||
fn from(value: DomainAttributeSchema) -> Self {
|
||||
Self {
|
||||
schema: value,
|
||||
_phantom: std::marker::PhantomData,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Eq, Debug, Serialize, Deserialize)]
|
||||
pub struct AttributeValue<Handler: BackendHandler> {
|
||||
pub(super) attribute: DomainAttribute,
|
||||
pub(super) schema: AttributeSchema<Handler>,
|
||||
_phantom: std::marker::PhantomData<Box<Handler>>,
|
||||
}
|
||||
|
||||
#[graphql_object(context = Context<Handler>)]
|
||||
impl<Handler: BackendHandler> AttributeValue<Handler> {
|
||||
fn name(&self) -> &str {
|
||||
self.attribute.name.as_str()
|
||||
}
|
||||
|
||||
fn value(&self) -> FieldResult<Vec<String>> {
|
||||
Ok(serialize_attribute_to_graphql(&self.attribute.value))
|
||||
}
|
||||
|
||||
fn schema(&self) -> &AttributeSchema<Handler> {
|
||||
&self.schema
|
||||
}
|
||||
}
|
||||
|
||||
impl<Handler: BackendHandler> AttributeValue<Handler> {
|
||||
fn from_value(attr: DomainAttribute, schema: DomainAttributeSchema) -> Self {
|
||||
Self {
|
||||
attribute: attr,
|
||||
schema: AttributeSchema::<Handler> {
|
||||
schema,
|
||||
_phantom: std::marker::PhantomData,
|
||||
},
|
||||
_phantom: std::marker::PhantomData,
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn name(&self) -> &str {
|
||||
self.attribute.name.as_str()
|
||||
}
|
||||
}
|
||||
|
||||
impl<Handler: BackendHandler> Clone for AttributeValue<Handler> {
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
attribute: self.attribute.clone(),
|
||||
schema: self.schema.clone(),
|
||||
_phantom: std::marker::PhantomData,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn serialize_attribute_to_graphql(attribute_value: &DomainAttributeValue) -> Vec<String> {
|
||||
let convert_date = |&date| chrono::Utc.from_utc_datetime(&date).to_rfc3339();
|
||||
match attribute_value {
|
||||
DomainAttributeValue::String(Cardinality::Singleton(s)) => vec![s.clone()],
|
||||
DomainAttributeValue::String(Cardinality::Unbounded(l)) => l.clone(),
|
||||
DomainAttributeValue::Integer(Cardinality::Singleton(i)) => vec![i.to_string()],
|
||||
DomainAttributeValue::Integer(Cardinality::Unbounded(l)) => {
|
||||
l.iter().map(|i| i.to_string()).collect()
|
||||
}
|
||||
DomainAttributeValue::DateTime(Cardinality::Singleton(dt)) => vec![convert_date(dt)],
|
||||
DomainAttributeValue::DateTime(Cardinality::Unbounded(l)) => {
|
||||
l.iter().map(convert_date).collect()
|
||||
}
|
||||
DomainAttributeValue::JpegPhoto(Cardinality::Singleton(p)) => vec![String::from(p)],
|
||||
DomainAttributeValue::JpegPhoto(Cardinality::Unbounded(l)) => {
|
||||
l.iter().map(String::from).collect()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<Handler: BackendHandler> AttributeValue<Handler> {
|
||||
fn from_schema(a: DomainAttribute, schema: &DomainAttributeList) -> Option<Self> {
|
||||
schema
|
||||
.get_attribute_schema(&a.name)
|
||||
.map(|s| AttributeValue::<Handler>::from_value(a, s.clone()))
|
||||
}
|
||||
|
||||
pub fn user_attributes_from_schema(
|
||||
user: &mut DomainUser,
|
||||
schema: &PublicSchema,
|
||||
) -> Vec<AttributeValue<Handler>> {
|
||||
let user_attributes = std::mem::take(&mut user.attributes);
|
||||
let mut all_attributes = schema
|
||||
.get_schema()
|
||||
.user_attributes
|
||||
.attributes
|
||||
.iter()
|
||||
.filter(|a| a.is_hardcoded)
|
||||
.flat_map(|attribute_schema| {
|
||||
let value: Option<DomainAttributeValue> = match attribute_schema.name.as_str() {
|
||||
"user_id" => Some(user.user_id.clone().into_string().into()),
|
||||
"creation_date" => Some(user.creation_date.into()),
|
||||
"modified_date" => Some(user.modified_date.into()),
|
||||
"password_modified_date" => Some(user.password_modified_date.into()),
|
||||
"mail" => Some(user.email.clone().into_string().into()),
|
||||
"uuid" => Some(user.uuid.clone().into_string().into()),
|
||||
"display_name" => user.display_name.as_ref().map(|d| d.clone().into()),
|
||||
"avatar" | "first_name" | "last_name" => None,
|
||||
_ => panic!("Unexpected hardcoded attribute: {}", attribute_schema.name),
|
||||
};
|
||||
value.map(|v| (attribute_schema, v))
|
||||
})
|
||||
.map(|(attribute_schema, value)| {
|
||||
AttributeValue::<Handler>::from_value(
|
||||
DomainAttribute {
|
||||
name: attribute_schema.name.clone(),
|
||||
value,
|
||||
},
|
||||
attribute_schema.clone(),
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
user_attributes
|
||||
.into_iter()
|
||||
.flat_map(|a| {
|
||||
AttributeValue::<Handler>::from_schema(a, &schema.get_schema().user_attributes)
|
||||
})
|
||||
.for_each(|value| all_attributes.push(value));
|
||||
all_attributes
|
||||
}
|
||||
|
||||
pub fn group_attributes_from_schema(
|
||||
group: &mut DomainGroup,
|
||||
schema: &PublicSchema,
|
||||
) -> Vec<AttributeValue<Handler>> {
|
||||
let group_attributes = std::mem::take(&mut group.attributes);
|
||||
let mut all_attributes = schema
|
||||
.get_schema()
|
||||
.group_attributes
|
||||
.attributes
|
||||
.iter()
|
||||
.filter(|a| a.is_hardcoded)
|
||||
.map(|attribute_schema| {
|
||||
(
|
||||
attribute_schema,
|
||||
match attribute_schema.name.as_str() {
|
||||
"group_id" => (group.id.0 as i64).into(),
|
||||
"creation_date" => group.creation_date.into(),
|
||||
"modified_date" => group.modified_date.into(),
|
||||
"uuid" => group.uuid.clone().into_string().into(),
|
||||
"display_name" => group.display_name.clone().into_string().into(),
|
||||
_ => panic!("Unexpected hardcoded attribute: {}", attribute_schema.name),
|
||||
},
|
||||
)
|
||||
})
|
||||
.map(|(attribute_schema, value)| {
|
||||
AttributeValue::<Handler>::from_value(
|
||||
DomainAttribute {
|
||||
name: attribute_schema.name.clone(),
|
||||
value,
|
||||
},
|
||||
attribute_schema.clone(),
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
group_attributes
|
||||
.into_iter()
|
||||
.flat_map(|a| {
|
||||
AttributeValue::<Handler>::from_schema(a, &schema.get_schema().group_attributes)
|
||||
})
|
||||
.for_each(|value| all_attributes.push(value));
|
||||
all_attributes
|
||||
}
|
||||
|
||||
pub fn group_details_attributes_from_schema(
|
||||
group: &mut GroupDetails,
|
||||
schema: &PublicSchema,
|
||||
) -> Vec<AttributeValue<Handler>> {
|
||||
let group_attributes = std::mem::take(&mut group.attributes);
|
||||
let mut all_attributes = schema
|
||||
.get_schema()
|
||||
.group_attributes
|
||||
.attributes
|
||||
.iter()
|
||||
.filter(|a| a.is_hardcoded)
|
||||
.map(|attribute_schema| {
|
||||
(
|
||||
attribute_schema,
|
||||
match attribute_schema.name.as_str() {
|
||||
"group_id" => (group.group_id.0 as i64).into(),
|
||||
"creation_date" => group.creation_date.into(),
|
||||
"modified_date" => group.modified_date.into(),
|
||||
"uuid" => group.uuid.clone().into_string().into(),
|
||||
"display_name" => group.display_name.clone().into_string().into(),
|
||||
_ => panic!("Unexpected hardcoded attribute: {}", attribute_schema.name),
|
||||
},
|
||||
)
|
||||
})
|
||||
.map(|(attribute_schema, value)| {
|
||||
AttributeValue::<Handler>::from_value(
|
||||
DomainAttribute {
|
||||
name: attribute_schema.name.clone(),
|
||||
value,
|
||||
},
|
||||
attribute_schema.clone(),
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
group_attributes
|
||||
.into_iter()
|
||||
.flat_map(|a| {
|
||||
AttributeValue::<Handler>::from_schema(a, &schema.get_schema().group_attributes)
|
||||
})
|
||||
.for_each(|value| all_attributes.push(value));
|
||||
all_attributes
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
use anyhow::Context as AnyhowContext;
|
||||
use juniper::{FieldResult, GraphQLInputObject};
|
||||
use lldap_domain::deserialize::deserialize_attribute_value;
|
||||
use lldap_domain::public_schema::PublicSchema;
|
||||
use lldap_domain::types::GroupId;
|
||||
use lldap_domain::types::UserId;
|
||||
use lldap_domain_handlers::handler::UserRequestFilter as DomainRequestFilter;
|
||||
use lldap_domain_model::model::UserColumn;
|
||||
use lldap_ldap::{UserFieldType, map_user_field};
|
||||
|
||||
#[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<Vec<RequestFilter>>,
|
||||
all: Option<Vec<RequestFilter>>,
|
||||
not: Option<Box<RequestFilter>>,
|
||||
eq: Option<EqualityConstraint>,
|
||||
member_of: Option<String>,
|
||||
member_of_id: Option<i32>,
|
||||
}
|
||||
|
||||
impl RequestFilter {
|
||||
pub fn try_into_domain_filter(self, schema: &PublicSchema) -> FieldResult<DomainRequestFilter> {
|
||||
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::<FieldResult<Vec<_>>>()?,
|
||||
)),
|
||||
(None, None, Some(all), None, None, None) => Ok(DomainRequestFilter::And(
|
||||
all.into_iter()
|
||||
.map(|f| f.try_into_domain_filter(schema))
|
||||
.collect::<FieldResult<Vec<_>>>()?,
|
||||
)),
|
||||
(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,
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
use chrono::TimeZone;
|
||||
use juniper::{FieldResult, graphql_object};
|
||||
use lldap_access_control::ReadonlyBackendHandler;
|
||||
use lldap_domain::public_schema::PublicSchema;
|
||||
use lldap_domain::types::{Group as DomainGroup, GroupDetails, GroupId};
|
||||
use lldap_domain_handlers::handler::{BackendHandler, UserRequestFilter as DomainRequestFilter};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::sync::Arc;
|
||||
use tracing::{Instrument, debug, debug_span};
|
||||
|
||||
use super::attribute::AttributeValue;
|
||||
use super::user::User;
|
||||
use crate::api::{Context, field_error_callback};
|
||||
|
||||
#[derive(PartialEq, Eq, Debug, Serialize, Deserialize)]
|
||||
/// Represents a single group.
|
||||
pub struct Group<Handler: BackendHandler> {
|
||||
pub group_id: i32,
|
||||
pub display_name: String,
|
||||
creation_date: chrono::NaiveDateTime,
|
||||
uuid: String,
|
||||
attributes: Vec<AttributeValue<Handler>>,
|
||||
pub schema: Arc<PublicSchema>,
|
||||
_phantom: std::marker::PhantomData<Box<Handler>>,
|
||||
}
|
||||
|
||||
impl<Handler: BackendHandler> Group<Handler> {
|
||||
pub fn from_group(
|
||||
mut group: DomainGroup,
|
||||
schema: Arc<PublicSchema>,
|
||||
) -> FieldResult<Group<Handler>> {
|
||||
let attributes =
|
||||
AttributeValue::<Handler>::group_attributes_from_schema(&mut group, &schema);
|
||||
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,
|
||||
schema,
|
||||
_phantom: std::marker::PhantomData,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn from_group_details(
|
||||
mut group_details: GroupDetails,
|
||||
schema: Arc<PublicSchema>,
|
||||
) -> FieldResult<Group<Handler>> {
|
||||
let attributes = AttributeValue::<Handler>::group_details_attributes_from_schema(
|
||||
&mut group_details,
|
||||
&schema,
|
||||
);
|
||||
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,
|
||||
schema,
|
||||
_phantom: std::marker::PhantomData,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl<Handler: BackendHandler> Clone for Group<Handler> {
|
||||
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<Handler>)]
|
||||
impl<Handler: BackendHandler> Group<Handler> {
|
||||
fn id(&self) -> i32 {
|
||||
self.group_id
|
||||
}
|
||||
fn display_name(&self) -> String {
|
||||
self.display_name.clone()
|
||||
}
|
||||
fn creation_date(&self) -> chrono::DateTime<chrono::Utc> {
|
||||
chrono::Utc.from_utc_datetime(&self.creation_date)
|
||||
}
|
||||
fn uuid(&self) -> String {
|
||||
self.uuid.clone()
|
||||
}
|
||||
|
||||
/// User-defined attributes.
|
||||
fn attributes(&self) -> &[AttributeValue<Handler>] {
|
||||
&self.attributes
|
||||
}
|
||||
|
||||
/// The groups to which this user belongs.
|
||||
async fn users(&self, context: &Context<Handler>) -> FieldResult<Vec<User<Handler>>> {
|
||||
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::<Handler>::from_user_and_groups(u, self.schema.clone()))
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,539 @@
|
||||
pub mod attribute;
|
||||
pub mod filters;
|
||||
pub mod group;
|
||||
pub mod schema;
|
||||
pub mod user;
|
||||
|
||||
// Re-export public types
|
||||
pub use attribute::{AttributeSchema, AttributeValue, serialize_attribute_to_graphql};
|
||||
pub use filters::{EqualityConstraint, RequestFilter};
|
||||
pub use group::Group;
|
||||
pub use schema::{AttributeList, ObjectClassInfo, Schema};
|
||||
pub use user::User;
|
||||
|
||||
use juniper::{FieldResult, graphql_object};
|
||||
use lldap_access_control::{ReadonlyBackendHandler, UserReadableBackendHandler};
|
||||
use lldap_domain::public_schema::PublicSchema;
|
||||
use lldap_domain::types::{GroupId, UserId};
|
||||
use lldap_domain_handlers::handler::{BackendHandler, ReadSchemaBackendHandler};
|
||||
use std::sync::Arc;
|
||||
use tracing::{Instrument, Span, debug, debug_span};
|
||||
|
||||
use crate::api::{Context, field_error_callback};
|
||||
|
||||
#[derive(PartialEq, Eq, Debug)]
|
||||
/// The top-level GraphQL query type.
|
||||
pub struct Query<Handler: BackendHandler> {
|
||||
_phantom: std::marker::PhantomData<Box<Handler>>,
|
||||
}
|
||||
|
||||
impl<Handler: BackendHandler> Default for Query<Handler> {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl<Handler: BackendHandler> Query<Handler> {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
_phantom: std::marker::PhantomData,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[graphql_object(context = Context<Handler>)]
|
||||
impl<Handler: BackendHandler> Query<Handler> {
|
||||
fn api_version() -> &'static str {
|
||||
"1.0"
|
||||
}
|
||||
|
||||
pub async fn user(context: &Context<Handler>, user_id: String) -> FieldResult<User<Handler>> {
|
||||
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::<Handler>::from_user(user, schema)
|
||||
}
|
||||
|
||||
async fn users(
|
||||
context: &Context<Handler>,
|
||||
#[graphql(name = "where")] filters: Option<RequestFilter>,
|
||||
) -> FieldResult<Vec<User<Handler>>> {
|
||||
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::<Handler>::from_user_and_groups(u, schema.clone()))
|
||||
.collect()
|
||||
}
|
||||
|
||||
async fn groups(context: &Context<Handler>) -> FieldResult<Vec<Group<Handler>>> {
|
||||
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::<Handler>::from_group(g, schema.clone()))
|
||||
.collect()
|
||||
}
|
||||
|
||||
async fn group(context: &Context<Handler>, group_id: i32) -> FieldResult<Group<Handler>> {
|
||||
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::<Handler>::from_group_details(group_details, schema.clone())
|
||||
}
|
||||
|
||||
async fn schema(context: &Context<Handler>) -> FieldResult<Schema<Handler>> {
|
||||
let span = debug_span!("[GraphQL query] get_schema");
|
||||
self.get_schema(context, span).await.map(Into::into)
|
||||
}
|
||||
}
|
||||
|
||||
impl<Handler: BackendHandler> Query<Handler> {
|
||||
async fn get_schema(
|
||||
&self,
|
||||
context: &Context<Handler>,
|
||||
span: Span,
|
||||
) -> FieldResult<PublicSchema> {
|
||||
let handler = context
|
||||
.handler
|
||||
.get_user_restricted_lister_handler(&context.validation_result);
|
||||
Ok(handler
|
||||
.get_schema()
|
||||
.instrument(span)
|
||||
.await
|
||||
.map(Into::<PublicSchema>::into)?)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use chrono::TimeZone;
|
||||
use juniper::{
|
||||
DefaultScalarValue, EmptyMutation, EmptySubscription, GraphQLType, RootNode, Variables,
|
||||
execute, graphql_value,
|
||||
};
|
||||
use lldap_auth::access_control::{Permission, ValidationResults};
|
||||
use lldap_domain::schema::AttributeSchema as DomainAttributeSchema;
|
||||
use lldap_domain::types::{Attribute as DomainAttribute, GroupDetails, User as DomainUser};
|
||||
use lldap_domain::{
|
||||
schema::{AttributeList, Schema},
|
||||
types::{AttributeName, AttributeType, LdapObjectClass},
|
||||
};
|
||||
use lldap_domain_model::model::UserColumn;
|
||||
use lldap_test_utils::{MockTestBackendHandler, setup_default_schema};
|
||||
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<C>, EmptySubscription<C>>
|
||||
where
|
||||
Q: GraphQLType<DefaultScalarValue, Context = C, TypeInfo = ()> + 'q,
|
||||
{
|
||||
RootNode::new(
|
||||
query_root,
|
||||
EmptyMutation::<C>::new(),
|
||||
EmptySubscription::<C>::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(Schema {
|
||||
user_attributes: AttributeList {
|
||||
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: AttributeList {
|
||||
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(),
|
||||
display_name: None,
|
||||
creation_date: chrono::Utc.timestamp_millis_opt(42).unwrap().naive_utc(),
|
||||
modified_date: chrono::Utc.timestamp_opt(0, 0).unwrap().naive_utc(),
|
||||
password_modified_date: chrono::Utc.timestamp_opt(0, 0).unwrap().naive_utc(),
|
||||
uuid: lldap_domain::types::Uuid::from_name_and_date(
|
||||
"bob",
|
||||
&chrono::Utc.timestamp_millis_opt(42).unwrap().naive_utc(),
|
||||
),
|
||||
attributes: vec![
|
||||
DomainAttribute {
|
||||
name: "first_name".into(),
|
||||
value: "Bob".to_string().into(),
|
||||
},
|
||||
DomainAttribute {
|
||||
name: "last_name".into(),
|
||||
value: "Bobberson".to_string().into(),
|
||||
},
|
||||
],
|
||||
})
|
||||
});
|
||||
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: lldap_domain::types::Uuid::from_name_and_date(
|
||||
"Bobbersons",
|
||||
&chrono::Utc.timestamp_nanos(42).naive_utc(),
|
||||
),
|
||||
attributes: vec![DomainAttribute {
|
||||
name: "club_name".into(),
|
||||
value: "Gang of Four".to_string().into(),
|
||||
}],
|
||||
modified_date: chrono::Utc.timestamp_nanos(42).naive_utc(),
|
||||
});
|
||||
groups.insert(GroupDetails {
|
||||
group_id: GroupId(7),
|
||||
display_name: "Jefferees".into(),
|
||||
creation_date: chrono::Utc.timestamp_nanos(12).naive_utc(),
|
||||
uuid: lldap_domain::types::Uuid::from_name_and_date(
|
||||
"Jefferees",
|
||||
&chrono::Utc.timestamp_nanos(12).naive_utc(),
|
||||
),
|
||||
attributes: Vec::new(),
|
||||
modified_date: chrono::Utc.timestamp_nanos(12).naive_utc(),
|
||||
});
|
||||
mock.expect_get_user_groups()
|
||||
.with(eq(UserId::new("bob")))
|
||||
.return_once(|_| Ok(groups));
|
||||
|
||||
let context = Context::<MockTestBackendHandler>::new_for_tests(
|
||||
mock,
|
||||
ValidationResults {
|
||||
user: UserId::new("admin"),
|
||||
permission: Permission::Admin,
|
||||
},
|
||||
);
|
||||
|
||||
let schema = schema(Query::<MockTestBackendHandler>::new());
|
||||
let result = execute(QUERY, None, &schema, &Variables::new(), &context).await;
|
||||
assert!(result.is_ok(), "Query failed: {:?}", result);
|
||||
}
|
||||
|
||||
#[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(lldap_domain_handlers::handler::UserRequestFilter::Or(
|
||||
vec![
|
||||
lldap_domain_handlers::handler::UserRequestFilter::UserId(UserId::new(
|
||||
"bob",
|
||||
)),
|
||||
lldap_domain_handlers::handler::UserRequestFilter::Equality(
|
||||
UserColumn::Email,
|
||||
"robert@bobbers.on".to_owned(),
|
||||
),
|
||||
lldap_domain_handlers::handler::UserRequestFilter::AttributeEquality(
|
||||
AttributeName::from("first_name"),
|
||||
"robert".to_string().into(),
|
||||
),
|
||||
],
|
||||
))),
|
||||
eq(false),
|
||||
)
|
||||
.return_once(|_, _| {
|
||||
Ok(vec![
|
||||
lldap_domain::types::UserAndGroups {
|
||||
user: DomainUser {
|
||||
user_id: UserId::new("bob"),
|
||||
email: "bob@bobbers.on".into(),
|
||||
display_name: None,
|
||||
creation_date: chrono::Utc.timestamp_opt(0, 0).unwrap().naive_utc(),
|
||||
modified_date: chrono::Utc.timestamp_opt(0, 0).unwrap().naive_utc(),
|
||||
password_modified_date: chrono::Utc
|
||||
.timestamp_opt(0, 0)
|
||||
.unwrap()
|
||||
.naive_utc(),
|
||||
uuid: lldap_domain::types::Uuid::from_name_and_date(
|
||||
"bob",
|
||||
&chrono::Utc.timestamp_opt(0, 0).unwrap().naive_utc(),
|
||||
),
|
||||
attributes: Vec::new(),
|
||||
},
|
||||
groups: None,
|
||||
},
|
||||
lldap_domain::types::UserAndGroups {
|
||||
user: DomainUser {
|
||||
user_id: UserId::new("robert"),
|
||||
email: "robert@bobbers.on".into(),
|
||||
display_name: None,
|
||||
creation_date: chrono::Utc.timestamp_opt(0, 0).unwrap().naive_utc(),
|
||||
modified_date: chrono::Utc.timestamp_opt(0, 0).unwrap().naive_utc(),
|
||||
password_modified_date: chrono::Utc
|
||||
.timestamp_opt(0, 0)
|
||||
.unwrap()
|
||||
.naive_utc(),
|
||||
uuid: lldap_domain::types::Uuid::from_name_and_date(
|
||||
"robert",
|
||||
&chrono::Utc.timestamp_opt(0, 0).unwrap().naive_utc(),
|
||||
),
|
||||
attributes: Vec::new(),
|
||||
},
|
||||
groups: None,
|
||||
},
|
||||
])
|
||||
});
|
||||
|
||||
let context = Context::<MockTestBackendHandler>::new_for_tests(
|
||||
mock,
|
||||
ValidationResults {
|
||||
user: UserId::new("admin"),
|
||||
permission: Permission::Admin,
|
||||
},
|
||||
);
|
||||
|
||||
let schema = schema(Query::<MockTestBackendHandler>::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::<MockTestBackendHandler>::new_for_tests(
|
||||
mock,
|
||||
ValidationResults {
|
||||
user: UserId::new("admin"),
|
||||
permission: Permission::Admin,
|
||||
},
|
||||
);
|
||||
|
||||
let schema = schema(Query::<MockTestBackendHandler>::new());
|
||||
let result = execute(QUERY, None, &schema, &Variables::new(), &context).await;
|
||||
assert!(result.is_ok(), "Query failed: {:?}", result);
|
||||
}
|
||||
|
||||
#[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(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::<MockTestBackendHandler>::new_for_tests(
|
||||
mock,
|
||||
ValidationResults {
|
||||
user: UserId::new("bob"),
|
||||
permission: Permission::Regular,
|
||||
},
|
||||
);
|
||||
|
||||
let schema = schema(Query::<MockTestBackendHandler>::new());
|
||||
let result = execute(QUERY, None, &schema, &Variables::new(), &context).await;
|
||||
assert!(result.is_ok(), "Query failed: {:?}", result);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
use juniper::graphql_object;
|
||||
use lldap_domain::public_schema::PublicSchema;
|
||||
use lldap_domain::schema::AttributeList as DomainAttributeList;
|
||||
use lldap_domain::types::LdapObjectClass;
|
||||
use lldap_domain_handlers::handler::BackendHandler;
|
||||
use lldap_ldap::{get_default_group_object_classes, get_default_user_object_classes};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use super::attribute::AttributeSchema;
|
||||
use crate::api::Context;
|
||||
|
||||
#[derive(PartialEq, Eq, Debug, Serialize, Deserialize)]
|
||||
pub struct AttributeList<Handler: BackendHandler> {
|
||||
attributes: DomainAttributeList,
|
||||
default_classes: Vec<LdapObjectClass>,
|
||||
extra_classes: Vec<LdapObjectClass>,
|
||||
_phantom: std::marker::PhantomData<Box<Handler>>,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct ObjectClassInfo {
|
||||
object_class: String,
|
||||
is_hardcoded: bool,
|
||||
}
|
||||
|
||||
#[graphql_object]
|
||||
impl ObjectClassInfo {
|
||||
fn object_class(&self) -> &str {
|
||||
&self.object_class
|
||||
}
|
||||
|
||||
fn is_hardcoded(&self) -> bool {
|
||||
self.is_hardcoded
|
||||
}
|
||||
}
|
||||
|
||||
#[graphql_object(context = Context<Handler>)]
|
||||
impl<Handler: BackendHandler> AttributeList<Handler> {
|
||||
fn attributes(&self) -> Vec<AttributeSchema<Handler>> {
|
||||
self.attributes
|
||||
.attributes
|
||||
.clone()
|
||||
.into_iter()
|
||||
.map(Into::into)
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn extra_ldap_object_classes(&self) -> Vec<String> {
|
||||
self.extra_classes.iter().map(|c| c.to_string()).collect()
|
||||
}
|
||||
|
||||
fn ldap_object_classes(&self) -> Vec<ObjectClassInfo> {
|
||||
let mut all_object_classes: Vec<ObjectClassInfo> = self
|
||||
.default_classes
|
||||
.iter()
|
||||
.map(|c| ObjectClassInfo {
|
||||
object_class: c.to_string(),
|
||||
is_hardcoded: true,
|
||||
})
|
||||
.collect();
|
||||
|
||||
all_object_classes.extend(self.extra_classes.iter().map(|c| ObjectClassInfo {
|
||||
object_class: c.to_string(),
|
||||
is_hardcoded: false,
|
||||
}));
|
||||
|
||||
all_object_classes
|
||||
}
|
||||
}
|
||||
|
||||
impl<Handler: BackendHandler> AttributeList<Handler> {
|
||||
pub fn new(
|
||||
attributes: DomainAttributeList,
|
||||
default_classes: Vec<LdapObjectClass>,
|
||||
extra_classes: Vec<LdapObjectClass>,
|
||||
) -> Self {
|
||||
Self {
|
||||
attributes,
|
||||
default_classes,
|
||||
extra_classes,
|
||||
_phantom: std::marker::PhantomData,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Eq, Debug, Serialize, Deserialize)]
|
||||
pub struct Schema<Handler: BackendHandler> {
|
||||
schema: PublicSchema,
|
||||
_phantom: std::marker::PhantomData<Box<Handler>>,
|
||||
}
|
||||
|
||||
#[graphql_object(context = Context<Handler>)]
|
||||
impl<Handler: BackendHandler> Schema<Handler> {
|
||||
fn user_schema(&self) -> AttributeList<Handler> {
|
||||
AttributeList::<Handler>::new(
|
||||
self.schema.get_schema().user_attributes.clone(),
|
||||
get_default_user_object_classes(),
|
||||
self.schema.get_schema().extra_user_object_classes.clone(),
|
||||
)
|
||||
}
|
||||
fn group_schema(&self) -> AttributeList<Handler> {
|
||||
AttributeList::<Handler>::new(
|
||||
self.schema.get_schema().group_attributes.clone(),
|
||||
get_default_group_object_classes(),
|
||||
self.schema.get_schema().extra_group_object_classes.clone(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl<Handler: BackendHandler> From<PublicSchema> for Schema<Handler> {
|
||||
fn from(value: PublicSchema) -> Self {
|
||||
Self {
|
||||
schema: value,
|
||||
_phantom: std::marker::PhantomData,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
use chrono::TimeZone;
|
||||
use juniper::{FieldResult, graphql_object};
|
||||
use lldap_access_control::UserReadableBackendHandler;
|
||||
use lldap_domain::public_schema::PublicSchema;
|
||||
use lldap_domain::types::{User as DomainUser, UserAndGroups as DomainUserAndGroups};
|
||||
use lldap_domain_handlers::handler::BackendHandler;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::sync::Arc;
|
||||
use tracing::{Instrument, debug, debug_span};
|
||||
|
||||
use super::attribute::AttributeValue;
|
||||
use super::group::Group;
|
||||
use crate::api::Context;
|
||||
|
||||
#[derive(PartialEq, Eq, Debug, Serialize, Deserialize)]
|
||||
/// Represents a single user.
|
||||
pub struct User<Handler: BackendHandler> {
|
||||
user: DomainUser,
|
||||
attributes: Vec<AttributeValue<Handler>>,
|
||||
schema: Arc<PublicSchema>,
|
||||
groups: Option<Vec<Group<Handler>>>,
|
||||
_phantom: std::marker::PhantomData<Box<Handler>>,
|
||||
}
|
||||
|
||||
impl<Handler: BackendHandler> User<Handler> {
|
||||
pub fn from_user(mut user: DomainUser, schema: Arc<PublicSchema>) -> FieldResult<Self> {
|
||||
let attributes = AttributeValue::<Handler>::user_attributes_from_schema(&mut user, &schema);
|
||||
Ok(Self {
|
||||
user,
|
||||
attributes,
|
||||
schema,
|
||||
groups: None,
|
||||
_phantom: std::marker::PhantomData,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl<Handler: BackendHandler> User<Handler> {
|
||||
pub fn from_user_and_groups(
|
||||
DomainUserAndGroups { user, groups }: DomainUserAndGroups,
|
||||
schema: Arc<PublicSchema>,
|
||||
) -> FieldResult<Self> {
|
||||
let mut user = Self::from_user(user, schema.clone())?;
|
||||
if let Some(groups) = groups {
|
||||
user.groups = Some(
|
||||
groups
|
||||
.into_iter()
|
||||
.map(|g| Group::<Handler>::from_group_details(g, schema.clone()))
|
||||
.collect::<FieldResult<Vec<_>>>()?,
|
||||
);
|
||||
}
|
||||
Ok(user)
|
||||
}
|
||||
}
|
||||
|
||||
#[graphql_object(context = Context<Handler>)]
|
||||
impl<Handler: BackendHandler> User<Handler> {
|
||||
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.name() == "first_name")
|
||||
.map(|a| a.attribute.value.as_str().unwrap_or_default())
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
fn last_name(&self) -> &str {
|
||||
self.attributes
|
||||
.iter()
|
||||
.find(|a| a.name() == "last_name")
|
||||
.map(|a| a.attribute.value.as_str().unwrap_or_default())
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
fn avatar(&self) -> Option<String> {
|
||||
self.attributes
|
||||
.iter()
|
||||
.find(|a| a.name() == "avatar")
|
||||
.map(|a| {
|
||||
String::from(
|
||||
a.attribute
|
||||
.value
|
||||
.as_jpeg_photo()
|
||||
.expect("Invalid JPEG returned by the DB"),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
fn creation_date(&self) -> chrono::DateTime<chrono::Utc> {
|
||||
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<Handler>] {
|
||||
&self.attributes
|
||||
}
|
||||
|
||||
/// The groups to which this user belongs.
|
||||
async fn groups(&self, context: &Context<Handler>) -> FieldResult<Vec<Group<Handler>>> {
|
||||
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::<Handler>::from_group_details(g, self.schema.clone()))
|
||||
.collect::<FieldResult<Vec<Group<Handler>>>>()?;
|
||||
groups.sort_by(|g1, g2| g1.display_name.cmp(&g2.display_name));
|
||||
Ok(groups)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user