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
The following commit(s) were added to refs/heads/main by this push:
new 8d7a9d7 Add an LDAP search that discovers admin users
8d7a9d7 is described below
commit 8d7a9d7570fa28c2286aaa5771d12864b8288035
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 | 43 ++++++++++++++++++++++++++++++++++++---
tests/unit/test_ldap.py | 53 +++++++++++++++++++++++++++++++++++++++++++++++++
2 files changed, 93 insertions(+), 3 deletions(-)
diff --git a/atr/ldap.py b/atr/ldap.py
index 403c05b..a917def 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,41 @@ class SearchParameters:
email_only: bool = False
+async def fetch_admin_users() -> frozenset[str]:
+ import atr.config as config
+ import atr.log as log
+
+ conf = config.get()
+ bind_dn = conf.LDAP_BIND_DN
+ bind_password = conf.LDAP_BIND_PASSWORD
+
+ if (not bind_dn) or (not bind_password):
+ log.warning("LDAP bind DN or password not configured, returning empty
admin set")
+ return frozenset()
+
+ 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)
+
+
async def github_to_apache(github_numeric_uid: int) -> str:
import atr.config as config
@@ -116,9 +154,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..1fc4077
--- /dev/null
+++ b/tests/unit/test_ldap.py
@@ -0,0 +1,53 @@
+# 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]
+async def test_fetch_admin_users_contains_only_nonempty_strings():
+ 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():
+ admins = await ldap.fetch_admin_users()
+ assert "wave" in admins
+
+
[email protected]
+async def test_fetch_admin_users_is_idempotent():
+ # Could, of course, fail in rare situations
+ 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():
+ admins = await ldap.fetch_admin_users()
+ assert isinstance(admins, frozenset)
+
+
[email protected]
+async def test_fetch_admin_users_returns_reasonable_count():
+ admins = await ldap.fetch_admin_users()
+ assert len(admins) > 1
+ assert len(admins) < 100
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]