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

8
Cargo.lock generated
View File

@@ -2670,9 +2670,9 @@ dependencies = [
[[package]]
name = "mockall"
version = "0.11.3"
version = "0.11.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "50e4a1c770583dac7ab5e2f6c139153b783a53a1bbee9729613f193e59828326"
checksum = "4c84490118f2ee2d74570d114f3d0493cbf02790df303d2707606c3e14e07c96"
dependencies = [
"cfg-if",
"downcast",
@@ -2685,9 +2685,9 @@ dependencies = [
[[package]]
name = "mockall_derive"
version = "0.11.3"
version = "0.11.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "832663583d5fa284ca8810bf7015e46c9fff9622d3cf34bd1eea5003fec06dd0"
checksum = "22ce75669015c4f47b289fd4d4f56e894e4c96003ffdf3ac51313126f94c6cbb"
dependencies = [
"cfg-if",
"proc-macro2",

View File

@@ -128,7 +128,7 @@ features = ["dangerous_configuration"]
[dev-dependencies]
assert_cmd = "2.0"
mockall = "0.11"
mockall = "0.11.4"
nix = "0.26.2"
[dev-dependencies.graphql_client]

View File

@@ -134,10 +134,24 @@ pub struct AttributeSchema {
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)]
pub struct Schema {
pub user_attributes: Vec<AttributeSchema>,
pub group_attributes: Vec<AttributeSchema>,
pub user_attributes: AttributeList,
pub group_attributes: AttributeList,
}
#[async_trait]
@@ -146,12 +160,12 @@ pub trait LoginHandler: Send + Sync {
}
#[async_trait]
pub trait GroupListerBackendHandler {
pub trait GroupListerBackendHandler: SchemaBackendHandler {
async fn list_groups(&self, filters: Option<GroupRequestFilter>) -> Result<Vec<Group>>;
}
#[async_trait]
pub trait GroupBackendHandler {
pub trait GroupBackendHandler: SchemaBackendHandler {
async fn get_group_details(&self, group_id: GroupId) -> Result<GroupDetails>;
async fn update_group(&self, request: UpdateGroupRequest) -> Result<()>;
async fn create_group(&self, group_name: &str) -> Result<GroupId>;
@@ -159,7 +173,7 @@ pub trait GroupBackendHandler {
}
#[async_trait]
pub trait UserListerBackendHandler {
pub trait UserListerBackendHandler: SchemaBackendHandler {
async fn list_users(
&self,
filters: Option<UserRequestFilter>,
@@ -168,7 +182,7 @@ pub trait UserListerBackendHandler {
}
#[async_trait]
pub trait UserBackendHandler {
pub trait UserBackendHandler: SchemaBackendHandler {
async fn get_user_details(&self, user_id: &UserId) -> Result<User>;
async fn create_user(&self, request: CreateUserRequest) -> Result<()>;
async fn update_user(&self, request: UpdateUserRequest) -> Result<()>;

View File

@@ -5,11 +5,11 @@ use ldap3_proto::{
use tracing::{debug, instrument, warn};
use crate::domain::{
handler::{UserListerBackendHandler, UserRequestFilter},
handler::{Schema, UserListerBackendHandler, UserRequestFilter},
ldap::{
error::{LdapError, LdapResult},
utils::{
expand_attribute_wildcards, get_group_id_from_distinguished_name,
expand_attribute_wildcards, get_custom_attribute, get_group_id_from_distinguished_name,
get_user_id_from_distinguished_name, map_user_field, LdapInfo, UserFieldType,
},
},
@@ -22,6 +22,7 @@ pub fn get_user_attribute(
base_dn_str: &str,
groups: Option<&[GroupDetails]>,
ignored_user_attributes: &[String],
schema: &Schema,
) -> Option<Vec<Vec<u8>>> {
let attribute = attribute.to_ascii_lowercase();
let attribute_values = match attribute.as_str() {
@@ -36,9 +37,13 @@ pub fn get_user_attribute(
"uid" | "user_id" | "id" => vec![user.user_id.to_string().into_bytes()],
"entryuuid" | "uuid" => vec![user.uuid.to_string().into_bytes()],
"mail" | "email" => vec![user.email.clone().into_bytes()],
"givenname" | "first_name" | "firstname" => vec![user.first_name.clone()?.into_bytes()],
"sn" | "last_name" | "lastname" => vec![user.last_name.clone()?.into_bytes()],
"jpegphoto" | "avatar" => vec![user.avatar.clone()?.into_bytes()],
"givenname" | "first_name" | "firstname" => {
get_custom_attribute(&user.attributes, "first_name", schema)?
}
"sn" | "last_name" | "lastname" => {
get_custom_attribute(&user.attributes, "last_name", schema)?
}
"jpegphoto" | "avatar" => get_custom_attribute(&user.attributes, "avatar", schema)?,
"memberof" => groups
.into_iter()
.flatten()
@@ -98,6 +103,7 @@ fn make_ldap_search_user_result_entry(
attributes: &[String],
groups: Option<&[GroupDetails]>,
ignored_user_attributes: &[String],
schema: &Schema,
) -> LdapSearchResultEntry {
let expanded_attributes = expand_user_attribute_wildcards(attributes);
let dn = format!("uid={},ou=people,{}", user.user_id.as_str(), base_dn_str);
@@ -106,8 +112,14 @@ fn make_ldap_search_user_result_entry(
attributes: expanded_attributes
.iter()
.filter_map(|a| {
let values =
get_user_attribute(&user, a, base_dn_str, groups, ignored_user_attributes)?;
let values = get_user_attribute(
&user,
a,
base_dn_str,
groups,
ignored_user_attributes,
schema,
)?;
Some(LdapPartialAttribute {
atype: a.to_string(),
vals: values,
@@ -242,6 +254,7 @@ pub fn convert_users_to_ldap_op<'a>(
users: Vec<UserAndGroups>,
attributes: &'a [String],
ldap_info: &'a LdapInfo,
schema: &'a Schema,
) -> impl Iterator<Item = LdapOp> + 'a {
users.into_iter().map(move |u| {
LdapOp::SearchResultEntry(make_ldap_search_user_result_entry(
@@ -250,6 +263,7 @@ pub fn convert_users_to_ldap_op<'a>(
attributes,
u.groups.as_deref(),
&ldap_info.ignored_user_attributes,
schema,
))
})
}

View File

@@ -1,11 +1,12 @@
use chrono::{NaiveDateTime, TimeZone};
use itertools::Itertools;
use ldap3_proto::{proto::LdapSubstringFilter, LdapResultCode};
use tracing::{debug, instrument, warn};
use crate::domain::{
handler::SubStringFilter,
handler::{Schema, SubStringFilter},
ldap::error::{LdapError, LdapResult},
types::{UserColumn, UserId},
types::{AttributeType, AttributeValue, JpegPhoto, UserColumn, UserId},
};
impl From<LdapSubstringFilter> for SubStringFilter {
@@ -193,3 +194,35 @@ pub struct LdapInfo {
pub ignored_user_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!(),
})
})
}

View File

@@ -1,7 +1,7 @@
use sea_orm::entity::prelude::*;
use serde::{Deserialize, Serialize};
use crate::domain::types::{Serialized, UserId};
use crate::domain::types::{AttributeValue, Serialized, UserId};
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)]
#[sea_orm(table_name = "user_attributes")]
@@ -55,3 +55,18 @@ impl Related<super::UserAttributeSchema> for Entity {
}
impl ActiveModelBehavior for ActiveModel {}
impl From<Model> for AttributeValue {
fn from(
Model {
user_id: _,
attribute_name,
value,
}: Model,
) -> Self {
Self {
name: attribute_name,
value,
}
}
}

View File

@@ -115,11 +115,9 @@ impl From<Model> for crate::domain::types::User {
user_id: user.user_id,
email: user.email,
display_name: user.display_name,
first_name: None,
last_name: None,
creation_date: user.creation_date,
uuid: user.uuid,
avatar: None,
attributes: Vec::new(),
}
}
}

View File

@@ -7,12 +7,18 @@ use crate::domain::{
use async_trait::async_trait;
use sea_orm::{EntityTrait, QueryOrder};
use super::handler::AttributeList;
#[async_trait]
impl SchemaBackendHandler for SqlBackendHandler {
async fn get_schema(&self) -> Result<Schema> {
Ok(Schema {
user_attributes: self.get_user_attributes().await?,
group_attributes: self.get_group_attributes().await?,
user_attributes: AttributeList {
attributes: self.get_user_attributes().await?,
},
group_attributes: AttributeList {
attributes: self.get_group_attributes().await?,
},
})
}
}
@@ -42,7 +48,9 @@ impl SqlBackendHandler {
#[cfg(test)]
mod tests {
use super::*;
use crate::domain::{sql_backend_handler::tests::*, types::AttributeType};
use crate::domain::{
handler::AttributeList, sql_backend_handler::tests::*, types::AttributeType,
};
#[tokio::test]
async fn test_default_schema() {
@@ -50,33 +58,37 @@ mod tests {
assert_eq!(
fixture.handler.get_schema().await.unwrap(),
Schema {
user_attributes: vec![
AttributeSchema {
name: "avatar".to_owned(),
attribute_type: AttributeType::JpegPhoto,
is_list: false,
is_visible: true,
is_editable: true,
is_hardcoded: true,
},
AttributeSchema {
name: "first_name".to_owned(),
attribute_type: AttributeType::String,
is_list: false,
is_visible: true,
is_editable: true,
is_hardcoded: true,
},
AttributeSchema {
name: "last_name".to_owned(),
attribute_type: AttributeType::String,
is_list: false,
is_visible: true,
is_editable: true,
is_hardcoded: true,
}
],
group_attributes: Vec::new()
user_attributes: AttributeList {
attributes: vec![
AttributeSchema {
name: "avatar".to_owned(),
attribute_type: AttributeType::JpegPhoto,
is_list: false,
is_visible: true,
is_editable: true,
is_hardcoded: true,
},
AttributeSchema {
name: "first_name".to_owned(),
attribute_type: AttributeType::String,
is_list: false,
is_visible: true,
is_editable: true,
is_hardcoded: true,
},
AttributeSchema {
name: "last_name".to_owned(),
attribute_type: AttributeType::String,
is_list: false,
is_visible: true,
is_editable: true,
is_hardcoded: true,
}
]
},
group_attributes: AttributeList {
attributes: Vec::new()
}
}
);
}

View File

@@ -6,7 +6,7 @@ use crate::domain::{
},
model::{self, GroupColumn, UserColumn},
sql_backend_handler::SqlBackendHandler,
types::{GroupDetails, GroupId, Serialized, User, UserAndGroups, UserId, Uuid},
types::{AttributeValue, GroupDetails, GroupId, Serialized, User, UserAndGroups, UserId, Uuid},
};
use async_trait::async_trait;
use sea_orm::{
@@ -17,7 +17,7 @@ use sea_orm::{
QueryFilter, QueryOrder, QuerySelect, QueryTrait, Set, TransactionTrait,
};
use std::collections::HashSet;
use tracing::{debug, instrument, warn};
use tracing::{debug, instrument};
fn attribute_condition(name: String, value: String) -> Cond {
Expr::in_subquery(
@@ -149,27 +149,21 @@ impl UserListerBackendHandler for SqlBackendHandler {
let attributes = model::UserAttributes::find()
.filter(model::UserAttributesColumn::UserId.is_in(&user_ids))
.order_by_asc(model::UserAttributesColumn::UserId)
.order_by_asc(model::UserAttributesColumn::AttributeName)
.all(&self.sql_pool)
.await?;
let mut attributes_iter = attributes.iter().peekable();
let mut attributes_iter = attributes.into_iter().peekable();
for user in users.iter_mut() {
attributes_iter
.peeking_take_while(|u| u.user_id < user.user.user_id)
.for_each(|_| ());
assert!(attributes_iter
.peek()
.map(|u| u.user_id >= user.user.user_id)
.unwrap_or(true),
"Attributes are not sorted, users are not sorted, or previous user didn't consume all the attributes");
for model::user_attributes::Model {
user_id: _,
attribute_name,
value,
} in attributes_iter.take_while_ref(|u| u.user_id == user.user.user_id)
{
match attribute_name.as_str() {
"first_name" => user.user.first_name = Some(value.unwrap()),
"last_name" => user.user.last_name = Some(value.unwrap()),
"avatar" => user.user.avatar = Some(value.unwrap()),
_ => warn!("Unknown attribute name: {}", attribute_name),
}
}
user.user.attributes = attributes_iter
.take_while_ref(|u| u.user_id == user.user.user_id)
.map(AttributeValue::from)
.collect();
}
Ok(users)
}
@@ -188,21 +182,10 @@ impl UserBackendHandler for SqlBackendHandler {
);
let attributes = model::UserAttributes::find()
.filter(model::UserAttributesColumn::UserId.eq(user_id))
.order_by_asc(model::UserAttributesColumn::AttributeName)
.all(&self.sql_pool)
.await?;
for model::user_attributes::Model {
user_id: _,
attribute_name,
value,
} in attributes
{
match attribute_name.as_str() {
"first_name" => user.first_name = Some(value.unwrap()),
"last_name" => user.last_name = Some(value.unwrap()),
"avatar" => user.avatar = Some(value.unwrap()),
_ => warn!("Unknown attribute name: {}", attribute_name),
}
}
user.attributes = attributes.into_iter().map(AttributeValue::from).collect();
Ok(user)
}
@@ -762,9 +745,23 @@ mod tests {
.unwrap();
assert_eq!(user.email, "email");
assert_eq!(user.display_name.unwrap(), "display_name");
assert_eq!(user.first_name.unwrap(), "first_name");
assert_eq!(user.last_name.unwrap(), "last_name");
assert_eq!(user.avatar, Some(JpegPhoto::for_tests()));
assert_eq!(
user.attributes,
vec![
AttributeValue {
name: "avatar".to_owned(),
value: Serialized::from(&JpegPhoto::for_tests())
},
AttributeValue {
name: "first_name".to_owned(),
value: Serialized::from("first_name")
},
AttributeValue {
name: "last_name".to_owned(),
value: Serialized::from("last_name")
}
]
);
}
#[tokio::test]
@@ -789,9 +786,19 @@ mod tests {
.await
.unwrap();
assert_eq!(user.display_name.unwrap(), "display bob");
assert_eq!(user.first_name.unwrap(), "first bob");
assert_eq!(user.last_name, None);
assert_eq!(user.avatar, Some(JpegPhoto::for_tests()));
assert_eq!(
user.attributes,
vec![
AttributeValue {
name: "avatar".to_owned(),
value: Serialized::from(&JpegPhoto::for_tests())
},
AttributeValue {
name: "first_name".to_owned(),
value: Serialized::from("first bob")
}
]
);
}
#[tokio::test]
@@ -813,7 +820,11 @@ mod tests {
.get_user_details(&UserId::new("bob"))
.await
.unwrap();
assert_eq!(user.avatar, Some(JpegPhoto::for_tests()));
let avatar = AttributeValue {
name: "avatar".to_owned(),
value: Serialized::from(&JpegPhoto::for_tests()),
};
assert!(user.attributes.contains(&avatar));
fixture
.handler
.update_user(UpdateUserRequest {
@@ -829,7 +840,7 @@ mod tests {
.get_user_details(&UserId::new("bob"))
.await
.unwrap();
assert_eq!(user.avatar, None);
assert!(!user.attributes.contains(&avatar));
}
#[tokio::test]
@@ -856,9 +867,23 @@ mod tests {
.unwrap();
assert_eq!(user.email, "email");
assert_eq!(user.display_name.unwrap(), "display_name");
assert_eq!(user.first_name.unwrap(), "first_name");
assert_eq!(user.last_name.unwrap(), "last_name");
assert_eq!(user.avatar, Some(JpegPhoto::for_tests()));
assert_eq!(
user.attributes,
vec![
AttributeValue {
name: "avatar".to_owned(),
value: Serialized::from(&JpegPhoto::for_tests())
},
AttributeValue {
name: "first_name".to_owned(),
value: Serialized::from("first_name")
},
AttributeValue {
name: "last_name".to_owned(),
value: Serialized::from("last_name")
}
]
);
}
#[tokio::test]

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>);
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 {
fn from(t: &'a T) -> Self {
Self(bincode::serialize(&t).unwrap())
@@ -114,12 +147,16 @@ impl<'a, T: Serialize + ?Sized> From<&'a T> for Serialized {
}
impl Serialized {
fn convert_to<'a, T: Deserialize<'a>>(&'a self) -> bincode::Result<T> {
bincode::deserialize(&self.0)
}
pub fn unwrap<'a, T: Deserialize<'a>>(&'a self) -> T {
bincode::deserialize(&self.0).unwrap()
self.convert_to().unwrap()
}
pub fn expect<'a, T: Deserialize<'a>>(&'a self, message: &str) -> T {
bincode::deserialize(&self.0).expect(message)
self.convert_to().expect(message)
}
}
@@ -378,16 +415,20 @@ impl IntoActiveValue<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)]
pub struct User {
pub user_id: UserId,
pub email: 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 uuid: Uuid,
pub attributes: Vec<AttributeValue>,
}
#[cfg(test)]
@@ -398,11 +439,9 @@ impl Default for User {
user_id: UserId::default(),
email: String::new(),
display_name: None,
first_name: None,
last_name: None,
avatar: None,
creation_date: epoch,
uuid: Uuid::from_name_and_date("", &epoch),
attributes: Vec::new(),
}
}
}
@@ -513,3 +552,38 @@ pub struct UserAndGroups {
pub user: User,
pub groups: Option<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());
}
}

View File

@@ -6,8 +6,9 @@ use tracing::info;
use crate::domain::{
error::Result,
handler::{
BackendHandler, CreateUserRequest, GroupListerBackendHandler, GroupRequestFilter,
UpdateGroupRequest, UpdateUserRequest, UserListerBackendHandler, UserRequestFilter,
BackendHandler, CreateUserRequest, GroupBackendHandler, GroupListerBackendHandler,
GroupRequestFilter, Schema, SchemaBackendHandler, UpdateGroupRequest, UpdateUserRequest,
UserBackendHandler, UserListerBackendHandler, UserRequestFilter,
},
types::{Group, GroupDetails, GroupId, User, UserAndGroups, UserId},
};
@@ -72,6 +73,7 @@ impl ValidationResults {
pub trait UserReadableBackendHandler {
async fn get_user_details(&self, user_id: &UserId) -> Result<User>;
async fn get_user_groups(&self, user_id: &UserId) -> Result<HashSet<GroupDetails>>;
async fn get_schema(&self) -> Result<Schema>;
}
#[async_trait]
@@ -106,10 +108,13 @@ pub trait AdminBackendHandler:
#[async_trait]
impl<Handler: BackendHandler> UserReadableBackendHandler for Handler {
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>> {
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>,
get_groups: bool,
) -> 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>> {
self.list_groups(filters).await
<Handler as GroupListerBackendHandler>::list_groups(self, filters).await
}
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]
impl<Handler: BackendHandler> UserWriteableBackendHandler for Handler {
async fn update_user(&self, request: UpdateUserRequest) -> Result<()> {
self.update_user(request).await
<Handler as UserBackendHandler>::update_user(self, request).await
}
}
#[async_trait]
impl<Handler: BackendHandler> AdminBackendHandler for Handler {
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<()> {
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<()> {
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<()> {
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<()> {
self.update_group(request).await
<Handler as GroupBackendHandler>::update_group(self, request).await
}
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<()> {
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>,
}
#[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]
impl<'a, Handler: UserListerBackendHandler + Sync> UserListerBackendHandler
for UserRestrictedListerBackendHandler<'a, Handler>

View File

@@ -2,7 +2,7 @@ use crate::{
domain::{
handler::BackendHandler,
ldap::utils::{map_user_field, UserFieldType},
types::{GroupDetails, GroupId, UserColumn, UserId},
types::{GroupDetails, GroupId, JpegPhoto, UserColumn, UserId},
},
infra::{
access_control::{ReadonlyBackendHandler, UserReadableBackendHandler},
@@ -236,15 +236,29 @@ impl<Handler: BackendHandler> User<Handler> {
}
fn first_name(&self) -> &str {
self.user.first_name.as_deref().unwrap_or("")
self.user
.attributes
.iter()
.find(|a| a.name == "first_name")
.map(|a| a.value.unwrap())
.unwrap_or("")
}
fn last_name(&self) -> &str {
self.user.last_name.as_deref().unwrap_or("")
self.user
.attributes
.iter()
.find(|a| a.name == "last_name")
.map(|a| a.value.unwrap())
.unwrap_or("")
}
fn avatar(&self) -> Option<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> {

View File

@@ -1,6 +1,8 @@
use crate::{
domain::{
handler::{BackendHandler, BindRequest, CreateUserRequest, LoginHandler},
handler::{
BackendHandler, BindRequest, CreateUserRequest, LoginHandler, SchemaBackendHandler,
},
ldap::{
error::{LdapError, LdapResult},
group::{convert_groups_to_ldap_op, get_groups_list},
@@ -467,12 +469,17 @@ impl<Backend: BackendHandler + LoginHandler + OpaqueHandler> LdapHandler<Backend
.get_user_restricted_lister_handler(user_info);
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();
if let Some(users) = users {
results.extend(convert_users_to_ldap_op(
users,
&request.attrs,
&self.ldap_info,
&schema,
));
}
if let Some(groups) = groups {
@@ -769,6 +776,7 @@ mod tests {
});
Ok(set)
});
setup_default_schema(&mut mock);
let mut ldap_handler = LdapHandler::new_for_tests(mock, "dc=Example,dc=com");
let request = LdapBindRequest {
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
}
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]
async fn test_bind() {
let mut mock = MockTestBackendHandler::new();
@@ -1083,9 +1129,17 @@ mod tests {
user_id: UserId::new("bob_1"),
email: "bob@bobmail.bob".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"),
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()
},
groups: None,
@@ -1095,9 +1149,20 @@ mod tests {
user_id: UserId::new("jim"),
email: "jim@cricket.jim".to_string(),
display_name: Some("Jimminy Cricket".to_string()),
first_name: Some("Jim".to_string()),
last_name: Some("Cricket".to_string()),
avatar: Some(JpegPhoto::for_tests()),
attributes: vec![
AttributeValue {
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"),
creation_date: Utc
.with_ymd_and_hms(2014, 7, 8, 9, 10, 11)
@@ -1746,8 +1811,16 @@ mod tests {
user_id: UserId::new("bob_1"),
email: "bob@bobmail.bob".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()),
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()
},
groups: None,
@@ -1820,8 +1893,16 @@ mod tests {
user_id: UserId::new("bob_1"),
email: "bob@bobmail.bob".to_string(),
display_name: Some("Bôb Böbberson".to_string()),
last_name: Some("Böbberson".to_string()),
avatar: Some(JpegPhoto::for_tests()),
attributes: vec![
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"),
..Default::default()
},