This is an automated email from the ASF dual-hosted git repository.

sbp pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/tooling-trusted-releases.git

commit 809056b0352a2a970a4da45b0b15e74be9b4547d
Author: Sean B. Palmer <[email protected]>
AuthorDate: Fri Jan 23 14:24:59 2026 +0000

    Add an LDAP search that discovers admin users
---
 atr/ldap.py             | 50 +++++++++++++++++++++++++++++++++---
 tests/unit/test_ldap.py | 68 +++++++++++++++++++++++++++++++++++++++++++++++++
 2 files changed, 115 insertions(+), 3 deletions(-)

diff --git a/atr/ldap.py b/atr/ldap.py
index 403c05b..8cc429d 100644
--- a/atr/ldap.py
+++ b/atr/ldap.py
@@ -14,6 +14,7 @@
 # KIND, either express or implied.  See the License for the
 # specific language governing permissions and limitations
 # under the License.
+
 import asyncio
 import collections
 import dataclasses
@@ -23,8 +24,10 @@ import ldap3
 import ldap3.utils.conv as conv
 import ldap3.utils.dn as dn
 
+LDAP_ROOT_BASE: Final[str] = 
"cn=infrastructure-root,ou=groups,ou=services,dc=apache,dc=org"
 LDAP_SEARCH_BASE: Final[str] = "ou=people,dc=apache,dc=org"
 LDAP_SERVER_HOST: Final[str] = "ldap-eu.apache.org"
+LDAP_TOOLING_BASE: Final[str] = 
"cn=tooling,ou=groups,ou=services,dc=apache,dc=org"
 
 
 class Search:
@@ -94,6 +97,48 @@ class SearchParameters:
     email_only: bool = False
 
 
+async def fetch_admin_users() -> frozenset[str]:
+    import atr.log as log
+
+    credentials = get_bind_credentials()
+    if credentials is None:
+        log.warning("LDAP bind DN or password not configured, returning empty 
admin set")
+        return frozenset()
+
+    bind_dn, bind_password = credentials
+
+    def _query_ldap() -> frozenset[str]:
+        users: set[str] = set()
+        with Search(bind_dn, bind_password) as ldap_search:
+            for base in (LDAP_ROOT_BASE, LDAP_TOOLING_BASE):
+                try:
+                    result = ldap_search.search(ldap_base=base, 
ldap_scope="BASE")
+                    if (not result) or (len(result) != 1):
+                        continue
+                    members = result[0].get("member", [])
+                    if not isinstance(members, list):
+                        continue
+                    for member_dn in members:
+                        parsed = parse_dn(member_dn)
+                        uids = parsed.get("uid", [])
+                        if uids:
+                            users.add(uids[0])
+                except Exception as e:
+                    log.warning(f"Failed to query LDAP group {base}: {e}")
+        return frozenset(users)
+
+    return await asyncio.to_thread(_query_ldap)
+
+
+def get_bind_credentials() -> tuple[str, str] | None:
+    import atr.config as config
+
+    conf = config.get()
+    if conf.LDAP_BIND_DN and conf.LDAP_BIND_PASSWORD:
+        return (conf.LDAP_BIND_DN, conf.LDAP_BIND_PASSWORD)
+    return None
+
+
 async def github_to_apache(github_numeric_uid: int) -> str:
     import atr.config as config
 
@@ -116,9 +161,8 @@ async def github_to_apache(github_numeric_uid: int) -> str:
 def parse_dn(dn_string: str) -> dict[str, list[str]]:
     parsed = collections.defaultdict(list)
     parts = dn.parse_dn(dn_string)
-    for part in parts:
-        for attr, value in part:
-            parsed[attr].append(value)
+    for attr, value, _ in parts:
+        parsed[attr].append(value)
     return dict(parsed)
 
 
diff --git a/tests/unit/test_ldap.py b/tests/unit/test_ldap.py
new file mode 100644
index 0000000..5a51481
--- /dev/null
+++ b/tests/unit/test_ldap.py
@@ -0,0 +1,68 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+import pytest
+
+import atr.ldap as ldap
+
+
[email protected]
+def ldap_configured() -> bool:
+    return ldap.get_bind_credentials() is not None
+
+
[email protected]
+async def 
test_fetch_admin_users_contains_only_nonempty_strings(ldap_configured: bool):
+    _skip_if_unavailable(ldap_configured)
+    admins = await ldap.fetch_admin_users()
+    assert all(isinstance(uid, str) and uid for uid in admins)
+
+
[email protected]
+async def test_fetch_admin_users_includes_wave(ldap_configured: bool):
+    _skip_if_unavailable(ldap_configured)
+    admins = await ldap.fetch_admin_users()
+    assert "wave" in admins
+
+
[email protected]
+async def test_fetch_admin_users_is_idempotent(ldap_configured: bool):
+    # Could, of course, fail in rare situations
+    _skip_if_unavailable(ldap_configured)
+    admins1 = await ldap.fetch_admin_users()
+    admins2 = await ldap.fetch_admin_users()
+    assert admins1 == admins2
+
+
[email protected]
+async def test_fetch_admin_users_returns_frozenset(ldap_configured: bool):
+    _skip_if_unavailable(ldap_configured)
+    admins = await ldap.fetch_admin_users()
+    assert isinstance(admins, frozenset)
+
+
[email protected]
+async def test_fetch_admin_users_returns_reasonable_count(ldap_configured: 
bool):
+    _skip_if_unavailable(ldap_configured)
+    admins = await ldap.fetch_admin_users()
+    assert len(admins) > 1
+    assert len(admins) < 100
+
+
+def _skip_if_unavailable(ldap_configured: bool) -> None:
+    if not ldap_configured:
+        pytest.skip("LDAP not configured")


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to