domain + server: introduce new AttributeValue enum

This commit is contained in:
Simon Broeng Jensen
2025-02-19 14:43:26 +01:00
committed by nitnelave
parent cf0e9a01f1
commit 5d2f168554
18 changed files with 441 additions and 326 deletions
+118 -2
View File
@@ -130,6 +130,21 @@ impl Serialized {
} }
} }
impl From<AttributeValue> for Serialized {
fn from(val: AttributeValue) -> Serialized {
match &val {
AttributeValue::String(Cardinality::Singleton(s)) => Serialized::from(s),
AttributeValue::String(Cardinality::Unbounded(l)) => Serialized::from(l),
AttributeValue::Integer(Cardinality::Singleton(i)) => Serialized::from(i),
AttributeValue::Integer(Cardinality::Unbounded(l)) => Serialized::from(l),
AttributeValue::JpegPhoto(Cardinality::Singleton(p)) => Serialized::from(p),
AttributeValue::JpegPhoto(Cardinality::Unbounded(l)) => Serialized::from(l),
AttributeValue::DateTime(Cardinality::Singleton(dt)) => Serialized::from(dt),
AttributeValue::DateTime(Cardinality::Unbounded(l)) => Serialized::from(l),
}
}
}
fn compare_str_case_insensitive(s1: &str, s2: &str) -> Ordering { fn compare_str_case_insensitive(s1: &str, s2: &str) -> Ordering {
let mut it_1 = s1.chars().flat_map(|c| c.to_lowercase()); let mut it_1 = s1.chars().flat_map(|c| c.to_lowercase());
let mut it_2 = s2.chars().flat_map(|c| c.to_lowercase()); let mut it_2 = s2.chars().flat_map(|c| c.to_lowercase());
@@ -292,7 +307,7 @@ impl AsRef<GroupName> for GroupName {
} }
} }
#[derive(PartialEq, Eq, Clone, Serialize, Deserialize, DeriveValueType)] #[derive(PartialEq, Eq, Clone, Serialize, Deserialize, DeriveValueType, Hash)]
#[sea_orm(column_type = "Binary(BlobSize::Long)", array_type = "Bytes")] #[sea_orm(column_type = "Binary(BlobSize::Long)", array_type = "Bytes")]
pub struct JpegPhoto(#[serde(with = "serde_bytes")] Vec<u8>); pub struct JpegPhoto(#[serde(with = "serde_bytes")] Vec<u8>);
@@ -407,10 +422,111 @@ impl IntoActiveValue<Serialized> for JpegPhoto {
} }
} }
// Represents values that can be either a singleton or a list of a specific type
// Used by AttributeValue to model attributes with types that might be a list.
#[derive(PartialEq, Eq, Debug, Clone, Serialize, Deserialize, Hash)]
pub enum Cardinality<T: Clone> {
Singleton(T),
Unbounded(Vec<T>),
}
impl<T: Clone> Cardinality<T> {
pub fn into_vec(self) -> Vec<T> {
match self {
Self::Singleton(v) => vec![v],
Self::Unbounded(l) => l,
}
}
}
#[derive(PartialEq, Eq, Debug, Clone, Serialize, Deserialize, Hash)]
pub enum AttributeValue {
String(Cardinality<String>),
Integer(Cardinality<i64>),
JpegPhoto(Cardinality<JpegPhoto>),
DateTime(Cardinality<NaiveDateTime>),
}
impl AttributeValue {
pub fn get_attribute_type(&self) -> AttributeType {
match self {
Self::String(_) => AttributeType::String,
Self::Integer(_) => AttributeType::Integer,
Self::JpegPhoto(_) => AttributeType::JpegPhoto,
Self::DateTime(_) => AttributeType::DateTime,
}
}
pub fn as_str(&self) -> Option<&str> {
if let AttributeValue::String(Cardinality::Singleton(s)) = self {
Some(s.as_str())
} else {
None
}
}
pub fn into_string(self) -> Option<String> {
if let AttributeValue::String(Cardinality::Singleton(s)) = self {
Some(s)
} else {
None
}
}
pub fn as_jpeg_photo(&self) -> Option<&JpegPhoto> {
if let AttributeValue::JpegPhoto(Cardinality::Singleton(p)) = self {
Some(p)
} else {
None
}
}
}
impl From<String> for AttributeValue {
fn from(s: String) -> Self {
AttributeValue::String(Cardinality::Singleton(s))
}
}
impl From<Vec<String>> for AttributeValue {
fn from(l: Vec<String>) -> Self {
AttributeValue::String(Cardinality::Unbounded(l))
}
}
impl From<i64> for AttributeValue {
fn from(i: i64) -> Self {
AttributeValue::Integer(Cardinality::Singleton(i))
}
}
impl From<Vec<i64>> for AttributeValue {
fn from(l: Vec<i64>) -> Self {
AttributeValue::Integer(Cardinality::Unbounded(l))
}
}
impl From<JpegPhoto> for AttributeValue {
fn from(j: JpegPhoto) -> Self {
AttributeValue::JpegPhoto(Cardinality::Singleton(j))
}
}
impl From<Vec<JpegPhoto>> for AttributeValue {
fn from(l: Vec<JpegPhoto>) -> Self {
AttributeValue::JpegPhoto(Cardinality::Unbounded(l))
}
}
impl From<NaiveDateTime> for AttributeValue {
fn from(dt: NaiveDateTime) -> Self {
AttributeValue::DateTime(Cardinality::Singleton(dt))
}
}
impl From<Vec<NaiveDateTime>> for AttributeValue {
fn from(l: Vec<NaiveDateTime>) -> Self {
AttributeValue::DateTime(Cardinality::Unbounded(l))
}
}
#[derive(PartialEq, Eq, Debug, Clone, Serialize, Deserialize, Hash)] #[derive(PartialEq, Eq, Debug, Clone, Serialize, Deserialize, Hash)]
pub struct Attribute { pub struct Attribute {
pub name: AttributeName, pub name: AttributeName,
pub value: Serialized, pub value: AttributeValue,
} }
#[derive(PartialEq, Eq, Debug, Clone, Serialize, Deserialize)] #[derive(PartialEq, Eq, Debug, Clone, Serialize, Deserialize)]
+20 -29
View File
@@ -1,50 +1,41 @@
use anyhow::{bail, Context as AnyhowContext}; use anyhow::{bail, Context as AnyhowContext, Result};
use lldap_domain::types::{AttributeType, JpegPhoto, Serialized}; use lldap_domain::types::{AttributeType, AttributeValue, JpegPhoto};
pub fn deserialize_attribute_value( pub fn deserialize_attribute_value(
value: &[String], value: &[String],
typ: AttributeType, typ: AttributeType,
is_list: bool, is_list: bool,
) -> anyhow::Result<Serialized> { ) -> Result<AttributeValue> {
if !is_list && value.len() != 1 { if !is_list && value.len() != 1 {
bail!("Attribute is not a list, but multiple values were provided",); bail!("Attribute is not a list, but multiple values were provided",);
} }
let parse_int = |value: &String| -> anyhow::Result<i64> { let parse_int = |value: &String| -> Result<i64> {
value value
.parse::<i64>() .parse::<i64>()
.with_context(|| format!("Invalid integer value {}", value)) .with_context(|| format!("Invalid integer value {}", value))
}; };
let parse_date = |value: &String| -> anyhow::Result<chrono::NaiveDateTime> { let parse_date = |value: &String| -> Result<chrono::NaiveDateTime> {
Ok(chrono::DateTime::parse_from_rfc3339(value) Ok(chrono::DateTime::parse_from_rfc3339(value)
.with_context(|| format!("Invalid date value {}", value))? .with_context(|| format!("Invalid date value {}", value))?
.naive_utc()) .naive_utc())
}; };
let parse_photo = |value: &String| -> anyhow::Result<JpegPhoto> { let parse_photo = |value: &String| -> Result<JpegPhoto> {
JpegPhoto::try_from(value.as_str()).context("Provided image is not a valid JPEG") JpegPhoto::try_from(value.as_str()).context("Provided image is not a valid JPEG")
}; };
Ok(match (typ, is_list) { Ok(match (typ, is_list) {
(AttributeType::String, false) => Serialized::from(&value[0]), (AttributeType::String, false) => value[0].clone().into(),
(AttributeType::String, true) => Serialized::from(&value), (AttributeType::String, true) => value.to_vec().into(),
(AttributeType::Integer, false) => Serialized::from(&parse_int(&value[0])?), (AttributeType::Integer, false) => (parse_int(&value[0])?).into(),
(AttributeType::Integer, true) => Serialized::from( (AttributeType::Integer, true) => {
&value (value.iter().map(parse_int).collect::<Result<Vec<_>>>()?).into()
.iter() }
.map(parse_int) (AttributeType::DateTime, false) => (parse_date(&value[0])?).into(),
.collect::<anyhow::Result<Vec<_>>>()?, (AttributeType::DateTime, true) => {
), (value.iter().map(parse_date).collect::<Result<Vec<_>>>()?).into()
(AttributeType::DateTime, false) => Serialized::from(&parse_date(&value[0])?), }
(AttributeType::DateTime, true) => Serialized::from( (AttributeType::JpegPhoto, false) => (parse_photo(&value[0])?).into(),
&value (AttributeType::JpegPhoto, true) => {
.iter() (value.iter().map(parse_photo).collect::<Result<Vec<_>>>()?).into()
.map(parse_date) }
.collect::<anyhow::Result<Vec<_>>>()?,
),
(AttributeType::JpegPhoto, false) => Serialized::from(&parse_photo(&value[0])?),
(AttributeType::JpegPhoto, true) => Serialized::from(
&value
.iter()
.map(parse_photo)
.collect::<anyhow::Result<Vec<_>>>()?,
),
}) })
} }
+4 -4
View File
@@ -7,8 +7,8 @@ use lldap_domain::{
}, },
schema::Schema, schema::Schema,
types::{ types::{
AttributeName, Group, GroupDetails, GroupId, GroupName, LdapObjectClass, Serialized, User, AttributeName, AttributeValue, Group, GroupDetails, GroupId, GroupName, LdapObjectClass,
UserAndGroups, UserId, Uuid, User, UserAndGroups, UserId, Uuid,
}, },
}; };
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@@ -59,7 +59,7 @@ pub enum UserRequestFilter {
UserId(UserId), UserId(UserId),
UserIdSubString(SubStringFilter), UserIdSubString(SubStringFilter),
Equality(UserColumn, String), Equality(UserColumn, String),
AttributeEquality(AttributeName, Serialized), AttributeEquality(AttributeName, AttributeValue),
SubString(UserColumn, SubStringFilter), SubString(UserColumn, SubStringFilter),
// Check if a user belongs to a group identified by name. // Check if a user belongs to a group identified by name.
MemberOf(GroupName), MemberOf(GroupName),
@@ -89,7 +89,7 @@ pub enum GroupRequestFilter {
GroupId(GroupId), GroupId(GroupId),
// Check if the group contains a user identified by uid. // Check if the group contains a user identified by uid.
Member(UserId), Member(UserId),
AttributeEquality(AttributeName, Serialized), AttributeEquality(AttributeName, AttributeValue),
CustomAttributePresent(AttributeName), CustomAttributePresent(AttributeName),
} }
+3 -6
View File
@@ -16,7 +16,7 @@ use crate::domain::{
GroupFieldType, LdapInfo, GroupFieldType, LdapInfo,
}, },
}, },
schema::{PublicSchema, SchemaGroupAttributeExtractor}, schema::PublicSchema,
}; };
use lldap_domain::types::{ use lldap_domain::types::{
AttributeName, AttributeType, Group, GroupId, LdapObjectClass, UserId, Uuid, AttributeName, AttributeType, Group, GroupId, LdapObjectClass, UserId, Uuid,
@@ -62,9 +62,7 @@ pub fn get_group_attribute(
.map(|u| format!("uid={},ou=people,{}", u, base_dn_str).into_bytes()) .map(|u| format!("uid={},ou=people,{}", u, base_dn_str).into_bytes())
.collect(), .collect(),
GroupFieldType::Uuid => vec![group.uuid.to_string().into_bytes()], GroupFieldType::Uuid => vec![group.uuid.to_string().into_bytes()],
GroupFieldType::Attribute(attr, _, _) => { GroupFieldType::Attribute(attr, _, _) => get_custom_attribute(&group.attributes, &attr)?,
get_custom_attribute::<SchemaGroupAttributeExtractor>(&group.attributes, &attr, schema)?
}
GroupFieldType::NoMatch => match attribute.as_str() { GroupFieldType::NoMatch => match attribute.as_str() {
"1.1" => return None, "1.1" => return None,
// We ignore the operational attribute wildcard // We ignore the operational attribute wildcard
@@ -79,10 +77,9 @@ pub fn get_group_attribute(
if ignored_group_attributes.contains(attribute) { if ignored_group_attributes.contains(attribute) {
return None; return None;
} }
get_custom_attribute::<SchemaGroupAttributeExtractor>( get_custom_attribute(
&group.attributes, &group.attributes,
attribute, attribute,
schema,
).or_else(||{warn!( ).or_else(||{warn!(
r#"Ignoring unrecognized group attribute: {}. To disable this warning, add it to "ignored_group_attributes" in the config."#, r#"Ignoring unrecognized group attribute: {}. To disable this warning, add it to "ignored_group_attributes" in the config."#,
attribute attribute
+3 -10
View File
@@ -17,7 +17,7 @@ use crate::domain::{
}, },
}, },
model::UserColumn, model::UserColumn,
schema::{PublicSchema, SchemaUserAttributeExtractor}, schema::PublicSchema,
}; };
use lldap_domain::types::{ use lldap_domain::types::{
AttributeName, AttributeType, GroupDetails, LdapObjectClass, User, UserAndGroups, UserId, AttributeName, AttributeType, GroupDetails, LdapObjectClass, User, UserAndGroups, UserId,
@@ -78,9 +78,7 @@ pub fn get_user_attribute(
.from_utc_datetime(&user.creation_date) .from_utc_datetime(&user.creation_date)
.to_rfc3339() .to_rfc3339()
.into_bytes()], .into_bytes()],
UserFieldType::Attribute(attr, _, _) => { UserFieldType::Attribute(attr, _, _) => get_custom_attribute(&user.attributes, &attr)?,
get_custom_attribute::<SchemaUserAttributeExtractor>(&user.attributes, &attr, schema)?
}
UserFieldType::NoMatch => match attribute.as_str() { UserFieldType::NoMatch => match attribute.as_str() {
"1.1" => return None, "1.1" => return None,
// We ignore the operational attribute wildcard. // We ignore the operational attribute wildcard.
@@ -95,12 +93,7 @@ pub fn get_user_attribute(
if ignored_user_attributes.contains(attribute) { if ignored_user_attributes.contains(attribute) {
return None; return None;
} }
get_custom_attribute::<SchemaUserAttributeExtractor>( get_custom_attribute(&user.attributes, attribute).or_else(|| {
&user.attributes,
attribute,
schema,
)
.or_else(|| {
warn!( warn!(
r#"Ignoring unrecognized user attribute: {}. To disable this warning, add it to "ignored_user_attributes" in the config."#, r#"Ignoring unrecognized user attribute: {}. To disable this warning, add it to "ignored_user_attributes" in the config."#,
attribute attribute
+35 -51
View File
@@ -1,6 +1,6 @@
use std::collections::BTreeMap; use std::collections::BTreeMap;
use chrono::{NaiveDateTime, TimeZone}; use chrono::TimeZone;
use ldap3_proto::{proto::LdapSubstringFilter, LdapResultCode}; use ldap3_proto::{proto::LdapSubstringFilter, LdapResultCode};
use tracing::{debug, instrument, warn}; use tracing::{debug, instrument, warn};
@@ -8,9 +8,11 @@ use crate::domain::{
handler::SubStringFilter, handler::SubStringFilter,
ldap::error::{LdapError, LdapResult}, ldap::error::{LdapError, LdapResult},
model::UserColumn, model::UserColumn,
schema::{PublicSchema, SchemaAttributeExtractor}, schema::PublicSchema,
};
use lldap_domain::types::{
Attribute, AttributeName, AttributeType, AttributeValue, Cardinality, GroupName, UserId,
}; };
use lldap_domain::types::{Attribute, AttributeName, AttributeType, GroupName, JpegPhoto, UserId};
impl From<LdapSubstringFilter> for SubStringFilter { impl From<LdapSubstringFilter> for SubStringFilter {
fn from( fn from(
@@ -284,62 +286,44 @@ pub struct LdapInfo {
pub ignored_group_attributes: Vec<AttributeName>, pub ignored_group_attributes: Vec<AttributeName>,
} }
pub fn get_custom_attribute<Extractor: SchemaAttributeExtractor>( pub fn get_custom_attribute(
attributes: &[Attribute], attributes: &[Attribute],
attribute_name: &AttributeName, attribute_name: &AttributeName,
schema: &PublicSchema,
) -> Option<Vec<Vec<u8>>> { ) -> Option<Vec<Vec<u8>>> {
let convert_date = |date| { let convert_date = |date| {
chrono::Utc chrono::Utc
.from_utc_datetime(&date) .from_utc_datetime(date)
.to_rfc3339() .to_rfc3339()
.into_bytes() .into_bytes()
}; };
Extractor::get_attributes(schema) attributes
.get_attribute_type(attribute_name) .iter()
.and_then(|attribute_type| { .find(|a| &a.name == attribute_name)
attributes .map(|attribute| match &attribute.value {
AttributeValue::String(Cardinality::Singleton(s)) => {
vec![s.clone().into_bytes()]
}
AttributeValue::String(Cardinality::Unbounded(l)) => {
l.iter().map(|s| s.clone().into_bytes()).collect()
}
AttributeValue::Integer(Cardinality::Singleton(i)) => {
// LDAP integers are encoded as strings.
vec![i.to_string().into_bytes()]
}
AttributeValue::Integer(Cardinality::Unbounded(l)) => l
.iter() .iter()
.find(|a| &a.name == attribute_name) // LDAP integers are encoded as strings.
.map(|attribute| match attribute_type { .map(|i| i.to_string().into_bytes())
(AttributeType::String, false) => { .collect(),
vec![attribute.value.unwrap::<String>().into_bytes()] AttributeValue::JpegPhoto(Cardinality::Singleton(p)) => {
} vec![p.clone().into_bytes()]
(AttributeType::Integer, false) => { }
// LDAP integers are encoded as strings. AttributeValue::JpegPhoto(Cardinality::Unbounded(l)) => {
vec![attribute.value.unwrap::<i64>().to_string().into_bytes()] l.iter().map(|p| p.clone().into_bytes()).collect()
} }
(AttributeType::JpegPhoto, false) => { AttributeValue::DateTime(Cardinality::Singleton(dt)) => vec![convert_date(dt)],
vec![attribute.value.unwrap::<JpegPhoto>().into_bytes()] AttributeValue::DateTime(Cardinality::Unbounded(l)) => {
} l.iter().map(convert_date).collect()
(AttributeType::DateTime, false) => { }
vec![convert_date(attribute.value.unwrap::<NaiveDateTime>())]
}
(AttributeType::String, true) => attribute
.value
.unwrap::<Vec<String>>()
.into_iter()
.map(String::into_bytes)
.collect(),
(AttributeType::Integer, true) => attribute
.value
.unwrap::<Vec<i64>>()
.into_iter()
.map(|i| i.to_string())
.map(String::into_bytes)
.collect(),
(AttributeType::JpegPhoto, true) => attribute
.value
.unwrap::<Vec<JpegPhoto>>()
.into_iter()
.map(JpegPhoto::into_bytes)
.collect(),
(AttributeType::DateTime, true) => attribute
.value
.unwrap::<Vec<NaiveDateTime>>()
.into_iter()
.map(convert_date)
.collect(),
})
}) })
} }
+57
View File
@@ -0,0 +1,57 @@
use crate::domain::error::DomainError;
use lldap_domain::{
schema::AttributeList,
types::{Attribute, AttributeName, AttributeType, AttributeValue, Cardinality, Serialized},
};
// Value must be a serialized attribute value of the type denoted by typ,
// and either a singleton or unbounded list, depending on is_list.
pub(crate) fn deserialize_attribute_value(
value: &Serialized,
typ: AttributeType,
is_list: bool,
) -> AttributeValue {
match (typ, is_list) {
(AttributeType::String, false) => {
AttributeValue::String(Cardinality::Singleton(value.unwrap()))
}
(AttributeType::String, true) => {
AttributeValue::String(Cardinality::Unbounded(value.unwrap()))
}
(AttributeType::Integer, false) => {
AttributeValue::Integer(Cardinality::Singleton(value.unwrap::<i64>()))
}
(AttributeType::Integer, true) => {
AttributeValue::Integer(Cardinality::Unbounded(value.unwrap()))
}
(AttributeType::DateTime, false) => {
AttributeValue::DateTime(Cardinality::Singleton(value.unwrap()))
}
(AttributeType::DateTime, true) => {
AttributeValue::DateTime(Cardinality::Unbounded(value.unwrap()))
}
(AttributeType::JpegPhoto, false) => {
AttributeValue::JpegPhoto(Cardinality::Singleton(value.unwrap()))
}
(AttributeType::JpegPhoto, true) => {
AttributeValue::JpegPhoto(Cardinality::Unbounded(value.unwrap()))
}
}
}
pub(crate) fn deserialize_attribute(
name: AttributeName,
value: &Serialized,
schema: &AttributeList,
) -> Result<Attribute, DomainError> {
match schema.get_attribute_type(&name) {
Some((typ, is_list)) => Ok(Attribute {
name,
value: deserialize_attribute_value(value, typ, is_list),
}),
None => Err(DomainError::InternalError(format!(
"Unable to find schema for attribute named '{}'",
name.into_string()
))),
}
}
+1 -16
View File
@@ -1,7 +1,7 @@
use sea_orm::entity::prelude::*; use sea_orm::entity::prelude::*;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use lldap_domain::types::{Attribute, AttributeName, GroupId, Serialized}; use lldap_domain::types::{AttributeName, GroupId, Serialized};
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)] #[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)]
#[sea_orm(table_name = "group_attributes")] #[sea_orm(table_name = "group_attributes")]
@@ -55,18 +55,3 @@ impl Related<super::GroupAttributeSchema> for Entity {
} }
impl ActiveModelBehavior for ActiveModel {} impl ActiveModelBehavior for ActiveModel {}
impl From<Model> for Attribute {
fn from(
Model {
group_id: _,
attribute_name,
value,
}: Model,
) -> Self {
Self {
name: attribute_name,
value,
}
}
}
+1
View File
@@ -1,5 +1,6 @@
pub mod prelude; pub mod prelude;
pub(crate) mod deserialize;
pub mod groups; pub mod groups;
pub mod jwt_refresh_storage; pub mod jwt_refresh_storage;
pub mod jwt_storage; pub mod jwt_storage;
+1 -16
View File
@@ -1,7 +1,7 @@
use sea_orm::entity::prelude::*; use sea_orm::entity::prelude::*;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use lldap_domain::types::{Attribute, AttributeName, Serialized, UserId}; use lldap_domain::types::{AttributeName, Serialized, UserId};
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)] #[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)]
#[sea_orm(table_name = "user_attributes")] #[sea_orm(table_name = "user_attributes")]
@@ -55,18 +55,3 @@ impl Related<super::UserAttributeSchema> for Entity {
} }
impl ActiveModelBehavior for ActiveModel {} impl ActiveModelBehavior for ActiveModel {}
impl From<Model> for Attribute {
fn from(
Model {
user_id: _,
attribute_name,
value,
}: Model,
) -> Self {
Self {
name: attribute_name,
value,
}
}
}
+1 -21
View File
@@ -1,5 +1,5 @@
use lldap_domain::{ use lldap_domain::{
schema::{AttributeList, AttributeSchema, Schema}, schema::{AttributeSchema, Schema},
types::AttributeType, types::AttributeType,
}; };
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@@ -13,26 +13,6 @@ impl PublicSchema {
} }
} }
pub trait SchemaAttributeExtractor: std::marker::Send {
fn get_attributes(schema: &PublicSchema) -> &AttributeList;
}
pub struct SchemaUserAttributeExtractor;
impl SchemaAttributeExtractor for SchemaUserAttributeExtractor {
fn get_attributes(schema: &PublicSchema) -> &AttributeList {
&schema.get_schema().user_attributes
}
}
pub struct SchemaGroupAttributeExtractor;
impl SchemaAttributeExtractor for SchemaGroupAttributeExtractor {
fn get_attributes(schema: &PublicSchema) -> &AttributeList {
&schema.get_schema().group_attributes
}
}
impl From<Schema> for PublicSchema { impl From<Schema> for PublicSchema {
fn from(mut schema: Schema) -> Self { fn from(mut schema: Schema) -> Self {
schema.user_attributes.attributes.extend_from_slice(&[ schema.user_attributes.attributes.extend_from_slice(&[
+3 -3
View File
@@ -33,7 +33,7 @@ pub mod tests {
use lldap_auth::{opaque, registration}; use lldap_auth::{opaque, registration};
use lldap_domain::{ use lldap_domain::{
requests::{CreateGroupRequest, CreateUserRequest}, requests::{CreateGroupRequest, CreateUserRequest},
types::{Attribute as DomainAttribute, GroupId, Serialized, UserId}, types::{Attribute as DomainAttribute, GroupId, UserId},
}; };
use pretty_assertions::assert_eq; use pretty_assertions::assert_eq;
use sea_orm::Database; use sea_orm::Database;
@@ -95,11 +95,11 @@ pub mod tests {
attributes: vec![ attributes: vec![
DomainAttribute { DomainAttribute {
name: "first_name".into(), name: "first_name".into(),
value: Serialized::from(("first ".to_string() + name).as_str()), value: ("first ".to_string() + name).into(),
}, },
DomainAttribute { DomainAttribute {
name: "last_name".into(), name: "last_name".into(),
value: Serialized::from(("last ".to_string() + name).as_str()), value: ("last ".to_string() + name).into(),
}, },
], ],
}) })
+39 -18
View File
@@ -1,13 +1,16 @@
use crate::domain::{ use crate::{
error::{DomainError, Result}, domain::{
handler::{GroupBackendHandler, GroupListerBackendHandler, GroupRequestFilter}, error::{DomainError, Result},
model::{self, GroupColumn, MembershipColumn}, handler::{GroupBackendHandler, GroupListerBackendHandler, GroupRequestFilter},
sql_backend_handler::SqlBackendHandler, model::{self, deserialize, GroupColumn, MembershipColumn},
sql_backend_handler::SqlBackendHandler,
},
infra::access_control::UserReadableBackendHandler,
}; };
use async_trait::async_trait; use async_trait::async_trait;
use lldap_domain::{ use lldap_domain::{
requests::{CreateGroupRequest, UpdateGroupRequest}, requests::{CreateGroupRequest, UpdateGroupRequest},
types::{Attribute, AttributeName, Group, GroupDetails, GroupId, Serialized, Uuid}, types::{AttributeName, Group, GroupDetails, GroupId, Serialized, Uuid},
}; };
use sea_orm::{ use sea_orm::{
sea_query::{Alias, Cond, Expr, Func, IntoCondition, OnConflict, SimpleExpr}, sea_query::{Alias, Cond, Expr, Func, IntoCondition, OnConflict, SimpleExpr},
@@ -75,7 +78,7 @@ fn get_group_filter_expr(filter: GroupRequestFilter) -> Cond {
)))) ))))
.like(filter.to_sql_filter()) .like(filter.to_sql_filter())
.into_condition(), .into_condition(),
AttributeEquality(name, value) => attribute_condition(name, Some(value)), AttributeEquality(name, value) => attribute_condition(name, Some(value.into())),
CustomAttributePresent(name) => attribute_condition(name, None), CustomAttributePresent(name) => attribute_condition(name, None),
} }
} }
@@ -114,6 +117,8 @@ impl GroupListerBackendHandler for SqlBackendHandler {
} }
}) })
.collect(); .collect();
// TODO: should be wrapped in a transaction
let schema = self.get_schema().await?;
let attributes = model::GroupAttributes::find() let attributes = model::GroupAttributes::find()
.filter( .filter(
model::GroupAttributesColumn::GroupId.in_subquery( model::GroupAttributesColumn::GroupId.in_subquery(
@@ -133,8 +138,14 @@ impl GroupListerBackendHandler for SqlBackendHandler {
for group in groups.iter_mut() { for group in groups.iter_mut() {
group.attributes = attributes_iter group.attributes = attributes_iter
.take_while_ref(|u| u.group_id == group.id) .take_while_ref(|u| u.group_id == group.id)
.map(Attribute::from) .map(|a| {
.collect(); deserialize::deserialize_attribute(
a.attribute_name,
&a.value,
&schema.get_schema().group_attributes,
)
})
.collect::<Result<Vec<_>>>()?;
} }
groups.sort_by(|g1, g2| g1.display_name.cmp(&g2.display_name)); groups.sort_by(|g1, g2| g1.display_name.cmp(&g2.display_name));
Ok(groups) Ok(groups)
@@ -155,7 +166,17 @@ impl GroupBackendHandler for SqlBackendHandler {
.order_by_asc(model::GroupAttributesColumn::AttributeName) .order_by_asc(model::GroupAttributesColumn::AttributeName)
.all(&self.sql_pool) .all(&self.sql_pool)
.await?; .await?;
group_details.attributes = attributes.into_iter().map(Attribute::from).collect(); let schema = self.get_schema().await?;
group_details.attributes = attributes
.into_iter()
.map(|a| {
deserialize::deserialize_attribute(
a.attribute_name,
&a.value,
&schema.get_schema().group_attributes,
)
})
.collect::<Result<Vec<_>>>()?;
Ok(group_details) Ok(group_details)
} }
@@ -199,7 +220,7 @@ impl GroupBackendHandler for SqlBackendHandler {
new_group_attributes.push(model::group_attributes::ActiveModel { new_group_attributes.push(model::group_attributes::ActiveModel {
group_id: Set(group_id), group_id: Set(group_id),
attribute_name: Set(attribute.name), attribute_name: Set(attribute.name),
value: Set(attribute.value), value: Set(attribute.value.into()),
}); });
} else { } else {
return Err(DomainError::InternalError(format!( return Err(DomainError::InternalError(format!(
@@ -263,7 +284,7 @@ impl SqlBackendHandler {
update_group_attributes.push(model::group_attributes::ActiveModel { update_group_attributes.push(model::group_attributes::ActiveModel {
group_id: Set(request.group_id), group_id: Set(request.group_id),
attribute_name: Set(attribute.name.to_owned()), attribute_name: Set(attribute.name.to_owned()),
value: Set(attribute.value), value: Set(attribute.value.into()),
}); });
} else { } else {
return Err(DomainError::InternalError(format!( return Err(DomainError::InternalError(format!(
@@ -319,7 +340,7 @@ mod tests {
}; };
use lldap_domain::{ use lldap_domain::{
requests::CreateAttributeRequest, requests::CreateAttributeRequest,
types::{AttributeType, GroupName, Serialized, UserId}, types::{Attribute, AttributeType, GroupName, UserId},
}; };
use pretty_assertions::assert_eq; use pretty_assertions::assert_eq;
@@ -449,7 +470,7 @@ mod tests {
delete_attributes: Vec::new(), delete_attributes: Vec::new(),
insert_attributes: vec![Attribute { insert_attributes: vec![Attribute {
name: "gid".into(), name: "gid".into(),
value: Serialized::from(&512), value: 512.into(),
}], }],
}) })
.await .await
@@ -459,7 +480,7 @@ mod tests {
&fixture.handler, &fixture.handler,
Some(GroupRequestFilter::AttributeEquality( Some(GroupRequestFilter::AttributeEquality(
AttributeName::from("gid"), AttributeName::from("gid"),
Serialized::from(&512), 512.into(),
)), )),
) )
.await, .await,
@@ -550,7 +571,7 @@ mod tests {
display_name: "New Group".into(), display_name: "New Group".into(),
attributes: vec![Attribute { attributes: vec![Attribute {
name: "new_attribute".into(), name: "new_attribute".into(),
value: Serialized::from("value"), value: "value".to_string().into(),
}], }],
}) })
.await .await
@@ -565,7 +586,7 @@ mod tests {
group_details.attributes, group_details.attributes,
vec![Attribute { vec![Attribute {
name: "new_attribute".into(), name: "new_attribute".into(),
value: Serialized::from("value"), value: "value".to_string().into(),
}] }]
); );
} }
@@ -587,7 +608,7 @@ mod tests {
let group_id = fixture.groups[0]; let group_id = fixture.groups[0];
let attributes = vec![Attribute { let attributes = vec![Attribute {
name: "new_attribute".into(), name: "new_attribute".into(),
value: Serialized::from(&42i64), value: 42i64.into(),
}]; }];
fixture fixture
.handler .handler
@@ -181,7 +181,7 @@ mod tests {
}; };
use lldap_domain::requests::UpdateUserRequest; use lldap_domain::requests::UpdateUserRequest;
use lldap_domain::schema::AttributeList; use lldap_domain::schema::AttributeList;
use lldap_domain::types::{Attribute, AttributeType, Serialized}; use lldap_domain::types::{Attribute, AttributeType};
use pretty_assertions::assert_eq; use pretty_assertions::assert_eq;
#[tokio::test] #[tokio::test]
@@ -298,7 +298,7 @@ mod tests {
user_id: "bob".into(), user_id: "bob".into(),
insert_attributes: vec![Attribute { insert_attributes: vec![Attribute {
name: "new_attribute".into(), name: "new_attribute".into(),
value: Serialized::from(&3), value: vec![3].into(),
}], }],
..Default::default() ..Default::default()
}) })
+59 -39
View File
@@ -1,16 +1,15 @@
use crate::domain::{ use crate::domain::{
error::{DomainError, Result}, error::{DomainError, Result},
handler::{UserBackendHandler, UserListerBackendHandler, UserRequestFilter}, handler::{
model::{self, GroupColumn, UserColumn}, ReadSchemaBackendHandler, UserBackendHandler, UserListerBackendHandler, UserRequestFilter,
},
model::{self, deserialize, GroupColumn, UserColumn},
sql_backend_handler::SqlBackendHandler, sql_backend_handler::SqlBackendHandler,
}; };
use async_trait::async_trait; use async_trait::async_trait;
use lldap_domain::{ use lldap_domain::{
requests::{CreateUserRequest, UpdateUserRequest}, requests::{CreateUserRequest, UpdateUserRequest},
types::{ types::{AttributeName, GroupDetails, GroupId, Serialized, User, UserAndGroups, UserId, Uuid},
Attribute, AttributeName, GroupDetails, GroupId, Serialized, User, UserAndGroups, UserId,
Uuid,
},
}; };
use sea_orm::{ use sea_orm::{
sea_query::{ sea_query::{
@@ -83,7 +82,7 @@ fn get_user_filter_expr(filter: UserRequestFilter) -> Cond {
ColumnTrait::eq(&column, value).into_condition() ColumnTrait::eq(&column, value).into_condition()
} }
} }
AttributeEquality(column, value) => attribute_condition(column, Some(value)), AttributeEquality(column, value) => attribute_condition(column, Some(value.into())),
MemberOf(group) => user_id_subcondition( MemberOf(group) => user_id_subcondition(
Expr::col((group_table, GroupColumn::LowercaseDisplayName)) Expr::col((group_table, GroupColumn::LowercaseDisplayName))
.eq(group.as_str().to_lowercase()) .eq(group.as_str().to_lowercase())
@@ -161,12 +160,20 @@ impl UserListerBackendHandler for SqlBackendHandler {
.all(&self.sql_pool) .all(&self.sql_pool)
.await?; .await?;
let mut attributes_iter = attributes.into_iter().peekable(); let mut attributes_iter = attributes.into_iter().peekable();
// TODO: should be wrapped in a transaction
use itertools::Itertools; // For take_while_ref use itertools::Itertools; // For take_while_ref
let schema = self.get_schema().await?;
for user in users.iter_mut() { for user in users.iter_mut() {
user.user.attributes = attributes_iter user.user.attributes = attributes_iter
.take_while_ref(|u| u.user_id == user.user.user_id) .take_while_ref(|u| u.user_id == user.user.user_id)
.map(Attribute::from) .map(|a| {
.collect(); deserialize::deserialize_attribute(
a.attribute_name,
&a.value,
&schema.user_attributes,
)
})
.collect::<Result<Vec<_>>>()?;
} }
Ok(users) Ok(users)
} }
@@ -208,7 +215,10 @@ impl SqlBackendHandler {
.get_attribute_type(&attribute.name) .get_attribute_type(&attribute.name)
.is_some() .is_some()
{ {
process_serialized(ActiveValue::Set(attribute.value), attribute.name.clone()); process_serialized(
ActiveValue::Set(attribute.value.into()),
attribute.name.clone(),
);
} else { } else {
return Err(DomainError::InternalError(format!( return Err(DomainError::InternalError(format!(
"User 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",
@@ -270,7 +280,17 @@ impl UserBackendHandler for SqlBackendHandler {
.order_by_asc(model::UserAttributesColumn::AttributeName) .order_by_asc(model::UserAttributesColumn::AttributeName)
.all(&self.sql_pool) .all(&self.sql_pool)
.await?; .await?;
user.attributes = attributes.into_iter().map(Attribute::from).collect(); let schema = self.get_schema().await?;
user.attributes = attributes
.into_iter()
.map(|a| {
deserialize::deserialize_attribute(
a.attribute_name,
&a.value,
&schema.user_attributes,
)
})
.collect::<Result<Vec<_>>>()?;
Ok(user) Ok(user)
} }
@@ -317,7 +337,7 @@ impl UserBackendHandler for SqlBackendHandler {
new_user_attributes.push(model::user_attributes::ActiveModel { new_user_attributes.push(model::user_attributes::ActiveModel {
user_id: Set(request.user_id.clone()), user_id: Set(request.user_id.clone()),
attribute_name: Set(attribute.name), attribute_name: Set(attribute.name),
value: Set(attribute.value), value: Set(attribute.value.into()),
}); });
} else { } else {
return Err(DomainError::InternalError(format!( return Err(DomainError::InternalError(format!(
@@ -397,7 +417,7 @@ mod tests {
use crate::domain::{ use crate::domain::{
handler::SubStringFilter, model::UserColumn, sql_backend_handler::tests::*, handler::SubStringFilter, model::UserColumn, sql_backend_handler::tests::*,
}; };
use lldap_domain::types::JpegPhoto; use lldap_domain::types::{Attribute, JpegPhoto};
use pretty_assertions::{assert_eq, assert_ne}; use pretty_assertions::{assert_eq, assert_ne};
#[tokio::test] #[tokio::test]
@@ -439,7 +459,7 @@ mod tests {
&fixture.handler, &fixture.handler,
Some(UserRequestFilter::AttributeEquality( Some(UserRequestFilter::AttributeEquality(
AttributeName::from("first_name"), AttributeName::from("first_name"),
Serialized::from("first bob"), "first bob".to_string().into(),
)), )),
) )
.await; .await;
@@ -814,15 +834,15 @@ mod tests {
insert_attributes: vec![ insert_attributes: vec![
Attribute { Attribute {
name: "first_name".into(), name: "first_name".into(),
value: Serialized::from("first_name"), value: "first_name".to_string().into(),
}, },
Attribute { Attribute {
name: "last_name".into(), name: "last_name".into(),
value: Serialized::from("last_name"), value: "last_name".to_string().into(),
}, },
Attribute { Attribute {
name: "avatar".into(), name: "avatar".into(),
value: Serialized::from(&JpegPhoto::for_tests()), value: JpegPhoto::for_tests().into(),
}, },
], ],
}) })
@@ -841,15 +861,15 @@ mod tests {
vec![ vec![
Attribute { Attribute {
name: "avatar".into(), name: "avatar".into(),
value: Serialized::from(&JpegPhoto::for_tests()) value: JpegPhoto::for_tests().into()
}, },
Attribute { Attribute {
name: "first_name".into(), name: "first_name".into(),
value: Serialized::from("first_name") value: "first_name".to_string().into()
}, },
Attribute { Attribute {
name: "last_name".into(), name: "last_name".into(),
value: Serialized::from("last_name") value: "last_name".to_string().into()
} }
] ]
); );
@@ -866,7 +886,7 @@ mod tests {
delete_attributes: vec!["last_name".into()], delete_attributes: vec!["last_name".into()],
insert_attributes: vec![Attribute { insert_attributes: vec![Attribute {
name: "avatar".into(), name: "avatar".into(),
value: Serialized::from(&JpegPhoto::for_tests()), value: JpegPhoto::for_tests().into(),
}], }],
..Default::default() ..Default::default()
}) })
@@ -884,11 +904,11 @@ mod tests {
vec![ vec![
Attribute { Attribute {
name: "avatar".into(), name: "avatar".into(),
value: Serialized::from(&JpegPhoto::for_tests()) value: JpegPhoto::for_tests().into()
}, },
Attribute { Attribute {
name: "first_name".into(), name: "first_name".into(),
value: Serialized::from("first bob") value: "first bob".to_string().into()
} }
] ]
); );
@@ -904,7 +924,7 @@ mod tests {
user_id: UserId::new("bob"), user_id: UserId::new("bob"),
insert_attributes: vec![Attribute { insert_attributes: vec![Attribute {
name: "first_name".into(), name: "first_name".into(),
value: Serialized::from("new first"), value: "new first".to_string().into(),
}], }],
..Default::default() ..Default::default()
}) })
@@ -921,11 +941,11 @@ mod tests {
vec![ vec![
Attribute { Attribute {
name: "first_name".into(), name: "first_name".into(),
value: Serialized::from("new first") value: "new first".to_string().into()
}, },
Attribute { Attribute {
name: "last_name".into(), name: "last_name".into(),
value: Serialized::from("last bob") value: "last bob".to_string().into()
} }
] ]
); );
@@ -954,7 +974,7 @@ mod tests {
user.attributes, user.attributes,
vec![Attribute { vec![Attribute {
name: "last_name".into(), name: "last_name".into(),
value: Serialized::from("last bob") value: "last bob".to_string().into()
}] }]
); );
} }
@@ -970,7 +990,7 @@ mod tests {
delete_attributes: vec!["first_name".into()], delete_attributes: vec!["first_name".into()],
insert_attributes: vec![Attribute { insert_attributes: vec![Attribute {
name: "first_name".into(), name: "first_name".into(),
value: Serialized::from("new first"), value: "new first".to_string().into(),
}], }],
..Default::default() ..Default::default()
}) })
@@ -987,11 +1007,11 @@ mod tests {
vec![ vec![
Attribute { Attribute {
name: "first_name".into(), name: "first_name".into(),
value: Serialized::from("new first") value: "new first".to_string().into()
}, },
Attribute { Attribute {
name: "last_name".into(), name: "last_name".into(),
value: Serialized::from("last bob") value: "last bob".to_string().into()
}, },
] ]
); );
@@ -1007,7 +1027,7 @@ mod tests {
user_id: UserId::new("bob"), user_id: UserId::new("bob"),
insert_attributes: vec![Attribute { insert_attributes: vec![Attribute {
name: "avatar".into(), name: "avatar".into(),
value: Serialized::from(&JpegPhoto::for_tests()), value: JpegPhoto::for_tests().into(),
}], }],
..Default::default() ..Default::default()
}) })
@@ -1021,7 +1041,7 @@ mod tests {
.unwrap(); .unwrap();
let avatar = Attribute { let avatar = Attribute {
name: "avatar".into(), name: "avatar".into(),
value: Serialized::from(&JpegPhoto::for_tests()), value: JpegPhoto::for_tests().into(),
}; };
assert!(user.attributes.contains(&avatar)); assert!(user.attributes.contains(&avatar));
fixture fixture
@@ -1030,7 +1050,7 @@ mod tests {
user_id: UserId::new("bob"), user_id: UserId::new("bob"),
insert_attributes: vec![Attribute { insert_attributes: vec![Attribute {
name: "avatar".into(), name: "avatar".into(),
value: Serialized::from(&JpegPhoto::null()), value: JpegPhoto::null().into(),
}], }],
..Default::default() ..Default::default()
}) })
@@ -1058,15 +1078,15 @@ mod tests {
attributes: vec![ attributes: vec![
Attribute { Attribute {
name: "first_name".into(), name: "first_name".into(),
value: Serialized::from("First Name"), value: "First Name".to_string().into(),
}, },
Attribute { Attribute {
name: "last_name".into(), name: "last_name".into(),
value: Serialized::from("last_name"), value: "last_name".to_string().into(),
}, },
Attribute { Attribute {
name: "avatar".into(), name: "avatar".into(),
value: Serialized::from(&JpegPhoto::for_tests()), value: JpegPhoto::for_tests().into(),
}, },
], ],
}) })
@@ -1085,15 +1105,15 @@ mod tests {
vec![ vec![
Attribute { Attribute {
name: "avatar".into(), name: "avatar".into(),
value: Serialized::from(&JpegPhoto::for_tests()) value: JpegPhoto::for_tests().into()
}, },
Attribute { Attribute {
name: "first_name".into(), name: "first_name".into(),
value: Serialized::from("First Name") value: "First Name".to_string().into()
}, },
Attribute { Attribute {
name: "last_name".into(), name: "last_name".into(),
value: Serialized::from("last_name") value: "last_name".to_string().into()
} }
] ]
); );
+2 -2
View File
@@ -151,7 +151,7 @@ fn unpack_attributes(
.cloned() .cloned()
.map(|attr| deserialize_attribute(&schema.get_schema().user_attributes, attr, is_admin)) .map(|attr| deserialize_attribute(&schema.get_schema().user_attributes, attr, is_admin))
.transpose()? .transpose()?
.map(|attr| attr.value.unwrap::<String>()) .map(|attr| attr.value.into_string().unwrap())
.map(Email::from); .map(Email::from);
let display_name = attributes let display_name = attributes
.iter() .iter()
@@ -159,7 +159,7 @@ fn unpack_attributes(
.cloned() .cloned()
.map(|attr| deserialize_attribute(&schema.get_schema().user_attributes, attr, is_admin)) .map(|attr| deserialize_attribute(&schema.get_schema().user_attributes, attr, is_admin))
.transpose()? .transpose()?
.map(|attr| attr.value.unwrap::<String>()); .map(|attr| attr.value.into_string().unwrap());
let attributes = attributes let attributes = attributes
.into_iter() .into_iter()
.filter(|attr| attr.name != "mail" && attr.name != "display_name") .filter(|attr| attr.name != "mail" && attr.name != "display_name")
+75 -90
View File
@@ -14,10 +14,10 @@ use crate::{
}, },
}; };
use anyhow::Context as AnyhowContext; use anyhow::Context as AnyhowContext;
use chrono::{NaiveDateTime, TimeZone}; use chrono::TimeZone;
use juniper::{graphql_object, FieldResult, GraphQLInputObject}; use juniper::{graphql_object, FieldResult, GraphQLInputObject};
use lldap_domain::types::{ use lldap_domain::types::{
AttributeType, GroupDetails, GroupId, JpegPhoto, LdapObjectClass, Serialized, UserId, AttributeType, Cardinality, GroupDetails, GroupId, LdapObjectClass, UserId,
}; };
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use tracing::{debug, debug_span, Instrument, Span}; use tracing::{debug, debug_span, Instrument, Span};
@@ -29,6 +29,7 @@ type DomainUserAndGroups = lldap_domain::types::UserAndGroups;
type DomainAttributeList = lldap_domain::schema::AttributeList; type DomainAttributeList = lldap_domain::schema::AttributeList;
type DomainAttributeSchema = lldap_domain::schema::AttributeSchema; type DomainAttributeSchema = lldap_domain::schema::AttributeSchema;
type DomainAttribute = lldap_domain::types::Attribute; type DomainAttribute = lldap_domain::types::Attribute;
type DomainAttributeValue = lldap_domain::types::AttributeValue;
#[derive(PartialEq, Eq, Debug, GraphQLInputObject)] #[derive(PartialEq, Eq, Debug, GraphQLInputObject)]
/// A filter for requests, specifying a boolean expression based on field constraints. Only one of /// A filter for requests, specifying a boolean expression based on field constraints. Only one of
@@ -296,23 +297,30 @@ impl<Handler: BackendHandler> User<Handler> {
self.attributes self.attributes
.iter() .iter()
.find(|a| a.attribute.name.as_str() == "first_name") .find(|a| a.attribute.name.as_str() == "first_name")
.map(|a| a.attribute.value.unwrap()) .map(|a| a.attribute.value.as_str().unwrap_or_default())
.unwrap_or("") .unwrap_or_default()
} }
fn last_name(&self) -> &str { fn last_name(&self) -> &str {
self.attributes self.attributes
.iter() .iter()
.find(|a| a.attribute.name.as_str() == "last_name") .find(|a| a.attribute.name.as_str() == "last_name")
.map(|a| a.attribute.value.unwrap()) .map(|a| a.attribute.value.as_str().unwrap_or_default())
.unwrap_or("") .unwrap_or_default()
} }
fn avatar(&self) -> Option<String> { fn avatar(&self) -> Option<String> {
self.attributes self.attributes
.iter() .iter()
.find(|a| a.attribute.name.as_str() == "avatar") .find(|a| a.attribute.name.as_str() == "avatar")
.map(|a| String::from(&a.attribute.value.unwrap::<JpegPhoto>())) .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> { fn creation_date(&self) -> chrono::DateTime<chrono::Utc> {
@@ -590,7 +598,7 @@ impl<Handler: BackendHandler> AttributeValue<Handler> {
} }
fn value(&self) -> FieldResult<Vec<String>> { fn value(&self) -> FieldResult<Vec<String>> {
Ok(serialize_attribute(&self.attribute, &self.schema.schema)) Ok(serialize_attribute_to_graphql(&self.attribute.value))
} }
fn schema(&self) -> &AttributeSchema<Handler> { fn schema(&self) -> &AttributeSchema<Handler> {
@@ -599,9 +607,9 @@ impl<Handler: BackendHandler> AttributeValue<Handler> {
} }
impl<Handler: BackendHandler> AttributeValue<Handler> { impl<Handler: BackendHandler> AttributeValue<Handler> {
fn from_domain(value: DomainAttribute, schema: DomainAttributeSchema) -> Self { fn from_value(attr: DomainAttribute, schema: DomainAttributeSchema) -> Self {
Self { Self {
attribute: value, attribute: attr,
schema: AttributeSchema::<Handler> { schema: AttributeSchema::<Handler> {
schema, schema,
_phantom: std::marker::PhantomData, _phantom: std::marker::PhantomData,
@@ -621,46 +629,23 @@ impl<Handler: BackendHandler> Clone for AttributeValue<Handler> {
} }
} }
pub fn serialize_attribute( pub fn serialize_attribute_to_graphql(attribute_value: &DomainAttributeValue) -> Vec<String> {
attribute: &DomainAttribute, let convert_date = |&date| chrono::Utc.from_utc_datetime(&date).to_rfc3339();
attribute_schema: &DomainAttributeSchema, match attribute_value {
) -> Vec<String> { DomainAttributeValue::String(Cardinality::Singleton(s)) => vec![s.clone()],
let convert_date = |date| chrono::Utc.from_utc_datetime(&date).to_rfc3339(); DomainAttributeValue::String(Cardinality::Unbounded(l)) => l.clone(),
match (attribute_schema.attribute_type, attribute_schema.is_list) { DomainAttributeValue::Integer(Cardinality::Singleton(i)) => vec![i.to_string()],
(AttributeType::String, false) => vec![attribute.value.unwrap::<String>()], DomainAttributeValue::Integer(Cardinality::Unbounded(l)) => {
(AttributeType::Integer, false) => { l.iter().map(|i| i.to_string()).collect()
// LDAP integers are encoded as strings.
vec![attribute.value.unwrap::<i64>().to_string()]
} }
(AttributeType::JpegPhoto, false) => { DomainAttributeValue::DateTime(Cardinality::Singleton(dt)) => vec![convert_date(dt)],
vec![String::from(&attribute.value.unwrap::<JpegPhoto>())] DomainAttributeValue::DateTime(Cardinality::Unbounded(l)) => {
l.iter().map(convert_date).collect()
} }
(AttributeType::DateTime, false) => { DomainAttributeValue::JpegPhoto(Cardinality::Singleton(p)) => vec![String::from(p)],
vec![convert_date(attribute.value.unwrap::<NaiveDateTime>())] DomainAttributeValue::JpegPhoto(Cardinality::Unbounded(l)) => {
l.iter().map(String::from).collect()
} }
(AttributeType::String, true) => attribute
.value
.unwrap::<Vec<String>>()
.into_iter()
.collect(),
(AttributeType::Integer, true) => attribute
.value
.unwrap::<Vec<i64>>()
.into_iter()
.map(|i| i.to_string())
.collect(),
(AttributeType::JpegPhoto, true) => attribute
.value
.unwrap::<Vec<JpegPhoto>>()
.iter()
.map(String::from)
.collect(),
(AttributeType::DateTime, true) => attribute
.value
.unwrap::<Vec<NaiveDateTime>>()
.into_iter()
.map(convert_date)
.collect(),
} }
} }
@@ -668,7 +653,7 @@ impl<Handler: BackendHandler> AttributeValue<Handler> {
fn from_schema(a: DomainAttribute, schema: &DomainAttributeList) -> Option<Self> { fn from_schema(a: DomainAttribute, schema: &DomainAttributeList) -> Option<Self> {
schema schema
.get_attribute_schema(&a.name) .get_attribute_schema(&a.name)
.map(|s| AttributeValue::<Handler>::from_domain(a, s.clone())) .map(|s| AttributeValue::<Handler>::from_value(a, s.clone()))
} }
fn user_attributes_from_schema( fn user_attributes_from_schema(
@@ -682,25 +667,25 @@ impl<Handler: BackendHandler> AttributeValue<Handler> {
.attributes .attributes
.iter() .iter()
.filter(|a| a.is_hardcoded) .filter(|a| a.is_hardcoded)
.flat_map(|attribute| { .flat_map(|attribute_schema| {
let value = match attribute.name.as_str() { let value: Option<DomainAttributeValue> = match attribute_schema.name.as_str() {
"user_id" => Some(Serialized::from(&user.user_id)), "user_id" => Some(user.user_id.clone().into_string().into()),
"creation_date" => Some(Serialized::from(&user.creation_date)), "creation_date" => Some(user.creation_date.into()),
"mail" => Some(Serialized::from(&user.email)), "mail" => Some(user.email.clone().into_string().into()),
"uuid" => Some(Serialized::from(&user.uuid)), "uuid" => Some(user.uuid.clone().into_string().into()),
"display_name" => user.display_name.as_ref().map(Serialized::from), "display_name" => user.display_name.as_ref().map(|d| d.clone().into()),
"avatar" | "first_name" | "last_name" => None, "avatar" | "first_name" | "last_name" => None,
_ => panic!("Unexpected hardcoded attribute: {}", attribute.name), _ => panic!("Unexpected hardcoded attribute: {}", attribute_schema.name),
}; };
value.map(|v| (attribute, v)) value.map(|v| (attribute_schema, v))
}) })
.map(|(attribute, value)| { .map(|(attribute_schema, value)| {
AttributeValue::<Handler>::from_domain( AttributeValue::<Handler>::from_value(
DomainAttribute { DomainAttribute {
name: attribute.name.clone(), name: attribute_schema.name.clone(),
value, value,
}, },
attribute.clone(), attribute_schema.clone(),
) )
}) })
.collect::<Vec<_>>(); .collect::<Vec<_>>();
@@ -724,25 +709,25 @@ impl<Handler: BackendHandler> AttributeValue<Handler> {
.attributes .attributes
.iter() .iter()
.filter(|a| a.is_hardcoded) .filter(|a| a.is_hardcoded)
.map(|attribute| { .map(|attribute_schema| {
( (
attribute, attribute_schema,
match attribute.name.as_str() { match attribute_schema.name.as_str() {
"group_id" => Serialized::from(&(group.id.0 as i64)), "group_id" => (group.id.0 as i64).into(),
"creation_date" => Serialized::from(&group.creation_date), "creation_date" => group.creation_date.into(),
"uuid" => Serialized::from(&group.uuid), "uuid" => group.uuid.clone().into_string().into(),
"display_name" => Serialized::from(&group.display_name), "display_name" => group.display_name.clone().into_string().into(),
_ => panic!("Unexpected hardcoded attribute: {}", attribute.name), _ => panic!("Unexpected hardcoded attribute: {}", attribute_schema.name),
}, },
) )
}) })
.map(|(attribute, value)| { .map(|(attribute_schema, value)| {
AttributeValue::<Handler>::from_domain( AttributeValue::<Handler>::from_value(
DomainAttribute { DomainAttribute {
name: attribute.name.clone(), name: attribute_schema.name.clone(),
value, value,
}, },
attribute.clone(), attribute_schema.clone(),
) )
}) })
.collect::<Vec<_>>(); .collect::<Vec<_>>();
@@ -766,25 +751,25 @@ impl<Handler: BackendHandler> AttributeValue<Handler> {
.attributes .attributes
.iter() .iter()
.filter(|a| a.is_hardcoded) .filter(|a| a.is_hardcoded)
.map(|attribute| { .map(|attribute_schema| {
( (
attribute, attribute_schema,
match attribute.name.as_str() { match attribute_schema.name.as_str() {
"group_id" => Serialized::from(&(group.group_id.0 as i64)), "group_id" => (group.group_id.0 as i64).into(),
"creation_date" => Serialized::from(&group.creation_date), "creation_date" => group.creation_date.into(),
"uuid" => Serialized::from(&group.uuid), "uuid" => group.uuid.clone().into_string().into(),
"display_name" => Serialized::from(&group.display_name), "display_name" => group.display_name.clone().into_string().into(),
_ => panic!("Unexpected hardcoded attribute: {}", attribute.name), _ => panic!("Unexpected hardcoded attribute: {}", attribute_schema.name),
}, },
) )
}) })
.map(|(attribute, value)| { .map(|(attribute_schema, value)| {
AttributeValue::<Handler>::from_domain( AttributeValue::<Handler>::from_value(
DomainAttribute { DomainAttribute {
name: attribute.name.clone(), name: attribute_schema.name.clone(),
value, value,
}, },
attribute.clone(), attribute_schema.clone(),
) )
}) })
.collect::<Vec<_>>(); .collect::<Vec<_>>();
@@ -812,7 +797,7 @@ mod tests {
}; };
use lldap_domain::{ use lldap_domain::{
schema::{AttributeList, Schema}, schema::{AttributeList, Schema},
types::{AttributeName, AttributeType, LdapObjectClass, Serialized}, types::{AttributeName, AttributeType, LdapObjectClass},
}; };
use mockall::predicate::eq; use mockall::predicate::eq;
use pretty_assertions::assert_eq; use pretty_assertions::assert_eq;
@@ -910,11 +895,11 @@ mod tests {
attributes: vec![ attributes: vec![
DomainAttribute { DomainAttribute {
name: "first_name".into(), name: "first_name".into(),
value: Serialized::from("Bob"), value: "Bob".to_string().into(),
}, },
DomainAttribute { DomainAttribute {
name: "last_name".into(), name: "last_name".into(),
value: Serialized::from("Bobberson"), value: "Bobberson".to_string().into(),
}, },
], ],
..Default::default() ..Default::default()
@@ -928,7 +913,7 @@ mod tests {
uuid: lldap_domain::uuid!("a1a2a3a4b1b2c1c2d1d2d3d4d5d6d7d8"), uuid: lldap_domain::uuid!("a1a2a3a4b1b2c1c2d1d2d3d4d5d6d7d8"),
attributes: vec![DomainAttribute { attributes: vec![DomainAttribute {
name: "club_name".into(), name: "club_name".into(),
value: Serialized::from("Gang of Four"), value: "Gang of Four".to_string().into(),
}], }],
}); });
groups.insert(GroupDetails { groups.insert(GroupDetails {
@@ -1074,7 +1059,7 @@ mod tests {
), ),
DomainRequestFilter::AttributeEquality( DomainRequestFilter::AttributeEquality(
AttributeName::from("first_name"), AttributeName::from("first_name"),
Serialized::from("robert"), "robert".to_string().into(),
), ),
]))), ]))),
eq(false), eq(false),
+17 -17
View File
@@ -1300,11 +1300,11 @@ mod tests {
attributes: vec![ attributes: vec![
Attribute { Attribute {
name: "first_name".into(), name: "first_name".into(),
value: Serialized::from("Bôb"), value: "Bôb".to_string().into(),
}, },
Attribute { Attribute {
name: "last_name".into(), name: "last_name".into(),
value: Serialized::from("Böbberson"), value: "Böbberson".to_string().into(),
}, },
], ],
..Default::default() ..Default::default()
@@ -1319,15 +1319,15 @@ mod tests {
attributes: vec![ attributes: vec![
Attribute { Attribute {
name: "avatar".into(), name: "avatar".into(),
value: Serialized::from(&JpegPhoto::for_tests()), value: JpegPhoto::for_tests().into(),
}, },
Attribute { Attribute {
name: "first_name".into(), name: "first_name".into(),
value: Serialized::from("Jim"), value: "Jim".to_string().into(),
}, },
Attribute { Attribute {
name: "last_name".into(), name: "last_name".into(),
value: Serialized::from("Cricket"), value: "Cricket".to_string().into(),
}, },
], ],
uuid: uuid!("04ac75e0-2900-3e21-926c-2f732c26b3fc"), uuid: uuid!("04ac75e0-2900-3e21-926c-2f732c26b3fc"),
@@ -1720,11 +1720,11 @@ mod tests {
.with(eq(Some(GroupRequestFilter::Or(vec![ .with(eq(Some(GroupRequestFilter::Or(vec![
GroupRequestFilter::AttributeEquality( GroupRequestFilter::AttributeEquality(
AttributeName::from("attr"), AttributeName::from("attr"),
Serialized::from("TEST"), "TEST".to_string().into(),
), ),
GroupRequestFilter::AttributeEquality( GroupRequestFilter::AttributeEquality(
AttributeName::from("attr"), AttributeName::from("attr"),
Serialized::from("test"), "test".to_string().into(),
), ),
])))) ]))))
.times(1) .times(1)
@@ -1737,7 +1737,7 @@ mod tests {
uuid: uuid!("04ac75e0-2900-3e21-926c-2f732c26b3fc"), uuid: uuid!("04ac75e0-2900-3e21-926c-2f732c26b3fc"),
attributes: vec![Attribute { attributes: vec![Attribute {
name: "Attr".into(), name: "Attr".into(),
value: Serialized::from("TEST"), value: "TEST".to_string().into(),
}], }],
}]) }])
}); });
@@ -1914,11 +1914,11 @@ mod tests {
UserRequestFilter::Or(vec![ UserRequestFilter::Or(vec![
UserRequestFilter::AttributeEquality( UserRequestFilter::AttributeEquality(
AttributeName::from("first_name"), AttributeName::from("first_name"),
Serialized::from("FirstName"), "FirstName".to_string().into(),
), ),
UserRequestFilter::AttributeEquality( UserRequestFilter::AttributeEquality(
AttributeName::from("first_name"), AttributeName::from("first_name"),
Serialized::from("firstname"), "firstname".to_string().into(),
), ),
]), ]),
false.into(), false.into(),
@@ -2173,11 +2173,11 @@ mod tests {
attributes: vec![ attributes: vec![
Attribute { Attribute {
name: "first_name".into(), name: "first_name".into(),
value: Serialized::from("Bôb"), value: "Bôb".to_string().into(),
}, },
Attribute { Attribute {
name: "last_name".into(), name: "last_name".to_string().into(),
value: Serialized::from("Böbberson"), value: "Böbberson".to_string().into(),
}, },
], ],
..Default::default() ..Default::default()
@@ -2257,11 +2257,11 @@ mod tests {
attributes: vec![ attributes: vec![
Attribute { Attribute {
name: "avatar".into(), name: "avatar".into(),
value: Serialized::from(&JpegPhoto::for_tests()), value: JpegPhoto::for_tests().into(),
}, },
Attribute { Attribute {
name: "last_name".into(), name: "last_name".into(),
value: Serialized::from("Böbberson"), value: "Böbberson".to_string().into(),
}, },
], ],
uuid: uuid!("b4ac75e0-2900-3e21-926c-2f732c26b3fc"), uuid: uuid!("b4ac75e0-2900-3e21-926c-2f732c26b3fc"),
@@ -3078,7 +3078,7 @@ mod tests {
user_id: UserId::new("test"), user_id: UserId::new("test"),
attributes: vec![Attribute { attributes: vec![Attribute {
name: "nickname".into(), name: "nickname".into(),
value: Serialized::from("Bob the Builder"), value: "Bob the Builder".to_string().into(),
}], }],
..Default::default() ..Default::default()
}, },
@@ -3094,7 +3094,7 @@ mod tests {
uuid: uuid!("04ac75e0-2900-3e21-926c-2f732c26b3fc"), uuid: uuid!("04ac75e0-2900-3e21-926c-2f732c26b3fc"),
attributes: vec![Attribute { attributes: vec![Attribute {
name: "club_name".into(), name: "club_name".into(),
value: Serialized::from("Breakfast Club"), value: "Breakfast Club".to_string().into(),
}], }],
}]) }])
}); });