From 7a829646c4b3b784bdaaa650699e7c16e529a1db Mon Sep 17 00:00:00 2001 From: Sebastiano Tocci <sebastiano.tocci@proton.me> Date: Sat, 15 Feb 2025 00:37:56 +0100 Subject: [PATCH 1/2] Fixes #3406: add configurable maximum queryable attributes for LDAP --- proto/src/constants.rs | 2 + server/core/src/config.rs | 15 +++- server/core/src/lib.rs | 2 +- server/lib/src/idm/ldap.rs | 163 +++++++++++++++++++++++++++++++++---- 4 files changed, 162 insertions(+), 20 deletions(-) diff --git a/proto/src/constants.rs b/proto/src/constants.rs index 9575a0a41..cbfbeb2ff 100644 --- a/proto/src/constants.rs +++ b/proto/src/constants.rs @@ -39,6 +39,8 @@ pub const DEFAULT_SERVER_ADDRESS: &str = "127.0.0.1:8443"; pub const DEFAULT_SERVER_LOCALHOST: &str = "localhost:8443"; /// The default LDAP bind address for the Kanidm client pub const DEFAULT_LDAP_LOCALHOST: &str = "localhost:636"; +/// The default amount of attributes that can be queried in LDAP +pub const DEFAULT_LDAP_MAXIMUM_QUERYABLE_ATTRIBUTES: usize = 100; /// Default replication configuration pub const DEFAULT_REPLICATION_ADDRESS: &str = "127.0.0.1:8444"; pub const DEFAULT_REPLICATION_ORIGIN: &str = "repl://localhost:8444"; diff --git a/server/core/src/config.rs b/server/core/src/config.rs index 78a9cdf72..981ef3988 100644 --- a/server/core/src/config.rs +++ b/server/core/src/config.rs @@ -14,6 +14,7 @@ use kanidm_proto::constants::DEFAULT_SERVER_ADDRESS; use kanidm_proto::internal::FsType; use kanidm_proto::messages::ConsoleOutputMode; +use kanidmd_lib::prelude::DEFAULT_LDAP_MAXIMUM_QUERYABLE_ATTRIBUTES; use serde::Deserialize; use sketching::LogLevel; use url::Url; @@ -116,7 +117,9 @@ pub struct ServerConfig { /// /// If unset, the LDAP server will be disabled. pub ldapbindaddress: Option<String>, - + /// The maximum number of LDAP attributes that can be queried in one operation. + /// Defaults to [kanidm_proto::constants::DEFAULT_LDAP_MAXIMUM_QUERYABLE_ATTRIBUTES] + pub ldap_maximum_queryable_attributes: Option<usize>, /// The role of this server, one of write_replica, write_replica_no_ui, read_only_replica, defaults to [ServerRole::WriteReplica] #[serde(default)] pub role: ServerRole, @@ -477,6 +480,7 @@ pub struct IntegrationReplConfig { pub struct Configuration { pub address: String, pub ldapaddress: Option<String>, + pub ldap_maximum_queryable_attrs: usize, pub adminbindpath: String, pub threads: usize, // db type later @@ -571,6 +575,7 @@ impl Configuration { Configuration { address: DEFAULT_SERVER_ADDRESS.to_string(), ldapaddress: None, + ldap_maximum_queryable_attrs: DEFAULT_LDAP_MAXIMUM_QUERYABLE_ATTRIBUTES, adminbindpath: env!("KANIDM_SERVER_ADMIN_BIND_PATH").to_string(), threads: std::thread::available_parallelism() .map(|t| t.get()) @@ -648,6 +653,9 @@ impl Configuration { self.update_ldapbind(&sconfig.ldapbindaddress); self.update_online_backup(&sconfig.online_backup); self.update_log_level(&sconfig.log_level); + if let Some(ldap_maximum_queryable_attrs) = sconfig.ldap_maximum_queryable_attributes { + self.update_ldap_maximum_queryable_attrs(ldap_maximum_queryable_attrs); + } } pub fn update_trust_x_forward_for(&mut self, t: Option<bool>) { @@ -734,4 +742,9 @@ impl Configuration { pub fn update_threads_count(&mut self, threads: usize) { self.threads = std::cmp::min(self.threads, threads); } + + // Updates the maximum number of LDAP attributes that can be queried in a single operation + pub fn update_ldap_maximum_queryable_attrs(&mut self, maximum_queryable_attrs: usize) { + self.ldap_maximum_queryable_attrs = maximum_queryable_attrs; + } } diff --git a/server/core/src/lib.rs b/server/core/src/lib.rs index 4826ec83f..4c466468d 100644 --- a/server/core/src/lib.rs +++ b/server/core/src/lib.rs @@ -937,7 +937,7 @@ pub async fn create_server_core( } } - let ldap = match LdapServer::new(&idms).await { + let ldap = match LdapServer::new(&idms, config.ldap_maximum_queryable_attrs).await { Ok(l) => l, Err(e) => { error!("Unable to start LdapServer -> {:?}", e); diff --git a/server/lib/src/idm/ldap.rs b/server/lib/src/idm/ldap.rs index 63956762f..cc85463f6 100644 --- a/server/lib/src/idm/ldap.rs +++ b/server/lib/src/idm/ldap.rs @@ -60,6 +60,7 @@ pub struct LdapServer { basedn: String, dnre: Regex, binddnre: Regex, + maximum_queryable_attrs: usize, } #[derive(Debug)] @@ -70,7 +71,10 @@ enum LdapBindTarget { } impl LdapServer { - pub async fn new(idms: &IdmServer) -> Result<Self, OperationError> { + pub async fn new( + idms: &IdmServer, + maximum_queryable_attrs: usize, + ) -> Result<Self, OperationError> { // let ct = duration_from_epoch_now(); let mut idms_prox_read = idms.proxy_read().await?; // This is the rootdse path. @@ -154,6 +158,7 @@ impl LdapServer { basedn, dnre, binddnre, + maximum_queryable_attrs, }) } @@ -239,11 +244,10 @@ impl LdapServer { let mut all_attrs = false; let mut all_op_attrs = false; - // TODO #3406: limit the number of attributes here! if sr.attrs.is_empty() { // If [], then "all" attrs all_attrs = true; - } else { + } else if sr.attrs.len() < self.maximum_queryable_attrs { sr.attrs.iter().for_each(|a| { if a == "*" { all_attrs = true; @@ -267,6 +271,9 @@ impl LdapServer { } } }) + } else { + return Err(OperationError::ResourceLimit); + // TODO: Should we return ResourceLimit or InvalidRequestState here? } // We need to retain this to know what the client requested. @@ -859,7 +866,9 @@ mod tests { #[idm_test] async fn test_ldap_simple_bind(idms: &IdmServer, _idms_delayed: &IdmServerDelayed) { - let ldaps = LdapServer::new(idms).await.expect("failed to start ldap"); + let ldaps = LdapServer::new(idms, 1000) + .await + .expect("failed to start ldap"); let mut idms_prox_write = idms.proxy_write(duration_from_epoch_now()).await.unwrap(); // make the admin a valid posix account @@ -1054,7 +1063,9 @@ mod tests { #[idm_test] async fn test_ldap_application_dnre(idms: &IdmServer, _idms_delayed: &IdmServerDelayed) { - let ldaps = LdapServer::new(idms).await.expect("failed to start ldap"); + let ldaps = LdapServer::new(idms, 1000) + .await + .expect("failed to start ldap"); let testdn = format!("app=app1,{0}", ldaps.basedn); let captures = ldaps.dnre.captures(testdn.as_str()).unwrap(); @@ -1077,7 +1088,9 @@ mod tests { #[idm_test] async fn test_ldap_application_search(idms: &IdmServer, _idms_delayed: &IdmServerDelayed) { - let ldaps = LdapServer::new(idms).await.expect("failed to start ldap"); + let ldaps = LdapServer::new(idms, 1000) + .await + .expect("failed to start ldap"); let usr_uuid = Uuid::new_v4(); let grp_uuid = Uuid::new_v4(); @@ -1160,7 +1173,9 @@ mod tests { #[idm_test] async fn test_ldap_spn_search(idms: &IdmServer, _idms_delayed: &IdmServerDelayed) { - let ldaps = LdapServer::new(idms).await.expect("failed to start ldap"); + let ldaps = LdapServer::new(idms, 1000) + .await + .expect("failed to start ldap"); let usr_uuid = Uuid::new_v4(); let usr_name = "panko"; @@ -1242,7 +1257,9 @@ mod tests { #[idm_test] async fn test_ldap_application_bind(idms: &IdmServer, _idms_delayed: &IdmServerDelayed) { - let ldaps = LdapServer::new(idms).await.expect("failed to start ldap"); + let ldaps = LdapServer::new(idms, 1000) + .await + .expect("failed to start ldap"); let usr_uuid = Uuid::new_v4(); let grp_uuid = Uuid::new_v4(); @@ -1398,7 +1415,9 @@ mod tests { idms: &IdmServer, _idms_delayed: &IdmServerDelayed, ) { - let ldaps = LdapServer::new(idms).await.expect("failed to start ldap"); + let ldaps = LdapServer::new(idms, 1000) + .await + .expect("failed to start ldap"); let usr_uuid = Uuid::new_v4(); let usr_name = "testuser1"; @@ -1602,7 +1621,9 @@ mod tests { idms: &IdmServer, _idms_delayed: &IdmServerDelayed, ) { - let ldaps = LdapServer::new(idms).await.expect("failed to start ldap"); + let ldaps = LdapServer::new(idms, 1000) + .await + .expect("failed to start ldap"); let usr_uuid = Uuid::new_v4(); let usr_name = "testuser1"; @@ -1739,7 +1760,9 @@ mod tests { idms: &IdmServer, _idms_delayed: &IdmServerDelayed, ) { - let ldaps = LdapServer::new(idms).await.expect("failed to start ldap"); + let ldaps = LdapServer::new(idms, 1000) + .await + .expect("failed to start ldap"); let ssh_ed25519 = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAeGW1P6Pc2rPq0XqbRaDKBcXZUPRklo0L1EyR30CwoP william@amethyst"; @@ -1902,7 +1925,9 @@ mod tests { _idms_delayed: &IdmServerDelayed, ) { // Setup the ldap server - let ldaps = LdapServer::new(idms).await.expect("failed to start ldap"); + let ldaps = LdapServer::new(idms, 1000) + .await + .expect("failed to start ldap"); // Prebuild the search req we'll be using this test. let sr = SearchRequest { @@ -2142,7 +2167,9 @@ mod tests { idms: &IdmServer, _idms_delayed: &IdmServerDelayed, ) { - let ldaps = LdapServer::new(idms).await.expect("failed to start ldap"); + let ldaps = LdapServer::new(idms, 1000) + .await + .expect("failed to start ldap"); let acct_uuid = uuid!("cc8e95b4-c24f-4d68-ba54-8bed76f63930"); @@ -2211,7 +2238,9 @@ mod tests { // Test behaviour of the 1.1 attribute. #[idm_test] async fn test_ldap_one_dot_one_attribute(idms: &IdmServer, _idms_delayed: &IdmServerDelayed) { - let ldaps = LdapServer::new(idms).await.expect("failed to start ldap"); + let ldaps = LdapServer::new(idms, 1000) + .await + .expect("failed to start ldap"); let acct_uuid = uuid!("cc8e95b4-c24f-4d68-ba54-8bed76f63930"); @@ -2300,7 +2329,9 @@ mod tests { #[idm_test] async fn test_ldap_rootdse_basedn_change(idms: &IdmServer, _idms_delayed: &IdmServerDelayed) { - let ldaps = LdapServer::new(idms).await.expect("failed to start ldap"); + let ldaps = LdapServer::new(idms, 1000) + .await + .expect("failed to start ldap"); let anon_t = ldaps.do_bind(idms, "", "").await.unwrap().unwrap(); assert_eq!( @@ -2356,7 +2387,9 @@ mod tests { assert!(idms_prox_write.commit().is_ok()); // Now re-test - let ldaps = LdapServer::new(idms).await.expect("failed to start ldap"); + let ldaps = LdapServer::new(idms, 1000) + .await + .expect("failed to start ldap"); let anon_t = ldaps.do_bind(idms, "", "").await.unwrap().unwrap(); assert_eq!( @@ -2397,7 +2430,9 @@ mod tests { #[idm_test] async fn test_ldap_sssd_compat(idms: &IdmServer, _idms_delayed: &IdmServerDelayed) { - let ldaps = LdapServer::new(idms).await.expect("failed to start ldap"); + let ldaps = LdapServer::new(idms, 1000) + .await + .expect("failed to start ldap"); let acct_uuid = uuid!("cc8e95b4-c24f-4d68-ba54-8bed76f63930"); @@ -2505,7 +2540,9 @@ mod tests { #[idm_test] async fn test_ldap_compare_request(idms: &IdmServer, _idms_delayed: &IdmServerDelayed) { - let ldaps = LdapServer::new(idms).await.expect("failed to start ldap"); + let ldaps = LdapServer::new(idms, 1000) + .await + .expect("failed to start ldap"); // Setup a user we want to check. { @@ -2631,4 +2668,94 @@ mod tests { &OperationError::InvalidAttributeName("invalid".to_string()), ); } + + #[idm_test] + async fn test_ldap_maximum_queryable_attributes( + idms: &IdmServer, + _idms_delayed: &IdmServerDelayed, + ) { + let ldaps = LdapServer::new(idms, 2) + .await + .expect("failed to start ldap"); + + let usr_uuid = Uuid::new_v4(); + let grp_uuid = Uuid::new_v4(); + let app_uuid = Uuid::new_v4(); + let app_name = "testapp1"; + + // Setup person, group and application + { + let e1 = entry_init!( + (Attribute::Class, EntryClass::Object.to_value()), + (Attribute::Class, EntryClass::Account.to_value()), + (Attribute::Class, EntryClass::Person.to_value()), + (Attribute::Name, Value::new_iname("testperson1")), + (Attribute::Uuid, Value::Uuid(usr_uuid)), + (Attribute::Description, Value::new_utf8s("testperson1")), + (Attribute::DisplayName, Value::new_utf8s("testperson1")) + ); + + let e2 = entry_init!( + (Attribute::Class, EntryClass::Object.to_value()), + (Attribute::Class, EntryClass::Group.to_value()), + (Attribute::Name, Value::new_iname("testgroup1")), + (Attribute::Uuid, Value::Uuid(grp_uuid)) + ); + + let e3 = entry_init!( + (Attribute::Class, EntryClass::Object.to_value()), + (Attribute::Class, EntryClass::ServiceAccount.to_value()), + (Attribute::Class, EntryClass::Application.to_value()), + (Attribute::Name, Value::new_iname(app_name)), + (Attribute::Uuid, Value::Uuid(app_uuid)), + (Attribute::LinkedGroup, Value::Refer(grp_uuid)) + ); + + let ct = duration_from_epoch_now(); + let mut server_txn = idms.proxy_write(ct).await.unwrap(); + assert!(server_txn + .qs_write + .internal_create(vec![e1, e2, e3]) + .and_then(|_| server_txn.commit()) + .is_ok()); + } + + // Setup the anonymous login + let anon_t = ldaps.do_bind(idms, "", "").await.unwrap().unwrap(); + assert_eq!( + anon_t.effective_session, + LdapSession::UnixBind(UUID_ANONYMOUS) + ); + + let invalid_search = SearchRequest { + msgid: 1, + base: "dc=example,dc=com".to_string(), + scope: LdapSearchScope::Subtree, + filter: LdapFilter::Present(Attribute::ObjectClass.to_string()), + attrs: vec![ + "objectClass".to_string(), + "cn".to_string(), + "givenName".to_string(), + ], + }; + + let valid_search = SearchRequest { + msgid: 1, + base: "dc=example,dc=com".to_string(), + scope: LdapSearchScope::Subtree, + filter: LdapFilter::Present(Attribute::ObjectClass.to_string()), + attrs: vec!["objectClass: person".to_string()], + }; + + let invalid_res: Result<Vec<LdapMsg>, OperationError> = ldaps + .do_search(idms, &invalid_search, &anon_t, Source::Internal) + .await; + + let valid_res: Result<Vec<LdapMsg>, OperationError> = ldaps + .do_search(idms, &valid_search, &anon_t, Source::Internal) + .await; + + assert_eq!(invalid_res, Err(OperationError::ResourceLimit)); + assert_ne!(valid_res, Err(OperationError::ResourceLimit)); + } } From 31c28fe9a834df611ecbd3811fd1c1e06f7bda21 Mon Sep 17 00:00:00 2001 From: Sebastiano Tocci <sebastiano.tocci@proton.me> Date: Sat, 15 Feb 2025 23:20:32 +0100 Subject: [PATCH 2/2] Moved everything to the DB! --- libs/client/src/lib.rs | 16 ++++- proto/src/attribute.rs | 3 + proto/src/constants.rs | 1 + server/core/src/config.rs | 14 ---- server/core/src/lib.rs | 2 +- server/lib/src/constants/acp.rs | 9 +++ server/lib/src/constants/schema.rs | 16 +++++ server/lib/src/constants/uuids.rs | 3 +- server/lib/src/idm/ldap.rs | 102 +++++++++++++--------------- server/lib/src/plugins/protected.rs | 1 + server/lib/src/server/migrations.rs | 1 + server/testkit/tests/domain.rs | 13 ++++ server/testkit/tests/integration.rs | 13 ++++ tools/cli/src/cli/domain/mod.rs | 20 +++++- tools/cli/src/opt/kanidm.rs | 11 ++- 15 files changed, 147 insertions(+), 78 deletions(-) diff --git a/libs/client/src/lib.rs b/libs/client/src/lib.rs index e664019d3..15a18af1d 100644 --- a/libs/client/src/lib.rs +++ b/libs/client/src/lib.rs @@ -30,8 +30,8 @@ use compact_jwt::Jwk; use kanidm_proto::constants::uri::V1_AUTH_VALID; use kanidm_proto::constants::{ ATTR_DOMAIN_DISPLAY_NAME, ATTR_DOMAIN_LDAP_BASEDN, ATTR_DOMAIN_SSID, ATTR_ENTRY_MANAGED_BY, - ATTR_KEY_ACTION_REVOKE, ATTR_LDAP_ALLOW_UNIX_PW_BIND, ATTR_NAME, CLIENT_TOKEN_CACHE, KOPID, - KSESSIONID, KVERSION, + ATTR_KEY_ACTION_REVOKE, ATTR_LDAP_ALLOW_UNIX_PW_BIND, ATTR_LDAP_MAX_QUERYABLE_ATTRS, ATTR_NAME, + CLIENT_TOKEN_CACHE, KOPID, KSESSIONID, KVERSION, }; use kanidm_proto::internal::*; use kanidm_proto::v1::*; @@ -2075,6 +2075,18 @@ impl KanidmClient { .await } + /// Sets the maximum number of LDAP attributes that can be queryed in a single operation + pub async fn idm_domain_set_ldap_max_queryable_attrs( + &self, + max_queryable_attrs: usize, + ) -> Result<(), ClientError> { + self.perform_put_request( + &format!("/v1/domain/_attr/{}", ATTR_LDAP_MAX_QUERYABLE_ATTRS), + vec![max_queryable_attrs.to_string()], + ) + .await + } + pub async fn idm_set_ldap_allow_unix_password_bind( &self, enable: bool, diff --git a/proto/src/attribute.rs b/proto/src/attribute.rs index f0c00e907..9822ed355 100644 --- a/proto/src/attribute.rs +++ b/proto/src/attribute.rs @@ -94,6 +94,7 @@ pub enum Attribute { LdapEmailAddress, /// An LDAP Compatible sshkeys virtual attribute LdapKeys, + LdapMaxQueryableAttrs, LegalName, LimitSearchMaxResults, LimitSearchMaxFilterTest, @@ -322,6 +323,7 @@ impl Attribute { Attribute::LdapAllowUnixPwBind => ATTR_LDAP_ALLOW_UNIX_PW_BIND, Attribute::LdapEmailAddress => ATTR_LDAP_EMAIL_ADDRESS, Attribute::LdapKeys => ATTR_LDAP_KEYS, + Attribute::LdapMaxQueryableAttrs => ATTR_LDAP_MAX_QUERYABLE_ATTRS, Attribute::LdapSshPublicKey => ATTR_LDAP_SSHPUBLICKEY, Attribute::LegalName => ATTR_LEGALNAME, Attribute::LimitSearchMaxResults => ATTR_LIMIT_SEARCH_MAX_RESULTS, @@ -505,6 +507,7 @@ impl Attribute { ATTR_LDAP_ALLOW_UNIX_PW_BIND => Attribute::LdapAllowUnixPwBind, ATTR_LDAP_EMAIL_ADDRESS => Attribute::LdapEmailAddress, ATTR_LDAP_KEYS => Attribute::LdapKeys, + ATTR_LDAP_MAX_QUERYABLE_ATTRS => Attribute::LdapMaxQueryableAttrs, ATTR_SSH_PUBLICKEY => Attribute::SshPublicKey, ATTR_LEGALNAME => Attribute::LegalName, ATTR_LINKEDGROUP => Attribute::LinkedGroup, diff --git a/proto/src/constants.rs b/proto/src/constants.rs index cbfbeb2ff..9a60cd8b0 100644 --- a/proto/src/constants.rs +++ b/proto/src/constants.rs @@ -104,6 +104,7 @@ pub const ATTR_DYNGROUP_FILTER: &str = "dyngroup_filter"; pub const ATTR_DYNGROUP: &str = "dyngroup"; pub const ATTR_DYNMEMBER: &str = "dynmember"; pub const ATTR_LDAP_EMAIL_ADDRESS: &str = "emailaddress"; +pub const ATTR_LDAP_MAX_QUERYABLE_ATTRS: &str = "ldap_max_queryable_attrs"; pub const ATTR_EMAIL_ALTERNATIVE: &str = "emailalternative"; pub const ATTR_EMAIL_PRIMARY: &str = "emailprimary"; pub const ATTR_EMAIL: &str = "email"; diff --git a/server/core/src/config.rs b/server/core/src/config.rs index 981ef3988..ce6f7c33d 100644 --- a/server/core/src/config.rs +++ b/server/core/src/config.rs @@ -14,7 +14,6 @@ use kanidm_proto::constants::DEFAULT_SERVER_ADDRESS; use kanidm_proto::internal::FsType; use kanidm_proto::messages::ConsoleOutputMode; -use kanidmd_lib::prelude::DEFAULT_LDAP_MAXIMUM_QUERYABLE_ATTRIBUTES; use serde::Deserialize; use sketching::LogLevel; use url::Url; @@ -117,9 +116,6 @@ pub struct ServerConfig { /// /// If unset, the LDAP server will be disabled. pub ldapbindaddress: Option<String>, - /// The maximum number of LDAP attributes that can be queried in one operation. - /// Defaults to [kanidm_proto::constants::DEFAULT_LDAP_MAXIMUM_QUERYABLE_ATTRIBUTES] - pub ldap_maximum_queryable_attributes: Option<usize>, /// The role of this server, one of write_replica, write_replica_no_ui, read_only_replica, defaults to [ServerRole::WriteReplica] #[serde(default)] pub role: ServerRole, @@ -480,7 +476,6 @@ pub struct IntegrationReplConfig { pub struct Configuration { pub address: String, pub ldapaddress: Option<String>, - pub ldap_maximum_queryable_attrs: usize, pub adminbindpath: String, pub threads: usize, // db type later @@ -575,7 +570,6 @@ impl Configuration { Configuration { address: DEFAULT_SERVER_ADDRESS.to_string(), ldapaddress: None, - ldap_maximum_queryable_attrs: DEFAULT_LDAP_MAXIMUM_QUERYABLE_ATTRIBUTES, adminbindpath: env!("KANIDM_SERVER_ADMIN_BIND_PATH").to_string(), threads: std::thread::available_parallelism() .map(|t| t.get()) @@ -653,9 +647,6 @@ impl Configuration { self.update_ldapbind(&sconfig.ldapbindaddress); self.update_online_backup(&sconfig.online_backup); self.update_log_level(&sconfig.log_level); - if let Some(ldap_maximum_queryable_attrs) = sconfig.ldap_maximum_queryable_attributes { - self.update_ldap_maximum_queryable_attrs(ldap_maximum_queryable_attrs); - } } pub fn update_trust_x_forward_for(&mut self, t: Option<bool>) { @@ -742,9 +733,4 @@ impl Configuration { pub fn update_threads_count(&mut self, threads: usize) { self.threads = std::cmp::min(self.threads, threads); } - - // Updates the maximum number of LDAP attributes that can be queried in a single operation - pub fn update_ldap_maximum_queryable_attrs(&mut self, maximum_queryable_attrs: usize) { - self.ldap_maximum_queryable_attrs = maximum_queryable_attrs; - } } diff --git a/server/core/src/lib.rs b/server/core/src/lib.rs index 4c466468d..4826ec83f 100644 --- a/server/core/src/lib.rs +++ b/server/core/src/lib.rs @@ -937,7 +937,7 @@ pub async fn create_server_core( } } - let ldap = match LdapServer::new(&idms, config.ldap_maximum_queryable_attrs).await { + let ldap = match LdapServer::new(&idms).await { Ok(l) => l, Err(e) => { error!("Unable to start LdapServer -> {:?}", e); diff --git a/server/lib/src/constants/acp.rs b/server/lib/src/constants/acp.rs index 7c0487745..ab43bc6fd 100644 --- a/server/lib/src/constants/acp.rs +++ b/server/lib/src/constants/acp.rs @@ -1045,6 +1045,7 @@ lazy_static! { Attribute::DomainDisplayName, Attribute::DomainName, Attribute::DomainLdapBasedn, + Attribute::LdapMaxQueryableAttrs, Attribute::DomainSsid, Attribute::DomainUuid, // Grants read access to the key object. @@ -1058,6 +1059,7 @@ lazy_static! { Attribute::DomainDisplayName, Attribute::DomainSsid, Attribute::DomainLdapBasedn, + Attribute::LdapMaxQueryableAttrs, Attribute::LdapAllowUnixPwBind, Attribute::KeyActionRevoke, Attribute::KeyActionRotate, @@ -1065,6 +1067,7 @@ lazy_static! { modify_present_attrs: vec![ Attribute::DomainDisplayName, Attribute::DomainLdapBasedn, + Attribute::LdapMaxQueryableAttrs, Attribute::DomainSsid, Attribute::LdapAllowUnixPwBind, Attribute::KeyActionRevoke, @@ -1100,6 +1103,7 @@ lazy_static! { Attribute::DomainDisplayName, Attribute::DomainName, Attribute::DomainLdapBasedn, + Attribute::LdapMaxQueryableAttrs, Attribute::DomainSsid, Attribute::DomainUuid, Attribute::KeyInternalData, @@ -1111,6 +1115,7 @@ lazy_static! { Attribute::DomainDisplayName, Attribute::DomainSsid, Attribute::DomainLdapBasedn, + Attribute::LdapMaxQueryableAttrs, Attribute::LdapAllowUnixPwBind, Attribute::KeyActionRevoke, Attribute::KeyActionRotate, @@ -1119,6 +1124,7 @@ lazy_static! { modify_present_attrs: vec![ Attribute::DomainDisplayName, Attribute::DomainLdapBasedn, + Attribute::LdapMaxQueryableAttrs, Attribute::DomainSsid, Attribute::LdapAllowUnixPwBind, Attribute::KeyActionRevoke, @@ -1156,6 +1162,7 @@ lazy_static! { Attribute::DomainDisplayName, Attribute::DomainName, Attribute::DomainLdapBasedn, + Attribute::LdapMaxQueryableAttrs, Attribute::DomainSsid, Attribute::DomainUuid, Attribute::KeyInternalData, @@ -1167,6 +1174,7 @@ lazy_static! { Attribute::DomainDisplayName, Attribute::DomainSsid, Attribute::DomainLdapBasedn, + Attribute::LdapMaxQueryableAttrs, Attribute::DomainAllowEasterEggs, Attribute::LdapAllowUnixPwBind, Attribute::KeyActionRevoke, @@ -1176,6 +1184,7 @@ lazy_static! { modify_present_attrs: vec![ Attribute::DomainDisplayName, Attribute::DomainLdapBasedn, + Attribute::LdapMaxQueryableAttrs, Attribute::DomainSsid, Attribute::DomainAllowEasterEggs, Attribute::LdapAllowUnixPwBind, diff --git a/server/lib/src/constants/schema.rs b/server/lib/src/constants/schema.rs index d41680288..1471d9757 100644 --- a/server/lib/src/constants/schema.rs +++ b/server/lib/src/constants/schema.rs @@ -167,6 +167,17 @@ pub static ref SCHEMA_ATTR_DOMAIN_LDAP_BASEDN: SchemaAttribute = SchemaAttribute ..Default::default() }; +pub static ref SCHEMA_ATTR_LDAP_MAXIMUM_QUERYABLE_ATTRIBUTES: SchemaAttribute = SchemaAttribute { + uuid: UUID_SCHEMA_ATTR_LDAP_MAXIMUM_QUERYABLE_ATTRIBUTES, + name: Attribute::LdapMaxQueryableAttrs, + description: "The maximum number of LDAP attributes that can be queried in one operation".to_string(), + + multivalue: false, + sync_allowed: true, + syntax: SyntaxType::Uint32, + ..Default::default() +}; + pub static ref SCHEMA_ATTR_DOMAIN_DISPLAY_NAME: SchemaAttribute = SchemaAttribute { uuid: UUID_SCHEMA_ATTR_DOMAIN_DISPLAY_NAME, name: Attribute::DomainDisplayName, @@ -1133,6 +1144,7 @@ pub static ref SCHEMA_CLASS_DOMAIN_INFO_DL6: SchemaClass = SchemaClass { systemmay: vec![ Attribute::DomainSsid, Attribute::DomainLdapBasedn, + Attribute::LdapMaxQueryableAttrs, Attribute::LdapAllowUnixPwBind, Attribute::PrivateCookieKey, Attribute::FernetPrivateKeyStr, @@ -1158,6 +1170,7 @@ pub static ref SCHEMA_CLASS_DOMAIN_INFO_DL7: SchemaClass = SchemaClass { systemmay: vec![ Attribute::DomainSsid, Attribute::DomainLdapBasedn, + Attribute::LdapMaxQueryableAttrs, Attribute::LdapAllowUnixPwBind, Attribute::PatchLevel, Attribute::DomainDevelopmentTaint, @@ -1180,6 +1193,7 @@ pub static ref SCHEMA_CLASS_DOMAIN_INFO_DL8: SchemaClass = SchemaClass { systemmay: vec![ Attribute::DomainSsid, Attribute::DomainLdapBasedn, + Attribute::LdapMaxQueryableAttrs, Attribute::LdapAllowUnixPwBind, Attribute::Image, Attribute::PatchLevel, @@ -1203,6 +1217,7 @@ pub static ref SCHEMA_CLASS_DOMAIN_INFO_DL9: SchemaClass = SchemaClass { systemmay: vec![ Attribute::DomainSsid, Attribute::DomainLdapBasedn, + Attribute::LdapMaxQueryableAttrs, Attribute::LdapAllowUnixPwBind, Attribute::Image, Attribute::PatchLevel, @@ -1227,6 +1242,7 @@ pub static ref SCHEMA_CLASS_DOMAIN_INFO_DL10: SchemaClass = SchemaClass { systemmay: vec![ Attribute::DomainSsid, Attribute::DomainLdapBasedn, + Attribute::LdapMaxQueryableAttrs, Attribute::LdapAllowUnixPwBind, Attribute::Image, Attribute::PatchLevel, diff --git a/server/lib/src/constants/uuids.rs b/server/lib/src/constants/uuids.rs index 708cd218b..1389f465c 100644 --- a/server/lib/src/constants/uuids.rs +++ b/server/lib/src/constants/uuids.rs @@ -131,7 +131,8 @@ pub const UUID_SCHEMA_ATTR_PRIMARY_CREDENTIAL: Uuid = uuid!("00000000-0000-0000- pub const UUID_SCHEMA_CLASS_PERSON: Uuid = uuid!("00000000-0000-0000-0000-ffff00000044"); pub const UUID_SCHEMA_CLASS_GROUP: Uuid = uuid!("00000000-0000-0000-0000-ffff00000045"); pub const UUID_SCHEMA_CLASS_ACCOUNT: Uuid = uuid!("00000000-0000-0000-0000-ffff00000046"); -// GAP - 47 +pub const UUID_SCHEMA_ATTR_LDAP_MAXIMUM_QUERYABLE_ATTRIBUTES: Uuid = + uuid!("00000000-0000-0000-0000-ffff00000187"); pub const UUID_SCHEMA_ATTR_ATTRIBUTENAME: Uuid = uuid!("00000000-0000-0000-0000-ffff00000048"); pub const UUID_SCHEMA_ATTR_CLASSNAME: Uuid = uuid!("00000000-0000-0000-0000-ffff00000049"); pub const UUID_SCHEMA_ATTR_LEGALNAME: Uuid = uuid!("00000000-0000-0000-0000-ffff00000050"); diff --git a/server/lib/src/idm/ldap.rs b/server/lib/src/idm/ldap.rs index cc85463f6..dc8b48f28 100644 --- a/server/lib/src/idm/ldap.rs +++ b/server/lib/src/idm/ldap.rs @@ -60,7 +60,7 @@ pub struct LdapServer { basedn: String, dnre: Regex, binddnre: Regex, - maximum_queryable_attrs: usize, + max_queryable_attrs: usize, } #[derive(Debug)] @@ -71,10 +71,7 @@ enum LdapBindTarget { } impl LdapServer { - pub async fn new( - idms: &IdmServer, - maximum_queryable_attrs: usize, - ) -> Result<Self, OperationError> { + pub async fn new(idms: &IdmServer) -> Result<Self, OperationError> { // let ct = duration_from_epoch_now(); let mut idms_prox_read = idms.proxy_read().await?; // This is the rootdse path. @@ -83,6 +80,12 @@ impl LdapServer { .qs_read .internal_search_uuid(UUID_DOMAIN_INFO)?; + // Get the maximum number of queryable attributes from the domain entry + let max_queryable_attrs = domain_entry + .get_ava_single_uint32(Attribute::LdapMaxQueryableAttrs) + .map(|u| u as usize) + .unwrap_or(DEFAULT_LDAP_MAXIMUM_QUERYABLE_ATTRIBUTES); + let basedn = domain_entry .get_ava_single_iutf8(Attribute::DomainLdapBasedn) .map(|s| s.to_string()) @@ -158,7 +161,7 @@ impl LdapServer { basedn, dnre, binddnre, - maximum_queryable_attrs, + max_queryable_attrs, }) } @@ -244,10 +247,11 @@ impl LdapServer { let mut all_attrs = false; let mut all_op_attrs = false; + let attrs_len = sr.attrs.len(); if sr.attrs.is_empty() { // If [], then "all" attrs all_attrs = true; - } else if sr.attrs.len() < self.maximum_queryable_attrs { + } else if attrs_len < self.max_queryable_attrs { sr.attrs.iter().for_each(|a| { if a == "*" { all_attrs = true; @@ -272,6 +276,10 @@ impl LdapServer { } }) } else { + admin_error!( + "Too many LDAP attributes requested. Maximum allowed is {}, while your search query had {}", + self.max_queryable_attrs, attrs_len + ); return Err(OperationError::ResourceLimit); // TODO: Should we return ResourceLimit or InvalidRequestState here? } @@ -866,9 +874,7 @@ mod tests { #[idm_test] async fn test_ldap_simple_bind(idms: &IdmServer, _idms_delayed: &IdmServerDelayed) { - let ldaps = LdapServer::new(idms, 1000) - .await - .expect("failed to start ldap"); + let ldaps = LdapServer::new(idms).await.expect("failed to start ldap"); let mut idms_prox_write = idms.proxy_write(duration_from_epoch_now()).await.unwrap(); // make the admin a valid posix account @@ -1063,9 +1069,7 @@ mod tests { #[idm_test] async fn test_ldap_application_dnre(idms: &IdmServer, _idms_delayed: &IdmServerDelayed) { - let ldaps = LdapServer::new(idms, 1000) - .await - .expect("failed to start ldap"); + let ldaps = LdapServer::new(idms).await.expect("failed to start ldap"); let testdn = format!("app=app1,{0}", ldaps.basedn); let captures = ldaps.dnre.captures(testdn.as_str()).unwrap(); @@ -1088,9 +1092,7 @@ mod tests { #[idm_test] async fn test_ldap_application_search(idms: &IdmServer, _idms_delayed: &IdmServerDelayed) { - let ldaps = LdapServer::new(idms, 1000) - .await - .expect("failed to start ldap"); + let ldaps = LdapServer::new(idms).await.expect("failed to start ldap"); let usr_uuid = Uuid::new_v4(); let grp_uuid = Uuid::new_v4(); @@ -1173,9 +1175,7 @@ mod tests { #[idm_test] async fn test_ldap_spn_search(idms: &IdmServer, _idms_delayed: &IdmServerDelayed) { - let ldaps = LdapServer::new(idms, 1000) - .await - .expect("failed to start ldap"); + let ldaps = LdapServer::new(idms).await.expect("failed to start ldap"); let usr_uuid = Uuid::new_v4(); let usr_name = "panko"; @@ -1257,9 +1257,7 @@ mod tests { #[idm_test] async fn test_ldap_application_bind(idms: &IdmServer, _idms_delayed: &IdmServerDelayed) { - let ldaps = LdapServer::new(idms, 1000) - .await - .expect("failed to start ldap"); + let ldaps = LdapServer::new(idms).await.expect("failed to start ldap"); let usr_uuid = Uuid::new_v4(); let grp_uuid = Uuid::new_v4(); @@ -1415,9 +1413,7 @@ mod tests { idms: &IdmServer, _idms_delayed: &IdmServerDelayed, ) { - let ldaps = LdapServer::new(idms, 1000) - .await - .expect("failed to start ldap"); + let ldaps = LdapServer::new(idms).await.expect("failed to start ldap"); let usr_uuid = Uuid::new_v4(); let usr_name = "testuser1"; @@ -1621,9 +1617,7 @@ mod tests { idms: &IdmServer, _idms_delayed: &IdmServerDelayed, ) { - let ldaps = LdapServer::new(idms, 1000) - .await - .expect("failed to start ldap"); + let ldaps = LdapServer::new(idms).await.expect("failed to start ldap"); let usr_uuid = Uuid::new_v4(); let usr_name = "testuser1"; @@ -1760,9 +1754,7 @@ mod tests { idms: &IdmServer, _idms_delayed: &IdmServerDelayed, ) { - let ldaps = LdapServer::new(idms, 1000) - .await - .expect("failed to start ldap"); + let ldaps = LdapServer::new(idms).await.expect("failed to start ldap"); let ssh_ed25519 = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAeGW1P6Pc2rPq0XqbRaDKBcXZUPRklo0L1EyR30CwoP william@amethyst"; @@ -1925,9 +1917,7 @@ mod tests { _idms_delayed: &IdmServerDelayed, ) { // Setup the ldap server - let ldaps = LdapServer::new(idms, 1000) - .await - .expect("failed to start ldap"); + let ldaps = LdapServer::new(idms).await.expect("failed to start ldap"); // Prebuild the search req we'll be using this test. let sr = SearchRequest { @@ -2167,9 +2157,7 @@ mod tests { idms: &IdmServer, _idms_delayed: &IdmServerDelayed, ) { - let ldaps = LdapServer::new(idms, 1000) - .await - .expect("failed to start ldap"); + let ldaps = LdapServer::new(idms).await.expect("failed to start ldap"); let acct_uuid = uuid!("cc8e95b4-c24f-4d68-ba54-8bed76f63930"); @@ -2238,9 +2226,7 @@ mod tests { // Test behaviour of the 1.1 attribute. #[idm_test] async fn test_ldap_one_dot_one_attribute(idms: &IdmServer, _idms_delayed: &IdmServerDelayed) { - let ldaps = LdapServer::new(idms, 1000) - .await - .expect("failed to start ldap"); + let ldaps = LdapServer::new(idms).await.expect("failed to start ldap"); let acct_uuid = uuid!("cc8e95b4-c24f-4d68-ba54-8bed76f63930"); @@ -2329,9 +2315,7 @@ mod tests { #[idm_test] async fn test_ldap_rootdse_basedn_change(idms: &IdmServer, _idms_delayed: &IdmServerDelayed) { - let ldaps = LdapServer::new(idms, 1000) - .await - .expect("failed to start ldap"); + let ldaps = LdapServer::new(idms).await.expect("failed to start ldap"); let anon_t = ldaps.do_bind(idms, "", "").await.unwrap().unwrap(); assert_eq!( @@ -2387,9 +2371,7 @@ mod tests { assert!(idms_prox_write.commit().is_ok()); // Now re-test - let ldaps = LdapServer::new(idms, 1000) - .await - .expect("failed to start ldap"); + let ldaps = LdapServer::new(idms).await.expect("failed to start ldap"); let anon_t = ldaps.do_bind(idms, "", "").await.unwrap().unwrap(); assert_eq!( @@ -2430,9 +2412,7 @@ mod tests { #[idm_test] async fn test_ldap_sssd_compat(idms: &IdmServer, _idms_delayed: &IdmServerDelayed) { - let ldaps = LdapServer::new(idms, 1000) - .await - .expect("failed to start ldap"); + let ldaps = LdapServer::new(idms).await.expect("failed to start ldap"); let acct_uuid = uuid!("cc8e95b4-c24f-4d68-ba54-8bed76f63930"); @@ -2540,9 +2520,7 @@ mod tests { #[idm_test] async fn test_ldap_compare_request(idms: &IdmServer, _idms_delayed: &IdmServerDelayed) { - let ldaps = LdapServer::new(idms, 1000) - .await - .expect("failed to start ldap"); + let ldaps = LdapServer::new(idms).await.expect("failed to start ldap"); // Setup a user we want to check. { @@ -2674,9 +2652,21 @@ mod tests { idms: &IdmServer, _idms_delayed: &IdmServerDelayed, ) { - let ldaps = LdapServer::new(idms, 2) - .await - .expect("failed to start ldap"); + // Set the max queryable attrs to 2 + + let mut server_txn = idms.proxy_write(duration_from_epoch_now()).await.unwrap(); + + let set_ldap_maximum_queryable_attrs = ModifyEvent::new_internal_invalid( + filter!(f_eq(Attribute::Uuid, PartialValue::Uuid(UUID_DOMAIN_INFO))), + ModifyList::new_purge_and_set(Attribute::LdapMaxQueryableAttrs, Value::Uint32(2)), + ); + assert!(server_txn + .qs_write + .modify(&set_ldap_maximum_queryable_attrs) + .and_then(|_| server_txn.commit()) + .is_ok()); + + let ldaps = LdapServer::new(idms).await.expect("failed to start ldap"); let usr_uuid = Uuid::new_v4(); let grp_uuid = Uuid::new_v4(); @@ -2756,6 +2746,6 @@ mod tests { .await; assert_eq!(invalid_res, Err(OperationError::ResourceLimit)); - assert_ne!(valid_res, Err(OperationError::ResourceLimit)); + assert!(valid_res.is_ok()); } } diff --git a/server/lib/src/plugins/protected.rs b/server/lib/src/plugins/protected.rs index 0e7fcd587..ae58217ae 100644 --- a/server/lib/src/plugins/protected.rs +++ b/server/lib/src/plugins/protected.rs @@ -25,6 +25,7 @@ lazy_static! { // modification of some domain info types for local configuratiomn. Attribute::DomainSsid, Attribute::DomainLdapBasedn, + Attribute::LdapMaxQueryableAttrs, Attribute::LdapAllowUnixPwBind, Attribute::FernetPrivateKeyStr, Attribute::Es256PrivateKeyDer, diff --git a/server/lib/src/server/migrations.rs b/server/lib/src/server/migrations.rs index 80a1ab3b5..b7673fb6f 100644 --- a/server/lib/src/server/migrations.rs +++ b/server/lib/src/server/migrations.rs @@ -519,6 +519,7 @@ impl QueryServerWriteTransaction<'_> { // SCHEMA_ATTR_DISPLAYNAME.clone().into(), SCHEMA_ATTR_DOMAIN_DISPLAY_NAME.clone().into(), SCHEMA_ATTR_DOMAIN_LDAP_BASEDN.clone().into(), + SCHEMA_ATTR_LDAP_MAXIMUM_QUERYABLE_ATTRIBUTES.clone().into(), SCHEMA_ATTR_DOMAIN_NAME.clone().into(), SCHEMA_ATTR_LDAP_ALLOW_UNIX_PW_BIND.clone().into(), SCHEMA_ATTR_DOMAIN_SSID.clone().into(), diff --git a/server/testkit/tests/domain.rs b/server/testkit/tests/domain.rs index baacff0e2..37dfe9f10 100644 --- a/server/testkit/tests/domain.rs +++ b/server/testkit/tests/domain.rs @@ -26,6 +26,19 @@ async fn test_idm_domain_set_ldap_basedn(rsclient: KanidmClient) { .expect("Failed to set idm_domain_set_ldap_basedn"); } +#[kanidmd_testkit::test] +async fn test_idm_domain_set_ldap_max_queryable_attrs(rsclient: KanidmClient) { + rsclient + .auth_simple_password(ADMIN_TEST_USER, ADMIN_TEST_PASSWORD) + .await + .expect("Failed to login as admin"); + + rsclient + .idm_domain_set_ldap_max_queryable_attrs(30) + .await + .expect("Failed to set idm_domain_set_ldap_max_queryable_attrs"); +} + #[kanidmd_testkit::test] async fn test_idm_domain_set_display_name(rsclient: KanidmClient) { rsclient diff --git a/server/testkit/tests/integration.rs b/server/testkit/tests/integration.rs index 707ef35a5..efbef68d4 100644 --- a/server/testkit/tests/integration.rs +++ b/server/testkit/tests/integration.rs @@ -231,6 +231,19 @@ async fn test_idm_domain_set_ldap_basedn(rsclient: KanidmClient) { .is_err()); } +#[kanidmd_testkit::test] +async fn test_idm_domain_set_ldap_max_queryable_attrs(rsclient: KanidmClient) { + login_put_admin_idm_admins(&rsclient).await; + assert!(rsclient + .idm_domain_set_ldap_max_queryable_attrs(20) + .await + .is_ok()); + assert!(rsclient + .idm_domain_set_ldap_max_queryable_attrs(10) //TODO: Help, Rust's type safety is so good I can't come up with a way to pass an invalid value + .await + .is_ok()); // Ideally this should be "is_err" +} + #[kanidmd_testkit::test] /// Checks that a built-in group idm_all_persons has the "builtin" class as expected. async fn test_all_persons_has_builtin_class(rsclient: KanidmClient) { diff --git a/tools/cli/src/cli/domain/mod.rs b/tools/cli/src/cli/domain/mod.rs index 70067a50d..7834f84a2 100644 --- a/tools/cli/src/cli/domain/mod.rs +++ b/tools/cli/src/cli/domain/mod.rs @@ -14,7 +14,8 @@ impl DomainOpt { | DomainOpt::SetLdapAllowUnixPasswordBind { copt, .. } | DomainOpt::SetAllowEasterEggs { copt, .. } | DomainOpt::RevokeKey { copt, .. } - | DomainOpt::Show(copt) => copt.debug, + | DomainOpt::Show(copt) + | DomainOpt::SetLdapMaxQueryableAttrs { copt, .. } => copt.debug, } } @@ -34,6 +35,23 @@ impl DomainOpt { Err(e) => handle_client_error(e, opt.copt.output_mode), } } + DomainOpt::SetLdapMaxQueryableAttrs { + copt, + new_max_queryable_attrs, + } => { + eprintln!( + "Attempting to set the maximum number of queryable LDAP attributes to: {:?}", + new_max_queryable_attrs + ); + let client = copt.to_client(OpType::Write).await; + match client + .idm_domain_set_ldap_max_queryable_attrs(*new_max_queryable_attrs) + .await + { + Ok(_) => println!("Success"), + Err(e) => handle_client_error(e, copt.output_mode), + } + } DomainOpt::SetLdapBasedn { copt, new_basedn } => { eprintln!( "Attempting to set the domain's ldap basedn to: {:?}", diff --git a/tools/cli/src/opt/kanidm.rs b/tools/cli/src/opt/kanidm.rs index 1fb39d0ba..f9aea13e3 100644 --- a/tools/cli/src/opt/kanidm.rs +++ b/tools/cli/src/opt/kanidm.rs @@ -198,7 +198,6 @@ pub enum GroupAccountPolicyOpt { copt: CommonOpt, }, - /// Set the maximum time for privilege session expiry in seconds. #[clap(name = "privilege-expiry")] PrivilegedSessionExpiry { @@ -208,7 +207,6 @@ pub enum GroupAccountPolicyOpt { copt: CommonOpt, }, - /// The WebAuthn attestation CA list that should be enforced /// on members of this group. Prevents use of passkeys that are /// not in this list. To create this list, use `fido-mds-tool` @@ -294,7 +292,6 @@ pub enum GroupAccountPolicyOpt { #[clap(flatten)] copt: CommonOpt, }, - } #[derive(Debug, Subcommand)] @@ -1313,6 +1310,14 @@ pub enum DomainOpt { #[clap[name = "set-displayname"]] /// Set the domain display name SetDisplayname(OptSetDomainDisplayname), + /// Sets the maximum number of LDAP attributes that can be queried in one operation. + #[clap[name = "set-ldap-queryable-attrs"]] + SetLdapMaxQueryableAttrs { + #[clap(flatten)] + copt: CommonOpt, + #[clap(name = "maximum-queryable-attrs")] + new_max_queryable_attrs: usize, + }, #[clap[name = "set-ldap-basedn"]] /// Change the basedn of this server. Takes effect after a server restart. /// Examples are `o=organisation` or `dc=domain,dc=name`. Must be a valid ldap