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 dcc7ba2 Perform an LDAP search for keys without an ASF email UID
dcc7ba2 is described below
commit dcc7ba274dfaa78bfa435d25d41ad2f522adef9e
Author: Sean B. Palmer <[email protected]>
AuthorDate: Wed May 21 16:19:45 2025 +0100
Perform an LDAP search for keys without an ASF email UID
---
atr/blueprints/admin/admin.py | 96 ++-------------------
atr/blueprints/admin/templates/ldap-lookup.html | 2 +-
atr/db/interaction.py | 23 ++++-
atr/ldap.py | 106 ++++++++++++++++++++++++
4 files changed, 132 insertions(+), 95 deletions(-)
diff --git a/atr/blueprints/admin/admin.py b/atr/blueprints/admin/admin.py
index 65ebcc2..bf40c98 100644
--- a/atr/blueprints/admin/admin.py
+++ b/atr/blueprints/admin/admin.py
@@ -17,7 +17,6 @@
import asyncio
import collections
-import dataclasses
import logging
import os
import pathlib
@@ -31,8 +30,6 @@ import asfquart
import asfquart.base as base
import asfquart.session
import httpx
-import ldap3
-import ldap3.utils.conv as conv
import quart
import werkzeug.wrappers.response as response
import wtforms
@@ -42,14 +39,11 @@ import atr.datasources.apache as apache
import atr.db as db
import atr.db.interaction as interaction
import atr.db.models as models
+import atr.ldap as ldap
import atr.util as util
_LOGGER: Final = logging.getLogger(__name__)
-LDAP_ATTRIBUTES: Final[list[str]] = ["uid", "cn", "mail", "asf-altEmail",
"displayName"]
-LDAP_SEARCH_BASE: Final[str] = "ou=people,dc=apache,dc=org"
-LDAP_SERVER_HOST: Final[str] = "ldap-eu.apache.org"
-
class DeleteReleaseForm(util.QuartFormTyped):
"""Form for deleting releases."""
@@ -78,20 +72,6 @@ class LdapLookupForm(util.QuartFormTyped):
submit = wtforms.SubmitField("Lookup")
-# We use a dataclass to support ldap3.Connection objects
[email protected]
-class LdapSearchParams:
- uid_query: str | None = None
- email_query: str | None = None
- bind_dn_from_config: str | None = None
- bind_password_from_config: str | None = None
- results_list: list[dict[str, str | list[str]]] =
dataclasses.field(default_factory=list)
- err_msg: str | None = None
- srv_info: str | None = None
- detail_err: str | None = None
- connection: ldap3.Connection | None = None
-
-
@admin.BLUEPRINT.route("/data")
@admin.BLUEPRINT.route("/data/<model>")
async def admin_data(model: str = "Committee") -> str:
@@ -357,7 +337,7 @@ async def admin_toggle_view() -> response.Response:
@admin.BLUEPRINT.route("/ldap/", methods=["GET"])
-async def ldap() -> str:
+async def ldap_search() -> str:
form = await LdapLookupForm.create_form(data=quart.request.args)
asf_id_for_template: str | None = None
@@ -368,18 +348,18 @@ async def ldap() -> str:
uid_query = form.uid.data
email_query = form.email.data
- ldap_params: LdapSearchParams | None = None
+ ldap_params: ldap.SearchParameters | None = None
if (quart.request.method == "GET") and (uid_query or email_query):
bind_dn = quart.current_app.config.get("LDAP_BIND_DN")
bind_password = quart.current_app.config.get("LDAP_BIND_PASSWORD")
- ldap_params = LdapSearchParams(
+ ldap_params = ldap.SearchParameters(
uid_query=uid_query,
email_query=email_query,
bind_dn_from_config=bind_dn,
bind_password_from_config=bind_password,
)
- await asyncio.to_thread(_ldap_lookup_perform_search, ldap_params)
+ await asyncio.to_thread(ldap.search, ldap_params)
return await quart.render_template(
"ldap-lookup.html",
@@ -390,72 +370,6 @@ async def ldap() -> str:
)
-def _ldap_lookup_perform_search(params: LdapSearchParams) -> None:
- try:
- _ldap_lookup_perform_search_inner(params)
- except Exception as e:
- params.err_msg = f"An unexpected error occurred: {e!s}"
- params.detail_err = f"Details: {e.args}"
- finally:
- if params.connection and params.connection.bound:
- try:
- params.connection.unbind()
- except Exception:
- ...
-
-
-def _ldap_lookup_perform_search_inner(params: LdapSearchParams) -> None:
- params.results_list = []
- params.err_msg = None
- params.srv_info = None
- params.detail_err = None
- params.connection = None
-
- server = ldap3.Server(LDAP_SERVER_HOST, use_ssl=True, get_info=ldap3.ALL)
- params.srv_info = repr(server)
-
- if params.bind_dn_from_config and params.bind_password_from_config:
- params.connection = ldap3.Connection(
- server, user=params.bind_dn_from_config,
password=params.bind_password_from_config, auto_bind=True
- )
- else:
- params.connection = ldap3.Connection(server, auto_bind=True)
-
- filters: list[str] = []
- if params.uid_query:
- filters.append(f"(uid={conv.escape_filter_chars(params.uid_query)})")
-
- if params.email_query:
- escaped_email = conv.escape_filter_chars(params.email_query)
- if params.email_query.endswith("@apache.org"):
- filters.append(f"(mail={escaped_email})")
- else:
- filters.append(f"(asf-altEmail={escaped_email})")
-
- if not filters:
- params.err_msg = "Please provide a UID or an email address to search."
- return
-
- search_filter = f"(&{''.join(filters)})" if (len(filters) > 1) else
filters[0]
-
- if not params.connection:
- params.err_msg = "LDAP Connection object not established or auto_bind
failed."
- return
-
- params.connection.search(
- search_base=LDAP_SEARCH_BASE,
- search_filter=search_filter,
- attributes=LDAP_ATTRIBUTES,
- )
- for entry in params.connection.entries:
- result_item: dict[str, str | list[str]] = {"dn": entry.entry_dn}
- result_item.update(entry.entry_attributes_as_dict)
- params.results_list.append(result_item)
-
- if (not params.results_list) and (not params.err_msg):
- params.err_msg = "No results found for the given criteria."
-
-
@admin.BLUEPRINT.route("/ongoing-tasks/<project_name>/<version_name>/<revision>")
async def ongoing_tasks(project_name: str, version_name: str, revision: str)
-> quart.wrappers.response.Response:
try:
diff --git a/atr/blueprints/admin/templates/ldap-lookup.html
b/atr/blueprints/admin/templates/ldap-lookup.html
index f51c401..b9cd56b 100644
--- a/atr/blueprints/admin/templates/ldap-lookup.html
+++ b/atr/blueprints/admin/templates/ldap-lookup.html
@@ -17,7 +17,7 @@
<h5 class="mb-0">Search criteria</h5>
</div>
<div class="card-body">
- <form method="get" action="{{ url_for('admin.ldap') }}">
+ <form method="get" action="{{ url_for('admin.ldap_search') }}">
{{ form.csrf_token }}
<div class="mb-3">
{{ form.uid.label(class="form-label") }}
diff --git a/atr/db/interaction.py b/atr/db/interaction.py
index e557597..b3a25be 100644
--- a/atr/db/interaction.py
+++ b/atr/db/interaction.py
@@ -31,6 +31,7 @@ import sqlmodel
import atr.analysis as analysis
import atr.db as db
import atr.db.models as models
+import atr.ldap as ldap
import atr.schema as schema
import atr.user as user
import atr.util as util
@@ -62,13 +63,15 @@ async def key_user_add(asf_uid: str | None, public_key:
str, selected_committees
# Determine ASF UID if not provided
if asf_uid is None:
- for uid in key["uids"]:
- match = re.search(r"([A-Za-z0-9]+)@apache.org", uid)
- if match:
+ for uid_str in key["uids"]:
+ if match := re.search(r"([A-Za-z0-9]+)@apache.org", uid_str):
asf_uid = match.group(1).lower()
break
else:
_LOGGER.warning(f"key_user_add called with no ASF UID found in key
UIDs: {key.get('uids')}")
+ for uid_str in key.get("uids", []):
+ if asf_uid := await asyncio.to_thread(_asf_uid_from_uid_str,
uid_str):
+ break
if asf_uid is None:
# We place this here to make it easier on the type checkers
raise RuntimeError("No Apache UID found in the key UIDs")
@@ -288,6 +291,20 @@ async def unfinished_releases(asfuid: str) -> dict[str,
list[models.Release]]:
return releases
+def _asf_uid_from_uid_str(uid_str: str) -> str | None:
+ if not (email_match := re.search(r"<([^>]+)>", uid_str)):
+ return None
+ email = email_match.group(1)
+ if email.endswith("@apache.org"):
+ return None
+ ldap_params = ldap.SearchParameters(email_query=email)
+ ldap.search(ldap_params)
+ if not (ldap_params.results_list and ("uid" in
ldap_params.results_list[0])):
+ return None
+ ldap_uid_val = ldap_params.results_list[0]["uid"]
+ return ldap_uid_val[0] if isinstance(ldap_uid_val, list) else ldap_uid_val
+
+
async def _key_user_add_validate_key_properties(public_key: str) ->
tuple[dict, str]:
"""Validate GPG key string, import it, and return its properties and
fingerprint."""
import gnupg
diff --git a/atr/ldap.py b/atr/ldap.py
new file mode 100644
index 0000000..05985a3
--- /dev/null
+++ b/atr/ldap.py
@@ -0,0 +1,106 @@
+# 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 dataclasses
+from typing import Final
+
+import ldap3
+import ldap3.utils.conv as conv
+
+LDAP_ATTRIBUTES: Final[list[str]] = ["uid", "cn", "mail", "asf-altEmail",
"displayName"]
+LDAP_SEARCH_BASE: Final[str] = "ou=people,dc=apache,dc=org"
+LDAP_SERVER_HOST: Final[str] = "ldap-eu.apache.org"
+
+
+# We use a dataclass to support ldap3.Connection objects
[email protected]
+class SearchParameters:
+ uid_query: str | None = None
+ email_query: str | None = None
+ bind_dn_from_config: str | None = None
+ bind_password_from_config: str | None = None
+ results_list: list[dict[str, str | list[str]]] =
dataclasses.field(default_factory=list)
+ err_msg: str | None = None
+ srv_info: str | None = None
+ detail_err: str | None = None
+ connection: ldap3.Connection | None = None
+
+
+def search(params: SearchParameters) -> None:
+ try:
+ _search_core(params)
+ except Exception as e:
+ params.err_msg = f"An unexpected error occurred: {e!s}"
+ params.detail_err = f"Details: {e.args}"
+ finally:
+ if params.connection and params.connection.bound:
+ try:
+ params.connection.unbind()
+ except Exception:
+ ...
+
+
+def _search_core(params: SearchParameters) -> None:
+ params.results_list = []
+ params.err_msg = None
+ params.srv_info = None
+ params.detail_err = None
+ params.connection = None
+
+ server = ldap3.Server(LDAP_SERVER_HOST, use_ssl=True, get_info=ldap3.ALL)
+ params.srv_info = repr(server)
+
+ if params.bind_dn_from_config and params.bind_password_from_config:
+ params.connection = ldap3.Connection(
+ server, user=params.bind_dn_from_config,
password=params.bind_password_from_config, auto_bind=True
+ )
+ else:
+ params.connection = ldap3.Connection(server, auto_bind=True)
+
+ filters: list[str] = []
+ if params.uid_query:
+ filters.append(f"(uid={conv.escape_filter_chars(params.uid_query)})")
+
+ if params.email_query:
+ escaped_email = conv.escape_filter_chars(params.email_query)
+ if params.email_query.endswith("@apache.org"):
+ filters.append(f"(mail={escaped_email})")
+ else:
+ filters.append(f"(asf-altEmail={escaped_email})")
+
+ if not filters:
+ params.err_msg = "Please provide a UID or an email address to search."
+ return
+
+ search_filter = f"(&{''.join(filters)})" if (len(filters) > 1) else
filters[0]
+
+ if not params.connection:
+ params.err_msg = "LDAP Connection object not established or auto_bind
failed."
+ return
+
+ params.connection.search(
+ search_base=LDAP_SEARCH_BASE,
+ search_filter=search_filter,
+ attributes=LDAP_ATTRIBUTES,
+ )
+ for entry in params.connection.entries:
+ result_item: dict[str, str | list[str]] = {"dn": entry.entry_dn}
+ result_item.update(entry.entry_attributes_as_dict)
+ params.results_list.append(result_item)
+
+ if (not params.results_list) and (not params.err_msg):
+ params.err_msg = "No results found for the given criteria."
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]