From 2a5fd014391695187e2f7c76d093f18a3f8454fb Mon Sep 17 00:00:00 2001 From: Valentin Tolmer Date: Thu, 28 Sep 2023 01:37:48 +0200 Subject: [PATCH] server: add support for creating a group with attributes --- server/src/domain/handler.rs | 10 +- server/src/domain/sql_backend_handler.rs | 10 +- .../src/domain/sql_group_backend_handler.rs | 240 ++++++++++++++++-- server/src/domain/sql_user_backend_handler.rs | 6 +- server/src/infra/access_control.rs | 8 +- server/src/infra/graphql/mutation.rs | 17 +- server/src/infra/test_utils.rs | 2 +- server/src/main.rs | 9 +- 8 files changed, 262 insertions(+), 40 deletions(-) diff --git a/server/src/domain/handler.rs b/server/src/domain/handler.rs index 65d85fe..d8afc1e 100644 --- a/server/src/domain/handler.rs +++ b/server/src/domain/handler.rs @@ -120,10 +120,18 @@ pub struct UpdateUserRequest { pub insert_attributes: Vec, } +#[derive(PartialEq, Eq, Debug, Serialize, Deserialize, Clone, Default)] +pub struct CreateGroupRequest { + pub display_name: String, + pub attributes: Vec, +} + #[derive(PartialEq, Eq, Debug, Serialize, Deserialize, Clone)] pub struct UpdateGroupRequest { pub group_id: GroupId, pub display_name: Option, + pub delete_attributes: Vec, + pub insert_attributes: Vec, } #[derive(PartialEq, Eq, Debug, Serialize, Deserialize, Clone)] @@ -180,7 +188,7 @@ pub trait GroupListerBackendHandler: ReadSchemaBackendHandler { pub trait GroupBackendHandler: ReadSchemaBackendHandler { async fn get_group_details(&self, group_id: GroupId) -> Result; async fn update_group(&self, request: UpdateGroupRequest) -> Result<()>; - async fn create_group(&self, group_name: &str) -> Result; + async fn create_group(&self, request: CreateGroupRequest) -> Result; async fn delete_group(&self, group_id: GroupId) -> Result<()>; } diff --git a/server/src/domain/sql_backend_handler.rs b/server/src/domain/sql_backend_handler.rs index cc906a2..00e8318 100644 --- a/server/src/domain/sql_backend_handler.rs +++ b/server/src/domain/sql_backend_handler.rs @@ -23,7 +23,7 @@ pub mod tests { use crate::{ domain::{ handler::{ - CreateUserRequest, GroupBackendHandler, UserBackendHandler, + CreateGroupRequest, CreateUserRequest, GroupBackendHandler, UserBackendHandler, UserListerBackendHandler, UserRequestFilter, }, sql_tables::init_table, @@ -98,7 +98,13 @@ pub mod tests { } pub async fn insert_group(handler: &SqlBackendHandler, name: &str) -> GroupId { - handler.create_group(name).await.unwrap() + handler + .create_group(CreateGroupRequest { + display_name: name.to_owned(), + ..Default::default() + }) + .await + .unwrap() } pub async fn insert_membership(handler: &SqlBackendHandler, group_id: GroupId, user_id: &str) { diff --git a/server/src/domain/sql_group_backend_handler.rs b/server/src/domain/sql_group_backend_handler.rs index 0752fcb..0ce402b 100644 --- a/server/src/domain/sql_group_backend_handler.rs +++ b/server/src/domain/sql_group_backend_handler.rs @@ -1,7 +1,8 @@ use crate::domain::{ error::{DomainError, Result}, handler::{ - GroupBackendHandler, GroupListerBackendHandler, GroupRequestFilter, UpdateGroupRequest, + CreateGroupRequest, GroupBackendHandler, GroupListerBackendHandler, GroupRequestFilter, + UpdateGroupRequest, }, model::{self, GroupColumn, MembershipColumn}, sql_backend_handler::SqlBackendHandler, @@ -9,9 +10,9 @@ use crate::domain::{ }; use async_trait::async_trait; use sea_orm::{ - sea_query::{Alias, Cond, Expr, Func, IntoCondition, SimpleExpr}, - ActiveModelTrait, ActiveValue, ColumnTrait, EntityTrait, QueryFilter, QueryOrder, QuerySelect, - QueryTrait, + sea_query::{Alias, Cond, Expr, Func, IntoCondition, OnConflict, SimpleExpr}, + ActiveModelTrait, ColumnTrait, DatabaseTransaction, EntityTrait, QueryFilter, QueryOrder, + QuerySelect, QueryTrait, Set, TransactionTrait, }; use tracing::instrument; @@ -139,29 +140,61 @@ impl GroupBackendHandler for SqlBackendHandler { #[instrument(skip(self), level = "debug", err, fields(group_id = ?request.group_id))] async fn update_group(&self, request: UpdateGroupRequest) -> Result<()> { - let update_group = model::groups::ActiveModel { - group_id: ActiveValue::Set(request.group_id), - display_name: request - .display_name - .map(ActiveValue::Set) - .unwrap_or_default(), - ..Default::default() - }; - update_group.update(&self.sql_pool).await?; - Ok(()) + Ok(self + .sql_pool + .transaction::<_, (), DomainError>(|transaction| { + Box::pin( + async move { Self::update_group_with_transaction(request, transaction).await }, + ) + }) + .await?) } #[instrument(skip(self), level = "debug", ret, err)] - async fn create_group(&self, group_name: &str) -> Result { + async fn create_group(&self, request: CreateGroupRequest) -> Result { let now = chrono::Utc::now().naive_utc(); - let uuid = Uuid::from_name_and_date(group_name, &now); + let uuid = Uuid::from_name_and_date(&request.display_name, &now); let new_group = model::groups::ActiveModel { - display_name: ActiveValue::Set(group_name.to_owned()), - creation_date: ActiveValue::Set(now), - uuid: ActiveValue::Set(uuid), + display_name: Set(request.display_name), + creation_date: Set(now), + uuid: Set(uuid), ..Default::default() }; - Ok(new_group.insert(&self.sql_pool).await?.group_id) + Ok(self + .sql_pool + .transaction::<_, GroupId, DomainError>(|transaction| { + Box::pin(async move { + let schema = Self::get_schema_with_transaction(transaction).await?; + let group_id = new_group.insert(transaction).await?.group_id; + let mut new_group_attributes = Vec::new(); + for attribute in request.attributes { + if schema + .group_attributes + .get_attribute_type(&attribute.name) + .is_some() + { + new_group_attributes.push(model::group_attributes::ActiveModel { + group_id: Set(group_id), + attribute_name: Set(attribute.name), + value: Set(attribute.value), + }); + } else { + return Err(DomainError::InternalError(format!( + "Attribute name {} doesn't exist in the group schema, + yet was attempted to be inserted in the database", + &attribute.name + ))); + } + } + if !new_group_attributes.is_empty() { + model::GroupAttributes::insert_many(new_group_attributes) + .exec(transaction) + .await?; + } + Ok(group_id) + }) + }) + .await?) } #[instrument(skip(self), level = "debug", err)] @@ -179,10 +212,84 @@ impl GroupBackendHandler for SqlBackendHandler { } } +impl SqlBackendHandler { + async fn update_group_with_transaction( + request: UpdateGroupRequest, + transaction: &DatabaseTransaction, + ) -> Result<()> { + let update_group = model::groups::ActiveModel { + group_id: Set(request.group_id), + display_name: request.display_name.map(Set).unwrap_or_default(), + ..Default::default() + }; + update_group.update(transaction).await?; + let mut update_group_attributes = Vec::new(); + let mut remove_group_attributes = Vec::new(); + let schema = Self::get_schema_with_transaction(transaction).await?; + for attribute in request.insert_attributes { + if schema + .group_attributes + .get_attribute_type(&attribute.name) + .is_some() + { + update_group_attributes.push(model::group_attributes::ActiveModel { + group_id: Set(request.group_id), + attribute_name: Set(attribute.name.to_owned()), + value: Set(attribute.value), + }); + } else { + return Err(DomainError::InternalError(format!( + "Group attribute name {} doesn't exist in the schema, yet was attempted to be inserted in the database", + &attribute.name + ))); + } + } + for attribute in request.delete_attributes { + if schema + .group_attributes + .get_attribute_type(&attribute) + .is_some() + { + remove_group_attributes.push(attribute); + } else { + return Err(DomainError::InternalError(format!( + "Group attribute name {} doesn't exist in the schema, yet was attempted to be removed from the database", + attribute + ))); + } + } + if !remove_group_attributes.is_empty() { + model::GroupAttributes::delete_many() + .filter(model::GroupAttributesColumn::GroupId.eq(request.group_id)) + .filter(model::GroupAttributesColumn::AttributeName.is_in(remove_group_attributes)) + .exec(transaction) + .await?; + } + if !update_group_attributes.is_empty() { + model::GroupAttributes::insert_many(update_group_attributes) + .on_conflict( + OnConflict::columns([ + model::GroupAttributesColumn::GroupId, + model::GroupAttributesColumn::AttributeName, + ]) + .update_column(model::GroupAttributesColumn::Value) + .to_owned(), + ) + .exec(transaction) + .await?; + } + Ok(()) + } +} + #[cfg(test)] mod tests { use super::*; - use crate::domain::{handler::SubStringFilter, sql_backend_handler::tests::*, types::UserId}; + use crate::domain::{ + handler::{CreateAttributeRequest, SchemaBackendHandler, SubStringFilter}, + sql_backend_handler::tests::*, + types::{AttributeType, Serialized, UserId}, + }; use pretty_assertions::assert_eq; async fn get_group_ids( @@ -304,6 +411,8 @@ mod tests { .update_group(UpdateGroupRequest { group_id: fixture.groups[0], display_name: Some("Awesomest Group".to_owned()), + delete_attributes: Vec::new(), + insert_attributes: Vec::new(), }) .await .unwrap(); @@ -332,4 +441,93 @@ mod tests { vec![fixture.groups[2], fixture.groups[1]] ); } + + #[tokio::test] + async fn test_create_group() { + let fixture = TestFixture::new().await; + assert_eq!( + get_group_ids(&fixture.handler, None).await, + vec![fixture.groups[0], fixture.groups[2], fixture.groups[1]] + ); + fixture + .handler + .add_group_attribute(CreateAttributeRequest { + name: "new_attribute".to_owned(), + attribute_type: AttributeType::String, + is_list: false, + is_visible: true, + is_editable: true, + }) + .await + .unwrap(); + let new_group_id = fixture + .handler + .create_group(CreateGroupRequest { + display_name: "New Group".to_owned(), + attributes: vec![AttributeValue { + name: "new_attribute".to_owned(), + value: Serialized::from("value"), + }], + }) + .await + .unwrap(); + let group_details = fixture + .handler + .get_group_details(new_group_id) + .await + .unwrap(); + assert_eq!(&group_details.display_name, "New Group"); + assert_eq!( + group_details.attributes, + vec![AttributeValue { + name: "new_attribute".to_owned(), + value: Serialized::from("value"), + }] + ); + } + + #[tokio::test] + async fn test_set_group_attributes() { + let fixture = TestFixture::new().await; + fixture + .handler + .add_group_attribute(CreateAttributeRequest { + name: "new_attribute".to_owned(), + attribute_type: AttributeType::Integer, + is_list: false, + is_visible: true, + is_editable: true, + }) + .await + .unwrap(); + let group_id = fixture.groups[0]; + let attributes = vec![AttributeValue { + name: "new_attribute".to_owned(), + value: Serialized::from(&42i64), + }]; + fixture + .handler + .update_group(UpdateGroupRequest { + group_id, + display_name: None, + delete_attributes: Vec::new(), + insert_attributes: attributes.clone(), + }) + .await + .unwrap(); + let details = fixture.handler.get_group_details(group_id).await.unwrap(); + assert_eq!(details.attributes, attributes); + fixture + .handler + .update_group(UpdateGroupRequest { + group_id, + display_name: None, + delete_attributes: vec!["new_attribute".to_owned()], + insert_attributes: Vec::new(), + }) + .await + .unwrap(); + let details = fixture.handler.get_group_details(group_id).await.unwrap(); + assert_eq!(details.attributes, Vec::new()); + } } diff --git a/server/src/domain/sql_user_backend_handler.rs b/server/src/domain/sql_user_backend_handler.rs index 8c1468b..006d4a4 100644 --- a/server/src/domain/sql_user_backend_handler.rs +++ b/server/src/domain/sql_user_backend_handler.rs @@ -205,7 +205,7 @@ impl SqlBackendHandler { process_serialized(ActiveValue::Set(attribute.value), &attribute.name); } else { return Err(DomainError::InternalError(format!( - "Attribute name {} doesn't exist in the schema, yet was attempted to be inserted in the database", + "User attribute name {} doesn't exist in the schema, yet was attempted to be inserted in the database", &attribute.name ))); } @@ -219,7 +219,7 @@ impl SqlBackendHandler { remove_user_attributes.push(attribute); } else { return Err(DomainError::InternalError(format!( - "Attribute name {} doesn't exist in the schema, yet was attempted to be removed from the database", + "User attribute name {} doesn't exist in the schema, yet was attempted to be removed from the database", attribute ))); } @@ -334,7 +334,7 @@ impl UserBackendHandler for SqlBackendHandler { }); } else { return Err(DomainError::InternalError(format!( - "Attribute name {} doesn't exist in the schema, + "Attribute name {} doesn't exist in the user schema, yet was attempted to be inserted in the database", &attribute.name ))); diff --git a/server/src/infra/access_control.rs b/server/src/infra/access_control.rs index 5be4745..0dfaf6c 100644 --- a/server/src/infra/access_control.rs +++ b/server/src/infra/access_control.rs @@ -6,7 +6,7 @@ use tracing::info; use crate::domain::{ error::Result, handler::{ - AttributeSchema, BackendHandler, CreateUserRequest, + AttributeSchema, BackendHandler, CreateGroupRequest, CreateUserRequest, GroupBackendHandler, GroupListerBackendHandler, GroupRequestFilter, ReadSchemaBackendHandler, Schema, UpdateGroupRequest, UpdateUserRequest, UserBackendHandler, UserListerBackendHandler, UserRequestFilter, @@ -101,7 +101,7 @@ pub trait AdminBackendHandler: async fn add_user_to_group(&self, user_id: &UserId, group_id: GroupId) -> Result<()>; async fn remove_user_from_group(&self, user_id: &UserId, group_id: GroupId) -> Result<()>; async fn update_group(&self, request: UpdateGroupRequest) -> Result<()>; - async fn create_group(&self, group_name: &str) -> Result; + async fn create_group(&self, request: CreateGroupRequest) -> Result; async fn delete_group(&self, group_id: GroupId) -> Result<()>; } @@ -155,8 +155,8 @@ impl AdminBackendHandler for Handler { async fn update_group(&self, request: UpdateGroupRequest) -> Result<()> { ::update_group(self, request).await } - async fn create_group(&self, group_name: &str) -> Result { - ::create_group(self, group_name).await + async fn create_group(&self, request: CreateGroupRequest) -> Result { + ::create_group(self, request).await } async fn delete_group(&self, group_id: GroupId) -> Result<()> { ::delete_group(self, group_id).await diff --git a/server/src/infra/graphql/mutation.rs b/server/src/infra/graphql/mutation.rs index 39d3d09..9bf28a8 100644 --- a/server/src/infra/graphql/mutation.rs +++ b/server/src/infra/graphql/mutation.rs @@ -1,6 +1,9 @@ use crate::{ domain::{ - handler::{BackendHandler, CreateUserRequest, UpdateGroupRequest, UpdateUserRequest}, + handler::{ + BackendHandler, CreateGroupRequest, CreateUserRequest, UpdateGroupRequest, + UpdateUserRequest, + }, types::{GroupId, JpegPhoto, UserId}, }, infra::{ @@ -8,7 +11,7 @@ use crate::{ AdminBackendHandler, ReadonlyBackendHandler, UserReadableBackendHandler, UserWriteableBackendHandler, }, - graphql::api::field_error_callback, + graphql::api::{field_error_callback, Context}, }, }; use anyhow::Context as AnyhowContext; @@ -16,8 +19,6 @@ use base64::Engine; use juniper::{graphql_object, FieldResult, GraphQLInputObject, GraphQLObject}; use tracing::{debug, debug_span, Instrument}; -use super::api::Context; - #[derive(PartialEq, Eq, Debug)] /// The top-level GraphQL mutation type. pub struct Mutation { @@ -126,7 +127,11 @@ impl Mutation { let handler = context .get_admin_handler() .ok_or_else(field_error_callback(&span, "Unauthorized group creation"))?; - let group_id = handler.create_group(&name).await?; + 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) @@ -188,6 +193,8 @@ impl Mutation { .update_group(UpdateGroupRequest { group_id: GroupId(group.id), display_name: group.display_name, + delete_attributes: Vec::new(), + insert_attributes: Vec::new(), }) .instrument(span) .await?; diff --git a/server/src/infra/test_utils.rs b/server/src/infra/test_utils.rs index 9b253aa..b8d3f67 100644 --- a/server/src/infra/test_utils.rs +++ b/server/src/infra/test_utils.rs @@ -20,7 +20,7 @@ mockall::mock! { impl GroupBackendHandler for TestBackendHandler { async fn get_group_details(&self, group_id: GroupId) -> Result; async fn update_group(&self, request: UpdateGroupRequest) -> Result<()>; - async fn create_group(&self, group_name: &str) -> Result; + async fn create_group(&self, request: CreateGroupRequest) -> Result; async fn delete_group(&self, group_id: GroupId) -> Result<()>; } #[async_trait] diff --git a/server/src/main.rs b/server/src/main.rs index 9047f89..d83dd00 100644 --- a/server/src/main.rs +++ b/server/src/main.rs @@ -8,8 +8,8 @@ use std::time::Duration; use crate::{ domain::{ handler::{ - CreateUserRequest, GroupBackendHandler, GroupListerBackendHandler, GroupRequestFilter, - UserBackendHandler, UserListerBackendHandler, UserRequestFilter, + CreateGroupRequest, CreateUserRequest, GroupBackendHandler, GroupListerBackendHandler, + GroupRequestFilter, UserBackendHandler, UserListerBackendHandler, UserRequestFilter, }, sql_backend_handler::SqlBackendHandler, sql_opaque_handler::register_password, @@ -63,7 +63,10 @@ async fn ensure_group_exists(handler: &SqlBackendHandler, group_name: &str) -> R { warn!("Could not find {} group, trying to create it", group_name); handler - .create_group(group_name) + .create_group(CreateGroupRequest { + display_name: group_name.to_owned(), + ..Default::default() + }) .await .context(format!("while creating {} group", group_name))?; }