server: read custom attributes from LDAP

This commit is contained in:
Valentin Tolmer
2023-09-15 00:53:24 +02:00
committed by nitnelave
parent 8e1515c27b
commit 39a75b2c35
9 changed files with 289 additions and 161 deletions

View File

@@ -6,13 +6,14 @@ use tracing::{debug, instrument, warn};
use crate::domain::{ use crate::domain::{
handler::{GroupListerBackendHandler, GroupRequestFilter}, handler::{GroupListerBackendHandler, GroupRequestFilter},
ldap::error::LdapError, ldap::error::LdapError,
schema::{PublicSchema, SchemaGroupAttributeExtractor},
types::{Group, UserId, Uuid}, types::{Group, UserId, Uuid},
}; };
use super::{ use super::{
error::LdapResult, error::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_group_field, LdapInfo, get_user_id_from_distinguished_name, map_group_field, LdapInfo,
}, },
}; };
@@ -23,6 +24,7 @@ pub fn get_group_attribute(
attribute: &str, attribute: &str,
user_filter: &Option<UserId>, user_filter: &Option<UserId>,
ignored_group_attributes: &[String], ignored_group_attributes: &[String],
schema: &PublicSchema,
) -> 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() {
@@ -46,13 +48,20 @@ pub fn get_group_attribute(
attribute attribute
) )
} }
_ => { attr => {
if !ignored_group_attributes.contains(&attribute) { if !ignored_group_attributes.contains(&attribute) {
warn!( match get_custom_attribute::<SchemaGroupAttributeExtractor>(
r#"Ignoring unrecognized group attribute: {}\n\ &group.attributes,
attr,
schema,
) {
Some(v) => return Some(v),
None => warn!(
r#"Ignoring unrecognized group attribute: {}\n\
To disable this warning, add it to "ignored_group_attributes" in the config."#, To disable this warning, add it to "ignored_group_attributes" in the config."#,
attribute attribute
); ),
};
} }
return None; return None;
} }
@@ -83,6 +92,7 @@ fn make_ldap_search_group_result_entry(
attributes: &[String], attributes: &[String],
user_filter: &Option<UserId>, user_filter: &Option<UserId>,
ignored_group_attributes: &[String], ignored_group_attributes: &[String],
schema: &PublicSchema,
) -> LdapSearchResultEntry { ) -> LdapSearchResultEntry {
let expanded_attributes = expand_group_attribute_wildcards(attributes); let expanded_attributes = expand_group_attribute_wildcards(attributes);
@@ -97,6 +107,7 @@ fn make_ldap_search_group_result_entry(
a, a,
user_filter, user_filter,
ignored_group_attributes, ignored_group_attributes,
schema,
)?; )?;
Some(LdapPartialAttribute { Some(LdapPartialAttribute {
atype: a.to_string(), atype: a.to_string(),
@@ -221,6 +232,7 @@ pub fn convert_groups_to_ldap_op<'a>(
attributes: &'a [String], attributes: &'a [String],
ldap_info: &'a LdapInfo, ldap_info: &'a LdapInfo,
user_filter: &'a Option<UserId>, user_filter: &'a Option<UserId>,
schema: &'a PublicSchema,
) -> impl Iterator<Item = LdapOp> + 'a { ) -> impl Iterator<Item = LdapOp> + 'a {
groups.into_iter().map(move |g| { groups.into_iter().map(move |g| {
LdapOp::SearchResultEntry(make_ldap_search_group_result_entry( LdapOp::SearchResultEntry(make_ldap_search_group_result_entry(
@@ -229,6 +241,7 @@ pub fn convert_groups_to_ldap_op<'a>(
attributes, attributes,
user_filter, user_filter,
&ldap_info.ignored_group_attributes, &ldap_info.ignored_group_attributes,
schema,
)) ))
}) })
} }

View File

@@ -5,7 +5,7 @@ use ldap3_proto::{
use tracing::{debug, instrument, warn}; use tracing::{debug, instrument, warn};
use crate::domain::{ use crate::domain::{
handler::{Schema, UserListerBackendHandler, UserRequestFilter}, handler::{UserListerBackendHandler, UserRequestFilter},
ldap::{ ldap::{
error::{LdapError, LdapResult}, error::{LdapError, LdapResult},
utils::{ utils::{
@@ -13,6 +13,7 @@ use crate::domain::{
get_user_id_from_distinguished_name, map_user_field, LdapInfo, UserFieldType, get_user_id_from_distinguished_name, map_user_field, LdapInfo, UserFieldType,
}, },
}, },
schema::{PublicSchema, SchemaUserAttributeExtractor},
types::{GroupDetails, User, UserAndGroups, UserColumn, UserId}, types::{GroupDetails, User, UserAndGroups, UserColumn, UserId},
}; };
@@ -22,7 +23,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, schema: &PublicSchema,
) -> 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() {
@@ -37,13 +38,21 @@ 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" => { "givenname" | "first_name" | "firstname" => get_custom_attribute::<
get_custom_attribute(&user.attributes, "first_name", schema)? SchemaUserAttributeExtractor,
} >(
"sn" | "last_name" | "lastname" => { &user.attributes, "first_name", schema
get_custom_attribute(&user.attributes, "last_name", schema)? )?,
} "sn" | "last_name" | "lastname" => get_custom_attribute::<SchemaUserAttributeExtractor>(
"jpegphoto" | "avatar" => get_custom_attribute(&user.attributes, "avatar", schema)?, &user.attributes,
"last_name",
schema,
)?,
"jpegphoto" | "avatar" => get_custom_attribute::<SchemaUserAttributeExtractor>(
&user.attributes,
"avatar",
schema,
)?,
"memberof" => groups "memberof" => groups
.into_iter() .into_iter()
.flatten() .flatten()
@@ -67,13 +76,20 @@ pub fn get_user_attribute(
attribute attribute
) )
} }
_ => { attr => {
if !ignored_user_attributes.contains(&attribute) { if !ignored_user_attributes.contains(&attribute) {
warn!( match get_custom_attribute::<SchemaUserAttributeExtractor>(
r#"Ignoring unrecognized group attribute: {}\n\ &user.attributes,
attr,
schema,
) {
Some(v) => return Some(v),
None => warn!(
r#"Ignoring unrecognized group attribute: {}\n\
To disable this warning, add it to "ignored_user_attributes" in the config."#, To disable this warning, add it to "ignored_user_attributes" in the config."#,
attribute attr
); ),
};
} }
return None; return None;
} }
@@ -103,7 +119,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, schema: &PublicSchema,
) -> 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);
@@ -253,7 +269,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, schema: &'a PublicSchema,
) -> 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(

View File

@@ -4,8 +4,9 @@ use ldap3_proto::{proto::LdapSubstringFilter, LdapResultCode};
use tracing::{debug, instrument, warn}; use tracing::{debug, instrument, warn};
use crate::domain::{ use crate::domain::{
handler::{Schema, SubStringFilter}, handler::SubStringFilter,
ldap::error::{LdapError, LdapResult}, ldap::error::{LdapError, LdapResult},
schema::{PublicSchema, SchemaAttributeExtractor},
types::{AttributeType, AttributeValue, JpegPhoto, UserColumn, UserId}, types::{AttributeType, AttributeValue, JpegPhoto, UserColumn, UserId},
}; };
@@ -195,10 +196,10 @@ pub struct LdapInfo {
pub ignored_group_attributes: Vec<String>, pub ignored_group_attributes: Vec<String>,
} }
pub fn get_custom_attribute( pub fn get_custom_attribute<Extractor: SchemaAttributeExtractor>(
attributes: &[AttributeValue], attributes: &[AttributeValue],
attribute_name: &str, attribute_name: &str,
schema: &Schema, schema: &PublicSchema,
) -> Option<Vec<Vec<u8>>> { ) -> Option<Vec<Vec<u8>>> {
let convert_date = |date| { let convert_date = |date| {
chrono::Utc chrono::Utc
@@ -206,8 +207,7 @@ pub fn get_custom_attribute(
.to_rfc3339() .to_rfc3339()
.into_bytes() .into_bytes()
}; };
schema Extractor::get_attributes(schema)
.user_attributes
.get_attribute_type(attribute_name) .get_attribute_type(attribute_name)
.and_then(|attribute_type| { .and_then(|attribute_type| {
attributes attributes

View File

@@ -3,6 +3,7 @@ pub mod handler;
pub mod ldap; pub mod ldap;
pub mod model; pub mod model;
pub mod opaque_handler; pub mod opaque_handler;
pub mod schema;
pub mod sql_backend_handler; pub mod sql_backend_handler;
pub mod sql_group_backend_handler; pub mod sql_group_backend_handler;
pub mod sql_migrations; pub mod sql_migrations;

124
server/src/domain/schema.rs Normal file
View File

@@ -0,0 +1,124 @@
use crate::domain::{
handler::{AttributeList, AttributeSchema, Schema},
types::AttributeType,
};
use serde::{Deserialize, Serialize};
#[derive(PartialEq, Eq, Debug, Serialize, Deserialize)]
pub struct PublicSchema(Schema);
impl PublicSchema {
pub fn get_schema(&self) -> &Schema {
&self.0
}
}
pub trait SchemaAttributeExtractor: std::marker::Send {
fn get_attributes(schema: &PublicSchema) -> &AttributeList;
}
pub struct SchemaUserAttributeExtractor;
impl SchemaAttributeExtractor for SchemaUserAttributeExtractor {
fn get_attributes(schema: &PublicSchema) -> &AttributeList {
&schema.get_schema().user_attributes
}
}
pub struct SchemaGroupAttributeExtractor;
impl SchemaAttributeExtractor for SchemaGroupAttributeExtractor {
fn get_attributes(schema: &PublicSchema) -> &AttributeList {
&schema.get_schema().group_attributes
}
}
impl From<Schema> for PublicSchema {
fn from(mut schema: Schema) -> Self {
schema.user_attributes.attributes.extend_from_slice(&[
AttributeSchema {
name: "user_id".to_owned(),
attribute_type: AttributeType::String,
is_list: false,
is_visible: true,
is_editable: false,
is_hardcoded: true,
},
AttributeSchema {
name: "creation_date".to_owned(),
attribute_type: AttributeType::DateTime,
is_list: false,
is_visible: true,
is_editable: false,
is_hardcoded: true,
},
AttributeSchema {
name: "mail".to_owned(),
attribute_type: AttributeType::String,
is_list: false,
is_visible: true,
is_editable: true,
is_hardcoded: true,
},
AttributeSchema {
name: "uuid".to_owned(),
attribute_type: AttributeType::String,
is_list: false,
is_visible: true,
is_editable: false,
is_hardcoded: true,
},
AttributeSchema {
name: "display_name".to_owned(),
attribute_type: AttributeType::String,
is_list: false,
is_visible: true,
is_editable: true,
is_hardcoded: true,
},
]);
schema
.user_attributes
.attributes
.sort_by(|a, b| a.name.cmp(&b.name));
schema.group_attributes.attributes.extend_from_slice(&[
AttributeSchema {
name: "group_id".to_owned(),
attribute_type: AttributeType::Integer,
is_list: false,
is_visible: true,
is_editable: false,
is_hardcoded: true,
},
AttributeSchema {
name: "creation_date".to_owned(),
attribute_type: AttributeType::DateTime,
is_list: false,
is_visible: true,
is_editable: false,
is_hardcoded: true,
},
AttributeSchema {
name: "uuid".to_owned(),
attribute_type: AttributeType::String,
is_list: false,
is_visible: true,
is_editable: false,
is_hardcoded: true,
},
AttributeSchema {
name: "display_name".to_owned(),
attribute_type: AttributeType::String,
is_list: false,
is_visible: true,
is_editable: true,
is_hardcoded: true,
},
]);
schema
.group_attributes
.attributes
.sort_by(|a, b| a.name.cmp(&b.name));
PublicSchema(schema)
}
}

View File

@@ -2,12 +2,15 @@ use crate::{
domain::{ domain::{
handler::{BackendHandler, SchemaBackendHandler}, handler::{BackendHandler, SchemaBackendHandler},
ldap::utils::{map_user_field, UserFieldType}, ldap::utils::{map_user_field, UserFieldType},
schema::{
PublicSchema, SchemaAttributeExtractor, SchemaGroupAttributeExtractor,
SchemaUserAttributeExtractor,
},
types::{AttributeType, GroupDetails, GroupId, JpegPhoto, UserColumn, UserId}, types::{AttributeType, GroupDetails, GroupId, JpegPhoto, UserColumn, UserId},
}, },
infra::{ infra::{
access_control::{ReadonlyBackendHandler, UserReadableBackendHandler}, access_control::{ReadonlyBackendHandler, UserReadableBackendHandler},
graphql::api::{field_error_callback, Context}, graphql::api::{field_error_callback, Context},
schema::PublicSchema,
}, },
}; };
use chrono::{NaiveDateTime, TimeZone}; use chrono::{NaiveDateTime, TimeZone};
@@ -19,7 +22,6 @@ type DomainRequestFilter = crate::domain::handler::UserRequestFilter;
type DomainUser = crate::domain::types::User; type DomainUser = crate::domain::types::User;
type DomainGroup = crate::domain::types::Group; type DomainGroup = crate::domain::types::Group;
type DomainUserAndGroups = crate::domain::types::UserAndGroups; type DomainUserAndGroups = crate::domain::types::UserAndGroups;
type DomainSchema = crate::infra::schema::PublicSchema;
type DomainAttributeList = crate::domain::handler::AttributeList; type DomainAttributeList = crate::domain::handler::AttributeList;
type DomainAttributeSchema = crate::domain::handler::AttributeSchema; type DomainAttributeSchema = crate::domain::handler::AttributeSchema;
type DomainAttributeValue = crate::domain::types::AttributeValue; type DomainAttributeValue = crate::domain::types::AttributeValue;
@@ -492,7 +494,7 @@ impl<Handler: BackendHandler> From<DomainAttributeList> for AttributeList<Handle
#[derive(PartialEq, Eq, Debug, Serialize, Deserialize)] #[derive(PartialEq, Eq, Debug, Serialize, Deserialize)]
pub struct Schema<Handler: BackendHandler> { pub struct Schema<Handler: BackendHandler> {
schema: DomainSchema, schema: PublicSchema,
_phantom: std::marker::PhantomData<Box<Handler>>, _phantom: std::marker::PhantomData<Box<Handler>>,
} }
@@ -506,8 +508,8 @@ impl<Handler: BackendHandler> Schema<Handler> {
} }
} }
impl<Handler: BackendHandler> From<DomainSchema> for Schema<Handler> { impl<Handler: BackendHandler> From<PublicSchema> for Schema<Handler> {
fn from(value: DomainSchema) -> Self { fn from(value: PublicSchema) -> Self {
Self { Self {
schema: value, schema: value,
_phantom: std::marker::PhantomData, _phantom: std::marker::PhantomData,
@@ -515,26 +517,6 @@ impl<Handler: BackendHandler> From<DomainSchema> for Schema<Handler> {
} }
} }
trait SchemaAttributeExtractor: std::marker::Send {
fn get_attributes(schema: &DomainSchema) -> &DomainAttributeList;
}
struct SchemaUserAttributeExtractor;
impl SchemaAttributeExtractor for SchemaUserAttributeExtractor {
fn get_attributes(schema: &DomainSchema) -> &DomainAttributeList {
&schema.get_schema().user_attributes
}
}
struct SchemaGroupAttributeExtractor;
impl SchemaAttributeExtractor for SchemaGroupAttributeExtractor {
fn get_attributes(schema: &DomainSchema) -> &DomainAttributeList {
&schema.get_schema().group_attributes
}
}
#[derive(PartialEq, Eq, Debug, Serialize, Deserialize)] #[derive(PartialEq, Eq, Debug, Serialize, Deserialize)]
pub struct AttributeValue<Handler: BackendHandler, Extractor> { pub struct AttributeValue<Handler: BackendHandler, Extractor> {
attribute: DomainAttributeValue, attribute: DomainAttributeValue,

View File

@@ -12,6 +12,7 @@ use crate::{
}, },
}, },
opaque_handler::OpaqueHandler, opaque_handler::OpaqueHandler,
schema::PublicSchema,
types::{Group, JpegPhoto, UserAndGroups, UserId}, types::{Group, JpegPhoto, UserAndGroups, UserId},
}, },
infra::access_control::{ infra::access_control::{
@@ -611,10 +612,11 @@ impl<Backend: BackendHandler + LoginHandler + OpaqueHandler> LdapHandler<Backend
.get_user_restricted_lister_handler(user_info); .get_user_restricted_lister_handler(user_info);
let search_results = self.do_search_internal(&backend_handler, request).await?; let search_results = self.do_search_internal(&backend_handler, request).await?;
let schema = backend_handler.get_schema().await.map_err(|e| LdapError { let schema =
code: LdapResultCode::OperationsError, PublicSchema::from(backend_handler.get_schema().await.map_err(|e| LdapError {
message: format!("Unable to get schema: {:#}", e), code: LdapResultCode::OperationsError,
})?; message: format!("Unable to get schema: {:#}", e),
})?);
let mut results = match search_results { let mut results = match search_results {
InternalSearchResults::UsersAndGroups(users, groups) => { InternalSearchResults::UsersAndGroups(users, groups) => {
convert_users_to_ldap_op(users, &request.attrs, &self.ldap_info, &schema) convert_users_to_ldap_op(users, &request.attrs, &self.ldap_info, &schema)
@@ -623,6 +625,7 @@ impl<Backend: BackendHandler + LoginHandler + OpaqueHandler> LdapHandler<Backend
&request.attrs, &request.attrs,
&self.ldap_info, &self.ldap_info,
&backend_handler.user_filter, &backend_handler.user_filter,
&schema,
)) ))
.collect() .collect()
} }
@@ -2723,4 +2726,98 @@ mod tests {
]) ])
); );
} }
#[tokio::test]
async fn test_custom_attribute_read() {
let mut mock = MockTestBackendHandler::new();
mock.expect_list_users().times(1).return_once(|_, _| {
Ok(vec![UserAndGroups {
user: User {
user_id: UserId::new("test"),
attributes: vec![AttributeValue {
name: "nickname".to_owned(),
value: Serialized::from("Bob the Builder"),
}],
..Default::default()
},
groups: None,
}])
});
mock.expect_list_groups().times(1).return_once(|_| {
Ok(vec![Group {
id: GroupId(1),
display_name: "group".to_string(),
creation_date: chrono::Utc.timestamp_opt(42, 42).unwrap().naive_utc(),
users: vec![UserId::new("bob")],
uuid: uuid!("04ac75e0-2900-3e21-926c-2f732c26b3fc"),
attributes: vec![AttributeValue {
name: "club_name".to_owned(),
value: Serialized::from("Breakfast Club"),
}],
}])
});
mock.expect_get_schema().returning(|| {
Ok(crate::domain::handler::Schema {
user_attributes: AttributeList {
attributes: vec![AttributeSchema {
name: "nickname".to_owned(),
attribute_type: AttributeType::String,
is_list: false,
is_visible: true,
is_editable: true,
is_hardcoded: false,
}],
},
group_attributes: AttributeList {
attributes: vec![AttributeSchema {
name: "club_name".to_owned(),
attribute_type: AttributeType::String,
is_list: false,
is_visible: true,
is_editable: true,
is_hardcoded: false,
}],
},
})
});
let mut ldap_handler = setup_bound_readonly_handler(mock).await;
let request = make_search_request(
"dc=example,dc=com",
LdapFilter::And(vec![]),
vec!["uid", "nickname", "club_name"],
);
assert_eq!(
ldap_handler.do_search_or_dse(&request).await,
Ok(vec![
LdapOp::SearchResultEntry(LdapSearchResultEntry {
dn: "uid=test,ou=people,dc=example,dc=com".to_string(),
attributes: vec![
LdapPartialAttribute {
atype: "uid".to_owned(),
vals: vec![b"test".to_vec()],
},
LdapPartialAttribute {
atype: "nickname".to_owned(),
vals: vec![b"Bob the Builder".to_vec()],
},
],
}),
LdapOp::SearchResultEntry(LdapSearchResultEntry {
dn: "cn=group,ou=groups,dc=example,dc=com".to_owned(),
attributes: vec![
LdapPartialAttribute {
atype: "uid".to_owned(),
vals: vec![b"group".to_vec()],
},
LdapPartialAttribute {
atype: "club_name".to_owned(),
vals: vec![b"Breakfast Club".to_vec()],
},
],
}),
make_search_success()
]),
);
}
} }

