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 1dbe129 Avoid failing a KEYS upload when any key is already associated
1dbe129 is described below
commit 1dbe129ceca8c6d1cdbcbe5f9c8cf3c32236fd50
Author: Sean B. Palmer <[email protected]>
AuthorDate: Thu Apr 24 16:51:16 2025 +0100
Avoid failing a KEYS upload when any key is already associated
---
atr/routes/keys.py | 153 +++++++++++++++++++++++++++--------
atr/templates/keys-upload.html | 177 ++++++++++++++++++++++++++++-------------
2 files changed, 244 insertions(+), 86 deletions(-)
diff --git a/atr/routes/keys.py b/atr/routes/keys.py
index 6f51c44..97e4173 100644
--- a/atr/routes/keys.py
+++ b/atr/routes/keys.py
@@ -32,6 +32,7 @@ from collections.abc import AsyncGenerator, Sequence
import asfquart as asfquart
import gnupg
import quart
+import sqlmodel
import werkzeug.datastructures as datastructures
import werkzeug.wrappers.response as response
import wtforms
@@ -256,41 +257,80 @@ async def key_user_session_add(
raise routes.FlashError("Invalid key fingerprint")
fingerprint = fingerprint.lower()
uids = key.get("uids")
+ key_record: models.PublicSigningKey | None = None
+
async with data.begin():
- if existing := await data.public_signing_key(fingerprint=fingerprint,
apache_uid=asf_uid).get():
- raise routes.FlashError(f"Key already exists:
{existing.fingerprint}")
- created = datetime.datetime.fromtimestamp(int(key["date"]),
tz=datetime.UTC)
- expires = datetime.datetime.fromtimestamp(int(key["expires"]),
tz=datetime.UTC) if key.get("expires") else None
- # Create new key record
- key_record = models.PublicSigningKey(
- fingerprint=fingerprint,
- algorithm=int(key["algo"]),
- length=int(key.get("length", "0")),
- created=created,
- expires=expires,
- declared_uid=uids[0] if uids else None,
- apache_uid=asf_uid,
- ascii_armored_key=public_key,
- )
- data.add(key_record)
+ existing = await data.public_signing_key(fingerprint=fingerprint,
apache_uid=asf_uid).get()
+
+ if existing:
+ logging.info(f"Found existing key {fingerprint}, updating
associations")
+ key_record = existing
+ else:
+ # Key doesn't exist, create it
+ logging.info(f"Adding new key {fingerprint}")
+ created = datetime.datetime.fromtimestamp(int(key["date"]),
tz=datetime.UTC)
+ expires = (
+ datetime.datetime.fromtimestamp(int(key["expires"]),
tz=datetime.UTC) if key.get("expires") else None
+ )
+
+ key_record = models.PublicSigningKey(
+ fingerprint=fingerprint,
+ algorithm=int(key["algo"]),
+ length=int(key.get("length", "0")),
+ created=created,
+ expires=expires,
+ declared_uid=uids[0] if uids else None,
+ apache_uid=asf_uid,
+ ascii_armored_key=public_key,
+ )
+ data.add(key_record)
+ await data.flush()
+ await data.refresh(key_record)
+
+ # Safety check, in case of strange flushes
+ if not key_record:
+ raise RuntimeError(f"Failed to obtain valid key record for
fingerprint {fingerprint}")
- # Link key to selected PMCs
+ # Link key to selected PMCs and track status for each
+ committee_statuses: dict[str, str] = {}
for committee_name in selected_committees:
committee = await data.committee(name=committee_name).get()
if committee and committee.id:
- link = models.KeyLink(committee_id=committee.id,
key_fingerprint=key_record.fingerprint)
- data.add(link)
+ # Check whether the link already exists
+ link_exists = await data.execute(
+ sqlmodel.select(models.KeyLink).where(
+ models.KeyLink.committee_id == committee.id,
+ models.KeyLink.key_fingerprint ==
key_record.fingerprint,
+ )
+ )
+ if link_exists.scalar_one_or_none() is None:
+ committee_statuses[committee_name] = "newly_linked"
+ # Link doesn't exist, create it
+ logging.debug(f"Linking key {fingerprint} to committee
{committee_name}")
+ link = models.KeyLink(committee_id=committee.id,
key_fingerprint=key_record.fingerprint)
+ data.add(link)
+ else:
+ committee_statuses[committee_name] = "already_linked"
+ logging.debug(f"Link already exists for key {fingerprint}
and committee {committee_name}")
else:
- # TODO: Log? Add to "error"?
+ logging.warning(f"Could not find committee {committee_name} to
link key {fingerprint}")
continue
+ # Extract email for sorting
+ user_id_str = key_record.declared_uid or ""
+ email_match = re.search(r"<([^>]+)>", user_id_str)
+ email = email_match.group(1) if email_match else user_id_str
+
return {
- "key_id": key["keyid"],
- "fingerprint": key["fingerprint"].lower() if key.get("fingerprint")
else "Unknown",
- "user_id": key["uids"][0] if key.get("uids") else "Unknown",
- "creation_date": created,
- "expiration_date": expires,
+ "key_id": key_record.fingerprint[:16],
+ "fingerprint": key_record.fingerprint,
+ "user_id": user_id_str,
+ "email": email,
+ "creation_date": key_record.created,
+ "expiration_date": key_record.expires,
"data": pprint.pformat(key),
+ "committee_statuses": committee_statuses,
+ "status": "success",
}
@@ -368,18 +408,30 @@ async def upload(session: routes.CommitterSession) -> str:
form = await UploadKeyForm.create_form()
results: list[dict] = []
+ submitted_committees: list[str] | None = None
- async def render(error: str | None = None) -> str:
+ async def render(
+ error: str | None = None,
+ submitted_committees_list: list[str] | None = None,
+ all_user_committees: Sequence[models.Committee] | None = None,
+ ) -> str:
# For easier happy pathing
if error is not None:
await quart.flash(error, "error")
+
+ # Determine which committee list to use
+ current_committees = all_user_committees if (all_user_committees is
not None) else user_committees
+ committee_map = {c.name: c.display_name for c in current_committees}
+
return await quart.render_template(
"keys-upload.html",
asf_id=session.uid,
- user_committees=user_committees,
+ user_committees=current_committees,
+ committee_map=committee_map,
form=form,
results=results,
algorithms=routes.algorithms,
+ submitted_committees=submitted_committees_list,
)
if await form.validate_on_submit():
@@ -405,6 +457,9 @@ async def upload(session: routes.CommitterSession) -> str:
if invalid_committees:
return await render(error=f"Invalid committee selection: {',
'.join(invalid_committees)}")
+ # TODO: Do we modify this? Store a copy just in case, for the template
to use
+ submitted_committees = selected_committees[:]
+
# Process each key block
results = await _upload_process_key_blocks(key_blocks,
selected_committees)
if not results:
@@ -416,6 +471,8 @@ async def upload(session: routes.CommitterSession) -> str:
f"Processed {len(results)} keys: {success_count} successful,
{error_count} failed",
"success" if success_count > 0 else "error",
)
+ return await render(submitted_committees_list=submitted_committees,
all_user_committees=user_committees)
+
return await render()
@@ -495,19 +552,51 @@ async def _upload_process_key_blocks(key_blocks:
list[str], selected_committees:
try:
key_info = await key_user_add(None, key_block, selected_committees)
if key_info:
- key_info["status"] = "success"
- key_info["message"] = "Key added successfully"
+ key_info["status"] = key_info.get("status", "success")
+ key_info["email"] = key_info.get("email", "Unknown")
+ key_info["committee_statuses"] =
key_info.get("committee_statuses", {})
results.append(key_info)
+ else:
+ # Handle case where key_user_add might return None
+ results.append(
+ {
+ "status": "error",
+ "message": "Failed to process key (key_user_add
returned None)",
+ "key_id": f"Key #{i + 1}",
+ "fingerprint": "Unknown",
+ "user_id": "Unknown",
+ "email": "Unknown",
+ "committee_statuses": {},
+ }
+ )
+ except routes.FlashError as e:
+ logging.warning(f"FlashError processing key #{i + 1}: {e}")
+ results.append(
+ {
+ "status": "error",
+ "message": f"Validation Error: {e}",
+ "key_id": f"Key #{i + 1}",
+ "fingerprint": "Invalid",
+ "user_id": "Unknown",
+ "email": "Unknown",
+ "committee_statuses": {},
+ }
+ )
except Exception as e:
- logging.exception("Exception adding key:")
+ logging.exception(f"Exception processing key #{i + 1}:")
results.append(
{
"status": "error",
- "message": f"Exception: {e}",
+ "message": f"Internal Exception: {e}",
"key_id": f"Key #{i + 1}",
"fingerprint": "Error",
"user_id": "Unknown",
+ "email": "Unknown",
+ "committee_statuses": {},
}
)
- return results
+ # Primary key is email, secondary key is fingerprint
+ results_sorted = sorted(results, key=lambda x: (x.get("email",
"").lower(), x.get("fingerprint", "")))
+
+ return results_sorted
diff --git a/atr/templates/keys-upload.html b/atr/templates/keys-upload.html
index 1a10a47..6a579a6 100644
--- a/atr/templates/keys-upload.html
+++ b/atr/templates/keys-upload.html
@@ -8,6 +8,74 @@
Upload a KEYS file containing multiple GPG public keys.
{% endblock description %}
+{% block stylesheets %}
+ {{ super() }}
+ <style>
+ .page-rotated-header {
+ height: 180px;
+ position: relative;
+ vertical-align: bottom;
+ padding-bottom: 5px;
+ width: 40px;
+ }
+
+ .page-rotated-header>div {
+ transform-origin: bottom left;
+ transform: translateX(25px) rotate(-90deg);
+ position: absolute;
+ bottom: 12px;
+ left: 6px;
+ white-space: nowrap;
+ text-align: left;
+ }
+
+ .table th,
+ .table td {
+ text-align: center;
+ vertical-align: middle;
+ }
+
+ .table td.page-key-details {
+ text-align: left;
+ font-family: ui-monospace, "SFMono-Regular", "Menlo", "Monaco",
"Consolas", monospace;
+ font-size: 0.9em;
+ word-break: break-all;
+ }
+
+ .page-status-cell-new {
+ background-color: #197a4e !important;
+ }
+
+ .page-status-cell-existing {
+ background-color: #868686 !important;
+ }
+
+ .page-status-cell-unknown {
+ background-color: #ffecb5 !important;
+ }
+
+ .page-status-cell-error {
+ background-color: #dc3545 !important;
+ }
+
+ .page-status-square {
+ display: inline-block;
+ width: 36px;
+ height: 36px;
+ vertical-align: middle;
+ }
+
+ .page-table-bordered th,
+ .page-table-bordered td {
+ border: 1px solid #dee2e6;
+ }
+
+ tbody tr {
+ height: 40px;
+ }
+ </style>
+{% endblock stylesheets %}
+
{% block content %}
<h1>Upload KEYS file</h1>
<p>Upload a KEYS file containing multiple GPG public keys.</p>
@@ -21,60 +89,61 @@
</div>
{% endif %}
- {% if results %}
+ {% if results and submitted_committees %}
<h2>KEYS processing results</h2>
- <p>The following keys were found in your KEYS file:</p>
+ <p>
+ The following keys were found in your KEYS file and processed against
the selected committees. Green squares indicate that a key was added, grey
squares indicate that a key already existed, and red squares indicate an error.
+ </p>
+ <div class="table-responsive">
+ <table class="table table-striped page-table-bordered table-sm mt-3">
+ <thead>
+ <tr>
+ <th scope="col">Fingerprint</th>
+ <th scope="col">User ID</th>
+ {% for committee_name in submitted_committees %}
+ <th scope="col" class="page-rotated-header">
+ <div>{{ committee_map.get(committee_name, committee_name)
}}</div>
+ </th>
+ {% endfor %}
+ </tr>
+ </thead>
+ <tbody>
+ {% for key_info in results %}
+ <tr>
+ <td class="page-key-details px-2">
+ <code>{{ key_info.fingerprint[:16]|upper }}</code>
+ </td>
+ <td class="page-key-details px-2">{{ key_info.email }}</td>
+ {% for committee_name in submitted_committees %}
+ {% set status =
key_info.committee_statuses.get(committee_name) %}
+ {% set cell_class = 'page-status-cell-error' if
key_info.status == 'error'
+ else 'page-status-cell-new' if status ==
'newly_linked'
+ else 'page-status-cell-existing' if status
== 'already_linked'
+ else 'page-status-cell-unknown' %}
+ {% set title_text = 'Error processing key' if key_info.status
== 'error'
+ else 'Newly linked' if status ==
'newly_linked'
+ else 'Already linked' if status ==
'already_linked'
+ else 'Unknown status' %}
+ <td class="text-center align-middle
page-status-cell-container">
+ <span class="page-status-square {{ cell_class }}" title="{{
title_text }}"></span>
+ </td>
+ {% endfor %}
+ </tr>
+ {% endfor %}
+ </tbody>
+ </table>
+ </div>
+
+ {% set processing_errors = results | selectattr('status', 'equalto',
'error') | list %}
+ {% if processing_errors %}
+ <h3 class="text-danger mt-4">Processing errors</h3>
+ {% for error_info in processing_errors %}
+ <div class="alert alert-danger p-2 mb-3">
+ <strong>{{ error_info.key_id }} / {{ error_info.fingerprint
}}</strong>: {{ error_info.message }}
+ </div>
+ {% endfor %}
+ {% endif %}
- {% for key_info in results %}
- {% if key_info.status == "success" %}
- {% set bg_class = "bg-light" %}
- {% else %}
- {% set bg_class = "bg-danger-subtle" %}
- {% endif %}
- <div class="my-3 p-3 rounded {{ bg_class }}">
- <h3 class="mt-0">
- {% if key_info.status == "success" %}
- Success: Added Key
- {% else %}
- Error: Failed to add key
- {% endif %}
- </h3>
- <dl class="row mb-0">
- <dt class="col-sm-3 fw-bold">Key ID</dt>
- <dd class="col-sm-9 mb-2">
- {{ key_info.key_id }}
- </dd>
- <dt class="col-sm-3 fw-bold">Fingerprint</dt>
- <dd class="col-sm-9 mb-2">
- {{ key_info.fingerprint }}
- </dd>
- <dt class="col-sm-3 fw-bold">User ID</dt>
- <dd class="col-sm-9 mb-2">
- {{ key_info.user_id }}
- </dd>
- {% if key_info.status == 'success' %}
- <dt class="col-sm-3 fw-bold">Created</dt>
- <dd class="col-sm-9 mb-2">
- {{ key_info.creation_date }}
- </dd>
- <dt class="col-sm-3 fw-bold">Expires</dt>
- <dd class="col-sm-9 mb-2">
- {{ key_info.expiration_date or 'Never' }}
- </dd>
- {% endif %}
- <dt class="col-sm-3 fw-bold">Status</dt>
- <dd class="col-sm-9 mb-2">
- {{ key_info.message }}
- </dd>
- {% if key_info.status == 'success' and key_info.data %}
- <details>
- <summary>Key Data</summary>
- <pre class="mb-0">{{ key_info.data }}</pre>
- </details>
- {% endif %}
- </dl>
- </div>
- {% endfor %}
{% endif %}
<form method="post"
@@ -101,7 +170,7 @@
{% if user_committees %}
<div class="row mb-3 pb-3 border-bottom">
- <div class="col-md-2 text-md-end fw-medium pt-2">Associate keys with
committee</div>
+ <div class="col-md-2 text-md-end fw-medium pt-2">Associate keys with
committees</div>
<div class="col-md-9">
<div class="row">
{% for value, label in form.selected_committees.choices %}
@@ -125,7 +194,7 @@
class="btn btn-sm btn-outline-secondary">Select
all</button>
</div>
<small class="form-text text-muted mt-2">
- Select the committee with which to associate these keys. You
must be a member of the selected committee.
+ Select the committees with which to associate these keys. You
must be a member of the selected committees.
</small>
{% if form.selected_committees.errors %}
<div class="invalid-feedback d-block">
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]