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-release.git


The following commit(s) were added to refs/heads/main by this push:
     new 267fd3b  Use LDAP to get user committees when admins browse as another 
user
267fd3b is described below

commit 267fd3bff899187ed329e933bf88ec8f2effe47d
Author: Sean B. Palmer <[email protected]>
AuthorDate: Fri Sep 19 17:18:34 2025 +0100

    Use LDAP to get user committees when admins browse as another user
---
 atr/blueprints/admin/admin.py | 45 +++++++++++++++++++++++++++++++++++--------
 atr/principal.py              | 27 ++++++++++++--------------
 2 files changed, 49 insertions(+), 23 deletions(-)

diff --git a/atr/blueprints/admin/admin.py b/atr/blueprints/admin/admin.py
index df3d3d6..59b0081 100644
--- a/atr/blueprints/admin/admin.py
+++ b/atr/blueprints/admin/admin.py
@@ -44,6 +44,7 @@ import atr.forms as forms
 import atr.ldap as ldap
 import atr.log as log
 import atr.models.sql as sql
+import atr.principal as principal
 import atr.routes.mapping as mapping
 import atr.storage as storage
 import atr.storage.outcome as outcome
@@ -110,8 +111,7 @@ async def admin_browse_as() -> str | response.Response:
     if not (current_session := await session.read()):
         raise base.ASFQuartException("Not authenticated", 401)
 
-    bind_dn = quart.current_app.config.get("LDAP_BIND_DN")
-    bind_password = quart.current_app.config.get("LDAP_BIND_PASSWORD")
+    bind_dn, bind_password = principal.get_ldap_bind_dn_and_password()
     ldap_params = ldap.SearchParameters(
         uid_query=new_uid,
         bind_dn_from_config=bind_dn,
@@ -127,7 +127,15 @@ async def admin_browse_as() -> str | response.Response:
     committee_data = await apache.get_active_committee_data()
     ldap_data = ldap_params.results_list[0]
     log.info("Current ASFQuart session data: %s", current_session)
-    new_session_data = _session_data(ldap_data, new_uid, current_session, 
ldap_projects_data, committee_data)
+    new_session_data = _session_data(
+        ldap_data,
+        new_uid,
+        current_session,
+        ldap_projects_data,
+        committee_data,
+        bind_dn,
+        bind_password,
+    )
     log.info("New Quart cookie (not ASFQuart session) data: %s", 
new_session_data)
     session.write(new_session_data)
 
@@ -812,6 +820,29 @@ async def _get_filesystem_dirs_unfinished(filesystem_dirs: 
list[str]) -> None:
                         filesystem_dirs.append(version_dir_path)
 
 
+def _get_user_committees_from_ldap(uid: str, bind_dn: str, bind_password: str) 
-> set[str]:
+    with ldap.Search(bind_dn, bind_password) as ldap_search:
+        result = ldap_search.search(
+            ldap_base="ou=project,ou=groups,dc=apache,dc=org",
+            ldap_scope="SUBTREE",
+            
ldap_query=f"(|(ownerUid={uid})(owner=uid={uid},ou=people,dc=apache,dc=org))",
+            ldap_attrs=["cn"],
+        )
+
+    committees = set()
+    for hit in result:
+        if not isinstance(hit, dict):
+            continue
+        pmc = hit.get("cn")
+        if not (isinstance(pmc, list) and (len(pmc) == 1)):
+            continue
+        project_name = pmc[0]
+        if project_name and isinstance(project_name, str):
+            committees.add(project_name)
+
+    return committees
+
+
 async def _process_undiscovered(data: db.Session) -> tuple[int, int]:
     added_count = 0
     updated_count = 0
@@ -855,16 +886,14 @@ def _session_data(
     current_session: session.ClientSession,
     ldap_projects: apache.LDAPProjectsData,
     committee_data: apache.CommitteeData,
+    bind_dn: str,
+    bind_password: str,
 ) -> dict[str, Any]:
     # This is not quite accurate
     # For example, this misses "tooling" for tooling members
     projects = {p.name for p in ldap_projects.projects if (new_uid in 
p.members) or (new_uid in p.owners)}
     # And this adds "incubator", which is not in the OAuth data
-    committees = set()
-    for c in committee_data.committees:
-        for user in c.roster:
-            if user.id == new_uid:
-                committees.add(c.name)
+    committees = _get_user_committees_from_ldap(new_uid, bind_dn, 
bind_password)
 
     # Or asf-member-status?
     is_member = bool(projects or committees)
diff --git a/atr/principal.py b/atr/principal.py
index af35fb0..8ad1d5d 100644
--- a/atr/principal.py
+++ b/atr/principal.py
@@ -40,28 +40,33 @@ LDAP_TOOLING_BASE = 
"cn=tooling,ou=groups,ou=services,dc=apache,dc=org"
 
 
 class AuthenticationError(Exception):
-    """Simple exception with a message and an optional origin exception 
(WIP)"""
-
     def __init__(self, message, origin=None):
         super().__init__(message)
         self.origin = origin
 
 
 class CommitterError(Exception):
-    """Simple exception with a message and an optional origin exception 
(WIP)"""
-
     def __init__(self, message, origin=None):
         super().__init__(message)
         self.origin = origin
 
 
 def attr_to_list(attr):
-    """Converts a list of bytestring attribute values to a unique list of 
strings"""
+    """Converts a list of bytestring attribute values to a unique list of 
strings."""
     return list(set([value for value in attr or []]))
 
 
+def get_ldap_bind_dn_and_password() -> tuple[str, str]:
+    conf = config.get()
+    bind_dn = conf.LDAP_BIND_DN
+    bind_password = conf.LDAP_BIND_PASSWORD
+    if (not bind_dn) or (not bind_password):
+        raise CommitterError("LDAP bind DN or password not set")
+    return bind_dn, bind_password
+
+
 class Committer:
-    """Verifies and loads a committers credentials via LDAP"""
+    """Verifies and loads a committer's credentials via LDAP."""
 
     def __init__(self, user: str) -> None:
         if not re.match(r"^[-_a-z0-9]+$", user):
@@ -79,7 +84,7 @@ class Committer:
         self.pmcs: list[str] = []
         self.projects: list[str] = []
 
-        self.__bind_dn, self.__bind_password = 
self._get_ldap_bind_dn_and_password()
+        self.__bind_dn, self.__bind_password = get_ldap_bind_dn_and_password()
 
     def verify(self) -> dict[str, Any]:
         with ldap.Search(self.__bind_dn, self.__bind_password) as ldap_search:
@@ -176,14 +181,6 @@ class Committer:
             raise CommitterError("Common backend assertions failed, LDAP 
corruption?")
         return members
 
-    def _get_ldap_bind_dn_and_password(self) -> tuple[str, str]:
-        conf = config.AppConfig()
-        bind_dn = conf.LDAP_BIND_DN
-        bind_password = conf.LDAP_BIND_PASSWORD
-        if (not bind_dn) or (not bind_password):
-            raise CommitterError("LDAP bind DN or password not set")
-        return bind_dn, bind_password
-
     def _get_project_memberships(self, ldap_search: ldap.Search, ldap_filter: 
str) -> list[str]:
         try:
             result = ldap_search.search(


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

Reply via email to