View File

@@ -10,7 +10,6 @@ pub mod ldap_handler;
pub mod ldap_server; pub mod ldap_server;
pub mod logging; pub mod logging;
pub mod mail; pub mod mail;
pub mod schema;
pub mod sql_backend_handler; pub mod sql_backend_handler;
pub mod tcp_backend_handler; pub mod tcp_backend_handler;
pub mod tcp_server; pub mod tcp_server;

View File

@@ -1,104 +0,0 @@
use crate::domain::{
handler::{AttributeSchema, Schema},
types::AttributeType,
};
use serde::{Deserialize, Serialize};
#[derive(PartialEq, Eq, Debug, Serialize, Deserialize)]
pub struct PublicSchema(Schema);
impl PublicSchema {
pub fn get_schema(&self) -> &Schema {
&self.0
}
}
impl From<Schema> for PublicSchema {
fn from(mut schema: Schema) -> Self {
schema.user_attributes.attributes.extend_from_slice(&[
AttributeSchema {
name: "user_id".to_owned(),
attribute_type: AttributeType::String,
is_list: false,
is_visible: true,
is_editable: false,
is_hardcoded: true,
},
AttributeSchema {
name: "creation_date".to_owned(),
attribute_type: AttributeType::DateTime,
is_list: false,
is_visible: true,
is_editable: false,
is_hardcoded: true,
},
AttributeSchema {
name: "mail".to_owned(),
attribute_type: AttributeType::String,
is_list: false,
is_visible: true,
is_editable: true,
is_hardcoded: true,
},
AttributeSchema {
name: "uuid".to_owned(),
attribute_type: AttributeType::String,
is_list: false,
is_visible: true,
is_editable: false,
is_hardcoded: true,
},
AttributeSchema {
name: "display_name".to_owned(),
attribute_type: AttributeType::String,
is_list: false,
is_visible: true,
is_editable: true,
is_hardcoded: true,
},
]);
schema
.user_attributes
.attributes
.sort_by(|a, b| a.name.cmp(&b.name));
schema.group_attributes.attributes.extend_from_slice(&[
AttributeSchema {
name: "group_id".to_owned(),
attribute_type: AttributeType::Integer,
is_list: false,
is_visible: true,
is_editable: false,
is_hardcoded: true,
},
AttributeSchema {
name: "creation_date".to_owned(),
attribute_type: AttributeType::DateTime,
is_list: false,
is_visible: true,
is_editable: false,
is_hardcoded: true,
},
AttributeSchema {
name: "uuid".to_owned(),
attribute_type: AttributeType::String,
is_list: false,
is_visible: true,
is_editable: false,
is_hardcoded: true,
},
AttributeSchema {
name: "display_name".to_owned(),
attribute_type: AttributeType::String,
is_list: false,
is_visible: true,
is_editable: true,
is_hardcoded: true,
},
]);
schema
.group_attributes
.attributes
.sort_by(|a, b| a.name.cmp(&b.name));
PublicSchema(schema)
}
}