diff --git a/Cargo.lock b/Cargo.lock index 1895198..79c895f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2659,6 +2659,7 @@ dependencies = [ "chrono", "derive_more 1.0.0", "image", + "itertools", "juniper", "lldap_auth", "pretty_assertions", @@ -2745,6 +2746,8 @@ version = "0.1.0" dependencies = [ "anyhow", "chrono", + "derive_more 1.0.0", + "itertools", "ldap3_proto", "lldap_access_control", "lldap_auth", diff --git a/crates/access-control/src/lib.rs b/crates/access-control/src/lib.rs index e420938..51d13cd 100644 --- a/crates/access-control/src/lib.rs +++ b/crates/access-control/src/lib.rs @@ -178,6 +178,13 @@ impl AccessControlledBackendHandler { Self { handler } } + pub fn get_schema_only_handler( + &self, + _validation_result: &ValidationResults, + ) -> Option<&impl ReadSchemaBackendHandler> { + Some(&self.handler) + } + pub fn get_admin_handler( &self, validation_result: &ValidationResults, diff --git a/crates/domain/Cargo.toml b/crates/domain/Cargo.toml index 96a81ce..8000899 100644 --- a/crates/domain/Cargo.toml +++ b/crates/domain/Cargo.toml @@ -17,6 +17,7 @@ test = [] anyhow = "*" base64 = "0.21" bincode = "1.3" +itertools = "0.10" juniper = "0.15" serde_bytes = "0.11" diff --git a/crates/domain/src/schema.rs b/crates/domain/src/schema.rs index 62c2110..15e7d06 100644 --- a/crates/domain/src/schema.rs +++ b/crates/domain/src/schema.rs @@ -36,4 +36,12 @@ impl AttributeList { self.get_attribute_schema(name) .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::>() + .join(" $ ") + } } diff --git a/crates/graphql-server/src/query.rs b/crates/graphql-server/src/query.rs index 55002ed..e16c301 100644 --- a/crates/graphql-server/src/query.rs +++ b/crates/graphql-server/src/query.rs @@ -10,7 +10,10 @@ use lldap_domain::{ }; use lldap_domain_handlers::handler::{BackendHandler, ReadSchemaBackendHandler}; 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 std::sync::Arc; use tracing::{Instrument, Span, debug, debug_span}; @@ -522,10 +525,28 @@ impl From for AttributeSchema { attributes: DomainAttributeList, + default_classes: Vec, extra_classes: Vec, _phantom: std::marker::PhantomData>, } +#[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)] impl AttributeList { fn attributes(&self) -> Vec> { @@ -540,12 +561,35 @@ impl AttributeList { fn extra_ldap_object_classes(&self) -> Vec { self.extra_classes.iter().map(|c| c.to_string()).collect() } + + fn ldap_object_classes(&self) -> Vec { + let mut all_object_classes: Vec = 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 AttributeList { - fn new(attributes: DomainAttributeList, extra_classes: Vec) -> Self { + fn new( + attributes: DomainAttributeList, + default_classes: Vec, + extra_classes: Vec, + ) -> Self { Self { attributes, + default_classes, extra_classes, _phantom: std::marker::PhantomData, } @@ -563,12 +607,14 @@ impl Schema { fn user_schema(&self) -> AttributeList { AttributeList::::new( self.schema.get_schema().user_attributes.clone(), + get_default_user_object_classes(), self.schema.get_schema().extra_user_object_classes.clone(), ) } fn group_schema(&self) -> AttributeList { AttributeList::::new( self.schema.get_schema().group_attributes.clone(), + get_default_group_object_classes(), self.schema.get_schema().extra_group_object_classes.clone(), ) } diff --git a/crates/ldap/Cargo.toml b/crates/ldap/Cargo.toml index d309837..2c5d94e 100644 --- a/crates/ldap/Cargo.toml +++ b/crates/ldap/Cargo.toml @@ -12,6 +12,12 @@ repository.workspace = true anyhow = "*" ldap3_proto = "0.6.0" tracing = "*" +itertools = "0.10" + +[dependencies.derive_more] +features = ["from"] +default-features = false +version = "1" [dependencies.chrono] features = ["serde"] @@ -54,3 +60,7 @@ pretty_assertions = "1" [dev-dependencies.tokio] features = ["full"] version = "1.25" + +[dev-dependencies.lldap_domain] +path = "../domain" +features = ["test"] \ No newline at end of file diff --git a/crates/ldap/src/core/group.rs b/crates/ldap/src/core/group.rs index b51af7c..089a807 100644 --- a/crates/ldap/src/core/group.rs +++ b/crates/ldap/src/core/group.rs @@ -18,6 +18,24 @@ use lldap_domain::{ use lldap_domain_handlers::handler::{GroupListerBackendHandler, GroupRequestFilter}; 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> { + DEFAULT_GROUP_OBJECT_CLASSES + .iter() + .map(|c| c.as_bytes().to_vec()) + .collect() +} + +pub fn get_default_group_object_classes() -> Vec { + DEFAULT_GROUP_OBJECT_CLASSES + .iter() + .map(|&c| LdapObjectClass::from(c)) + .collect() +} + pub fn get_group_attribute( group: &Group, base_dn_str: &str, @@ -28,7 +46,8 @@ pub fn get_group_attribute( ) -> Option>> { let attribute_values = match map_group_field(attribute, schema) { GroupFieldType::ObjectClass => { - let mut classes = vec![b"groupOfUniqueNames".to_vec()]; + let mut classes: Vec> = get_default_group_object_classes_as_bytes(); + classes.extend( schema .get_schema() @@ -205,7 +224,9 @@ fn convert_group_filter( GroupRequestFilter::from(false) })), 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 .get_schema() .extra_group_object_classes diff --git a/crates/ldap/src/core/user.rs b/crates/ldap/src/core/user.rs index a4f5f3b..e93fc05 100644 --- a/crates/ldap/src/core/user.rs +++ b/crates/ldap/src/core/user.rs @@ -21,6 +21,25 @@ use lldap_domain_handlers::handler::{UserListerBackendHandler, UserRequestFilter use lldap_domain_model::model::UserColumn; 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> { + DEFAULT_USER_OBJECT_CLASSES + .iter() + .map(|c| c.as_bytes().to_vec()) + .collect() +} + +pub fn get_default_user_object_classes() -> Vec { + DEFAULT_USER_OBJECT_CLASSES + .iter() + .map(|&c| LdapObjectClass::from(c)) + .collect() +} + pub fn get_user_attribute( user: &User, attribute: &AttributeName, @@ -31,12 +50,8 @@ pub fn get_user_attribute( ) -> Option>> { let attribute_values = match map_user_field(attribute, schema) { UserFieldType::ObjectClass => { - let mut classes = vec![ - b"inetOrgPerson".to_vec(), - b"posixAccount".to_vec(), - b"mailAccount".to_vec(), - b"person".to_vec(), - ]; + let mut classes: Vec> = get_default_user_object_classes_vec_u8(); + classes.extend( schema .get_schema() @@ -227,13 +242,13 @@ fn convert_user_filter( Ok(UserRequestFilter::from(false)) } UserFieldType::ObjectClass => Ok(UserRequestFilter::from( - matches!( - value_lc.as_str(), - "person" | "inetorgperson" | "posixaccount" | "mailaccount" - ) || schema - .get_schema() - .extra_user_object_classes - .contains(&LdapObjectClass::from(value_lc)), + get_default_user_object_classes() + .iter() + .any(|class| class.as_str().eq_ignore_ascii_case(value_lc.as_str())) + || schema + .get_schema() + .extra_user_object_classes + .contains(&LdapObjectClass::from(value_lc)), )), UserFieldType::MemberOf => Ok(get_group_id_from_distinguished_name_or_plain_name( &value_lc, diff --git a/crates/ldap/src/core/utils.rs b/crates/ldap/src/core/utils.rs index e6cd212..8744ee1 100644 --- a/crates/ldap/src/core/utils.rs +++ b/crates/ldap/src/core/utils.rs @@ -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 itertools::join; use ldap3_proto::LdapResultCode; use lldap_domain::{ public_schema::PublicSchema, + schema::{AttributeList, Schema}, types::{ - Attribute, AttributeName, AttributeType, AttributeValue, Cardinality, GroupName, UserId, + Attribute, AttributeName, AttributeType, AttributeValue, Cardinality, GroupName, + LdapObjectClass, UserId, }, }; use lldap_domain_model::model::UserColumn; @@ -330,6 +337,139 @@ pub fn get_custom_attribute( }) } +#[derive(derive_more::From)] +pub struct ObjectClassList(Vec); + +// 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> { + let mut formatted_list: Vec> = 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)] mod tests { use super::*; diff --git a/crates/ldap/src/handler.rs b/crates/ldap/src/handler.rs index bb425d6..bde29ef 100644 --- a/crates/ldap/src/handler.rs +++ b/crates/ldap/src/handler.rs @@ -7,8 +7,8 @@ use crate::{ create, delete, modify, password::{self, do_password_modification}, search::{ - self, is_root_dse_request, make_search_error, make_search_request, make_search_success, - root_dse_response, + self, is_root_dse_request, is_subschema_entry_request, make_ldap_subschema_entry, + make_search_error, make_search_request, make_search_success, root_dse_response, }, }; use ldap3_proto::proto::{ @@ -18,8 +18,8 @@ use ldap3_proto::proto::{ }; use lldap_access_control::AccessControlledBackendHandler; use lldap_auth::access_control::ValidationResults; -use lldap_domain::types::AttributeName; -use lldap_domain_handlers::handler::{BackendHandler, LoginHandler}; +use lldap_domain::{public_schema::PublicSchema, types::AttributeName}; +use lldap_domain_handlers::handler::{BackendHandler, LoginHandler, ReadSchemaBackendHandler}; use lldap_opaque_handler::OpaqueHandler; use tracing::{debug, instrument}; @@ -141,6 +141,26 @@ impl LdapHandler LdapOp { atype: "isGlobalCatalogReady".to_string(), 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 } +pub(crate) fn is_subschema_entry_request(request: &LdapSearchRequest) -> bool { + request.base == "cn=Subschema" && request.scope == LdapSearchScope::Base +} + async fn do_search_internal( ldap_info: &LdapInfo, backend_handler: &impl UserAndGroupListerBackendHandler, @@ -318,7 +403,7 @@ mod tests { 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 lldap_domain::{ schema::{AttributeList, AttributeSchema, Schema}, @@ -356,6 +441,156 @@ mod tests { ); } + fn assert_timestamp_within_margin( + timestamp_bytes: &[u8], + base_timestamp_dt: DateTime, + 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.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 = 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] async fn test_search_regular_user() { let mut mock = MockTestBackendHandler::new(); @@ -710,7 +945,7 @@ mod tests { }, LdapPartialAttribute { atype: "objectClass".to_string(), - vals: vec![b"groupOfUniqueNames".to_vec(),] + vals: vec![b"groupOfUniqueNames".to_vec(), b"groupOfNames".to_vec(),], }, LdapPartialAttribute { atype: "uniqueMember".to_string(), @@ -738,7 +973,7 @@ mod tests { }, LdapPartialAttribute { atype: "objectClass".to_string(), - vals: vec![b"groupOfUniqueNames".to_vec(),] + vals: vec![b"groupOfUniqueNames".to_vec(), b"groupOfNames".to_vec(),], }, LdapPartialAttribute { atype: "uniqueMember".to_string(), @@ -1433,7 +1668,7 @@ mod tests { }, LdapPartialAttribute { 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 { atype: "objectclass".to_string(), - vals: vec![b"groupOfUniqueNames".to_vec()], + vals: vec![b"groupOfUniqueNames".to_vec(), b"groupOfNames".to_vec()], }, // UID LdapPartialAttribute { diff --git a/schema.graphql b/schema.graphql index caf6a9c..c9bfaba 100644 --- a/schema.graphql +++ b/schema.graphql @@ -80,6 +80,11 @@ input CreateUserInput { "Attributes." attributes: [AttributeValueInput!] } +type ObjectClassInfo { + objectClass: String! + isHardcoded: Boolean! +} + type AttributeSchema { name: String! attributeType: AttributeType! @@ -186,6 +191,7 @@ enum AttributeType { type AttributeList { attributes: [AttributeSchema!]! extraLdapObjectClasses: [String!]! + ldapObjectClasses: [ObjectClassInfo!]! } type Success {