server: Use schema to populate attributes
This commit is contained in:
committed by
nitnelave
parent
829ebf59f7
commit
3140af63de
8
Cargo.lock
generated
8
Cargo.lock
generated
@@ -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",
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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<()>;
|
||||
|
||||
@@ -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,
|
||||
))
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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!(),
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -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()
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user