diff --git a/server/src/domain/handler.rs b/server/src/domain/handler.rs index 9eb0d52..ef43e93 100644 --- a/server/src/domain/handler.rs +++ b/server/src/domain/handler.rs @@ -1,223 +1,13 @@ -use super::error::*; +use super::{ + error::Result, + types::{ + Group, GroupDetails, GroupId, JpegPhoto, User, UserAndGroups, UserColumn, UserId, Uuid, + }, +}; use async_trait::async_trait; use serde::{Deserialize, Serialize}; use std::collections::HashSet; -pub use super::model::{GroupColumn, UserColumn}; - -#[derive(PartialEq, Hash, Eq, Clone, Debug, Default, Serialize, Deserialize)] -#[serde(try_from = "&str")] -pub struct Uuid(String); - -impl Uuid { - pub fn from_name_and_date(name: &str, creation_date: &chrono::DateTime) -> Self { - Uuid( - uuid::Uuid::new_v3( - &uuid::Uuid::NAMESPACE_X500, - &[name.as_bytes(), creation_date.to_rfc3339().as_bytes()].concat(), - ) - .to_string(), - ) - } - - pub fn as_str(&self) -> &str { - &self.0 - } - - pub fn into_string(self) -> String { - self.0 - } -} - -impl<'a> std::convert::TryFrom<&'a str> for Uuid { - type Error = anyhow::Error; - fn try_from(s: &'a str) -> anyhow::Result { - Ok(Uuid(uuid::Uuid::parse_str(s)?.to_string())) - } -} - -impl std::string::ToString for Uuid { - fn to_string(&self) -> String { - self.0.clone() - } -} - -impl sea_orm::TryGetable for Uuid { - fn try_get( - res: &sea_orm::QueryResult, - pre: &str, - col: &str, - ) -> std::result::Result { - Ok(Uuid(String::try_get(res, pre, col)?)) - } -} - -#[cfg(test)] -#[macro_export] -macro_rules! uuid { - ($s:literal) => { - <$crate::domain::handler::Uuid as std::convert::TryFrom<_>>::try_from($s).unwrap() - }; -} - -#[derive(PartialEq, Eq, Clone, Debug, Default, Serialize, Deserialize)] -#[serde(from = "String")] -pub struct UserId(String); - -impl UserId { - pub fn new(user_id: &str) -> Self { - Self(user_id.to_lowercase()) - } - - pub fn as_str(&self) -> &str { - self.0.as_str() - } - - pub fn into_string(self) -> String { - self.0 - } -} - -impl std::fmt::Display for UserId { - fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { - write!(f, "{}", self.0) - } -} - -impl From for UserId { - fn from(s: String) -> Self { - Self::new(&s) - } -} - -#[derive(PartialEq, Eq, Clone, Debug, Serialize, Deserialize)] -pub struct JpegPhoto(#[serde(with = "serde_bytes")] Vec); - -impl JpegPhoto { - pub fn null() -> Self { - Self(vec![]) - } -} - -impl From for sea_orm::Value { - fn from(photo: JpegPhoto) -> Self { - photo.0.into() - } -} - -impl From<&JpegPhoto> for sea_orm::Value { - fn from(photo: &JpegPhoto) -> Self { - photo.0.as_slice().into() - } -} - -impl TryFrom<&[u8]> for JpegPhoto { - type Error = anyhow::Error; - fn try_from(bytes: &[u8]) -> anyhow::Result { - if bytes.is_empty() { - return Ok(JpegPhoto::null()); - } - // Confirm that it's a valid Jpeg, then store only the bytes. - image::io::Reader::with_format(std::io::Cursor::new(bytes), image::ImageFormat::Jpeg) - .decode()?; - Ok(JpegPhoto(bytes.to_vec())) - } -} - -impl TryFrom> for JpegPhoto { - type Error = anyhow::Error; - fn try_from(bytes: Vec) -> anyhow::Result { - if bytes.is_empty() { - return Ok(JpegPhoto::null()); - } - // Confirm that it's a valid Jpeg, then store only the bytes. - image::io::Reader::with_format( - std::io::Cursor::new(bytes.as_slice()), - image::ImageFormat::Jpeg, - ) - .decode()?; - Ok(JpegPhoto(bytes)) - } -} - -impl TryFrom for JpegPhoto { - type Error = anyhow::Error; - fn try_from(string: String) -> anyhow::Result { - // The String format is in base64. - Self::try_from(base64::decode(string.as_str())?) - } -} - -impl From<&JpegPhoto> for String { - fn from(val: &JpegPhoto) -> Self { - base64::encode(&val.0) - } -} - -impl JpegPhoto { - pub fn into_bytes(self) -> Vec { - self.0 - } - - #[cfg(test)] - pub fn for_tests() -> Self { - use image::{ImageOutputFormat, Rgb, RgbImage}; - let img = RgbImage::from_fn(32, 32, |x, y| { - if (x + y) % 2 == 0 { - Rgb([0, 0, 0]) - } else { - Rgb([255, 255, 255]) - } - }); - let mut bytes: Vec = Vec::new(); - img.write_to( - &mut std::io::Cursor::new(&mut bytes), - ImageOutputFormat::Jpeg(0), - ) - .unwrap(); - Self(bytes) - } -} - -#[derive(PartialEq, Eq, Debug, Clone, Serialize, Deserialize, sea_orm::FromQueryResult)] -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: chrono::DateTime, - pub uuid: Uuid, -} - -#[cfg(test)] -impl Default for User { - fn default() -> Self { - use chrono::TimeZone; - let epoch = chrono::Utc.timestamp_opt(0, 0).unwrap(); - 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), - } - } -} - -#[derive(PartialEq, Eq, Debug, Serialize, Deserialize)] -pub struct Group { - pub id: GroupId, - pub display_name: String, - pub creation_date: chrono::DateTime, - pub uuid: Uuid, - pub users: Vec, -} - #[derive(PartialEq, Eq, Debug, Serialize, Deserialize, Clone)] pub struct BindRequest { pub name: UserId, @@ -282,23 +72,6 @@ pub trait LoginHandler: Clone + Send { async fn bind(&self, request: BindRequest) -> Result<()>; } -#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] -pub struct GroupId(pub i32); - -#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, sea_orm::FromQueryResult)] -pub struct GroupDetails { - pub group_id: GroupId, - pub display_name: String, - pub creation_date: chrono::DateTime, - pub uuid: Uuid, -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct UserAndGroups { - pub user: User, - pub groups: Option>, -} - #[async_trait] pub trait GroupBackendHandler { async fn list_groups(&self, filters: Option) -> Result>; diff --git a/server/src/domain/ldap/group.rs b/server/src/domain/ldap/group.rs index cd4759a..2ab5b64 100644 --- a/server/src/domain/ldap/group.rs +++ b/server/src/domain/ldap/group.rs @@ -4,8 +4,9 @@ use ldap3_proto::{ use tracing::{debug, info, instrument, warn}; use crate::domain::{ - handler::{BackendHandler, Group, GroupColumn, GroupRequestFilter, UserId, Uuid}, + handler::{BackendHandler, GroupRequestFilter}, ldap::error::LdapError, + types::{Group, GroupColumn, UserId, Uuid}, }; use super::{ diff --git a/server/src/domain/ldap/user.rs b/server/src/domain/ldap/user.rs index 35919e1..08f9853 100644 --- a/server/src/domain/ldap/user.rs +++ b/server/src/domain/ldap/user.rs @@ -4,8 +4,9 @@ use ldap3_proto::{ use tracing::{debug, info, instrument, warn}; use crate::domain::{ - handler::{BackendHandler, GroupDetails, User, UserColumn, UserId, UserRequestFilter}, + handler::{BackendHandler, UserRequestFilter}, ldap::{error::LdapError, utils::expand_attribute_wildcards}, + types::{GroupDetails, User, UserColumn, UserId}, }; use super::{ diff --git a/server/src/domain/ldap/utils.rs b/server/src/domain/ldap/utils.rs index b1de711..e2cbec4 100644 --- a/server/src/domain/ldap/utils.rs +++ b/server/src/domain/ldap/utils.rs @@ -2,9 +2,10 @@ use itertools::Itertools; use ldap3_proto::LdapResultCode; use tracing::{debug, instrument, warn}; -use crate::domain::handler::{GroupColumn, UserColumn, UserId}; - -use super::error::{LdapError, LdapResult}; +use crate::domain::{ + ldap::error::{LdapError, LdapResult}, + types::{GroupColumn, UserColumn, UserId}, +}; fn make_dn_pair(mut iter: I) -> LdapResult<(String, String)> where diff --git a/server/src/domain/mod.rs b/server/src/domain/mod.rs index 8047331..c122cf8 100644 --- a/server/src/domain/mod.rs +++ b/server/src/domain/mod.rs @@ -9,3 +9,4 @@ pub mod sql_migrations; pub mod sql_opaque_handler; pub mod sql_tables; pub mod sql_user_backend_handler; +pub mod types; diff --git a/server/src/domain/model/groups.rs b/server/src/domain/model/groups.rs index eb5342e..748a61e 100644 --- a/server/src/domain/model/groups.rs +++ b/server/src/domain/model/groups.rs @@ -3,7 +3,7 @@ use sea_orm::entity::prelude::*; use serde::{Deserialize, Serialize}; -use crate::domain::handler::{GroupId, Uuid}; +use crate::domain::types::{GroupId, Uuid}; #[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)] #[sea_orm(table_name = "groups")] @@ -15,29 +15,6 @@ pub struct Model { pub uuid: Uuid, } -impl From for crate::domain::handler::Group { - fn from(group: Model) -> Self { - Self { - id: group.group_id, - display_name: group.display_name, - creation_date: group.creation_date, - uuid: group.uuid, - users: vec![], - } - } -} - -impl From for crate::domain::handler::GroupDetails { - fn from(group: Model) -> Self { - Self { - group_id: group.group_id, - display_name: group.display_name, - creation_date: group.creation_date, - uuid: group.uuid, - } - } -} - #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] pub enum Relation { #[sea_orm(has_many = "super::memberships::Entity")] @@ -51,3 +28,26 @@ impl Related for Entity { } impl ActiveModelBehavior for ActiveModel {} + +impl From for crate::domain::types::Group { + fn from(group: Model) -> Self { + Self { + id: group.group_id, + display_name: group.display_name, + creation_date: group.creation_date, + uuid: group.uuid, + users: vec![], + } + } +} + +impl From for crate::domain::types::GroupDetails { + fn from(group: Model) -> Self { + Self { + group_id: group.group_id, + display_name: group.display_name, + creation_date: group.creation_date, + uuid: group.uuid, + } + } +} diff --git a/server/src/domain/model/jwt_refresh_storage.rs b/server/src/domain/model/jwt_refresh_storage.rs index 16b35ef..d7753ff 100644 --- a/server/src/domain/model/jwt_refresh_storage.rs +++ b/server/src/domain/model/jwt_refresh_storage.rs @@ -3,7 +3,7 @@ use sea_orm::entity::prelude::*; use serde::{Deserialize, Serialize}; -use crate::domain::handler::UserId; +use crate::domain::types::UserId; #[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)] #[sea_orm(table_name = "jwt_refresh_storage")] diff --git a/server/src/domain/model/jwt_storage.rs b/server/src/domain/model/jwt_storage.rs index 0df0144..6fc6a4e 100644 --- a/server/src/domain/model/jwt_storage.rs +++ b/server/src/domain/model/jwt_storage.rs @@ -3,7 +3,7 @@ use sea_orm::entity::prelude::*; use serde::{Deserialize, Serialize}; -use crate::domain::handler::UserId; +use crate::domain::types::UserId; #[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)] #[sea_orm(table_name = "jwt_storage")] diff --git a/server/src/domain/model/memberships.rs b/server/src/domain/model/memberships.rs index aff6b3e..a1fb5a7 100644 --- a/server/src/domain/model/memberships.rs +++ b/server/src/domain/model/memberships.rs @@ -3,7 +3,7 @@ use sea_orm::entity::prelude::*; use serde::{Deserialize, Serialize}; -use crate::domain::handler::{GroupId, UserId}; +use crate::domain::types::{GroupId, UserId}; #[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)] #[sea_orm(table_name = "memberships")] diff --git a/server/src/domain/model/password_reset_tokens.rs b/server/src/domain/model/password_reset_tokens.rs index 03ee09b..54b1bea 100644 --- a/server/src/domain/model/password_reset_tokens.rs +++ b/server/src/domain/model/password_reset_tokens.rs @@ -3,7 +3,7 @@ use sea_orm::entity::prelude::*; use serde::{Deserialize, Serialize}; -use crate::domain::handler::UserId; +use crate::domain::types::UserId; #[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)] #[sea_orm(table_name = "password_reset_tokens")] diff --git a/server/src/domain/model/users.rs b/server/src/domain/model/users.rs index 6421084..a9f1b02 100644 --- a/server/src/domain/model/users.rs +++ b/server/src/domain/model/users.rs @@ -3,7 +3,7 @@ use sea_orm::entity::prelude::*; use serde::{Deserialize, Serialize}; -use crate::domain::handler::{JpegPhoto, UserId, Uuid}; +use crate::domain::types::{JpegPhoto, UserId, Uuid}; #[derive(Copy, Clone, Default, Debug, DeriveEntity)] pub struct Entity; @@ -118,7 +118,7 @@ impl Related for Entity { impl ActiveModelBehavior for ActiveModel {} -impl From for crate::domain::handler::User { +impl From for crate::domain::types::User { fn from(user: Model) -> Self { Self { user_id: user.user_id, diff --git a/server/src/domain/opaque_handler.rs b/server/src/domain/opaque_handler.rs index 7e78299..d5f71dd 100644 --- a/server/src/domain/opaque_handler.rs +++ b/server/src/domain/opaque_handler.rs @@ -1,4 +1,4 @@ -use crate::domain::{error::*, handler::UserId}; +use crate::domain::{error::Result, types::UserId}; use async_trait::async_trait; pub use lldap_auth::{login, registration}; diff --git a/server/src/domain/sql_backend_handler.rs b/server/src/domain/sql_backend_handler.rs index 7640510..86181f7 100644 --- a/server/src/domain/sql_backend_handler.rs +++ b/server/src/domain/sql_backend_handler.rs @@ -1,4 +1,4 @@ -use super::{handler::*, sql_tables::*}; +use super::{handler::BackendHandler, sql_tables::DbConnection}; use crate::infra::configuration::Configuration; use async_trait::async_trait; @@ -20,8 +20,16 @@ impl BackendHandler for SqlBackendHandler {} #[cfg(test)] pub mod tests { use super::*; - use crate::domain::sql_tables::init_table; - use crate::infra::configuration::ConfigurationBuilder; + use crate::{ + domain::{ + handler::{ + CreateUserRequest, GroupBackendHandler, UserBackendHandler, UserRequestFilter, + }, + sql_tables::init_table, + types::{GroupId, UserId}, + }, + infra::configuration::ConfigurationBuilder, + }; use lldap_auth::{opaque, registration}; use sea_orm::Database; diff --git a/server/src/domain/sql_group_backend_handler.rs b/server/src/domain/sql_group_backend_handler.rs index 44e5863..f3cf463 100644 --- a/server/src/domain/sql_group_backend_handler.rs +++ b/server/src/domain/sql_group_backend_handler.rs @@ -1,12 +1,9 @@ -use crate::domain::handler::Uuid; - -use super::{ +use crate::domain::{ error::{DomainError, Result}, - handler::{ - Group, GroupBackendHandler, GroupDetails, GroupId, GroupRequestFilter, UpdateGroupRequest, - }, + handler::{GroupBackendHandler, GroupRequestFilter, UpdateGroupRequest}, model::{self, GroupColumn, MembershipColumn}, sql_backend_handler::SqlBackendHandler, + types::{Group, GroupDetails, GroupId, Uuid}, }; use async_trait::async_trait; use sea_orm::{ @@ -155,7 +152,7 @@ impl GroupBackendHandler for SqlBackendHandler { #[cfg(test)] mod tests { use super::*; - use crate::domain::{handler::UserId, sql_backend_handler::tests::*}; + use crate::domain::{sql_backend_handler::tests::*, types::UserId}; async fn get_group_ids( handler: &SqlBackendHandler, diff --git a/server/src/domain/sql_migrations.rs b/server/src/domain/sql_migrations.rs index 7d252b8..62d9e59 100644 --- a/server/src/domain/sql_migrations.rs +++ b/server/src/domain/sql_migrations.rs @@ -1,6 +1,6 @@ -use super::{ - handler::{GroupId, UserId, Uuid}, +use crate::domain::{ sql_tables::{DbConnection, SchemaVersion}, + types::{GroupId, UserId, Uuid}, }; use sea_orm::{ConnectionTrait, FromQueryResult, Statement}; use sea_query::{ColumnDef, Expr, ForeignKey, ForeignKeyAction, Iden, Query, Table, Value}; diff --git a/server/src/domain/sql_opaque_handler.rs b/server/src/domain/sql_opaque_handler.rs index dda433d..9fd7e66 100644 --- a/server/src/domain/sql_opaque_handler.rs +++ b/server/src/domain/sql_opaque_handler.rs @@ -1,9 +1,10 @@ use super::{ error::{DomainError, Result}, - handler::{BindRequest, LoginHandler, UserId}, + handler::{BindRequest, LoginHandler}, model::{self, UserColumn}, opaque_handler::{login, registration, OpaqueHandler}, sql_backend_handler::SqlBackendHandler, + types::UserId, }; use async_trait::async_trait; use lldap_auth::opaque; diff --git a/server/src/domain/sql_tables.rs b/server/src/domain/sql_tables.rs index 1e09ae3..af5615a 100644 --- a/server/src/domain/sql_tables.rs +++ b/server/src/domain/sql_tables.rs @@ -1,8 +1,5 @@ -use super::{ - handler::{GroupId, JpegPhoto, UserId, Uuid}, - sql_migrations::{get_schema_version, migrate_from_version, upgrade_to_v1}, -}; -use sea_orm::{DbErr, Value}; +use super::sql_migrations::{get_schema_version, migrate_from_version, upgrade_to_v1}; +use sea_orm::Value; pub type DbConnection = sea_orm::DatabaseConnection; @@ -19,181 +16,6 @@ impl sea_orm::TryGetable for SchemaVersion { } } -impl From for sea_orm::Value { - fn from(group_id: GroupId) -> Self { - group_id.0.into() - } -} - -impl sea_orm::TryGetable for GroupId { - fn try_get( - res: &sea_orm::QueryResult, - pre: &str, - col: &str, - ) -> Result { - Ok(GroupId(i32::try_get(res, pre, col)?)) - } -} - -impl sea_orm::sea_query::value::ValueType for GroupId { - fn try_from(v: sea_orm::Value) -> Result { - Ok(GroupId(::try_from( - v, - )?)) - } - - fn type_name() -> String { - "GroupId".to_owned() - } - - fn array_type() -> sea_orm::sea_query::ArrayType { - sea_orm::sea_query::ArrayType::Int - } - - fn column_type() -> sea_orm::sea_query::ColumnType { - sea_orm::sea_query::ColumnType::Integer(None) - } -} - -impl sea_orm::TryFromU64 for GroupId { - fn try_from_u64(n: u64) -> Result { - Ok(GroupId(i32::try_from_u64(n)?)) - } -} - -impl From for sea_orm::Value { - fn from(user_id: UserId) -> Self { - user_id.into_string().into() - } -} - -impl From<&UserId> for sea_orm::Value { - fn from(user_id: &UserId) -> Self { - user_id.as_str().into() - } -} - -impl sea_orm::TryGetable for UserId { - fn try_get( - res: &sea_orm::QueryResult, - pre: &str, - col: &str, - ) -> Result { - Ok(UserId::new(&String::try_get(res, pre, col)?)) - } -} - -impl sea_orm::TryFromU64 for UserId { - fn try_from_u64(_n: u64) -> Result { - Err(sea_orm::DbErr::ConvertFromU64( - "UserId cannot be constructed from u64", - )) - } -} - -impl sea_orm::sea_query::value::ValueType for UserId { - fn try_from(v: sea_orm::Value) -> Result { - Ok(UserId::new( - ::try_from(v)?.as_str(), - )) - } - - fn type_name() -> String { - "UserId".to_owned() - } - - fn array_type() -> sea_orm::sea_query::ArrayType { - sea_orm::sea_query::ArrayType::String - } - - fn column_type() -> sea_orm::sea_query::ColumnType { - sea_orm::sea_query::ColumnType::String(Some(255)) - } -} - -impl From for sea_query::Value { - fn from(uuid: Uuid) -> Self { - uuid.as_str().into() - } -} - -impl From<&Uuid> for sea_query::Value { - fn from(uuid: &Uuid) -> Self { - uuid.as_str().into() - } -} - -impl sea_orm::TryGetable for JpegPhoto { - fn try_get( - res: &sea_orm::QueryResult, - pre: &str, - col: &str, - ) -> Result { - >>::try_from(Vec::::try_get(res, pre, col)?) - .map_err(|e| { - sea_orm::TryGetError::DbErr(DbErr::TryIntoErr { - from: "[u8]", - into: "JpegPhoto", - source: e.into(), - }) - }) - } -} - -impl sea_orm::sea_query::value::ValueType for JpegPhoto { - fn try_from(v: sea_orm::Value) -> Result { - >::try_from( - as sea_orm::sea_query::ValueType>::try_from(v)?.as_slice(), - ) - .map_err(|_| sea_orm::sea_query::ValueTypeErr {}) - } - - fn type_name() -> String { - "JpegPhoto".to_owned() - } - - fn array_type() -> sea_orm::sea_query::ArrayType { - sea_orm::sea_query::ArrayType::Bytes - } - - fn column_type() -> sea_orm::sea_query::ColumnType { - sea_orm::sea_query::ColumnType::Binary(sea_orm::sea_query::BlobSize::Long) - } -} - -impl sea_orm::sea_query::Nullable for JpegPhoto { - fn null() -> sea_orm::Value { - JpegPhoto::null().into() - } -} - -impl sea_orm::entity::IntoActiveValue for JpegPhoto { - fn into_active_value(self) -> sea_orm::ActiveValue { - sea_orm::ActiveValue::Set(self) - } -} - -impl sea_orm::sea_query::value::ValueType for Uuid { - fn try_from(v: sea_orm::Value) -> Result { - >::try_from( - ::try_from(v)?.as_str(), - ) - .map_err(|_| sea_orm::sea_query::ValueTypeErr {}) - } - - fn type_name() -> String { - "Uuid".to_owned() - } - - fn array_type() -> sea_orm::sea_query::ArrayType { - sea_orm::sea_query::ArrayType::String - } - - fn column_type() -> sea_orm::sea_query::ColumnType { - sea_orm::sea_query::ColumnType::String(Some(36)) - } -} - impl From for Value { fn from(version: SchemaVersion) -> Self { version.0.into() @@ -215,7 +37,10 @@ pub async fn init_table(pool: &DbConnection) -> anyhow::Result<()> { #[cfg(test)] mod tests { - use crate::domain::sql_migrations; + use crate::domain::{ + sql_migrations, + types::{GroupId, Uuid}, + }; use super::*; use chrono::prelude::*; diff --git a/server/src/domain/sql_user_backend_handler.rs b/server/src/domain/sql_user_backend_handler.rs index 8b4e65e..c19c195 100644 --- a/server/src/domain/sql_user_backend_handler.rs +++ b/server/src/domain/sql_user_backend_handler.rs @@ -1,11 +1,9 @@ use super::{ error::{DomainError, Result}, - handler::{ - CreateUserRequest, GroupDetails, GroupId, UpdateUserRequest, User, UserAndGroups, - UserBackendHandler, UserId, UserRequestFilter, Uuid, - }, + handler::{CreateUserRequest, UpdateUserRequest, UserBackendHandler, UserRequestFilter}, model::{self, GroupColumn, UserColumn}, sql_backend_handler::SqlBackendHandler, + types::{GroupDetails, GroupId, User, UserAndGroups, UserId, Uuid}, }; use async_trait::async_trait; use sea_orm::{ @@ -245,8 +243,8 @@ impl UserBackendHandler for SqlBackendHandler { mod tests { use super::*; use crate::domain::{ - handler::{JpegPhoto, UserColumn}, sql_backend_handler::tests::*, + types::{JpegPhoto, UserColumn}, }; #[tokio::test] diff --git a/server/src/domain/types.rs b/server/src/domain/types.rs new file mode 100644 index 0000000..76673d8 --- /dev/null +++ b/server/src/domain/types.rs @@ -0,0 +1,393 @@ +use sea_orm::{ + entity::IntoActiveValue, + sea_query::{value::ValueType, ArrayType, ColumnType, Nullable, ValueTypeErr}, + DbErr, FromQueryResult, QueryResult, TryFromU64, TryGetError, TryGetable, Value, +}; +use serde::{Deserialize, Serialize}; + +pub use super::model::{GroupColumn, UserColumn}; + +pub type DateTime = chrono::DateTime; + +#[derive(PartialEq, Hash, Eq, Clone, Debug, Default, Serialize, Deserialize)] +#[serde(try_from = "&str")] +pub struct Uuid(String); + +impl Uuid { + pub fn from_name_and_date(name: &str, creation_date: &DateTime) -> Self { + Uuid( + uuid::Uuid::new_v3( + &uuid::Uuid::NAMESPACE_X500, + &[name.as_bytes(), creation_date.to_rfc3339().as_bytes()].concat(), + ) + .to_string(), + ) + } + + pub fn as_str(&self) -> &str { + &self.0 + } + + pub fn into_string(self) -> String { + self.0 + } +} + +impl<'a> std::convert::TryFrom<&'a str> for Uuid { + type Error = anyhow::Error; + fn try_from(s: &'a str) -> anyhow::Result { + Ok(Uuid(uuid::Uuid::parse_str(s)?.to_string())) + } +} + +impl std::string::ToString for Uuid { + fn to_string(&self) -> String { + self.0.clone() + } +} + +impl TryGetable for Uuid { + fn try_get(res: &QueryResult, pre: &str, col: &str) -> std::result::Result { + Ok(Uuid(String::try_get(res, pre, col)?)) + } +} + +impl ValueType for Uuid { + fn try_from(v: Value) -> Result { + >::try_from( + ::try_from(v)?.as_str(), + ) + .map_err(|_| ValueTypeErr {}) + } + + fn type_name() -> String { + "Uuid".to_owned() + } + + fn array_type() -> ArrayType { + ArrayType::String + } + + fn column_type() -> ColumnType { + ColumnType::String(Some(36)) + } +} + +impl From for Value { + fn from(uuid: Uuid) -> Self { + uuid.as_str().into() + } +} + +impl From<&Uuid> for Value { + fn from(uuid: &Uuid) -> Self { + uuid.as_str().into() + } +} + +#[cfg(test)] +#[macro_export] +macro_rules! uuid { + ($s:literal) => { + <$crate::domain::types::Uuid as std::convert::TryFrom<_>>::try_from($s).unwrap() + }; +} + +#[derive(PartialEq, Eq, Clone, Debug, Default, Serialize, Deserialize)] +#[serde(from = "String")] +pub struct UserId(String); + +impl UserId { + pub fn new(user_id: &str) -> Self { + Self(user_id.to_lowercase()) + } + + pub fn as_str(&self) -> &str { + self.0.as_str() + } + + pub fn into_string(self) -> String { + self.0 + } +} + +impl std::fmt::Display for UserId { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + write!(f, "{}", self.0) + } +} + +impl From for UserId { + fn from(s: String) -> Self { + Self::new(&s) + } +} + +impl From for Value { + fn from(user_id: UserId) -> Self { + user_id.into_string().into() + } +} + +impl From<&UserId> for Value { + fn from(user_id: &UserId) -> Self { + user_id.as_str().into() + } +} + +impl TryGetable for UserId { + fn try_get(res: &QueryResult, pre: &str, col: &str) -> Result { + Ok(UserId::new(&String::try_get(res, pre, col)?)) + } +} + +impl TryFromU64 for UserId { + fn try_from_u64(_n: u64) -> Result { + Err(DbErr::ConvertFromU64( + "UserId cannot be constructed from u64", + )) + } +} + +impl ValueType for UserId { + fn try_from(v: Value) -> Result { + Ok(UserId::new(::try_from(v)?.as_str())) + } + + fn type_name() -> String { + "UserId".to_owned() + } + + fn array_type() -> ArrayType { + ArrayType::String + } + + fn column_type() -> ColumnType { + ColumnType::String(Some(255)) + } +} + +#[derive(PartialEq, Eq, Clone, Debug, Serialize, Deserialize)] +pub struct JpegPhoto(#[serde(with = "serde_bytes")] Vec); + +impl From for Value { + fn from(photo: JpegPhoto) -> Self { + photo.0.into() + } +} + +impl From<&JpegPhoto> for Value { + fn from(photo: &JpegPhoto) -> Self { + photo.0.as_slice().into() + } +} + +impl TryFrom<&[u8]> for JpegPhoto { + type Error = anyhow::Error; + fn try_from(bytes: &[u8]) -> anyhow::Result { + if bytes.is_empty() { + return Ok(JpegPhoto::null()); + } + // Confirm that it's a valid Jpeg, then store only the bytes. + image::io::Reader::with_format(std::io::Cursor::new(bytes), image::ImageFormat::Jpeg) + .decode()?; + Ok(JpegPhoto(bytes.to_vec())) + } +} + +impl TryFrom> for JpegPhoto { + type Error = anyhow::Error; + fn try_from(bytes: Vec) -> anyhow::Result { + if bytes.is_empty() { + return Ok(JpegPhoto::null()); + } + // Confirm that it's a valid Jpeg, then store only the bytes. + image::io::Reader::with_format( + std::io::Cursor::new(bytes.as_slice()), + image::ImageFormat::Jpeg, + ) + .decode()?; + Ok(JpegPhoto(bytes)) + } +} + +impl TryFrom for JpegPhoto { + type Error = anyhow::Error; + fn try_from(string: String) -> anyhow::Result { + // The String format is in base64. + >::try_from(base64::decode(string.as_str())?) + } +} + +impl From<&JpegPhoto> for String { + fn from(val: &JpegPhoto) -> Self { + base64::encode(&val.0) + } +} + +impl JpegPhoto { + pub fn null() -> Self { + Self(vec![]) + } + + pub fn into_bytes(self) -> Vec { + self.0 + } + + #[cfg(test)] + pub fn for_tests() -> Self { + use image::{ImageOutputFormat, Rgb, RgbImage}; + let img = RgbImage::from_fn(32, 32, |x, y| { + if (x + y) % 2 == 0 { + Rgb([0, 0, 0]) + } else { + Rgb([255, 255, 255]) + } + }); + let mut bytes: Vec = Vec::new(); + img.write_to( + &mut std::io::Cursor::new(&mut bytes), + ImageOutputFormat::Jpeg(0), + ) + .unwrap(); + Self(bytes) + } +} + +impl TryGetable for JpegPhoto { + fn try_get(res: &QueryResult, pre: &str, col: &str) -> Result { + >>::try_from(Vec::::try_get(res, pre, col)?) + .map_err(|e| { + TryGetError::DbErr(DbErr::TryIntoErr { + from: "[u8]", + into: "JpegPhoto", + source: e.into(), + }) + }) + } +} + +impl ValueType for JpegPhoto { + fn try_from(v: Value) -> Result { + >::try_from( + as sea_orm::sea_query::ValueType>::try_from(v)?.as_slice(), + ) + .map_err(|_| ValueTypeErr {}) + } + + fn type_name() -> String { + "JpegPhoto".to_owned() + } + + fn array_type() -> ArrayType { + ArrayType::Bytes + } + + fn column_type() -> ColumnType { + ColumnType::Binary(sea_orm::sea_query::BlobSize::Long) + } +} + +impl Nullable for JpegPhoto { + fn null() -> Value { + JpegPhoto::null().into() + } +} + +impl IntoActiveValue for JpegPhoto { + fn into_active_value(self) -> sea_orm::ActiveValue { + sea_orm::ActiveValue::Set(self) + } +} + +#[derive(PartialEq, Eq, Debug, Clone, Serialize, Deserialize, FromQueryResult)] +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: DateTime, + pub uuid: Uuid, +} + +#[cfg(test)] +impl Default for User { + fn default() -> Self { + use chrono::TimeZone; + let epoch = chrono::Utc.timestamp_opt(0, 0).unwrap(); + 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), + } + } +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub struct GroupId(pub i32); + +impl From for Value { + fn from(group_id: GroupId) -> Self { + group_id.0.into() + } +} + +impl TryGetable for GroupId { + fn try_get(res: &QueryResult, pre: &str, col: &str) -> Result { + Ok(GroupId(i32::try_get(res, pre, col)?)) + } +} + +impl ValueType for GroupId { + fn try_from(v: Value) -> Result { + Ok(GroupId(::try_from(v)?)) + } + + fn type_name() -> String { + "GroupId".to_owned() + } + + fn array_type() -> ArrayType { + ArrayType::Int + } + + fn column_type() -> ColumnType { + ColumnType::Integer(None) + } +} + +impl TryFromU64 for GroupId { + fn try_from_u64(n: u64) -> Result { + Ok(GroupId(i32::try_from_u64(n)?)) + } +} + +#[derive(PartialEq, Eq, Debug, Serialize, Deserialize)] +pub struct Group { + pub id: GroupId, + pub display_name: String, + pub creation_date: DateTime, + pub uuid: Uuid, + pub users: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, FromQueryResult)] +pub struct GroupDetails { + pub group_id: GroupId, + pub display_name: String, + pub creation_date: DateTime, + pub uuid: Uuid, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct UserAndGroups { + pub user: User, + pub groups: Option>, +} diff --git a/server/src/infra/auth_service.rs b/server/src/infra/auth_service.rs index 207d1fb..e8f8c7e 100644 --- a/server/src/infra/auth_service.rs +++ b/server/src/infra/auth_service.rs @@ -22,12 +22,12 @@ use tracing::{debug, instrument, warn}; use lldap_auth::{login, password_reset, registration, JWTClaims}; -use crate::domain::handler::UserRequestFilter; use crate::{ domain::{ error::DomainError, - handler::{BackendHandler, BindRequest, GroupDetails, LoginHandler, UserColumn, UserId}, + handler::{BackendHandler, BindRequest, LoginHandler, UserRequestFilter}, opaque_handler::OpaqueHandler, + types::{GroupDetails, UserColumn, UserId}, }, infra::{ tcp_backend_handler::*, diff --git a/server/src/infra/configuration.rs b/server/src/infra/configuration.rs index bfdc3ee..209adbe 100644 --- a/server/src/infra/configuration.rs +++ b/server/src/infra/configuration.rs @@ -1,5 +1,5 @@ use crate::{ - domain::handler::UserId, + domain::types::UserId, infra::cli::{GeneralConfigOpts, LdapsOpts, RunOpts, SmtpEncryption, SmtpOpts, TestEmailOpts}, }; use anyhow::{Context, Result}; diff --git a/server/src/infra/graphql/mutation.rs b/server/src/infra/graphql/mutation.rs index 74ed3b2..9a25013 100644 --- a/server/src/infra/graphql/mutation.rs +++ b/server/src/infra/graphql/mutation.rs @@ -1,6 +1,6 @@ -use crate::domain::handler::{ - BackendHandler, CreateUserRequest, GroupId, JpegPhoto, UpdateGroupRequest, UpdateUserRequest, - UserId, +use crate::domain::{ + handler::{BackendHandler, CreateUserRequest, UpdateGroupRequest, UpdateUserRequest}, + types::{GroupId, JpegPhoto, UserId}, }; use anyhow::Context as AnyhowContext; use juniper::{graphql_object, FieldResult, GraphQLInputObject, GraphQLObject}; diff --git a/server/src/infra/graphql/query.rs b/server/src/infra/graphql/query.rs index 4754ee6..03091f6 100644 --- a/server/src/infra/graphql/query.rs +++ b/server/src/infra/graphql/query.rs @@ -1,15 +1,16 @@ use crate::domain::{ - handler::{BackendHandler, GroupDetails, GroupId, UserColumn, UserId}, + handler::BackendHandler, ldap::utils::map_user_field, + types::{GroupDetails, GroupId, UserColumn, UserId}, }; use juniper::{graphql_object, FieldResult, GraphQLInputObject}; use serde::{Deserialize, Serialize}; use tracing::{debug, debug_span, Instrument}; type DomainRequestFilter = crate::domain::handler::UserRequestFilter; -type DomainUser = crate::domain::handler::User; -type DomainGroup = crate::domain::handler::Group; -type DomainUserAndGroups = crate::domain::handler::UserAndGroups; +type DomainUser = crate::domain::types::User; +type DomainGroup = crate::domain::types::Group; +type DomainUserAndGroups = crate::domain::types::UserAndGroups; use super::api::Context; #[derive(PartialEq, Eq, Debug, GraphQLInputObject)] @@ -345,10 +346,7 @@ impl From for Group { #[cfg(test)] mod tests { use super::*; - use crate::{ - domain::handler::{MockTestBackendHandler, UserRequestFilter}, - infra::auth_service::ValidationResults, - }; + use crate::{domain::handler::MockTestBackendHandler, infra::auth_service::ValidationResults}; use chrono::TimeZone; use juniper::{ execute, graphql_value, DefaultScalarValue, EmptyMutation, EmptySubscription, GraphQLType, @@ -457,9 +455,12 @@ mod tests { let mut mock = MockTestBackendHandler::new(); mock.expect_list_users() .with( - eq(Some(UserRequestFilter::Or(vec![ - UserRequestFilter::UserId(UserId::new("bob")), - UserRequestFilter::Equality(UserColumn::Email, "robert@bobbers.on".to_string()), + eq(Some(DomainRequestFilter::Or(vec![ + DomainRequestFilter::UserId(UserId::new("bob")), + DomainRequestFilter::Equality( + UserColumn::Email, + "robert@bobbers.on".to_string(), + ), ]))), eq(false), ) diff --git a/server/src/infra/ldap_handler.rs b/server/src/infra/ldap_handler.rs index 94b7e93..790d2e2 100644 --- a/server/src/infra/ldap_handler.rs +++ b/server/src/infra/ldap_handler.rs @@ -1,10 +1,6 @@ -use std::collections::HashMap; - use crate::{ domain::{ - handler::{ - BackendHandler, BindRequest, CreateUserRequest, JpegPhoto, LoginHandler, UserId, - }, + handler::{BackendHandler, BindRequest, CreateUserRequest, LoginHandler}, ldap::{ error::{LdapError, LdapResult}, group::get_groups_list, @@ -14,6 +10,7 @@ use crate::{ }, }, opaque_handler::OpaqueHandler, + types::{JpegPhoto, UserId}, }, infra::auth_service::{Permission, ValidationResults}, }; @@ -24,6 +21,7 @@ use ldap3_proto::proto::{ LdapResult as LdapResultOp, LdapResultCode, LdapSearchRequest, LdapSearchResultEntry, LdapSearchScope, }; +use std::collections::HashMap; use tracing::{debug, instrument, warn}; #[derive(Debug, PartialEq, Eq, Clone)] @@ -569,7 +567,7 @@ impl LdapHandler