File gh-pr68650_ldap_attrlist.patch of Package salt

From 443bcb8473d4c3958c3ad87fba4a31b9e89b07b1 Mon Sep 17 00:00:00 2001
From: Georg Pfuetzenreuter <georg.pfuetzenreuter@suse.com>
Date: Tue, 20 Jan 2026 23:54:00 +0100
Subject: [PATCH] Support attrlist in ldap.managed

This resolves not being able to manage operational attributes by
introducing a way to pass the list to filter in the LDAP search through
to the search function. Passing a customizable list was deemed most
flexible, as one might not want to control all but only specific
operational attributes. For example, one can set attrlist to ["*",
"aci"] to manage all user attributes plus the "aci" operational
attribute.

Signed-off-by: Georg Pfuetzenreuter <georg.pfuetzenreuter@suse.com>
---
 changelog/53364.fixed.md               |  1 +
 salt/states/ldap.py                    | 14 ++++++++----
 tests/pytests/unit/states/test_ldap.py | 31 ++++++++++++++++++--------
 3 files changed, 33 insertions(+), 13 deletions(-)
 create mode 100644 changelog/53364.fixed.md

diff --git a/changelog/53364.fixed.md b/changelog/53364.fixed.md
new file mode 100644
index 00000000000..91c6a5733d2
--- /dev/null
+++ b/changelog/53364.fixed.md
@@ -0,0 +1 @@
+Support attrlist in ldap.managed
diff --git a/salt/states/ldap.py b/salt/states/ldap.py
index db5b5c5983b..d39b579412b 100644
--- a/salt/states/ldap.py
+++ b/salt/states/ldap.py
@@ -19,7 +19,7 @@
 log = logging.getLogger(__name__)
 
 
-def managed(name, entries, connect_spec=None):
+def managed(name, entries, connect_spec=None, attrlist=None):
     """Ensure the existence (or not) of LDAP entries and their attributes
 
     Example:
@@ -183,6 +183,12 @@ def managed(name, entries, connect_spec=None):
         the ``'url'`` entry is set to the value of the ``name``
         parameter.
 
+    :param attrlist:
+        Passed directly to :py:func:`ldap3.connect <salt.modules.ldap3.search>`
+        to filter the attributes returned by the LDAP server. By default, all
+        user attributes will be requested, and this should only need to be
+        modified if management of operational attributes is desired.
+
     :returns:
         A dict with the following keys:
 
@@ -254,7 +260,7 @@ def managed(name, entries, connect_spec=None):
 
     with connect(connect_spec) as l:
 
-        old, new = _process_entries(l, entries)
+        old, new = _process_entries(l, attrlist, entries)
 
         # collect all of the affected entries (only the key is
         # important in this dict; would have used an OrderedSet if
@@ -367,7 +373,7 @@ def managed(name, entries, connect_spec=None):
     return ret
 
 
-def _process_entries(l, entries):
+def _process_entries(l, attrlist, entries):
     """Helper for managed() to process entries and return before/after views
 
     Collect the current database state and update it according to the
@@ -420,7 +426,7 @@ def _process_entries(l, entries):
             olde = new.get(dn, None)
             if olde is None:
                 # next check the database
-                results = __salt__["ldap3.search"](l, dn, "base")
+                results = __salt__["ldap3.search"](l, dn, "base", attrlist=attrlist)
                 if len(results) == 1:
                     attrs = results[dn]
                     olde = {
diff --git a/tests/pytests/unit/states/test_ldap.py b/tests/pytests/unit/states/test_ldap.py
index 99d0e36a36e..3a533f6d902 100644
--- a/tests/pytests/unit/states/test_ldap.py
+++ b/tests/pytests/unit/states/test_ldap.py
@@ -30,7 +30,7 @@ class LdapDB:
     def dummy_connect(self, connect_spec):
         return _dummy_ctx()
 
-    def dummy_search(self, connect_spec, base, scope):
+    def dummy_search(self, connect_spec, base, scope, attrlist):
         if base not in self.db:
             return {}
         return {
@@ -38,6 +38,7 @@ def dummy_search(self, connect_spec, base, scope):
                 attr: list(self.db[base][attr])
                 for attr in self.db[base]
                 if len(self.db[base][attr])
+                and (attrlist is None or attr in attrlist or "*" in attrlist)
             }
         }
 
