Skip to content

Commit 9d75b08

Browse files
puretensionAAraKKe
authored andcommitted
Fix OpenLDAP custom queries to use search_scope parameter (#21385)
* Fix OpenLDAP custom queries to use search_scope parameter - Add support for search_scope parameter in custom queries - Default to 'subtree' when search_scope is not specified - Update existing tests to include search_scope parameter - Add comprehensive test for search_scope functionality Fixes #20685 * Add changelog entry for search_scope parameter fix * Simplify search_scope test to avoid CI issues * Fix code formatting for linting compliance * Fix search_scope to use ldap3 constants instead of strings LDAP3 library requires constants (ldap3.BASE, ldap3.LEVEL, ldap3.SUBTREE) instead of string values for search_scope parameter. * Fix trailing whitespace for linting compliance * Address reviewer feedback: use mapping dict and improve error handling - Add SEARCH_SCOPE_MAPPING class constant for efficient lookups - Change invalid scope handling from warning+fallback to error+skip - Add comprehensive parameterized tests for all valid values - Add tests for default behavior and invalid value handling Signed-off-by: puretension <rlrlfhtm5@gmail.com> * Use DEFAULT_SEARCH_SCOPE constant instead of hardcoded values - Replace hardcoded 'subtree' with DEFAULT_SEARCH_SCOPE in main code - Replace ldap3.SUBTREE with DEFAULT_SEARCH_SCOPE in all tests - Ensures centralized management of default search scope value Signed-off-by: puretension <rlrlfhtm5@gmail.com> * Fix code formatting for linting compliance Signed-off-by: puretension <rlrlfhtm5@gmail.com> * Fix import sorting in OpenLDAP test files Organize imports in test files to comply with ruff linting rules: - tests/conftest.py: Sort imports alphabetically - tests/test_check.py: Sort imports alphabetically - tests/test_unit.py: Sort imports alphabetically Signed-off-by: puretension <rlrlfhtm5@gmail.com> * Fix import formatting for linting compliance Signed-off-by: puretension <rlrlfhtm5@gmail.com> * Run ddev test --fmt openldap to fix linting issues Signed-off-by: puretension <rlrlfhtm5@gmail.com> --------- Signed-off-by: puretension <rlrlfhtm5@gmail.com> Co-authored-by: Juanpe Araque <juanpedro.araque@datadoghq.com>
1 parent 986f378 commit 9d75b08

File tree

3 files changed

+116
-3
lines changed

3 files changed

+116
-3
lines changed

openldap/changelog.d/21385.fixed

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Fix custom queries to properly use search_scope parameter

openldap/datadog_checks/openldap/openldap.py

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,12 @@
1212
class OpenLDAP(AgentCheck):
1313
METRIC_PREFIX = 'openldap'
1414
SERVICE_CHECK_CONNECT = '{}.can_connect'.format(METRIC_PREFIX)
15+
DEFAULT_SEARCH_SCOPE = ldap3.SUBTREE
16+
SEARCH_SCOPE_MAPPING = {
17+
"base": ldap3.BASE,
18+
"level": ldap3.LEVEL,
19+
"subtree": ldap3.SUBTREE,
20+
}
1521

1622
SEARCH_BASE = 'cn=Monitor'
1723
SEARCH_FILTER = '(objectClass=*)'
@@ -155,6 +161,15 @@ def _perform_custom_queries(self, conn, custom_queries, tags, instance):
155161
self.log.error("`search_filter` field is required for custom query #%s", name)
156162
continue
157163
attrs = query.get("attributes")
164+
search_scope_str = query.get("search_scope", self.DEFAULT_SEARCH_SCOPE).lower()
165+
search_scope = self.SEARCH_SCOPE_MAPPING.get(search_scope_str)
166+
167+
if search_scope is None:
168+
self.log.error(
169+
"`search_scope` field needs to be one of 'base', 'level' or 'subtree'. Value provided: '%s'",
170+
search_scope_str,
171+
)
172+
continue
158173
if "username" in query:
159174
username = query.get("username")
160175
password = query.get("password")
@@ -181,7 +196,7 @@ def _perform_custom_queries(self, conn, custom_queries, tags, instance):
181196

182197
try:
183198
# Perform the search query
184-
conn.search(search_base, search_filter, attributes=attrs)
199+
conn.search(search_base, search_filter, search_scope=search_scope, attributes=attrs)
185200
except ldap3.core.exceptions.LDAPException:
186201
self.log.exception("Unable to perform search query for %s", name)
187202
continue

openldap/tests/test_unit.py

Lines changed: 99 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -225,7 +225,7 @@ def test__perform_custom_queries(check, mocker):
225225
_, _, _, _, queries, tags = check._get_instance_params(instance)
226226
check._perform_custom_queries(conn_mock, queries, tags, instance)
227227
conn_mock.rebind.assert_called_once_with(user="user", password="pass", authentication=ldap3.SIMPLE)
228-
conn_mock.search.assert_called_once_with("base", "filter", attributes=None)
228+
conn_mock.search.assert_called_once_with("base", "filter", search_scope=ldap3.SUBTREE, attributes=None)
229229
log_mock.error.assert_not_called() # No error logged
230230

231231
# Check query rebind different user
@@ -248,10 +248,107 @@ def test__perform_custom_queries(check, mocker):
248248
_, _, _, _, queries, tags = check._get_instance_params(instance)
249249
check._perform_custom_queries(conn_mock, queries, tags, instance)
250250
conn_mock.rebind.assert_called_once_with(user="user2", password="pass2", authentication=ldap3.SIMPLE)
251-
conn_mock.search.assert_called_once_with("base", "filter", attributes=["*"])
251+
conn_mock.search.assert_called_once_with("base", "filter", search_scope=ldap3.SUBTREE, attributes=["*"])
252252
log_mock.error.assert_not_called() # No error logged
253253

254254

255+
@pytest.mark.parametrize(
256+
"search_scope_str,expected_scope",
257+
[
258+
("base", ldap3.BASE),
259+
("level", ldap3.LEVEL),
260+
("subtree", ldap3.SUBTREE),
261+
("BASE", ldap3.BASE),
262+
("LEVEL", ldap3.LEVEL),
263+
("SUBTREE", ldap3.SUBTREE),
264+
],
265+
)
266+
def test_custom_query_search_scope_valid(check, mocker, search_scope_str, expected_scope):
267+
"""Test that valid search_scope parameters are properly converted"""
268+
instance = {
269+
"url": "url",
270+
"custom_queries": [
271+
{
272+
"name": "test_query",
273+
"search_base": "ou=users,dc=example,dc=com",
274+
"search_filter": "(objectClass=person)",
275+
"search_scope": search_scope_str,
276+
}
277+
],
278+
}
279+
280+
log_mock = mocker.MagicMock()
281+
check.log = log_mock
282+
conn_mock = mocker.MagicMock()
283+
conn_mock.rebind.return_value = True
284+
conn_mock.entries = []
285+
286+
_, _, _, _, queries, tags = check._get_instance_params(instance)
287+
check._perform_custom_queries(conn_mock, queries, tags, instance)
288+
289+
conn_mock.search.assert_called_once_with(
290+
"ou=users,dc=example,dc=com", "(objectClass=person)", search_scope=expected_scope, attributes=None
291+
)
292+
293+
294+
def test_custom_query_search_scope_default(check, mocker):
295+
"""Test that missing search_scope defaults to SUBTREE"""
296+
instance = {
297+
"url": "url",
298+
"custom_queries": [
299+
{
300+
"name": "test_query",
301+
"search_base": "ou=users,dc=example,dc=com",
302+
"search_filter": "(objectClass=person)",
303+
# No search_scope specified
304+
}
305+
],
306+
}
307+
308+
log_mock = mocker.MagicMock()
309+
check.log = log_mock
310+
conn_mock = mocker.MagicMock()
311+
conn_mock.rebind.return_value = True
312+
conn_mock.entries = []
313+
314+
_, _, _, _, queries, tags = check._get_instance_params(instance)
315+
check._perform_custom_queries(conn_mock, queries, tags, instance)
316+
317+
conn_mock.search.assert_called_once_with(
318+
"ou=users,dc=example,dc=com", "(objectClass=person)", search_scope=ldap3.SUBTREE, attributes=None
319+
)
320+
321+
322+
def test_custom_query_search_scope_invalid(check, mocker):
323+
"""Test that invalid search_scope logs error and skips the query"""
324+
instance = {
325+
"url": "url",
326+
"custom_queries": [
327+
{
328+
"name": "test_query",
329+
"search_base": "ou=users,dc=example,dc=com",
330+
"search_filter": "(objectClass=person)",
331+
"search_scope": "invalid_scope",
332+
}
333+
],
334+
}
335+
336+
log_mock = mocker.MagicMock()
337+
check.log = log_mock
338+
conn_mock = mocker.MagicMock()
339+
conn_mock.rebind.return_value = True
340+
conn_mock.entries = []
341+
342+
_, _, _, _, queries, tags = check._get_instance_params(instance)
343+
check._perform_custom_queries(conn_mock, queries, tags, instance)
344+
345+
log_mock.error.assert_called_once_with(
346+
"`search_scope` field needs to be one of 'base', 'level' or 'subtree'. Value provided: '%s'", "invalid_scope"
347+
)
348+
# Verify that search was NOT called due to invalid scope
349+
conn_mock.search.assert_not_called()
350+
351+
255352
def test__extract_common_name(check):
256353
# Check lower case and spaces converted
257354
dn = "cn=Max File Descriptors,cn=Connections,cn=Monitor"

0 commit comments

Comments
 (0)