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 b8c14e7 Add a route for admins to check the consistency of public
signing keys
b8c14e7 is described below
commit b8c14e738a5174b0258bc9fc9602e497b6f83d0a
Author: Sean B. Palmer <[email protected]>
AuthorDate: Wed Jun 18 16:53:59 2025 +0100
Add a route for admins to check the consistency of public signing keys
---
atr/blueprints/admin/admin.py | 43 +++++++++++++++++++++++++++++++++++
atr/db/interaction.py | 50 ++++++++++++++++++++++++++---------------
atr/templates/keys-details.html | 2 +-
atr/util.py | 33 +++++++++++++++++++++++++++
scripts/keys_import.py | 25 ++-------------------
5 files changed, 111 insertions(+), 42 deletions(-)
diff --git a/atr/blueprints/admin/admin.py b/atr/blueprints/admin/admin.py
index e2179a7..efb9119 100644
--- a/atr/blueprints/admin/admin.py
+++ b/atr/blueprints/admin/admin.py
@@ -358,6 +358,29 @@ async def admin_env() -> quart.wrappers.response.Response:
return quart.Response("\n".join(env_vars), mimetype="text/plain")
[email protected]("/keys/check", methods=["GET", "POST"])
+async def admin_keys_check() -> quart.Response:
+ """Check public signing key details."""
+ if quart.request.method != "POST":
+ empty_form = await util.EmptyForm.create_form()
+ return quart.Response(
+ f"""
+<form method="post">
+ <button type="submit">Check public signing key details</button>
+ {empty_form.hidden_tag()}
+</form>
+""",
+ mimetype="text/html",
+ )
+
+ try:
+ result = await _check_keys()
+ return quart.Response(result, mimetype="text/plain")
+ except Exception as e:
+ _LOGGER.exception("Exception during key check:")
+ return quart.Response(f"Exception during key check: {e!s}",
mimetype="text/plain")
+
+
@admin.BLUEPRINT.route("/keys/regenerate-all", methods=["GET", "POST"])
async def admin_keys_regenerate_all() -> quart.Response:
"""Regenerate the KEYS file for all committees."""
@@ -609,6 +632,26 @@ async def ongoing_tasks(project_name: str, version_name:
str, revision: str) ->
return quart.Response("", mimetype="text/plain")
+async def _check_keys() -> str:
+ email_to_uid = await util.email_to_uid_map()
+ bad_keys = []
+ async with db.session() as data:
+ keys = await data.public_signing_key().all()
+ for key in keys:
+ uids = []
+ if key.primary_declared_uid:
+ uids.append(key.primary_declared_uid)
+ if key.secondary_declared_uids:
+ uids.extend(key.secondary_declared_uids)
+ asf_uid = await util.asf_uid_from_uids(uids,
ldap_data=email_to_uid)
+ if asf_uid != key.apache_uid:
+ bad_keys.append(f"{key.fingerprint} detected: {asf_uid}, key:
{key.apache_uid}")
+ message = f"Checked {len(keys)} keys"
+ if bad_keys:
+ message += f"\nFound {len(bad_keys)} bad keys:\n{'\n'.join(bad_keys)}"
+ return message
+
+
async def _delete_release_data(release_name: str) -> None:
"""Handle the deletion of database records and filesystem data for a
release."""
async with db.session() as data:
diff --git a/atr/db/interaction.py b/atr/db/interaction.py
index 21485fb..e4cbf77 100644
--- a/atr/db/interaction.py
+++ b/atr/db/interaction.py
@@ -73,6 +73,7 @@ async def key_user_add(
public_key: str,
selected_committees: list[str],
ldap_data: dict[str, str] | None = None,
+ update_existing: bool = False,
) -> list[dict]:
if not public_key:
raise PublicKeyError("Public key is required")
@@ -93,7 +94,9 @@ async def key_user_add(
raise InteractionError(f"Key {key.get('fingerprint', '').upper()}
is not associated with your ASF account")
async with db.session() as data:
# Store the key in the database
- added = await key_user_session_add(asf_uid, public_key, key,
selected_committees, data)
+ added = await key_user_session_add(
+ asf_uid, public_key, key, selected_committees, data,
update_existing=update_existing
+ )
if added:
added_keys.append(added)
else:
@@ -107,6 +110,7 @@ async def key_user_session_add(
key: dict,
selected_committees: list[str],
data: db.Session,
+ update_existing: bool = False,
) -> dict | None:
# TODO: Check if key already exists
# psk_statement =
select(PublicSigningKey).where(PublicSigningKey.apache_uid == session.uid)
@@ -138,24 +142,26 @@ async def key_user_session_add(
existing = await data.public_signing_key(fingerprint=fingerprint).get()
# TODO: This can race
if existing:
- # TODO: Always update?
+ update = update_existing
# If the new key has a latest self signature
if latest_self_signature is not None:
# And the self signature is newer, update it
if (existing.latest_self_signature is None) or
(existing.latest_self_signature < latest_self_signature):
- existing.fingerprint = fingerprint
- existing.algorithm = int(key["algo"])
- existing.length = int(key.get("length", "0"))
- existing.created = created
- existing.latest_self_signature = latest_self_signature
- existing.expires = expires
- existing.primary_declared_uid = uids[0] if uids else None
- existing.secondary_declared_uids = uids[1:]
- existing.apache_uid = asf_uid
- existing.ascii_armored_key = public_key
- logging.info(f"Found existing key {fingerprint.upper()},
updating associations")
- else:
- logging.info(f"Found existing key {fingerprint.upper()},
no update needed")
+ update = True
+ if update:
+ existing.fingerprint = fingerprint
+ existing.algorithm = int(key["algo"])
+ existing.length = int(key.get("length", "0"))
+ existing.created = created
+ existing.latest_self_signature = latest_self_signature
+ existing.expires = expires
+ existing.primary_declared_uid = uids[0] if uids else None
+ existing.secondary_declared_uids = uids[1:]
+ existing.apache_uid = asf_uid
+ existing.ascii_armored_key = public_key
+ logging.info(f"Found existing key {fingerprint.upper()},
updating associations")
+ else:
+ logging.info(f"Found existing key {fingerprint.upper()}, no
update needed")
key_record = existing
else:
# Key doesn't exist, create it
@@ -326,6 +332,7 @@ async def upload_keys_bytes(
keys_bytes: bytes,
selected_committees: list[str],
ldap_data: dict[str, str] | None = None,
+ update_existing: bool = False,
) -> tuple[list[dict], int, int, list[str]]:
key_blocks = util.parse_key_blocks_bytes(keys_bytes)
if not key_blocks:
@@ -340,7 +347,9 @@ async def upload_keys_bytes(
submitted_committees = selected_committees[:]
# Process each key block
- results = await _upload_process_key_blocks(key_blocks,
selected_committees, ldap_data=ldap_data)
+ results = await _upload_process_key_blocks(
+ key_blocks, selected_committees, ldap_data=ldap_data,
update_existing=update_existing
+ )
# if not results:
# raise InteractionError("No keys were added")
@@ -446,7 +455,10 @@ async def _successes_errors_warnings(
async def _upload_process_key_blocks(
- key_blocks: list[str], selected_committees: list[str], ldap_data:
dict[str, str] | None = None
+ key_blocks: list[str],
+ selected_committees: list[str],
+ ldap_data: dict[str, str] | None = None,
+ update_existing: bool = False,
) -> list[dict]:
"""Process OpenPGP key blocks and add them to the user's account."""
results: list[dict] = []
@@ -454,7 +466,9 @@ async def _upload_process_key_blocks(
# Process each key block
for i, key_block in enumerate(key_blocks):
try:
- added_keys = await key_user_add(None, key_block,
selected_committees, ldap_data=ldap_data)
+ added_keys = await key_user_add(
+ None, key_block, selected_committees, ldap_data=ldap_data,
update_existing=update_existing
+ )
for key_info in added_keys:
key_info["status"] = key_info.get("status", "success")
key_info["email"] = key_info.get("email", "Unknown")
diff --git a/atr/templates/keys-details.html b/atr/templates/keys-details.html
index 08bce30..f7840d9 100644
--- a/atr/templates/keys-details.html
+++ b/atr/templates/keys-details.html
@@ -103,6 +103,6 @@
</tbody>
</table>
<h2>ASCII armored key</h2>
- <pre class="mt-3 border border-2 p-3">{{ key.ascii_armored_key }}</pre>
+ <pre class="mt-3 border border-2 p-3">{{ key.ascii_armored_key if
isinstance(key.ascii_armored_key, str) else
key.ascii_armored_key.decode('utf-8') }}</pre>
{# TODO: Add download button for the ASCII armored key #}
{% endblock content %}
diff --git a/atr/util.py b/atr/util.py
index 477c767..f25f9e9 100644
--- a/atr/util.py
+++ b/atr/util.py
@@ -280,6 +280,39 @@ def email_from_uid(uid: str) -> str | None:
return None
+async def email_to_uid_map() -> dict[str, str]:
+ def get(entry: dict, prop: str) -> str | None:
+ if prop in entry:
+ values = entry[prop]
+ if values:
+ return values[0]
+ return None
+
+ # Get all email addresses in LDAP
+ conf = config.AppConfig()
+ bind_dn = conf.LDAP_BIND_DN
+ bind_password = conf.LDAP_BIND_PASSWORD
+ ldap_params = ldap.SearchParameters(
+ uid_query="*",
+ bind_dn_from_config=bind_dn,
+ bind_password_from_config=bind_password,
+ email_only=True,
+ )
+ await asyncio.to_thread(ldap.search, ldap_params)
+
+ # Map the LDAP addresses to Apache UIDs
+ email_to_uid = {}
+ for entry in ldap_params.results_list:
+ uid = entry.get("uid", [""])[0]
+ if mail := get(entry, "mail"):
+ email_to_uid[mail] = uid
+ if alt_email := get(entry, "asf-altEmail"):
+ email_to_uid[alt_email] = uid
+ if committer_email := get(entry, "asf-committer-email"):
+ email_to_uid[committer_email] = uid
+ return email_to_uid
+
+
async def file_sha3(path: str) -> str:
"""Compute SHA3-256 hash of a file."""
sha3 = hashlib.sha3_256()
diff --git a/scripts/keys_import.py b/scripts/keys_import.py
index 7e3f73c..c2db36d 100644
--- a/scripts/keys_import.py
+++ b/scripts/keys_import.py
@@ -13,7 +13,6 @@ sys.path.append(".")
import atr.config as config
import atr.db as db
import atr.db.interaction as interaction
-import atr.ldap as ldap
import atr.util as util
@@ -57,30 +56,10 @@ async def keys_import(conf: config.AppConfig) -> None:
# Get all email addresses in LDAP
# We'll discard them when we're finished
- conf = config.AppConfig()
- bind_dn = conf.LDAP_BIND_DN
- bind_password = conf.LDAP_BIND_PASSWORD
- ldap_params = ldap.SearchParameters(
- uid_query="*",
- bind_dn_from_config=bind_dn,
- bind_password_from_config=bind_password,
- email_only=True,
- )
start = time.perf_counter_ns()
- await asyncio.to_thread(ldap.search, ldap_params)
+ email_to_uid = await util.email_to_uid_map()
end = time.perf_counter_ns()
write(f"LDAP search took {(end - start) / 1000000} ms")
-
- # Map the LDAP addresses to Apache UIDs
- email_to_uid = {}
- for entry in ldap_params.results_list:
- uid = entry.get("uid", [""])[0]
- if mail := get(entry, "mail"):
- email_to_uid[mail] = uid
- if alt_email := get(entry, "asf-altEmail"):
- email_to_uid[alt_email] = uid
- if committer_email := get(entry, "asf-committer-email"):
- email_to_uid[committer_email] = uid
write(f"Email addresses from LDAP: {len(email_to_uid)}")
# Open an ATR database connection
@@ -104,7 +83,7 @@ async def keys_import(conf: config.AppConfig) -> None:
# Then we could use the bulk upsert query method
try:
_result, yes, no, _committees = await
interaction.upload_keys_bytes(
- [committee_name], content, [committee_name],
ldap_data=email_to_uid
+ [committee_name], content, [committee_name],
ldap_data=email_to_uid, update_existing=True
)
except Exception as e:
write(f"{committee_name} error: {e}")
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]