@@ -200,7 +201,7 @@ def configure_loader_modules(db):
     return {salt.states.ldap: {"__opts__": {"test": False}, "__salt__": salt_dunder}}
 
 
-def _test_helper(init_db, expected_ret, replace, delete_others=False):
+def _test_helper(init_db, expected_ret, replace, delete_others=False, attrlist=None):
     old = init_db.dump_db()
     new = init_db.dump_db()
     expected_db = copy.deepcopy(init_db.db)
@@ -267,13 +268,13 @@ def _test_helper(init_db, expected_ret, replace, delete_others=False):
         {dn: [{"replace": attrs}, {"delete_others": delete_others}]}
         for dn, attrs in replace.items()
     ]
-    actual = salt.states.ldap.managed(name, entries)
+    actual = salt.states.ldap.managed(name, entries, attrlist=attrlist)
     assert expected_ret == actual
     assert expected_db == init_db.db
 
 
-def _test_helper_success(db, replace, delete_others=False):
-    _test_helper(db, {}, replace, delete_others)
+def _test_helper_success(db, replace, delete_others=False, attrlist=None):
+    _test_helper(db, {}, replace, delete_others, attrlist)
 
 
 def _test_helper_nochange(db, replace, delete_others=False):
@@ -284,7 +285,7 @@ def _test_helper_nochange(db, replace, delete_others=False):
     _test_helper(db, expected, replace, delete_others)
 
 
-def _test_helper_add(db, expected_ret, add_items, delete_others=False):
+def _test_helper_add(db, expected_ret, add_items, delete_others=False, attrlist=None):
     old = db.dump_db()
     new = db.dump_db()
     expected_db = copy.deepcopy(db.db)
@@ -355,13 +356,13 @@ def _test_helper_add(db, expected_ret, add_items, delete_others=False):
         {dn: [{"add": attrs}, {"delete_others": delete_others}]}
         for dn, attrs in add_items.items()
     ]
-    actual = salt.states.ldap.managed(name, entries)
+    actual = salt.states.ldap.managed(name, entries, attrlist=attrlist)
     assert expected_ret == actual
     assert expected_db == db.db
 
 
-def _test_helper_success_add(db, add_items, delete_others=False):
-    _test_helper_add(db, {}, add_items, delete_others)
+def _test_helper_success_add(db, add_items, delete_others=False, attrlist=None):
+    _test_helper_add(db, {}, add_items, delete_others, attrlist)
 
 
 def test_managed_empty(db):
@@ -383,10 +384,22 @@ def test_managed_add_entry(db):
 def test_managed_add_attr(complex_db):
     _test_helper_success_add(complex_db, {"dnfoo": {"attrfoo1": ["valfoo1.3"]}})
     _test_helper_success_add(complex_db, {"dnfoo": {"attrfoo4": ["valfoo4.1"]}})
+    _test_helper_success_add(
+        complex_db, {"dnfoo": {"attrfoo10": ["valfoo10"]}}, attrlist=["*"]
+    )
+    _test_helper_success_add(
+        complex_db, {"dnfoo11": {"attrfoo11": ["valfoo11"]}}, attrlist=["attrfoo11"]
+    )
 
 
 def test_managed_replace_attr(complex_db):
     _test_helper_success(complex_db, {"dnfoo": {"attrfoo3": ["valfoo3.1"]}})
+    _test_helper_success(
+        complex_db, {"dnfoo": {"attrfoo12": ["valfoo12"]}}, attrlist=["*"]
+    )
+    _test_helper_success(
+        complex_db, {"dnfoo13": {"attrfoo13": ["valfoo13"]}}, attrlist=["attrfoo13"]
+    )
 
 
 def test_managed_simplereplace(complex_db):
openSUSE Build Service is sponsored by