mirror of
https://github.com/lldap/lldap.git
synced 2026-03-31 15:07:48 +01:00
server, ldap: add support for Subschema requests (#1071)
Add a subschema entry to the rootDSE, which shows all attributes and objectclasses present on the LLDAP server, which is needed for some applications that need to index the LDAP server. The current implementation's goal is to have a bare minimum working subschema which follows the LDAP RFC. It also updates the GraphQL interface to follow the changes that have been made in actually separating out objectclasses, instead of having them as an attribute. Co-authored-by: nitnelave <valentin@tolmer.fr>
This commit is contained in:
Generated
+3
@@ -2659,6 +2659,7 @@ dependencies = [
|
|||||||
"chrono",
|
"chrono",
|
||||||
"derive_more 1.0.0",
|
"derive_more 1.0.0",
|
||||||
"image",
|
"image",
|
||||||
|
"itertools",
|
||||||
"juniper",
|
"juniper",
|
||||||
"lldap_auth",
|
"lldap_auth",
|
||||||
"pretty_assertions",
|
"pretty_assertions",
|
||||||
@@ -2745,6 +2746,8 @@ version = "0.1.0"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"chrono",
|
"chrono",
|
||||||
|
"derive_more 1.0.0",
|
||||||
|
"itertools",
|
||||||
"ldap3_proto",
|
"ldap3_proto",
|
||||||
"lldap_access_control",
|
"lldap_access_control",
|
||||||
"lldap_auth",
|
"lldap_auth",
|
||||||
|
|||||||
@@ -178,6 +178,13 @@ impl<Handler: BackendHandler> AccessControlledBackendHandler<Handler> {
|
|||||||
Self { handler }
|
Self { handler }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn get_schema_only_handler(
|
||||||
|
&self,
|
||||||
|
_validation_result: &ValidationResults,
|
||||||
|
) -> Option<&impl ReadSchemaBackendHandler> {
|
||||||
|
Some(&self.handler)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn get_admin_handler(
|
pub fn get_admin_handler(
|
||||||
&self,
|
&self,
|
||||||
validation_result: &ValidationResults,
|
validation_result: &ValidationResults,
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ test = []
|
|||||||
anyhow = "*"
|
anyhow = "*"
|
||||||
base64 = "0.21"
|
base64 = "0.21"
|
||||||
bincode = "1.3"
|
bincode = "1.3"
|
||||||
|
itertools = "0.10"
|
||||||
juniper = "0.15"
|
juniper = "0.15"
|
||||||
serde_bytes = "0.11"
|
serde_bytes = "0.11"
|
||||||
|
|
||||||
|
|||||||
@@ -36,4 +36,12 @@ impl AttributeList {
|
|||||||
self.get_attribute_schema(name)
|
self.get_attribute_schema(name)
|
||||||
.map(|a| (a.attribute_type, a.is_list))
|
.map(|a| (a.attribute_type, a.is_list))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn format_for_ldap_schema_description(&self) -> String {
|
||||||
|
self.attributes
|
||||||
|
.iter()
|
||||||
|
.map(|a| a.name.as_str())
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join(" $ ")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,10 @@ use lldap_domain::{
|
|||||||
};
|
};
|
||||||
use lldap_domain_handlers::handler::{BackendHandler, ReadSchemaBackendHandler};
|
use lldap_domain_handlers::handler::{BackendHandler, ReadSchemaBackendHandler};
|
||||||
use lldap_domain_model::model::UserColumn;
|
use lldap_domain_model::model::UserColumn;
|
||||||
use lldap_ldap::{UserFieldType, map_user_field};
|
use lldap_ldap::{
|
||||||
|
UserFieldType, get_default_group_object_classes, get_default_user_object_classes,
|
||||||
|
map_user_field,
|
||||||
|
};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use tracing::{Instrument, Span, debug, debug_span};
|
use tracing::{Instrument, Span, debug, debug_span};
|
||||||
@@ -522,10 +525,28 @@ impl<Handler: BackendHandler> From<DomainAttributeSchema> for AttributeSchema<Ha
|
|||||||
#[derive(PartialEq, Eq, Debug, Serialize, Deserialize)]
|
#[derive(PartialEq, Eq, Debug, Serialize, Deserialize)]
|
||||||
pub struct AttributeList<Handler: BackendHandler> {
|
pub struct AttributeList<Handler: BackendHandler> {
|
||||||
attributes: DomainAttributeList,
|
attributes: DomainAttributeList,
|
||||||
|
default_classes: Vec<LdapObjectClass>,
|
||||||
extra_classes: Vec<LdapObjectClass>,
|
extra_classes: Vec<LdapObjectClass>,
|
||||||
_phantom: std::marker::PhantomData<Box<Handler>>,
|
_phantom: std::marker::PhantomData<Box<Handler>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct ObjectClassInfo {
|
||||||
|
object_class: String,
|
||||||
|
is_hardcoded: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[graphql_object]
|
||||||
|
impl ObjectClassInfo {
|
||||||
|
fn object_class(&self) -> &str {
|
||||||
|
&self.object_class
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_hardcoded(&self) -> bool {
|
||||||
|
self.is_hardcoded
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[graphql_object(context = Context<Handler>)]
|
#[graphql_object(context = Context<Handler>)]
|
||||||
impl<Handler: BackendHandler> AttributeList<Handler> {
|
impl<Handler: BackendHandler> AttributeList<Handler> {
|
||||||
fn attributes(&self) -> Vec<AttributeSchema<Handler>> {
|
fn attributes(&self) -> Vec<AttributeSchema<Handler>> {
|
||||||
@@ -540,12 +561,35 @@ impl<Handler: BackendHandler> AttributeList<Handler> {
|
|||||||
fn extra_ldap_object_classes(&self) -> Vec<String> {
|
fn extra_ldap_object_classes(&self) -> Vec<String> {
|
||||||
self.extra_classes.iter().map(|c| c.to_string()).collect()
|
self.extra_classes.iter().map(|c| c.to_string()).collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn ldap_object_classes(&self) -> Vec<ObjectClassInfo> {
|
||||||
|
let mut all_object_classes: Vec<ObjectClassInfo> = self
|
||||||
|
.default_classes
|
||||||
|
.iter()
|
||||||
|
.map(|c| ObjectClassInfo {
|
||||||
|
object_class: c.to_string(),
|
||||||
|
is_hardcoded: true,
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
all_object_classes.extend(self.extra_classes.iter().map(|c| ObjectClassInfo {
|
||||||
|
object_class: c.to_string(),
|
||||||
|
is_hardcoded: false,
|
||||||
|
}));
|
||||||
|
|
||||||
|
all_object_classes
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<Handler: BackendHandler> AttributeList<Handler> {
|
impl<Handler: BackendHandler> AttributeList<Handler> {
|
||||||
fn new(attributes: DomainAttributeList, extra_classes: Vec<LdapObjectClass>) -> Self {
|
fn new(
|
||||||
|
attributes: DomainAttributeList,
|
||||||
|
default_classes: Vec<LdapObjectClass>,
|
||||||
|
extra_classes: Vec<LdapObjectClass>,
|
||||||
|
) -> Self {
|
||||||
Self {
|
Self {
|
||||||
attributes,
|
attributes,
|
||||||
|
default_classes,
|
||||||
extra_classes,
|
extra_classes,
|
||||||
_phantom: std::marker::PhantomData,
|
_phantom: std::marker::PhantomData,
|
||||||
}
|
}
|
||||||
@@ -563,12 +607,14 @@ impl<Handler: BackendHandler> Schema<Handler> {
|
|||||||
fn user_schema(&self) -> AttributeList<Handler> {
|
fn user_schema(&self) -> AttributeList<Handler> {
|
||||||
AttributeList::<Handler>::new(
|
AttributeList::<Handler>::new(
|
||||||
self.schema.get_schema().user_attributes.clone(),
|
self.schema.get_schema().user_attributes.clone(),
|
||||||
|
get_default_user_object_classes(),
|
||||||
self.schema.get_schema().extra_user_object_classes.clone(),
|
self.schema.get_schema().extra_user_object_classes.clone(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
fn group_schema(&self) -> AttributeList<Handler> {
|
fn group_schema(&self) -> AttributeList<Handler> {
|
||||||
AttributeList::<Handler>::new(
|
AttributeList::<Handler>::new(
|
||||||
self.schema.get_schema().group_attributes.clone(),
|
self.schema.get_schema().group_attributes.clone(),
|
||||||
|
get_default_group_object_classes(),
|
||||||
self.schema.get_schema().extra_group_object_classes.clone(),
|
self.schema.get_schema().extra_group_object_classes.clone(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,12 @@ repository.workspace = true
|
|||||||
anyhow = "*"
|
anyhow = "*"
|
||||||
ldap3_proto = "0.6.0"
|
ldap3_proto = "0.6.0"
|
||||||
tracing = "*"
|
tracing = "*"
|
||||||
|
itertools = "0.10"
|
||||||
|
|
||||||
|
[dependencies.derive_more]
|
||||||
|
features = ["from"]
|
||||||
|
default-features = false
|
||||||
|
version = "1"
|
||||||
|
|
||||||
[dependencies.chrono]
|
[dependencies.chrono]
|
||||||
features = ["serde"]
|
features = ["serde"]
|
||||||
@@ -54,3 +60,7 @@ pretty_assertions = "1"
|
|||||||
[dev-dependencies.tokio]
|
[dev-dependencies.tokio]
|
||||||
features = ["full"]
|
features = ["full"]
|
||||||
version = "1.25"
|
version = "1.25"
|
||||||
|
|
||||||
|
[dev-dependencies.lldap_domain]
|
||||||
|
path = "../domain"
|
||||||
|
features = ["test"]
|
||||||
@@ -18,6 +18,24 @@ use lldap_domain::{
|
|||||||
use lldap_domain_handlers::handler::{GroupListerBackendHandler, GroupRequestFilter};
|
use lldap_domain_handlers::handler::{GroupListerBackendHandler, GroupRequestFilter};
|
||||||
use tracing::{debug, instrument, warn};
|
use tracing::{debug, instrument, warn};
|
||||||
|
|
||||||
|
pub const REQUIRED_GROUP_ATTRIBUTES: &[&str] = &["display_name"];
|
||||||
|
|
||||||
|
const DEFAULT_GROUP_OBJECT_CLASSES: &[&str] = &["groupOfUniqueNames", "groupOfNames"];
|
||||||
|
|
||||||
|
fn get_default_group_object_classes_as_bytes() -> Vec<Vec<u8>> {
|
||||||
|
DEFAULT_GROUP_OBJECT_CLASSES
|
||||||
|
.iter()
|
||||||
|
.map(|c| c.as_bytes().to_vec())
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_default_group_object_classes() -> Vec<LdapObjectClass> {
|
||||||
|
DEFAULT_GROUP_OBJECT_CLASSES
|
||||||
|
.iter()
|
||||||
|
.map(|&c| LdapObjectClass::from(c))
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
pub fn get_group_attribute(
|
pub fn get_group_attribute(
|
||||||
group: &Group,
|
group: &Group,
|
||||||
base_dn_str: &str,
|
base_dn_str: &str,
|
||||||
@@ -28,7 +46,8 @@ pub fn get_group_attribute(
|
|||||||
) -> Option<Vec<Vec<u8>>> {
|
) -> Option<Vec<Vec<u8>>> {
|
||||||
let attribute_values = match map_group_field(attribute, schema) {
|
let attribute_values = match map_group_field(attribute, schema) {
|
||||||
GroupFieldType::ObjectClass => {
|
GroupFieldType::ObjectClass => {
|
||||||
let mut classes = vec![b"groupOfUniqueNames".to_vec()];
|
let mut classes: Vec<Vec<u8>> = get_default_group_object_classes_as_bytes();
|
||||||
|
|
||||||
classes.extend(
|
classes.extend(
|
||||||
schema
|
schema
|
||||||
.get_schema()
|
.get_schema()
|
||||||
@@ -205,7 +224,9 @@ fn convert_group_filter(
|
|||||||
GroupRequestFilter::from(false)
|
GroupRequestFilter::from(false)
|
||||||
})),
|
})),
|
||||||
GroupFieldType::ObjectClass => Ok(GroupRequestFilter::from(
|
GroupFieldType::ObjectClass => Ok(GroupRequestFilter::from(
|
||||||
matches!(value_lc.as_str(), "groupofuniquenames" | "groupofnames")
|
get_default_group_object_classes()
|
||||||
|
.iter()
|
||||||
|
.any(|class| class.as_str().eq_ignore_ascii_case(value_lc.as_str()))
|
||||||
|| schema
|
|| schema
|
||||||
.get_schema()
|
.get_schema()
|
||||||
.extra_group_object_classes
|
.extra_group_object_classes
|
||||||
|
|||||||
@@ -21,6 +21,25 @@ use lldap_domain_handlers::handler::{UserListerBackendHandler, UserRequestFilter
|
|||||||
use lldap_domain_model::model::UserColumn;
|
use lldap_domain_model::model::UserColumn;
|
||||||
use tracing::{debug, instrument, warn};
|
use tracing::{debug, instrument, warn};
|
||||||
|
|
||||||
|
pub const REQUIRED_USER_ATTRIBUTES: &[&str] = &["user_id", "mail"];
|
||||||
|
|
||||||
|
const DEFAULT_USER_OBJECT_CLASSES: &[&str] =
|
||||||
|
&["inetOrgPerson", "posixAccount", "mailAccount", "person"];
|
||||||
|
|
||||||
|
fn get_default_user_object_classes_vec_u8() -> Vec<Vec<u8>> {
|
||||||
|
DEFAULT_USER_OBJECT_CLASSES
|
||||||
|
.iter()
|
||||||
|
.map(|c| c.as_bytes().to_vec())
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_default_user_object_classes() -> Vec<LdapObjectClass> {
|
||||||
|
DEFAULT_USER_OBJECT_CLASSES
|
||||||
|
.iter()
|
||||||
|
.map(|&c| LdapObjectClass::from(c))
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
pub fn get_user_attribute(
|
pub fn get_user_attribute(
|
||||||
user: &User,
|
user: &User,
|
||||||
attribute: &AttributeName,
|
attribute: &AttributeName,
|
||||||
@@ -31,12 +50,8 @@ pub fn get_user_attribute(
|
|||||||
) -> Option<Vec<Vec<u8>>> {
|
) -> Option<Vec<Vec<u8>>> {
|
||||||
let attribute_values = match map_user_field(attribute, schema) {
|
let attribute_values = match map_user_field(attribute, schema) {
|
||||||
UserFieldType::ObjectClass => {
|
UserFieldType::ObjectClass => {
|
||||||
let mut classes = vec![
|
let mut classes: Vec<Vec<u8>> = get_default_user_object_classes_vec_u8();
|
||||||
b"inetOrgPerson".to_vec(),
|
|
||||||
b"posixAccount".to_vec(),
|
|
||||||
b"mailAccount".to_vec(),
|
|
||||||
b"person".to_vec(),
|
|
||||||
];
|
|
||||||
classes.extend(
|
classes.extend(
|
||||||
schema
|
schema
|
||||||
.get_schema()
|
.get_schema()
|
||||||
@@ -227,10 +242,10 @@ fn convert_user_filter(
|
|||||||
Ok(UserRequestFilter::from(false))
|
Ok(UserRequestFilter::from(false))
|
||||||
}
|
}
|
||||||
UserFieldType::ObjectClass => Ok(UserRequestFilter::from(
|
UserFieldType::ObjectClass => Ok(UserRequestFilter::from(
|
||||||
matches!(
|
get_default_user_object_classes()
|
||||||
value_lc.as_str(),
|
.iter()
|
||||||
"person" | "inetorgperson" | "posixaccount" | "mailaccount"
|
.any(|class| class.as_str().eq_ignore_ascii_case(value_lc.as_str()))
|
||||||
) || schema
|
|| schema
|
||||||
.get_schema()
|
.get_schema()
|
||||||
.extra_user_object_classes
|
.extra_user_object_classes
|
||||||
.contains(&LdapObjectClass::from(value_lc)),
|
.contains(&LdapObjectClass::from(value_lc)),
|
||||||
|
|||||||
@@ -1,10 +1,17 @@
|
|||||||
use crate::core::error::{LdapError, LdapResult};
|
use crate::core::{
|
||||||
|
error::{LdapError, LdapResult},
|
||||||
|
group::{REQUIRED_GROUP_ATTRIBUTES, get_default_group_object_classes},
|
||||||
|
user::{REQUIRED_USER_ATTRIBUTES, get_default_user_object_classes},
|
||||||
|
};
|
||||||
use chrono::TimeZone;
|
use chrono::TimeZone;
|
||||||
|
use itertools::join;
|
||||||
use ldap3_proto::LdapResultCode;
|
use ldap3_proto::LdapResultCode;
|
||||||
use lldap_domain::{
|
use lldap_domain::{
|
||||||
public_schema::PublicSchema,
|
public_schema::PublicSchema,
|
||||||
|
schema::{AttributeList, Schema},
|
||||||
types::{
|
types::{
|
||||||
Attribute, AttributeName, AttributeType, AttributeValue, Cardinality, GroupName, UserId,
|
Attribute, AttributeName, AttributeType, AttributeValue, Cardinality, GroupName,
|
||||||
|
LdapObjectClass, UserId,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
use lldap_domain_model::model::UserColumn;
|
use lldap_domain_model::model::UserColumn;
|
||||||
@@ -330,6 +337,139 @@ pub fn get_custom_attribute(
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(derive_more::From)]
|
||||||
|
pub struct ObjectClassList(Vec<LdapObjectClass>);
|
||||||
|
|
||||||
|
// See RFC4512 section 4.2.1 "objectClasses"
|
||||||
|
impl ObjectClassList {
|
||||||
|
pub fn format_for_ldap_schema_description(&self) -> String {
|
||||||
|
join(self.0.iter().map(|c| format!("'{}'", c)), " ")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// See RFC4512 section 4.2 "Subschema Subentries"
|
||||||
|
// This struct holds all information on what attributes and objectclasses are present on the server.
|
||||||
|
// It can be used to 'index' a server using a LDAP subschema call.
|
||||||
|
pub struct LdapSchemaDescription {
|
||||||
|
base: PublicSchema,
|
||||||
|
user_object_classes: ObjectClassList,
|
||||||
|
group_object_classes: ObjectClassList,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl LdapSchemaDescription {
|
||||||
|
pub fn from(schema: PublicSchema) -> Self {
|
||||||
|
let mut user_object_classes = get_default_user_object_classes();
|
||||||
|
user_object_classes.extend(schema.get_schema().extra_user_object_classes.clone());
|
||||||
|
let mut group_object_classes = get_default_group_object_classes();
|
||||||
|
group_object_classes.extend(schema.get_schema().extra_group_object_classes.clone());
|
||||||
|
|
||||||
|
Self {
|
||||||
|
base: schema,
|
||||||
|
user_object_classes: ObjectClassList(user_object_classes),
|
||||||
|
group_object_classes: ObjectClassList(group_object_classes),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn schema(&self) -> &Schema {
|
||||||
|
self.base.get_schema()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn user_object_classes(&self) -> &ObjectClassList {
|
||||||
|
&self.user_object_classes
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn group_object_classes(&self) -> &ObjectClassList {
|
||||||
|
&self.group_object_classes
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn required_user_attributes(&self) -> AttributeList {
|
||||||
|
let attributes = self
|
||||||
|
.schema()
|
||||||
|
.user_attributes
|
||||||
|
.attributes
|
||||||
|
.iter()
|
||||||
|
.filter(|a| REQUIRED_USER_ATTRIBUTES.contains(&a.name.as_str()))
|
||||||
|
.cloned()
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
AttributeList { attributes }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn optional_user_attributes(&self) -> AttributeList {
|
||||||
|
let attributes = self
|
||||||
|
.schema()
|
||||||
|
.user_attributes
|
||||||
|
.attributes
|
||||||
|
.iter()
|
||||||
|
.filter(|a| !REQUIRED_USER_ATTRIBUTES.contains(&a.name.as_str()))
|
||||||
|
.cloned()
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
AttributeList { attributes }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn required_group_attributes(&self) -> AttributeList {
|
||||||
|
let attributes = self
|
||||||
|
.schema()
|
||||||
|
.group_attributes
|
||||||
|
.attributes
|
||||||
|
.iter()
|
||||||
|
.filter(|a| REQUIRED_GROUP_ATTRIBUTES.contains(&a.name.as_str()))
|
||||||
|
.cloned()
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
AttributeList { attributes }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn optional_group_attributes(&self) -> AttributeList {
|
||||||
|
let attributes = self
|
||||||
|
.schema()
|
||||||
|
.group_attributes
|
||||||
|
.attributes
|
||||||
|
.iter()
|
||||||
|
.filter(|a| !REQUIRED_GROUP_ATTRIBUTES.contains(&a.name.as_str()))
|
||||||
|
.cloned()
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
AttributeList { attributes }
|
||||||
|
}
|
||||||
|
|
||||||
|
// See RFC4512 section 4.2.2 "attributeTypes"
|
||||||
|
// Parameter 'index_offset' is an offset for the enumeration of this list of attributes,
|
||||||
|
// it has been preceeded by the list of hardcoded attributes.
|
||||||
|
pub fn formatted_attribute_list(&self, index_offset: usize) -> Vec<Vec<u8>> {
|
||||||
|
let mut formatted_list: Vec<Vec<u8>> = Vec::new();
|
||||||
|
|
||||||
|
for (index, attribute) in self.all_attributes().attributes.into_iter().enumerate() {
|
||||||
|
formatted_list.push(
|
||||||
|
format!(
|
||||||
|
"( 2.{} NAME '{}' DESC 'LLDAP: {}' SUP {:?} )",
|
||||||
|
(index + index_offset),
|
||||||
|
attribute.name,
|
||||||
|
if attribute.is_hardcoded {
|
||||||
|
"builtin attribute"
|
||||||
|
} else {
|
||||||
|
"custom attribute"
|
||||||
|
},
|
||||||
|
attribute.attribute_type
|
||||||
|
)
|
||||||
|
.into_bytes()
|
||||||
|
.to_vec(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
formatted_list
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn all_attributes(&self) -> AttributeList {
|
||||||
|
let mut combined_attributes = self.schema().user_attributes.attributes.clone();
|
||||||
|
combined_attributes.extend_from_slice(&self.schema().group_attributes.attributes);
|
||||||
|
AttributeList {
|
||||||
|
attributes: combined_attributes,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|||||||
@@ -7,8 +7,8 @@ use crate::{
|
|||||||
create, delete, modify,
|
create, delete, modify,
|
||||||
password::{self, do_password_modification},
|
password::{self, do_password_modification},
|
||||||
search::{
|
search::{
|
||||||
self, is_root_dse_request, make_search_error, make_search_request, make_search_success,
|
self, is_root_dse_request, is_subschema_entry_request, make_ldap_subschema_entry,
|
||||||
root_dse_response,
|
make_search_error, make_search_request, make_search_success, root_dse_response,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
use ldap3_proto::proto::{
|
use ldap3_proto::proto::{
|
||||||
@@ -18,8 +18,8 @@ use ldap3_proto::proto::{
|
|||||||
};
|
};
|
||||||
use lldap_access_control::AccessControlledBackendHandler;
|
use lldap_access_control::AccessControlledBackendHandler;
|
||||||
use lldap_auth::access_control::ValidationResults;
|
use lldap_auth::access_control::ValidationResults;
|
||||||
use lldap_domain::types::AttributeName;
|
use lldap_domain::{public_schema::PublicSchema, types::AttributeName};
|
||||||
use lldap_domain_handlers::handler::{BackendHandler, LoginHandler};
|
use lldap_domain_handlers::handler::{BackendHandler, LoginHandler, ReadSchemaBackendHandler};
|
||||||
use lldap_opaque_handler::OpaqueHandler;
|
use lldap_opaque_handler::OpaqueHandler;
|
||||||
use tracing::{debug, instrument};
|
use tracing::{debug, instrument};
|
||||||
|
|
||||||
@@ -141,6 +141,26 @@ impl<Backend: BackendHandler + LoginHandler + OpaqueHandler> LdapHandler<Backend
|
|||||||
root_dse_response(&self.ldap_info.base_dn_str),
|
root_dse_response(&self.ldap_info.base_dn_str),
|
||||||
make_search_success(),
|
make_search_success(),
|
||||||
]);
|
]);
|
||||||
|
} else if is_subschema_entry_request(request) {
|
||||||
|
// See RFC4512 section 4.4 "Subschema discovery"
|
||||||
|
debug!("Schema request");
|
||||||
|
let backend_handler = self
|
||||||
|
.user_info
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|u| self.backend_handler.get_schema_only_handler(u))
|
||||||
|
.ok_or_else(|| LdapError {
|
||||||
|
code: LdapResultCode::InsufficentAccessRights,
|
||||||
|
message: "No user currently bound".to_string(),
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let schema = backend_handler.get_schema().await.map_err(|e| LdapError {
|
||||||
|
code: LdapResultCode::OperationsError,
|
||||||
|
message: format!("Unable to get schema: {:#}", e),
|
||||||
|
})?;
|
||||||
|
return Ok(vec![
|
||||||
|
make_ldap_subschema_entry(PublicSchema::from(schema)),
|
||||||
|
make_search_success(),
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
self.do_search(request).await
|
self.do_search(request).await
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,3 +9,6 @@ pub(crate) mod search;
|
|||||||
|
|
||||||
pub use core::utils::{UserFieldType, map_group_field, map_user_field};
|
pub use core::utils::{UserFieldType, map_group_field, map_user_field};
|
||||||
pub use handler::LdapHandler;
|
pub use handler::LdapHandler;
|
||||||
|
|
||||||
|
pub use core::group::get_default_group_object_classes;
|
||||||
|
pub use core::user::get_default_user_object_classes;
|
||||||
|
|||||||
+241
-6
@@ -2,8 +2,9 @@ use crate::core::{
|
|||||||
error::{LdapError, LdapResult},
|
error::{LdapError, LdapResult},
|
||||||
group::{convert_groups_to_ldap_op, get_groups_list},
|
group::{convert_groups_to_ldap_op, get_groups_list},
|
||||||
user::{convert_users_to_ldap_op, get_user_list},
|
user::{convert_users_to_ldap_op, get_user_list},
|
||||||
utils::{LdapInfo, is_subtree, parse_distinguished_name},
|
utils::{LdapInfo, LdapSchemaDescription, is_subtree, parse_distinguished_name},
|
||||||
};
|
};
|
||||||
|
use chrono::Utc;
|
||||||
use ldap3_proto::{
|
use ldap3_proto::{
|
||||||
LdapFilter, LdapPartialAttribute, LdapResultCode, LdapSearchResultEntry, LdapSearchScope,
|
LdapFilter, LdapPartialAttribute, LdapResultCode, LdapSearchResultEntry, LdapSearchScope,
|
||||||
proto::{
|
proto::{
|
||||||
@@ -164,6 +165,86 @@ pub(crate) fn root_dse_response(base_dn: &str) -> LdapOp {
|
|||||||
atype: "isGlobalCatalogReady".to_string(),
|
atype: "isGlobalCatalogReady".to_string(),
|
||||||
vals: vec![b"false".to_vec()],
|
vals: vec![b"false".to_vec()],
|
||||||
},
|
},
|
||||||
|
LdapPartialAttribute {
|
||||||
|
atype: "subschemaSubentry".to_string(),
|
||||||
|
vals: vec![b"cn=Subschema".to_vec()],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn make_ldap_subschema_entry(schema: PublicSchema) -> LdapOp {
|
||||||
|
let ldap_schema_description: LdapSchemaDescription = LdapSchemaDescription::from(schema);
|
||||||
|
let current_time_utc = Utc::now().format("%Y%m%d%H%M%SZ").to_string().into_bytes();
|
||||||
|
LdapOp::SearchResultEntry(LdapSearchResultEntry {
|
||||||
|
dn: "cn=Subschema".to_string(),
|
||||||
|
attributes: vec![
|
||||||
|
LdapPartialAttribute {
|
||||||
|
atype: "structuralObjectClass".to_string(),
|
||||||
|
vals: vec![b"subentry".to_vec()],
|
||||||
|
},
|
||||||
|
LdapPartialAttribute {
|
||||||
|
atype: "objectClass".to_string(),
|
||||||
|
vals: vec![b"top".to_vec(), b"subentry".to_vec(), b"subschema".to_vec(), b"extensibleObject".to_vec()],
|
||||||
|
},
|
||||||
|
LdapPartialAttribute {
|
||||||
|
atype: "cn".to_string(),
|
||||||
|
vals: vec![b"Subschema".to_vec()],
|
||||||
|
},
|
||||||
|
LdapPartialAttribute {
|
||||||
|
atype: "createTimestamp".to_string(),
|
||||||
|
vals: vec![current_time_utc.to_vec()],
|
||||||
|
},
|
||||||
|
LdapPartialAttribute {
|
||||||
|
atype: "modifyTimestamp".to_string(),
|
||||||
|
vals: vec![current_time_utc.to_vec()],
|
||||||
|
},
|
||||||
|
LdapPartialAttribute {
|
||||||
|
atype: "ldapSyntaxes".to_string(),
|
||||||
|
vals: vec![
|
||||||
|
b"( 1.3.6.1.4.1.1466.115.121.1.15 DESC 'Directory String' )".to_vec(),
|
||||||
|
b"( 1.3.6.1.4.1.1466.115.121.1.24 DESC 'Generalized Time' )".to_vec(),
|
||||||
|
b"( 1.3.6.1.4.1.1466.115.121.1.27 DESC 'Integer' )".to_vec(),
|
||||||
|
b"( 1.3.6.1.4.1.1466.115.121.1.28 DESC 'JPEG' X-NOT-HUMAN-READABLE 'TRUE' )".to_vec(),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
LdapPartialAttribute {
|
||||||
|
atype: "attributeTypes".to_string(),
|
||||||
|
vals: {
|
||||||
|
let hardcoded_attributes = [
|
||||||
|
b"( 2.0 NAME 'String' SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )".to_vec(),
|
||||||
|
b"( 2.1 NAME 'Integer' SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 )".to_vec(),
|
||||||
|
b"( 2.2 NAME 'JpegPhoto' SYNTAX 1.3.6.1.4.1.1466.115.121.1.28 )".to_vec(),
|
||||||
|
b"( 2.3 NAME 'DateTime' SYNTAX 1.3.6.1.4.1.1466.115.121.1.24 )".to_vec(),
|
||||||
|
];
|
||||||
|
let num_hardcoded_attributes = hardcoded_attributes.len();
|
||||||
|
hardcoded_attributes.into_iter().chain(
|
||||||
|
ldap_schema_description
|
||||||
|
.formatted_attribute_list(num_hardcoded_attributes)
|
||||||
|
).collect()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
LdapPartialAttribute {
|
||||||
|
atype: "objectClasses".to_string(),
|
||||||
|
vals: vec![
|
||||||
|
format!(
|
||||||
|
"( 3.0 NAME ( {} ) DESC 'LLDAP builtin: a person' STRUCTURAL MUST ( {} ) MAY ( {} ) )",
|
||||||
|
ldap_schema_description.user_object_classes().format_for_ldap_schema_description(),
|
||||||
|
ldap_schema_description.required_user_attributes().format_for_ldap_schema_description(),
|
||||||
|
ldap_schema_description.optional_user_attributes().format_for_ldap_schema_description(),
|
||||||
|
).into_bytes(),
|
||||||
|
format!(
|
||||||
|
"( 3.1 NAME ( {} ) DESC 'LLDAP builtin: a group' STRUCTURAL MUST ( {} ) MAY ( {} ) )",
|
||||||
|
ldap_schema_description.group_object_classes().format_for_ldap_schema_description(),
|
||||||
|
ldap_schema_description.required_group_attributes().format_for_ldap_schema_description(),
|
||||||
|
ldap_schema_description.optional_group_attributes().format_for_ldap_schema_description(),
|
||||||
|
).into_bytes(),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
LdapPartialAttribute {
|
||||||
|
atype: "subschemaSubentry".to_string(),
|
||||||
|
vals: vec![b"cn=Subschema".to_vec()],
|
||||||
|
},
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -179,6 +260,10 @@ pub(crate) fn is_root_dse_request(request: &LdapSearchRequest) -> bool {
|
|||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn is_subschema_entry_request(request: &LdapSearchRequest) -> bool {
|
||||||
|
request.base == "cn=Subschema" && request.scope == LdapSearchScope::Base
|
||||||
|
}
|
||||||
|
|
||||||
async fn do_search_internal(
|
async fn do_search_internal(
|
||||||
ldap_info: &LdapInfo,
|
ldap_info: &LdapInfo,
|
||||||
backend_handler: &impl UserAndGroupListerBackendHandler,
|
backend_handler: &impl UserAndGroupListerBackendHandler,
|
||||||
@@ -318,7 +403,7 @@ mod tests {
|
|||||||
setup_bound_handler_with_group, setup_bound_readonly_handler,
|
setup_bound_handler_with_group, setup_bound_readonly_handler,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
use chrono::TimeZone;
|
use chrono::{DateTime, Duration, NaiveDateTime, TimeZone};
|
||||||
use ldap3_proto::proto::{LdapDerefAliases, LdapSearchScope, LdapSubstringFilter};
|
use ldap3_proto::proto::{LdapDerefAliases, LdapSearchScope, LdapSubstringFilter};
|
||||||
use lldap_domain::{
|
use lldap_domain::{
|
||||||
schema::{AttributeList, AttributeSchema, Schema},
|
schema::{AttributeList, AttributeSchema, Schema},
|
||||||
@@ -356,6 +441,156 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn assert_timestamp_within_margin(
|
||||||
|
timestamp_bytes: &[u8],
|
||||||
|
base_timestamp_dt: DateTime<Utc>,
|
||||||
|
time_margin: Duration,
|
||||||
|
) {
|
||||||
|
let timestamp_str =
|
||||||
|
std::str::from_utf8(timestamp_bytes).expect("Invalid conversion from UTF-8 to string");
|
||||||
|
let timestamp_naive = NaiveDateTime::parse_from_str(timestamp_str, "%Y%m%d%H%M%SZ")
|
||||||
|
.expect("Invalid timestamp format");
|
||||||
|
let timestamp_dt: DateTime<Utc> = Utc.from_utc_datetime(×tamp_naive);
|
||||||
|
|
||||||
|
let within_range = (base_timestamp_dt - timestamp_dt).abs() <= time_margin;
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
within_range,
|
||||||
|
"Timestamp not within range: expected within [{} - {}], got [{}]",
|
||||||
|
base_timestamp_dt - time_margin,
|
||||||
|
base_timestamp_dt + time_margin,
|
||||||
|
timestamp_dt
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_subschema_response() {
|
||||||
|
let ldap_handler = setup_bound_admin_handler(MockTestBackendHandler::new()).await;
|
||||||
|
|
||||||
|
let request = LdapSearchRequest {
|
||||||
|
base: "cn=Subschema".to_string(),
|
||||||
|
scope: LdapSearchScope::Base,
|
||||||
|
aliases: LdapDerefAliases::Never,
|
||||||
|
sizelimit: 0,
|
||||||
|
timelimit: 0,
|
||||||
|
typesonly: false,
|
||||||
|
filter: LdapFilter::Present("objectClass".to_string()),
|
||||||
|
attrs: vec!["supportedExtension".to_string()],
|
||||||
|
};
|
||||||
|
|
||||||
|
let actual_reponse: Vec<LdapOp> = ldap_handler.do_search_or_dse(&request).await.unwrap();
|
||||||
|
|
||||||
|
let LdapOp::SearchResultEntry(search_result_entry) = &actual_reponse[0] else {
|
||||||
|
panic!("Expected SearchResultEntry");
|
||||||
|
};
|
||||||
|
|
||||||
|
let attrs = &search_result_entry.attributes;
|
||||||
|
assert_eq!(attrs.len(), 9);
|
||||||
|
assert_eq!(search_result_entry.dn, "cn=Subschema".to_owned());
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
attrs[0],
|
||||||
|
LdapPartialAttribute {
|
||||||
|
atype: "structuralObjectClass".to_owned(),
|
||||||
|
vals: vec![b"subentry".to_vec()]
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
attrs[1],
|
||||||
|
LdapPartialAttribute {
|
||||||
|
atype: "objectClass".to_owned(),
|
||||||
|
vals: vec![
|
||||||
|
b"top".to_vec(),
|
||||||
|
b"subentry".to_vec(),
|
||||||
|
b"subschema".to_vec(),
|
||||||
|
b"extensibleObject".to_vec()
|
||||||
|
]
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
attrs[2],
|
||||||
|
LdapPartialAttribute {
|
||||||
|
atype: "cn".to_owned(),
|
||||||
|
vals: vec![b"Subschema".to_vec()]
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
let check_timestamp_attribute = |attr: &LdapPartialAttribute, expected_type: &str| {
|
||||||
|
assert_eq!(attr.atype, expected_type);
|
||||||
|
assert_eq!(attr.vals.len(), 1);
|
||||||
|
assert_timestamp_within_margin(&attr.vals[0], Utc::now(), Duration::seconds(300));
|
||||||
|
};
|
||||||
|
check_timestamp_attribute(&attrs[3], "createTimestamp");
|
||||||
|
check_timestamp_attribute(&attrs[4], "modifyTimestamp");
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
attrs[5],
|
||||||
|
LdapPartialAttribute {
|
||||||
|
atype: "ldapSyntaxes".to_owned(),
|
||||||
|
vals: vec![
|
||||||
|
b"( 1.3.6.1.4.1.1466.115.121.1.15 DESC 'Directory String' )".to_vec(),
|
||||||
|
b"( 1.3.6.1.4.1.1466.115.121.1.24 DESC 'Generalized Time' )".to_vec(),
|
||||||
|
b"( 1.3.6.1.4.1.1466.115.121.1.27 DESC 'Integer' )".to_vec(),
|
||||||
|
b"( 1.3.6.1.4.1.1466.115.121.1.28 DESC 'JPEG' X-NOT-HUMAN-READABLE 'TRUE' )"
|
||||||
|
.to_vec()
|
||||||
|
]
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
attrs[6],
|
||||||
|
LdapPartialAttribute {
|
||||||
|
atype: "attributeTypes".to_owned(),
|
||||||
|
vals: vec![
|
||||||
|
b"( 2.0 NAME 'String' SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )".to_vec(),
|
||||||
|
b"( 2.1 NAME 'Integer' SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 )".to_vec(),
|
||||||
|
b"( 2.2 NAME 'JpegPhoto' SYNTAX 1.3.6.1.4.1.1466.115.121.1.28 )".to_vec(),
|
||||||
|
b"( 2.3 NAME 'DateTime' SYNTAX 1.3.6.1.4.1.1466.115.121.1.24 )".to_vec(),
|
||||||
|
b"( 2.4 NAME 'avatar' DESC 'LLDAP: builtin attribute' SUP JpegPhoto )".to_vec(),
|
||||||
|
b"( 2.5 NAME 'creation_date' DESC 'LLDAP: builtin attribute' SUP DateTime )"
|
||||||
|
.to_vec(),
|
||||||
|
b"( 2.6 NAME 'display_name' DESC 'LLDAP: builtin attribute' SUP String )"
|
||||||
|
.to_vec(),
|
||||||
|
b"( 2.7 NAME 'first_name' DESC 'LLDAP: builtin attribute' SUP String )"
|
||||||
|
.to_vec(),
|
||||||
|
b"( 2.8 NAME 'last_name' DESC 'LLDAP: builtin attribute' SUP String )".to_vec(),
|
||||||
|
b"( 2.9 NAME 'mail' DESC 'LLDAP: builtin attribute' SUP String )".to_vec(),
|
||||||
|
b"( 2.10 NAME 'user_id' DESC 'LLDAP: builtin attribute' SUP String )".to_vec(),
|
||||||
|
b"( 2.11 NAME 'uuid' DESC 'LLDAP: builtin attribute' SUP String )".to_vec(),
|
||||||
|
b"( 2.12 NAME 'creation_date' DESC 'LLDAP: builtin attribute' SUP DateTime )"
|
||||||
|
.to_vec(),
|
||||||
|
b"( 2.13 NAME 'display_name' DESC 'LLDAP: builtin attribute' SUP String )"
|
||||||
|
.to_vec(),
|
||||||
|
b"( 2.14 NAME 'group_id' DESC 'LLDAP: builtin attribute' SUP Integer )"
|
||||||
|
.to_vec(),
|
||||||
|
b"( 2.15 NAME 'uuid' DESC 'LLDAP: builtin attribute' SUP String )".to_vec()
|
||||||
|
]
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(attrs[7],
|
||||||
|
LdapPartialAttribute {
|
||||||
|
atype: "objectClasses".to_owned(),
|
||||||
|
vals: vec![
|
||||||
|
b"( 3.0 NAME ( 'inetOrgPerson' 'posixAccount' 'mailAccount' 'person' 'customUserClass' ) DESC 'LLDAP builtin: a person' STRUCTURAL MUST ( mail $ user_id ) MAY ( avatar $ creation_date $ display_name $ first_name $ last_name $ uuid ) )".to_vec(),
|
||||||
|
b"( 3.1 NAME ( 'groupOfUniqueNames' 'groupOfNames' ) DESC 'LLDAP builtin: a group' STRUCTURAL MUST ( display_name ) MAY ( creation_date $ group_id $ uuid ) )".to_vec(),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
attrs[8],
|
||||||
|
LdapPartialAttribute {
|
||||||
|
atype: "subschemaSubentry".to_owned(),
|
||||||
|
vals: vec![b"cn=Subschema".to_vec()]
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(actual_reponse[1], make_search_success());
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_search_regular_user() {
|
async fn test_search_regular_user() {
|
||||||
let mut mock = MockTestBackendHandler::new();
|
let mut mock = MockTestBackendHandler::new();
|
||||||
@@ -710,7 +945,7 @@ mod tests {
|
|||||||
},
|
},
|
||||||
LdapPartialAttribute {
|
LdapPartialAttribute {
|
||||||
atype: "objectClass".to_string(),
|
atype: "objectClass".to_string(),
|
||||||
vals: vec![b"groupOfUniqueNames".to_vec(),]
|
vals: vec![b"groupOfUniqueNames".to_vec(), b"groupOfNames".to_vec(),],
|
||||||
},
|
},
|
||||||
LdapPartialAttribute {
|
LdapPartialAttribute {
|
||||||
atype: "uniqueMember".to_string(),
|
atype: "uniqueMember".to_string(),
|
||||||
@@ -738,7 +973,7 @@ mod tests {
|
|||||||
},
|
},
|
||||||
LdapPartialAttribute {
|
LdapPartialAttribute {
|
||||||
atype: "objectClass".to_string(),
|
atype: "objectClass".to_string(),
|
||||||
vals: vec![b"groupOfUniqueNames".to_vec(),]
|
vals: vec![b"groupOfUniqueNames".to_vec(), b"groupOfNames".to_vec(),],
|
||||||
},
|
},
|
||||||
LdapPartialAttribute {
|
LdapPartialAttribute {
|
||||||
atype: "uniqueMember".to_string(),
|
atype: "uniqueMember".to_string(),
|
||||||
@@ -1433,7 +1668,7 @@ mod tests {
|
|||||||
},
|
},
|
||||||
LdapPartialAttribute {
|
LdapPartialAttribute {
|
||||||
atype: "objectClass".to_string(),
|
atype: "objectClass".to_string(),
|
||||||
vals: vec![b"groupOfUniqueNames".to_vec(),]
|
vals: vec![b"groupOfUniqueNames".to_vec(), b"groupOfNames".to_vec(),],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
}),
|
}),
|
||||||
@@ -1569,7 +1804,7 @@ mod tests {
|
|||||||
},
|
},
|
||||||
LdapPartialAttribute {
|
LdapPartialAttribute {
|
||||||
atype: "objectclass".to_string(),
|
atype: "objectclass".to_string(),
|
||||||
vals: vec![b"groupOfUniqueNames".to_vec()],
|
vals: vec![b"groupOfUniqueNames".to_vec(), b"groupOfNames".to_vec()],
|
||||||
},
|
},
|
||||||
// UID
|
// UID
|
||||||
LdapPartialAttribute {
|
LdapPartialAttribute {
|
||||||
|
|||||||
Generated
+6
@@ -80,6 +80,11 @@ input CreateUserInput {
|
|||||||
"Attributes." attributes: [AttributeValueInput!]
|
"Attributes." attributes: [AttributeValueInput!]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ObjectClassInfo {
|
||||||
|
objectClass: String!
|
||||||
|
isHardcoded: Boolean!
|
||||||
|
}
|
||||||
|
|
||||||
type AttributeSchema {
|
type AttributeSchema {
|
||||||
name: String!
|
name: String!
|
||||||
attributeType: AttributeType!
|
attributeType: AttributeType!
|
||||||
@@ -186,6 +191,7 @@ enum AttributeType {
|
|||||||
type AttributeList {
|
type AttributeList {
|
||||||
attributes: [AttributeSchema!]!
|
attributes: [AttributeSchema!]!
|
||||||
extraLdapObjectClasses: [String!]!
|
extraLdapObjectClasses: [String!]!
|
||||||
|
ldapObjectClasses: [ObjectClassInfo!]!
|
||||||
}
|
}
|
||||||
|
|
||||||
type Success {
|
type Success {
|
||||||
|
|||||||
Reference in New Issue
Block a user