From 3140af63de37d873a6fcc4f52b5c42cbe2f4a889 Mon Sep 17 00:00:00 2001 From: Valentin Tolmer Date: Wed, 28 Jun 2023 17:13:09 +0200 Subject: [PATCH] server: Use schema to populate attributes --- Cargo.lock | 8 +- server/Cargo.toml | 2 +- server/src/domain/handler.rs | 26 +++- server/src/domain/ldap/user.rs | 28 +++-- server/src/domain/ldap/utils.rs | 37 +++++- server/src/domain/model/user_attributes.rs | 17 ++- server/src/domain/model/users.rs | 4 +- .../src/domain/sql_schema_backend_handler.rs | 72 +++++++----- server/src/domain/sql_user_backend_handler.rs | 111 +++++++++++------- server/src/domain/types.rs | 92 +++++++++++++-- server/src/infra/access_control.rs | 44 ++++--- server/src/infra/graphql/query.rs | 22 +++- server/src/infra/ldap_handler.rs | 101 ++++++++++++++-- 13 files changed, 429 insertions(+), 135 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 5ea673d..36b7add 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2670,9 +2670,9 @@ dependencies = [ [[package]] name = "mockall" -version = "0.11.3" +version = "0.11.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50e4a1c770583dac7ab5e2f6c139153b783a53a1bbee9729613f193e59828326" +checksum = "4c84490118f2ee2d74570d114f3d0493cbf02790df303d2707606c3e14e07c96" dependencies = [ "cfg-if", "downcast", @@ -2685,9 +2685,9 @@ dependencies = [ [[package]] name = "mockall_derive" -version = "0.11.3" +version = "0.11.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "832663583d5fa284ca8810bf7015e46c9fff9622d3cf34bd1eea5003fec06dd0" +checksum = "22ce75669015c4f47b289fd4d4f56e894e4c96003ffdf3ac51313126f94c6cbb" dependencies = [ "cfg-if", "proc-macro2", diff --git a/server/Cargo.toml b/server/Cargo.toml index fde5807..f959035 100644 --- a/server/Cargo.toml +++ b/server/Cargo.toml @@ -128,7 +128,7 @@ features = ["dangerous_configuration"] [dev-dependencies] assert_cmd = "2.0" -mockall = "0.11" +mockall = "0.11.4" nix = "0.26.2" [dev-dependencies.graphql_client] diff --git a/server/src/domain/handler.rs b/server/src/domain/handler.rs index 3614748..767ed31 100644 --- a/server/src/domain/handler.rs +++ b/server/src/domain/handler.rs @@ -134,10 +134,24 @@ pub struct AttributeSchema { pub is_hardcoded: bool, } +#[derive(PartialEq, Eq, Debug, Serialize, Deserialize, Clone)] +pub struct AttributeList { + pub attributes: Vec, +} + +impl AttributeList { + pub fn get_attribute_type(&self, name: &str) -> Option<(AttributeType, bool)> { + self.attributes + .iter() + .find(|a| a.name == name) + .map(|a| (a.attribute_type, a.is_list)) + } +} + #[derive(PartialEq, Eq, Debug, Serialize, Deserialize, Clone)] pub struct Schema { - pub user_attributes: Vec, - pub group_attributes: Vec, + pub user_attributes: AttributeList, + pub group_attributes: AttributeList, } #[async_trait] @@ -146,12 +160,12 @@ pub trait LoginHandler: Send + Sync { } #[async_trait] -pub trait GroupListerBackendHandler { +pub trait GroupListerBackendHandler: SchemaBackendHandler { async fn list_groups(&self, filters: Option) -> Result>; } #[async_trait] -pub trait GroupBackendHandler { +pub trait GroupBackendHandler: SchemaBackendHandler { 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; @@ -159,7 +173,7 @@ pub trait GroupBackendHandler { } #[async_trait] -pub trait UserListerBackendHandler { +pub trait UserListerBackendHandler: SchemaBackendHandler { async fn list_users( &self, filters: Option, @@ -168,7 +182,7 @@ pub trait UserListerBackendHandler { } #[async_trait] -pub trait UserBackendHandler { +pub trait UserBackendHandler: SchemaBackendHandler { async fn get_user_details(&self, user_id: &UserId) -> Result; async fn create_user(&self, request: CreateUserRequest) -> Result<()>; async fn update_user(&self, request: UpdateUserRequest) -> Result<()>; diff --git a/server/src/domain/ldap/user.rs b/server/src/domain/ldap/user.rs index 02be15c..908a5d0 100644 --- a/server/src/domain/ldap/user.rs +++ b/server/src/domain/ldap/user.rs @@ -5,11 +5,11 @@ use ldap3_proto::{ use tracing::{debug, instrument, warn}; use crate::domain::{ - handler::{UserListerBackendHandler, UserRequestFilter}, + handler::{Schema, UserListerBackendHandler, UserRequestFilter}, ldap::{ error::{LdapError, LdapResult}, utils::{ - expand_attribute_wildcards, get_group_id_from_distinguished_name, + expand_attribute_wildcards, get_custom_attribute, get_group_id_from_distinguished_name, get_user_id_from_distinguished_name, map_user_field, LdapInfo, UserFieldType, }, }, @@ -22,6 +22,7 @@ pub fn get_user_attribute( base_dn_str: &str, groups: Option<&[GroupDetails]>, ignored_user_attributes: &[String], + schema: &Schema, ) -> Option>> { let attribute = attribute.to_ascii_lowercase(); let attribute_values = match attribute.as_str() { @@ -36,9 +37,13 @@ pub fn get_user_attribute( "uid" | "user_id" | "id" => vec![user.user_id.to_string().into_bytes()], "entryuuid" | "uuid" => vec![user.uuid.to_string().into_bytes()], "mail" | "email" => vec![user.email.clone().into_bytes()], - "givenname" | "first_name" | "firstname" => vec![user.first_name.clone()?.into_bytes()], - "sn" | "last_name" | "lastname" => vec![user.last_name.clone()?.into_bytes()], - "jpegphoto" | "avatar" => vec![user.avatar.clone()?.into_bytes()], + "givenname" | "first_name" | "firstname" => { + get_custom_attribute(&user.attributes, "first_name", schema)? + } + "sn" | "last_name" | "lastname" => { + get_custom_attribute(&user.attributes, "last_name", schema)? + } + "jpegphoto" | "avatar" => get_custom_attribute(&user.attributes, "avatar", schema)?, "memberof" => groups .into_iter() .flatten() @@ -98,6 +103,7 @@ fn make_ldap_search_user_result_entry( attributes: &[String], groups: Option<&[GroupDetails]>, ignored_user_attributes: &[String], + schema: &Schema, ) -> LdapSearchResultEntry { let expanded_attributes = expand_user_attribute_wildcards(attributes); let dn = format!("uid={},ou=people,{}", user.user_id.as_str(), base_dn_str); @@ -106,8 +112,14 @@ fn make_ldap_search_user_result_entry( attributes: expanded_attributes .iter() .filter_map(|a| { - let values = - get_user_attribute(&user, a, base_dn_str, groups, ignored_user_attributes)?; + let values = get_user_attribute( + &user, + a, + base_dn_str, + groups, + ignored_user_attributes, + schema, + )?; Some(LdapPartialAttribute { atype: a.to_string(), vals: values, @@ -242,6 +254,7 @@ pub fn convert_users_to_ldap_op<'a>( users: Vec, attributes: &'a [String], ldap_info: &'a LdapInfo, + schema: &'a Schema, ) -> impl Iterator + 'a { users.into_iter().map(move |u| { LdapOp::SearchResultEntry(make_ldap_search_user_result_entry( @@ -250,6 +263,7 @@ pub fn convert_users_to_ldap_op<'a>( attributes, u.groups.as_deref(), &ldap_info.ignored_user_attributes, + schema, )) }) } diff --git a/server/src/domain/ldap/utils.rs b/server/src/domain/ldap/utils.rs index 99f9328..bbd3830 100644 --- a/server/src/domain/ldap/utils.rs +++ b/server/src/domain/ldap/utils.rs @@ -1,11 +1,12 @@ +use chrono::{NaiveDateTime, TimeZone}; use itertools::Itertools; use ldap3_proto::{proto::LdapSubstringFilter, LdapResultCode}; use tracing::{debug, instrument, warn}; use crate::domain::{ - handler::SubStringFilter, + handler::{Schema, SubStringFilter}, ldap::error::{LdapError, LdapResult}, - types::{UserColumn, UserId}, + types::{AttributeType, AttributeValue, JpegPhoto, UserColumn, UserId}, }; impl From for SubStringFilter { @@ -193,3 +194,35 @@ pub struct LdapInfo { pub ignored_user_attributes: Vec, pub ignored_group_attributes: Vec, } + +pub fn get_custom_attribute( + attributes: &[AttributeValue], + attribute_name: &str, + schema: &Schema, +) -> Option>> { + schema + .user_attributes + .get_attribute_type(attribute_name) + .and_then(|attribute_type| { + attributes + .iter() + .find(|a| a.name == attribute_name) + .map(|attribute| match attribute_type { + (AttributeType::String, false) => { + vec![attribute.value.unwrap::().into_bytes()] + } + (AttributeType::Integer, false) => todo!(), + (AttributeType::JpegPhoto, false) => { + vec![attribute.value.unwrap::().into_bytes()] + } + (AttributeType::DateTime, false) => vec![chrono::Utc + .from_utc_datetime(&attribute.value.unwrap::()) + .to_rfc3339() + .into_bytes()], + (AttributeType::String, true) => todo!(), + (AttributeType::Integer, true) => todo!(), + (AttributeType::JpegPhoto, true) => todo!(), + (AttributeType::DateTime, true) => todo!(), + }) + }) +} diff --git a/server/src/domain/model/user_attributes.rs b/server/src/domain/model/user_attributes.rs index 1639c0a..2bef5d6 100644 --- a/server/src/domain/model/user_attributes.rs +++ b/server/src/domain/model/user_attributes.rs @@ -1,7 +1,7 @@ use sea_orm::entity::prelude::*; use serde::{Deserialize, Serialize}; -use crate::domain::types::{Serialized, UserId}; +use crate::domain::types::{AttributeValue, Serialized, UserId}; #[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)] #[sea_orm(table_name = "user_attributes")] @@ -55,3 +55,18 @@ impl Related for Entity { } impl ActiveModelBehavior for ActiveModel {} + +impl From for AttributeValue { + fn from( + Model { + user_id: _, + attribute_name, + value, + }: Model, + ) -> Self { + Self { + name: attribute_name, + value, + } + } +} diff --git a/server/src/domain/model/users.rs b/server/src/domain/model/users.rs index c48e8a4..3be7f7c 100644 --- a/server/src/domain/model/users.rs +++ b/server/src/domain/model/users.rs @@ -115,11 +115,9 @@ impl From for crate::domain::types::User { user_id: user.user_id, email: user.email, display_name: user.display_name, - first_name: None, - last_name: None, creation_date: user.creation_date, uuid: user.uuid, - avatar: None, + attributes: Vec::new(), } } } diff --git a/server/src/domain/sql_schema_backend_handler.rs b/server/src/domain/sql_schema_backend_handler.rs index 8f73970..c785a75 100644 --- a/server/src/domain/sql_schema_backend_handler.rs +++ b/server/src/domain/sql_schema_backend_handler.rs @@ -7,12 +7,18 @@ use crate::domain::{ use async_trait::async_trait; use sea_orm::{EntityTrait, QueryOrder}; +use super::handler::AttributeList; + #[async_trait] impl SchemaBackendHandler for SqlBackendHandler { async fn get_schema(&self) -> Result { Ok(Schema { - user_attributes: self.get_user_attributes().await?, - group_attributes: self.get_group_attributes().await?, + user_attributes: AttributeList { + attributes: self.get_user_attributes().await?, + }, + group_attributes: AttributeList { + attributes: self.get_group_attributes().await?, + }, }) } } @@ -42,7 +48,9 @@ impl SqlBackendHandler { #[cfg(test)] mod tests { use super::*; - use crate::domain::{sql_backend_handler::tests::*, types::AttributeType}; + use crate::domain::{ + handler::AttributeList, sql_backend_handler::tests::*, types::AttributeType, + }; #[tokio::test] async fn test_default_schema() { @@ -50,33 +58,37 @@ mod tests { assert_eq!( fixture.handler.get_schema().await.unwrap(), Schema { - user_attributes: vec![ - AttributeSchema { - name: "avatar".to_owned(), - attribute_type: AttributeType::JpegPhoto, - is_list: false, - is_visible: true, - is_editable: true, - is_hardcoded: true, - }, - AttributeSchema { - name: "first_name".to_owned(), - attribute_type: AttributeType::String, - is_list: false, - is_visible: true, - is_editable: true, - is_hardcoded: true, - }, - AttributeSchema { - name: "last_name".to_owned(), - attribute_type: AttributeType::String, - is_list: false, - is_visible: true, - is_editable: true, - is_hardcoded: true, - } - ], - group_attributes: Vec::new() + user_attributes: AttributeList { + attributes: vec![ + AttributeSchema { + name: "avatar".to_owned(), + attribute_type: AttributeType::JpegPhoto, + is_list: false, + is_visible: true, + is_editable: true, + is_hardcoded: true, + }, + AttributeSchema { + name: "first_name".to_owned(), + attribute_type: AttributeType::String, + is_list: false, + is_visible: true, + is_editable: true, + is_hardcoded: true, + }, + AttributeSchema { + name: "last_name".to_owned(), + attribute_type: AttributeType::String, + is_list: false, + is_visible: true, + is_editable: true, + is_hardcoded: true, + } + ] + }, + group_attributes: AttributeList { + attributes: Vec::new() + } } ); } diff --git a/server/src/domain/sql_user_backend_handler.rs b/server/src/domain/sql_user_backend_handler.rs index c5dfc2d..e89c8bb 100644 --- a/server/src/domain/sql_user_backend_handler.rs +++ b/server/src/domain/sql_user_backend_handler.rs @@ -6,7 +6,7 @@ use crate::domain::{ }, model::{self, GroupColumn, UserColumn}, sql_backend_handler::SqlBackendHandler, - types::{GroupDetails, GroupId, Serialized, User, UserAndGroups, UserId, Uuid}, + types::{AttributeValue, GroupDetails, GroupId, Serialized, User, UserAndGroups, UserId, Uuid}, }; use async_trait::async_trait; use sea_orm::{ @@ -17,7 +17,7 @@ use sea_orm::{ QueryFilter, QueryOrder, QuerySelect, QueryTrait, Set, TransactionTrait, }; use std::collections::HashSet; -use tracing::{debug, instrument, warn}; +use tracing::{debug, instrument}; fn attribute_condition(name: String, value: String) -> Cond { Expr::in_subquery( @@ -149,27 +149,21 @@ impl UserListerBackendHandler for SqlBackendHandler { let attributes = model::UserAttributes::find() .filter(model::UserAttributesColumn::UserId.is_in(&user_ids)) .order_by_asc(model::UserAttributesColumn::UserId) + .order_by_asc(model::UserAttributesColumn::AttributeName) .all(&self.sql_pool) .await?; - let mut attributes_iter = attributes.iter().peekable(); + let mut attributes_iter = attributes.into_iter().peekable(); for user in users.iter_mut() { - attributes_iter - .peeking_take_while(|u| u.user_id < user.user.user_id) - .for_each(|_| ()); + assert!(attributes_iter + .peek() + .map(|u| u.user_id >= user.user.user_id) + .unwrap_or(true), + "Attributes are not sorted, users are not sorted, or previous user didn't consume all the attributes"); - for model::user_attributes::Model { - user_id: _, - attribute_name, - value, - } in attributes_iter.take_while_ref(|u| u.user_id == user.user.user_id) - { - match attribute_name.as_str() { - "first_name" => user.user.first_name = Some(value.unwrap()), - "last_name" => user.user.last_name = Some(value.unwrap()), - "avatar" => user.user.avatar = Some(value.unwrap()), - _ => warn!("Unknown attribute name: {}", attribute_name), - } - } + user.user.attributes = attributes_iter + .take_while_ref(|u| u.user_id == user.user.user_id) + .map(AttributeValue::from) + .collect(); } Ok(users) } @@ -188,21 +182,10 @@ impl UserBackendHandler for SqlBackendHandler { ); let attributes = model::UserAttributes::find() .filter(model::UserAttributesColumn::UserId.eq(user_id)) + .order_by_asc(model::UserAttributesColumn::AttributeName) .all(&self.sql_pool) .await?; - for model::user_attributes::Model { - user_id: _, - attribute_name, - value, - } in attributes - { - match attribute_name.as_str() { - "first_name" => user.first_name = Some(value.unwrap()), - "last_name" => user.last_name = Some(value.unwrap()), - "avatar" => user.avatar = Some(value.unwrap()), - _ => warn!("Unknown attribute name: {}", attribute_name), - } - } + user.attributes = attributes.into_iter().map(AttributeValue::from).collect(); Ok(user) } @@ -762,9 +745,23 @@ mod tests { .unwrap(); assert_eq!(user.email, "email"); assert_eq!(user.display_name.unwrap(), "display_name"); - assert_eq!(user.first_name.unwrap(), "first_name"); - assert_eq!(user.last_name.unwrap(), "last_name"); - assert_eq!(user.avatar, Some(JpegPhoto::for_tests())); + assert_eq!( + user.attributes, + vec![ + AttributeValue { + name: "avatar".to_owned(), + value: Serialized::from(&JpegPhoto::for_tests()) + }, + AttributeValue { + name: "first_name".to_owned(), + value: Serialized::from("first_name") + }, + AttributeValue { + name: "last_name".to_owned(), + value: Serialized::from("last_name") + } + ] + ); } #[tokio::test] @@ -789,9 +786,19 @@ mod tests { .await .unwrap(); assert_eq!(user.display_name.unwrap(), "display bob"); - assert_eq!(user.first_name.unwrap(), "first bob"); - assert_eq!(user.last_name, None); - assert_eq!(user.avatar, Some(JpegPhoto::for_tests())); + assert_eq!( + user.attributes, + vec![ + AttributeValue { + name: "avatar".to_owned(), + value: Serialized::from(&JpegPhoto::for_tests()) + }, + AttributeValue { + name: "first_name".to_owned(), + value: Serialized::from("first bob") + } + ] + ); } #[tokio::test] @@ -813,7 +820,11 @@ mod tests { .get_user_details(&UserId::new("bob")) .await .unwrap(); - assert_eq!(user.avatar, Some(JpegPhoto::for_tests())); + let avatar = AttributeValue { + name: "avatar".to_owned(), + value: Serialized::from(&JpegPhoto::for_tests()), + }; + assert!(user.attributes.contains(&avatar)); fixture .handler .update_user(UpdateUserRequest { @@ -829,7 +840,7 @@ mod tests { .get_user_details(&UserId::new("bob")) .await .unwrap(); - assert_eq!(user.avatar, None); + assert!(!user.attributes.contains(&avatar)); } #[tokio::test] @@ -856,9 +867,23 @@ mod tests { .unwrap(); assert_eq!(user.email, "email"); assert_eq!(user.display_name.unwrap(), "display_name"); - assert_eq!(user.first_name.unwrap(), "first_name"); - assert_eq!(user.last_name.unwrap(), "last_name"); - assert_eq!(user.avatar, Some(JpegPhoto::for_tests())); + assert_eq!( + user.attributes, + vec![ + AttributeValue { + name: "avatar".to_owned(), + value: Serialized::from(&JpegPhoto::for_tests()) + }, + AttributeValue { + name: "first_name".to_owned(), + value: Serialized::from("first_name") + }, + AttributeValue { + name: "last_name".to_owned(), + value: Serialized::from("last_name") + } + ] + ); } #[tokio::test] diff --git a/server/src/domain/types.rs b/server/src/domain/types.rs index 15a3c8e..7326da9 100644 --- a/server/src/domain/types.rs +++ b/server/src/domain/types.rs @@ -104,9 +104,42 @@ macro_rules! uuid { }; } -#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct Serialized(Vec); +const SERIALIZED_I64_LEN: usize = 8; + +impl std::fmt::Debug for Serialized { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_tuple("Serialized") + .field( + &self + .convert_to() + .and_then(|s| { + String::from_utf8(s) + .map_err(|_| Box::new(bincode::ErrorKind::InvalidCharEncoding)) + }) + .or_else(|e| { + if self.0.len() == SERIALIZED_I64_LEN { + self.convert_to::() + .map(|i| i.to_string()) + .map_err(|_| Box::new(bincode::ErrorKind::InvalidCharEncoding)) + } else { + Err(e) + } + }) + .unwrap_or_else(|_| { + format!("hash: {:#016X}", { + let mut hasher = std::collections::hash_map::DefaultHasher::new(); + std::hash::Hash::hash(&self.0, &mut hasher); + std::hash::Hasher::finish(&hasher) + }) + }), + ) + .finish() + } +} + impl<'a, T: Serialize + ?Sized> From<&'a T> for Serialized { fn from(t: &'a T) -> Self { Self(bincode::serialize(&t).unwrap()) @@ -114,12 +147,16 @@ impl<'a, T: Serialize + ?Sized> From<&'a T> for Serialized { } impl Serialized { + fn convert_to<'a, T: Deserialize<'a>>(&'a self) -> bincode::Result { + bincode::deserialize(&self.0) + } + pub fn unwrap<'a, T: Deserialize<'a>>(&'a self) -> T { - bincode::deserialize(&self.0).unwrap() + self.convert_to().unwrap() } pub fn expect<'a, T: Deserialize<'a>>(&'a self, message: &str) -> T { - bincode::deserialize(&self.0).expect(message) + self.convert_to().expect(message) } } @@ -378,16 +415,20 @@ impl IntoActiveValue for JpegPhoto { } } +#[derive(PartialEq, Eq, Debug, Clone, Serialize, Deserialize)] +pub struct AttributeValue { + pub name: String, + pub value: Serialized, +} + #[derive(PartialEq, Eq, Debug, Clone, Serialize, Deserialize)] pub struct User { pub user_id: UserId, pub email: String, pub display_name: Option, - pub first_name: Option, - pub last_name: Option, - pub avatar: Option, pub creation_date: NaiveDateTime, pub uuid: Uuid, + pub attributes: Vec, } #[cfg(test)] @@ -398,11 +439,9 @@ impl Default for User { user_id: UserId::default(), email: String::new(), display_name: None, - first_name: None, - last_name: None, - avatar: None, creation_date: epoch, uuid: Uuid::from_name_and_date("", &epoch), + attributes: Vec::new(), } } } @@ -513,3 +552,38 @@ pub struct UserAndGroups { pub user: User, pub groups: Option>, } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_serialized_debug_string() { + assert_eq!( + &format!("{:?}", Serialized::from("abcd")), + "Serialized(\"abcd\")" + ); + assert_eq!( + &format!("{:?}", Serialized::from(&1234i64)), + "Serialized(\"1234\")" + ); + assert_eq!( + &format!("{:?}", Serialized::from(&JpegPhoto::for_tests())), + "Serialized(\"hash: 0xB947C77A16F3C3BD\")" + ); + } + + #[test] + fn test_serialized_i64_len() { + assert_eq!(SERIALIZED_I64_LEN, Serialized::from(&0i64).0.len()); + assert_eq!( + SERIALIZED_I64_LEN, + Serialized::from(&i64::max_value()).0.len() + ); + assert_eq!( + SERIALIZED_I64_LEN, + Serialized::from(&i64::min_value()).0.len() + ); + assert_eq!(SERIALIZED_I64_LEN, Serialized::from(&-1000i64).0.len()); + } +} diff --git a/server/src/infra/access_control.rs b/server/src/infra/access_control.rs index e4196cc..74d193a 100644 --- a/server/src/infra/access_control.rs +++ b/server/src/infra/access_control.rs @@ -6,8 +6,9 @@ use tracing::info; use crate::domain::{ error::Result, handler::{ - BackendHandler, CreateUserRequest, GroupListerBackendHandler, GroupRequestFilter, - UpdateGroupRequest, UpdateUserRequest, UserListerBackendHandler, UserRequestFilter, + BackendHandler, CreateUserRequest, GroupBackendHandler, GroupListerBackendHandler, + GroupRequestFilter, Schema, SchemaBackendHandler, UpdateGroupRequest, UpdateUserRequest, + UserBackendHandler, UserListerBackendHandler, UserRequestFilter, }, types::{Group, GroupDetails, GroupId, User, UserAndGroups, UserId}, }; @@ -72,6 +73,7 @@ impl ValidationResults { pub trait UserReadableBackendHandler { async fn get_user_details(&self, user_id: &UserId) -> Result; async fn get_user_groups(&self, user_id: &UserId) -> Result>; + async fn get_schema(&self) -> Result; } #[async_trait] @@ -106,10 +108,13 @@ pub trait AdminBackendHandler: #[async_trait] impl UserReadableBackendHandler for Handler { async fn get_user_details(&self, user_id: &UserId) -> Result { - self.get_user_details(user_id).await + ::get_user_details(self, user_id).await } async fn get_user_groups(&self, user_id: &UserId) -> Result> { - self.get_user_groups(user_id).await + ::get_user_groups(self, user_id).await + } + async fn get_schema(&self) -> Result { + ::get_schema(self).await } } @@ -120,44 +125,44 @@ impl ReadonlyBackendHandler for Handler { filters: Option, get_groups: bool, ) -> Result> { - self.list_users(filters, get_groups).await + ::list_users(self, filters, get_groups).await } async fn list_groups(&self, filters: Option) -> Result> { - self.list_groups(filters).await + ::list_groups(self, filters).await } async fn get_group_details(&self, group_id: GroupId) -> Result { - self.get_group_details(group_id).await + ::get_group_details(self, group_id).await } } #[async_trait] impl UserWriteableBackendHandler for Handler { async fn update_user(&self, request: UpdateUserRequest) -> Result<()> { - self.update_user(request).await + ::update_user(self, request).await } } #[async_trait] impl AdminBackendHandler for Handler { async fn create_user(&self, request: CreateUserRequest) -> Result<()> { - self.create_user(request).await + ::create_user(self, request).await } async fn delete_user(&self, user_id: &UserId) -> Result<()> { - self.delete_user(user_id).await + ::delete_user(self, user_id).await } async fn add_user_to_group(&self, user_id: &UserId, group_id: GroupId) -> Result<()> { - self.add_user_to_group(user_id, group_id).await + ::add_user_to_group(self, user_id, group_id).await } async fn remove_user_from_group(&self, user_id: &UserId, group_id: GroupId) -> Result<()> { - self.remove_user_from_group(user_id, group_id).await + ::remove_user_from_group(self, user_id, group_id).await } async fn update_group(&self, request: UpdateGroupRequest) -> Result<()> { - self.update_group(request).await + ::update_group(self, request).await } async fn create_group(&self, group_name: &str) -> Result { - self.create_group(group_name).await + ::create_group(self, group_name).await } async fn delete_group(&self, group_id: GroupId) -> Result<()> { - self.delete_group(group_id).await + ::delete_group(self, group_id).await } } @@ -262,6 +267,15 @@ pub struct UserRestrictedListerBackendHandler<'a, Handler> { pub user_filter: Option, } +#[async_trait] +impl<'a, Handler: SchemaBackendHandler + Sync> SchemaBackendHandler + for UserRestrictedListerBackendHandler<'a, Handler> +{ + async fn get_schema(&self) -> Result { + self.handler.get_schema().await + } +} + #[async_trait] impl<'a, Handler: UserListerBackendHandler + Sync> UserListerBackendHandler for UserRestrictedListerBackendHandler<'a, Handler> diff --git a/server/src/infra/graphql/query.rs b/server/src/infra/graphql/query.rs index 5e8d453..a876825 100644 --- a/server/src/infra/graphql/query.rs +++ b/server/src/infra/graphql/query.rs @@ -2,7 +2,7 @@ use crate::{ domain::{ handler::BackendHandler, ldap::utils::{map_user_field, UserFieldType}, - types::{GroupDetails, GroupId, UserColumn, UserId}, + types::{GroupDetails, GroupId, JpegPhoto, UserColumn, UserId}, }, infra::{ access_control::{ReadonlyBackendHandler, UserReadableBackendHandler}, @@ -236,15 +236,29 @@ impl User { } fn first_name(&self) -> &str { - self.user.first_name.as_deref().unwrap_or("") + self.user + .attributes + .iter() + .find(|a| a.name == "first_name") + .map(|a| a.value.unwrap()) + .unwrap_or("") } fn last_name(&self) -> &str { - self.user.last_name.as_deref().unwrap_or("") + self.user + .attributes + .iter() + .find(|a| a.name == "last_name") + .map(|a| a.value.unwrap()) + .unwrap_or("") } fn avatar(&self) -> Option { - self.user.avatar.as_ref().map(String::from) + self.user + .attributes + .iter() + .find(|a| a.name == "avatar") + .map(|a| String::from(&a.value.unwrap::())) } fn creation_date(&self) -> chrono::DateTime { diff --git a/server/src/infra/ldap_handler.rs b/server/src/infra/ldap_handler.rs index c99455c..510a162 100644 --- a/server/src/infra/ldap_handler.rs +++ b/server/src/infra/ldap_handler.rs @@ -1,6 +1,8 @@ use crate::{ domain::{ - handler::{BackendHandler, BindRequest, CreateUserRequest, LoginHandler}, + handler::{ + BackendHandler, BindRequest, CreateUserRequest, LoginHandler, SchemaBackendHandler, + }, ldap::{ error::{LdapError, LdapResult}, group::{convert_groups_to_ldap_op, get_groups_list}, @@ -467,12 +469,17 @@ impl LdapHandler