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):