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] 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