server: Use schema to populate attributes

This commit is contained in:
Valentin Tolmer
2023-06-28 17:13:09 +02:00
committed by nitnelave
parent 829ebf59f7
commit 3140af63de
13 changed files with 429 additions and 135 deletions
Generated
+4 -4
View File
@@ -2670,9 +2670,9 @@ dependencies = [
[[package]] [[package]]
name = "mockall" name = "mockall"
version = "0.11.3" version = "0.11.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "50e4a1c770583dac7ab5e2f6c139153b783a53a1bbee9729613f193e59828326" checksum = "4c84490118f2ee2d74570d114f3d0493cbf02790df303d2707606c3e14e07c96"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"downcast", "downcast",
@@ -2685,9 +2685,9 @@ dependencies = [
[[package]] [[package]]
name = "mockall_derive" name = "mockall_derive"
version = "0.11.3" version = "0.11.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "832663583d5fa284ca8810bf7015e46c9fff9622d3cf34bd1eea5003fec06dd0" checksum = "22ce75669015c4f47b289fd4d4f56e894e4c96003ffdf3ac51313126f94c6cbb"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"proc-macro2", "proc-macro2",
+1 -1
View File
@@ -128,7 +128,7 @@ features = ["dangerous_configuration"]
[dev-dependencies] [dev-dependencies]
assert_cmd = "2.0" assert_cmd = "2.0"
mockall = "0.11" mockall = "0.11.4"
nix = "0.26.2" nix = "0.26.2"
[dev-dependencies.graphql_client] [dev-dependencies.graphql_client]
+20 -6
View File
@@ -134,10 +134,24 @@ pub struct AttributeSchema {
pub is_hardcoded: bool, pub is_hardcoded: bool,
} }
#[derive(PartialEq, Eq, Debug, Serialize, Deserialize, Clone)]
pub struct AttributeList {
pub attributes: Vec<AttributeSchema>,
}
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)] #[derive(PartialEq, Eq, Debug, Serialize, Deserialize, Clone)]
pub struct Schema { pub struct Schema {
pub user_attributes: Vec<AttributeSchema>, pub user_attributes: AttributeList,
pub group_attributes: Vec<AttributeSchema>, pub group_attributes: AttributeList,
} }
#[async_trait] #[async_trait]
@@ -146,12 +160,12 @@ pub trait LoginHandler: Send + Sync {
} }
#[async_trait] #[async_trait]
pub trait GroupListerBackendHandler { pub trait GroupListerBackendHandler: SchemaBackendHandler {
async fn list_groups(&self, filters: Option<GroupRequestFilter>) -> Result<Vec<Group>>; async fn list_groups(&self, filters: Option<GroupRequestFilter>) -> Result<Vec<Group>>;
} }
#[async_trait] #[async_trait]
pub trait GroupBackendHandler { pub trait GroupBackendHandler: SchemaBackendHandler {
async fn get_group_details(&self, group_id: GroupId) -> Result<GroupDetails>; async fn get_group_details(&self, group_id: GroupId) -> Result<GroupDetails>;
async fn update_group(&self, request: UpdateGroupRequest) -> Result<()>; async fn update_group(&self, request: UpdateGroupRequest) -> Result<()>;
async fn create_group(&self, group_name: &str) -> Result<GroupId>; async fn create_group(&self, group_name: &str) -> Result<GroupId>;
@@ -159,7 +173,7 @@ pub trait GroupBackendHandler {
} }
#[async_trait] #[async_trait]
pub trait UserListerBackendHandler { pub trait UserListerBackendHandler: SchemaBackendHandler {
async fn list_users( async fn list_users(
&self, &self,
filters: Option<UserRequestFilter>, filters: Option<UserRequestFilter>,
@@ -168,7 +182,7 @@ pub trait UserListerBackendHandler {
} }
#[async_trait] #[async_trait]
pub trait UserBackendHandler { pub trait UserBackendHandler: SchemaBackendHandler {
async fn get_user_details(&self, user_id: &UserId) -> Result<User>; async fn get_user_details(&self, user_id: &UserId) -> Result<User>;
async fn create_user(&self, request: CreateUserRequest) -> Result<()>; async fn create_user(&self, request: CreateUserRequest) -> Result<()>;
async fn update_user(&self, request: UpdateUserRequest) -> Result<()>; async fn update_user(&self, request: UpdateUserRequest) -> Result<()>;
+21 -7
View File
@@ -5,11 +5,11 @@ use ldap3_proto::{
use tracing::{debug, instrument, warn}; use tracing::{debug, instrument, warn};
use crate::domain::{ use crate::domain::{
handler::{UserListerBackendHandler, UserRequestFilter}, handler::{Schema, UserListerBackendHandler, UserRequestFilter},
ldap::{ ldap::{
error::{LdapError, LdapResult}, error::{LdapError, LdapResult},
utils::{ 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, get_user_id_from_distinguished_name, map_user_field, LdapInfo, UserFieldType,
}, },
}, },
@@ -22,6 +22,7 @@ pub fn get_user_attribute(
base_dn_str: &str, base_dn_str: &str,
groups: Option<&[GroupDetails]>, groups: Option<&[GroupDetails]>,
ignored_user_attributes: &[String], ignored_user_attributes: &[String],
schema: &Schema,
) -> Option<Vec<Vec<u8>>> { ) -> Option<Vec<Vec<u8>>> {
let attribute = attribute.to_ascii_lowercase(); let attribute = attribute.to_ascii_lowercase();
let attribute_values = match attribute.as_str() { 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()], "uid" | "user_id" | "id" => vec![user.user_id.to_string().into_bytes()],
"entryuuid" | "uuid" => vec![user.uuid.to_string().into_bytes()], "entryuuid" | "uuid" => vec![user.uuid.to_string().into_bytes()],
"mail" | "email" => vec![user.email.clone().into_bytes()], "mail" | "email" => vec![user.email.clone().into_bytes()],
"givenname" | "first_name" | "firstname" => vec![user.first_name.clone()?.into_bytes()], "givenname" | "first_name" | "firstname" => {
"sn" | "last_name" | "lastname" => vec![user.last_name.clone()?.into_bytes()], get_custom_attribute(&user.attributes, "first_name", schema)?
"jpegphoto" | "avatar" => vec![user.avatar.clone()?.into_bytes()], }
"sn" | "last_name" | "lastname" => {
get_custom_attribute(&user.attributes, "last_name", schema)?
}
"jpegphoto" | "avatar" => get_custom_attribute(&user.attributes, "avatar", schema)?,
"memberof" => groups "memberof" => groups
.into_iter() .into_iter()
.flatten() .flatten()
@@ -98,6 +103,7 @@ fn make_ldap_search_user_result_entry(
attributes: &[String], attributes: &[String],
groups: Option<&[GroupDetails]>, groups: Option<&[GroupDetails]>,
ignored_user_attributes: &[String], ignored_user_attributes: &[String],
schema: &Schema,
) -> LdapSearchResultEntry { ) -> LdapSearchResultEntry {
let expanded_attributes = expand_user_attribute_wildcards(attributes); let expanded_attributes = expand_user_attribute_wildcards(attributes);
let dn = format!("uid={},ou=people,{}", user.user_id.as_str(), base_dn_str); 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 attributes: expanded_attributes
.iter() .iter()
.filter_map(|a| { .filter_map(|a| {
let values = let values = get_user_attribute(
get_user_attribute(&user, a, base_dn_str, groups, ignored_user_attributes)?; &user,
a,
base_dn_str,
groups,
ignored_user_attributes,
schema,
)?;
Some(LdapPartialAttribute { Some(LdapPartialAttribute {
atype: a.to_string(), atype: a.to_string(),
vals: values, vals: values,
@@ -242,6 +254,7 @@ pub fn convert_users_to_ldap_op<'a>(
users: Vec<UserAndGroups>, users: Vec<UserAndGroups>,
attributes: &'a [String], attributes: &'a [String],
ldap_info: &'a LdapInfo, ldap_info: &'a LdapInfo,
schema: &'a Schema,
) -> impl Iterator<Item = LdapOp> + 'a { ) -> impl Iterator<Item = LdapOp> + 'a {
users.into_iter().map(move |u| { users.into_iter().map(move |u| {
LdapOp::SearchResultEntry(make_ldap_search_user_result_entry( LdapOp::SearchResultEntry(make_ldap_search_user_result_entry(
@@ -250,6 +263,7 @@ pub fn convert_users_to_ldap_op<'a>(
attributes, attributes,
u.groups.as_deref(), u.groups.as_deref(),
&ldap_info.ignored_user_attributes, &ldap_info.ignored_user_attributes,
schema,
)) ))
}) })
} }
+35 -2
View File
@@ -1,11 +1,12 @@
use chrono::{NaiveDateTime, TimeZone};
use itertools::Itertools; use itertools::Itertools;
use ldap3_proto::{proto::LdapSubstringFilter, LdapResultCode}; use ldap3_proto::{proto::LdapSubstringFilter, LdapResultCode};
use tracing::{debug, instrument, warn}; use tracing::{debug, instrument, warn};
use crate::domain::{ use crate::domain::{
handler::SubStringFilter, handler::{Schema, SubStringFilter},
ldap::error::{LdapError, LdapResult}, ldap::error::{LdapError, LdapResult},
types::{UserColumn, UserId}, types::{AttributeType, AttributeValue, JpegPhoto, UserColumn, UserId},
}; };
impl From<LdapSubstringFilter> for SubStringFilter { impl From<LdapSubstringFilter> for SubStringFilter {
@@ -193,3 +194,35 @@ pub struct LdapInfo {
pub ignored_user_attributes: Vec<String>, pub ignored_user_attributes: Vec<String>,
pub ignored_group_attributes: Vec<String>, pub ignored_group_attributes: Vec<String>,
} }
pub fn get_custom_attribute(
attributes: &[AttributeValue],
attribute_name: &str,
schema: &Schema,
) -> Option<Vec<Vec<u8>>> {
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::<String>().into_bytes()]
}
(AttributeType::Integer, false) => todo!(),
(AttributeType::JpegPhoto, false) => {
vec![attribute.value.unwrap::<JpegPhoto>().into_bytes()]
}
(AttributeType::DateTime, false) => vec![chrono::Utc
.from_utc_datetime(&attribute.value.unwrap::<NaiveDateTime>())
.to_rfc3339()
.into_bytes()],
(AttributeType::String, true) => todo!(),
(AttributeType::Integer, true) => todo!(),
(AttributeType::JpegPhoto, true) => todo!(),
(AttributeType::DateTime, true) => todo!(),
})
})
}
+16 -1
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 crate::domain::types::{Serialized, UserId}; use crate::domain::types::{AttributeValue, 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,3 +55,18 @@ impl Related<super::UserAttributeSchema> for Entity {
} }
impl ActiveModelBehavior for ActiveModel {} impl ActiveModelBehavior for ActiveModel {}
impl From<Model> for AttributeValue {
fn from(
Model {
user_id: _,
attribute_name,
value,
}: Model,
) -> Self {
Self {
name: attribute_name,
value,
}
}
}
+1 -3
View File
@@ -115,11 +115,9 @@ impl From<Model> for crate::domain::types::User {
user_id: user.user_id, user_id: user.user_id,
email: user.email, email: user.email,
display_name: user.display_name, display_name: user.display_name,
first_name: None,
last_name: None,
creation_date: user.creation_date, creation_date: user.creation_date,
uuid: user.uuid, uuid: user.uuid,
avatar: None, attributes: Vec::new(),
} }
} }
} }
+42 -30
View File
@@ -7,12 +7,18 @@ use crate::domain::{
use async_trait::async_trait; use async_trait::async_trait;
use sea_orm::{EntityTrait, QueryOrder}; use sea_orm::{EntityTrait, QueryOrder};
use super::handler::AttributeList;
#[async_trait] #[async_trait]
impl SchemaBackendHandler for SqlBackendHandler { impl SchemaBackendHandler for SqlBackendHandler {
async fn get_schema(&self) -> Result<Schema> { async fn get_schema(&self) -> Result<Schema> {
Ok(Schema { Ok(Schema {
user_attributes: self.get_user_attributes().await?, user_attributes: AttributeList {
group_attributes: self.get_group_attributes().await?, attributes: self.get_user_attributes().await?,
},
group_attributes: AttributeList {
attributes: self.get_group_attributes().await?,
},
}) })
} }
} }
@@ -42,7 +48,9 @@ impl SqlBackendHandler {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use crate::domain::{sql_backend_handler::tests::*, types::AttributeType}; use crate::domain::{
handler::AttributeList, sql_backend_handler::tests::*, types::AttributeType,
};
#[tokio::test] #[tokio::test]
async fn test_default_schema() { async fn test_default_schema() {
@@ -50,33 +58,37 @@ mod tests {
assert_eq!( assert_eq!(
fixture.handler.get_schema().await.unwrap(), fixture.handler.get_schema().await.unwrap(),
Schema { Schema {
user_attributes: vec![ user_attributes: AttributeList {
AttributeSchema { attributes: vec![
name: "avatar".to_owned(), AttributeSchema {
attribute_type: AttributeType::JpegPhoto, name: "avatar".to_owned(),
is_list: false, attribute_type: AttributeType::JpegPhoto,
is_visible: true, is_list: false,
is_editable: true, is_visible: true,
is_hardcoded: true, is_editable: true,
}, is_hardcoded: true,
AttributeSchema { },
name: "first_name".to_owned(), AttributeSchema {
attribute_type: AttributeType::String, name: "first_name".to_owned(),
is_list: false, attribute_type: AttributeType::String,
is_visible: true, is_list: false,
is_editable: true, is_visible: true,
is_hardcoded: true, is_editable: true,
}, is_hardcoded: true,
AttributeSchema { },
name: "last_name".to_owned(), AttributeSchema {
attribute_type: AttributeType::String, name: "last_name".to_owned(),
is_list: false, attribute_type: AttributeType::String,
is_visible: true, is_list: false,
is_editable: true, is_visible: true,
is_hardcoded: true, is_editable: true,
} is_hardcoded: true,
], }
group_attributes: Vec::new() ]
},
group_attributes: AttributeList {
attributes: Vec::new()
}
} }
); );
} }
+68 -43
View File
@@ -6,7 +6,7 @@ use crate::domain::{
}, },
model::{self, GroupColumn, UserColumn}, model::{self, GroupColumn, UserColumn},
sql_backend_handler::SqlBackendHandler, 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 async_trait::async_trait;
use sea_orm::{ use sea_orm::{
@@ -17,7 +17,7 @@ use sea_orm::{
QueryFilter, QueryOrder, QuerySelect, QueryTrait, Set, TransactionTrait, QueryFilter, QueryOrder, QuerySelect, QueryTrait, Set, TransactionTrait,
}; };
use std::collections::HashSet; use std::collections::HashSet;
use tracing::{debug, instrument, warn}; use tracing::{debug, instrument};
fn attribute_condition(name: String, value: String) -> Cond { fn attribute_condition(name: String, value: String) -> Cond {
Expr::in_subquery( Expr::in_subquery(
@@ -149,27 +149,21 @@ impl UserListerBackendHandler for SqlBackendHandler {
let attributes = model::UserAttributes::find() let attributes = model::UserAttributes::find()
.filter(model::UserAttributesColumn::UserId.is_in(&user_ids)) .filter(model::UserAttributesColumn::UserId.is_in(&user_ids))
.order_by_asc(model::UserAttributesColumn::UserId) .order_by_asc(model::UserAttributesColumn::UserId)
.order_by_asc(model::UserAttributesColumn::AttributeName)
.all(&self.sql_pool) .all(&self.sql_pool)
.await?; .await?;
let mut attributes_iter = attributes.iter().peekable(); let mut attributes_iter = attributes.into_iter().peekable();
for user in users.iter_mut() { for user in users.iter_mut() {
attributes_iter assert!(attributes_iter
.peeking_take_while(|u| u.user_id < user.user.user_id) .peek()
.for_each(|_| ()); .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.user.attributes = attributes_iter
user_id: _, .take_while_ref(|u| u.user_id == user.user.user_id)
attribute_name, .map(AttributeValue::from)
value, .collect();
} 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),
}
}
} }
Ok(users) Ok(users)
} }
@@ -188,21 +182,10 @@ impl UserBackendHandler for SqlBackendHandler {
); );
let attributes = model::UserAttributes::find() let attributes = model::UserAttributes::find()
.filter(model::UserAttributesColumn::UserId.eq(user_id)) .filter(model::UserAttributesColumn::UserId.eq(user_id))
.order_by_asc(model::UserAttributesColumn::AttributeName)
.all(&self.sql_pool) .all(&self.sql_pool)
.await?; .await?;
for model::user_attributes::Model { user.attributes = attributes.into_iter().map(AttributeValue::from).collect();
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),
}
}
Ok(user) Ok(user)
} }
@@ -762,9 +745,23 @@ mod tests {
.unwrap(); .unwrap();
assert_eq!(user.email, "email"); assert_eq!(user.email, "email");
assert_eq!(user.display_name.unwrap(), "display_name"); assert_eq!(user.display_name.unwrap(), "display_name");
assert_eq!(user.first_name.unwrap(), "first_name"); assert_eq!(
assert_eq!(user.last_name.unwrap(), "last_name"); user.attributes,
assert_eq!(user.avatar, Some(JpegPhoto::for_tests())); 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] #[tokio::test]
@@ -789,9 +786,19 @@ mod tests {
.await .await
.unwrap(); .unwrap();
assert_eq!(user.display_name.unwrap(), "display bob"); assert_eq!(user.display_name.unwrap(), "display bob");
assert_eq!(user.first_name.unwrap(), "first bob"); assert_eq!(
assert_eq!(user.last_name, None); user.attributes,
assert_eq!(user.avatar, Some(JpegPhoto::for_tests())); 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] #[tokio::test]
@@ -813,7 +820,11 @@ mod tests {
.get_user_details(&UserId::new("bob")) .get_user_details(&UserId::new("bob"))
.await .await
.unwrap(); .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 fixture
.handler .handler
.update_user(UpdateUserRequest { .update_user(UpdateUserRequest {
@@ -829,7 +840,7 @@ mod tests {
.get_user_details(&UserId::new("bob")) .get_user_details(&UserId::new("bob"))
.await .await
.unwrap(); .unwrap();
assert_eq!(user.avatar, None); assert!(!user.attributes.contains(&avatar));
} }
#[tokio::test] #[tokio::test]
@@ -856,9 +867,23 @@ mod tests {
.unwrap(); .unwrap();
assert_eq!(user.email, "email"); assert_eq!(user.email, "email");
assert_eq!(user.display_name.unwrap(), "display_name"); assert_eq!(user.display_name.unwrap(), "display_name");
assert_eq!(user.first_name.unwrap(), "first_name"); assert_eq!(
assert_eq!(user.last_name.unwrap(), "last_name"); user.attributes,
assert_eq!(user.avatar, Some(JpegPhoto::for_tests())); 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] #[tokio::test]
+83 -9
View File
@@ -104,9 +104,42 @@ macro_rules! uuid {
}; };
} }
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] #[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Serialized(Vec<u8>); pub struct Serialized(Vec<u8>);
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::<i64>()
.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 { impl<'a, T: Serialize + ?Sized> From<&'a T> for Serialized {
fn from(t: &'a T) -> Self { fn from(t: &'a T) -> Self {
Self(bincode::serialize(&t).unwrap()) Self(bincode::serialize(&t).unwrap())
@@ -114,12 +147,16 @@ impl<'a, T: Serialize + ?Sized> From<&'a T> for Serialized {
} }
impl Serialized { impl Serialized {
fn convert_to<'a, T: Deserialize<'a>>(&'a self) -> bincode::Result<T> {
bincode::deserialize(&self.0)
}
pub fn unwrap<'a, T: Deserialize<'a>>(&'a self) -> T { 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 { 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<Serialized> 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)] #[derive(PartialEq, Eq, Debug, Clone, Serialize, Deserialize)]
pub struct User { pub struct User {
pub user_id: UserId, pub user_id: UserId,
pub email: String, pub email: String,
pub display_name: Option<String>, pub display_name: Option<String>,
pub first_name: Option<String>,
pub last_name: Option<String>,
pub avatar: Option<JpegPhoto>,
pub creation_date: NaiveDateTime, pub creation_date: NaiveDateTime,
pub uuid: Uuid, pub uuid: Uuid,
pub attributes: Vec<AttributeValue>,
} }
#[cfg(test)] #[cfg(test)]
@@ -398,11 +439,9 @@ impl Default for User {
user_id: UserId::default(), user_id: UserId::default(),
email: String::new(), email: String::new(),
display_name: None, display_name: None,
first_name: None,
last_name: None,
avatar: None,
creation_date: epoch, creation_date: epoch,
uuid: Uuid::from_name_and_date("", &epoch), uuid: Uuid::from_name_and_date("", &epoch),
attributes: Vec::new(),
} }
} }
} }
@@ -513,3 +552,38 @@ pub struct UserAndGroups {
pub user: User, pub user: User,
pub groups: Option<Vec<GroupDetails>>, pub groups: Option<Vec<GroupDetails>>,
} }
#[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());
}
}
+29 -15
View File
@@ -6,8 +6,9 @@ use tracing::info;
use crate::domain::{ use crate::domain::{
error::Result, error::Result,
handler::{ handler::{
BackendHandler, CreateUserRequest, GroupListerBackendHandler, GroupRequestFilter, BackendHandler, CreateUserRequest, GroupBackendHandler, GroupListerBackendHandler,
UpdateGroupRequest, UpdateUserRequest, UserListerBackendHandler, UserRequestFilter, GroupRequestFilter, Schema, SchemaBackendHandler, UpdateGroupRequest, UpdateUserRequest,
UserBackendHandler, UserListerBackendHandler, UserRequestFilter,
}, },
types::{Group, GroupDetails, GroupId, User, UserAndGroups, UserId}, types::{Group, GroupDetails, GroupId, User, UserAndGroups, UserId},
}; };
@@ -72,6 +73,7 @@ impl ValidationResults {
pub trait UserReadableBackendHandler { pub trait UserReadableBackendHandler {
async fn get_user_details(&self, user_id: &UserId) -> Result<User>; async fn get_user_details(&self, user_id: &UserId) -> Result<User>;
async fn get_user_groups(&self, user_id: &UserId) -> Result<HashSet<GroupDetails>>; async fn get_user_groups(&self, user_id: &UserId) -> Result<HashSet<GroupDetails>>;
async fn get_schema(&self) -> Result<Schema>;
} }
#[async_trait] #[async_trait]
@@ -106,10 +108,13 @@ pub trait AdminBackendHandler:
#[async_trait] #[async_trait]
impl<Handler: BackendHandler> UserReadableBackendHandler for Handler { impl<Handler: BackendHandler> UserReadableBackendHandler for Handler {
async fn get_user_details(&self, user_id: &UserId) -> Result<User> { async fn get_user_details(&self, user_id: &UserId) -> Result<User> {
self.get_user_details(user_id).await <Handler as UserBackendHandler>::get_user_details(self, user_id).await
} }
async fn get_user_groups(&self, user_id: &UserId) -> Result<HashSet<GroupDetails>> { async fn get_user_groups(&self, user_id: &UserId) -> Result<HashSet<GroupDetails>> {
self.get_user_groups(user_id).await <Handler as UserBackendHandler>::get_user_groups(self, user_id).await
}
async fn get_schema(&self) -> Result<Schema> {
<Handler as SchemaBackendHandler>::get_schema(self).await
} }
} }
@@ -120,44 +125,44 @@ impl<Handler: BackendHandler> ReadonlyBackendHandler for Handler {
filters: Option<UserRequestFilter>, filters: Option<UserRequestFilter>,
get_groups: bool, get_groups: bool,
) -> Result<Vec<UserAndGroups>> { ) -> Result<Vec<UserAndGroups>> {
self.list_users(filters, get_groups).await <Handler as UserListerBackendHandler>::list_users(self, filters, get_groups).await
} }
async fn list_groups(&self, filters: Option<GroupRequestFilter>) -> Result<Vec<Group>> { async fn list_groups(&self, filters: Option<GroupRequestFilter>) -> Result<Vec<Group>> {
self.list_groups(filters).await <Handler as GroupListerBackendHandler>::list_groups(self, filters).await
} }
async fn get_group_details(&self, group_id: GroupId) -> Result<GroupDetails> { async fn get_group_details(&self, group_id: GroupId) -> Result<GroupDetails> {
self.get_group_details(group_id).await <Handler as GroupBackendHandler>::get_group_details(self, group_id).await
} }
} }
#[async_trait] #[async_trait]
impl<Handler: BackendHandler> UserWriteableBackendHandler for Handler { impl<Handler: BackendHandler> UserWriteableBackendHandler for Handler {
async fn update_user(&self, request: UpdateUserRequest) -> Result<()> { async fn update_user(&self, request: UpdateUserRequest) -> Result<()> {
self.update_user(request).await <Handler as UserBackendHandler>::update_user(self, request).await
} }
} }
#[async_trait] #[async_trait]
impl<Handler: BackendHandler> AdminBackendHandler for Handler { impl<Handler: BackendHandler> AdminBackendHandler for Handler {
async fn create_user(&self, request: CreateUserRequest) -> Result<()> { async fn create_user(&self, request: CreateUserRequest) -> Result<()> {
self.create_user(request).await <Handler as UserBackendHandler>::create_user(self, request).await
} }
async fn delete_user(&self, user_id: &UserId) -> Result<()> { async fn delete_user(&self, user_id: &UserId) -> Result<()> {
self.delete_user(user_id).await <Handler as UserBackendHandler>::delete_user(self, user_id).await
} }
async fn add_user_to_group(&self, user_id: &UserId, group_id: GroupId) -> Result<()> { async fn add_user_to_group(&self, user_id: &UserId, group_id: GroupId) -> Result<()> {
self.add_user_to_group(user_id, group_id).await <Handler as UserBackendHandler>::add_user_to_group(self, user_id, group_id).await
} }
async fn remove_user_from_group(&self, user_id: &UserId, group_id: GroupId) -> Result<()> { async fn remove_user_from_group(&self, user_id: &UserId, group_id: GroupId) -> Result<()> {
self.remove_user_from_group(user_id, group_id).await <Handler as UserBackendHandler>::remove_user_from_group(self, user_id, group_id).await
} }
async fn update_group(&self, request: UpdateGroupRequest) -> Result<()> { async fn update_group(&self, request: UpdateGroupRequest) -> Result<()> {
self.update_group(request).await <Handler as GroupBackendHandler>::update_group(self, request).await
} }
async fn create_group(&self, group_name: &str) -> Result<GroupId> { async fn create_group(&self, group_name: &str) -> Result<GroupId> {
self.create_group(group_name).await <Handler as GroupBackendHandler>::create_group(self, group_name).await
} }
async fn delete_group(&self, group_id: GroupId) -> Result<()> { async fn delete_group(&self, group_id: GroupId) -> Result<()> {
self.delete_group(group_id).await <Handler as GroupBackendHandler>::delete_group(self, group_id).await
} }
} }
@@ -262,6 +267,15 @@ pub struct UserRestrictedListerBackendHandler<'a, Handler> {
pub user_filter: Option<UserId>, pub user_filter: Option<UserId>,
} }
#[async_trait]
impl<'a, Handler: SchemaBackendHandler + Sync> SchemaBackendHandler
for UserRestrictedListerBackendHandler<'a, Handler>
{
async fn get_schema(&self) -> Result<Schema> {
self.handler.get_schema().await
}
}
#[async_trait] #[async_trait]
impl<'a, Handler: UserListerBackendHandler + Sync> UserListerBackendHandler impl<'a, Handler: UserListerBackendHandler + Sync> UserListerBackendHandler
for UserRestrictedListerBackendHandler<'a, Handler> for UserRestrictedListerBackendHandler<'a, Handler>
+18 -4
View File
@@ -2,7 +2,7 @@ use crate::{
domain::{ domain::{
handler::BackendHandler, handler::BackendHandler,
ldap::utils::{map_user_field, UserFieldType}, ldap::utils::{map_user_field, UserFieldType},
types::{GroupDetails, GroupId, UserColumn, UserId}, types::{GroupDetails, GroupId, JpegPhoto, UserColumn, UserId},
}, },
infra::{ infra::{
access_control::{ReadonlyBackendHandler, UserReadableBackendHandler}, access_control::{ReadonlyBackendHandler, UserReadableBackendHandler},
@@ -236,15 +236,29 @@ impl<Handler: BackendHandler> User<Handler> {
} }
fn first_name(&self) -> &str { 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 { 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<String> { fn avatar(&self) -> Option<String> {
self.user.avatar.as_ref().map(String::from) self.user
.attributes
.iter()
.find(|a| a.name == "avatar")
.map(|a| String::from(&a.value.unwrap::<JpegPhoto>()))
} }
fn creation_date(&self) -> chrono::DateTime<chrono::Utc> { fn creation_date(&self) -> chrono::DateTime<chrono::Utc> {
+91 -10
View File
@@ -1,6 +1,8 @@
use crate::{ use crate::{
domain::{ domain::{
handler::{BackendHandler, BindRequest, CreateUserRequest, LoginHandler}, handler::{
BackendHandler, BindRequest, CreateUserRequest, LoginHandler, SchemaBackendHandler,
},
ldap::{ ldap::{
error::{LdapError, LdapResult}, error::{LdapError, LdapResult},
group::{convert_groups_to_ldap_op, get_groups_list}, group::{convert_groups_to_ldap_op, get_groups_list},
@@ -467,12 +469,17 @@ impl<Backend: BackendHandler + LoginHandler + OpaqueHandler> LdapHandler<Backend
.get_user_restricted_lister_handler(user_info); .get_user_restricted_lister_handler(user_info);
let (users, groups) = self.do_search_internal(&backend_handler, request).await?; let (users, groups) = self.do_search_internal(&backend_handler, request).await?;
let schema = backend_handler.get_schema().await.map_err(|e| LdapError {
code: LdapResultCode::OperationsError,
message: format!("Unable to get schema: {:#}", e),
})?;
let mut results = Vec::new(); let mut results = Vec::new();
if let Some(users) = users { if let Some(users) = users {
results.extend(convert_users_to_ldap_op( results.extend(convert_users_to_ldap_op(
users, users,
&request.attrs, &request.attrs,
&self.ldap_info, &self.ldap_info,
&schema,
)); ));
} }
if let Some(groups) = groups { if let Some(groups) = groups {
@@ -769,6 +776,7 @@ mod tests {
}); });
Ok(set) Ok(set)
}); });
setup_default_schema(&mut mock);
let mut ldap_handler = LdapHandler::new_for_tests(mock, "dc=Example,dc=com"); let mut ldap_handler = LdapHandler::new_for_tests(mock, "dc=Example,dc=com");
let request = LdapBindRequest { let request = LdapBindRequest {
dn: "uid=test,ou=people,dc=example,dc=coM".to_string(), dn: "uid=test,ou=people,dc=example,dc=coM".to_string(),
@@ -799,6 +807,44 @@ mod tests {
setup_bound_handler_with_group(mock, "lldap_admin").await setup_bound_handler_with_group(mock, "lldap_admin").await
} }
fn setup_default_schema(mock: &mut MockTestBackendHandler) {
mock.expect_get_schema().returning(|| {
Ok(Schema {
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(),
},
})
});
}
#[tokio::test] #[tokio::test]
async fn test_bind() { async fn test_bind() {
let mut mock = MockTestBackendHandler::new(); let mut mock = MockTestBackendHandler::new();
@@ -1083,9 +1129,17 @@ mod tests {
user_id: UserId::new("bob_1"), user_id: UserId::new("bob_1"),
email: "bob@bobmail.bob".to_string(), email: "bob@bobmail.bob".to_string(),
display_name: Some("Bôb Böbberson".to_string()), display_name: Some("Bôb Böbberson".to_string()),
first_name: Some("Bôb".to_string()),
last_name: Some("Böbberson".to_string()),
uuid: uuid!("698e1d5f-7a40-3151-8745-b9b8a37839da"), uuid: uuid!("698e1d5f-7a40-3151-8745-b9b8a37839da"),
attributes: vec![
AttributeValue {
name: "first_name".to_owned(),
value: Serialized::from("Bôb"),
},
AttributeValue {
name: "last_name".to_owned(),
value: Serialized::from("Böbberson"),
},
],
..Default::default() ..Default::default()
}, },
groups: None, groups: None,
@@ -1095,9 +1149,20 @@ mod tests {
user_id: UserId::new("jim"), user_id: UserId::new("jim"),
email: "jim@cricket.jim".to_string(), email: "jim@cricket.jim".to_string(),
display_name: Some("Jimminy Cricket".to_string()), display_name: Some("Jimminy Cricket".to_string()),
first_name: Some("Jim".to_string()), attributes: vec![
last_name: Some("Cricket".to_string()), AttributeValue {
avatar: Some(JpegPhoto::for_tests()), name: "avatar".to_owned(),
value: Serialized::from(&JpegPhoto::for_tests()),
},
AttributeValue {
name: "first_name".to_owned(),
value: Serialized::from("Jim"),
},
AttributeValue {
name: "last_name".to_owned(),
value: Serialized::from("Cricket"),
},
],
uuid: uuid!("04ac75e0-2900-3e21-926c-2f732c26b3fc"), uuid: uuid!("04ac75e0-2900-3e21-926c-2f732c26b3fc"),
creation_date: Utc creation_date: Utc
.with_ymd_and_hms(2014, 7, 8, 9, 10, 11) .with_ymd_and_hms(2014, 7, 8, 9, 10, 11)
@@ -1746,8 +1811,16 @@ mod tests {
user_id: UserId::new("bob_1"), user_id: UserId::new("bob_1"),
email: "bob@bobmail.bob".to_string(), email: "bob@bobmail.bob".to_string(),
display_name: Some("Bôb Böbberson".to_string()), display_name: Some("Bôb Böbberson".to_string()),
first_name: Some("Bôb".to_string()), attributes: vec![
last_name: Some("Böbberson".to_string()), AttributeValue {
name: "first_name".to_owned(),
value: Serialized::from("Bôb"),
},
AttributeValue {
name: "last_name".to_owned(),
value: Serialized::from("Böbberson"),
},
],
..Default::default() ..Default::default()
}, },
groups: None, groups: None,
@@ -1820,8 +1893,16 @@ mod tests {
user_id: UserId::new("bob_1"), user_id: UserId::new("bob_1"),
email: "bob@bobmail.bob".to_string(), email: "bob@bobmail.bob".to_string(),
display_name: Some("Bôb Böbberson".to_string()), display_name: Some("Bôb Böbberson".to_string()),
last_name: Some("Böbberson".to_string()), attributes: vec![
avatar: Some(JpegPhoto::for_tests()), AttributeValue {
name: "avatar".to_owned(),
value: Serialized::from(&JpegPhoto::for_tests()),
},
AttributeValue {
name: "last_name".to_owned(),
value: Serialized::from("Böbberson"),
},
],
uuid: uuid!("b4ac75e0-2900-3e21-926c-2f732c26b3fc"), uuid: uuid!("b4ac75e0-2900-3e21-926c-2f732c26b3fc"),
..Default::default() ..Default::default()
}, },