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 090b7ac Allow signing key committee associations to be updated by the
owner
090b7ac is described below
commit 090b7ac91b143de01cf76a3865ae0c09c1bb18b2
Author: Sean B. Palmer <[email protected]>
AuthorDate: Wed Jun 18 15:21:21 2025 +0100
Allow signing key committee associations to be updated by the owner
---
atr/routes/keys.py | 96 +++++++++++++++++++++++++++++++---------
atr/templates/keys-review.html | 4 +-
atr/templates/keys-show-gpg.html | 28 +++++++++---
3 files changed, 100 insertions(+), 28 deletions(-)
diff --git a/atr/routes/keys.py b/atr/routes/keys.py
index baa5de8..5d2db99 100644
--- a/atr/routes/keys.py
+++ b/atr/routes/keys.py
@@ -255,36 +255,63 @@ async def delete(session: routes.CommitterSession) ->
response.Response:
return await session.redirect(keys, error="Key not found or not
owned by you")
[email protected]("/keys/details/<fingerprint>", methods=["GET"])
-async def details(session: routes.CommitterSession, fingerprint: str) -> str:
[email protected]("/keys/details/<fingerprint>", methods=["GET", "POST"])
+async def details(session: routes.CommitterSession, fingerprint: str) -> str |
response.Response:
"""Display details for a specific GPG key."""
async with db.session() as data:
- key = await data.public_signing_key(fingerprint=fingerprint,
_committees=True).get()
+ key, is_owner = await _key_and_is_owner(data, session, fingerprint)
+ form = None
+ if is_owner:
+ project_list = session.committees + session.projects
+ user_committees = await data.committee(name_in=project_list).all()
+ committee_choices = [(c.name, c.display_name or c.name) for c in
user_committees]
+
+ class UpdateKeyCommitteesForm(util.QuartFormTyped):
+ selected_committees = wtforms.SelectMultipleField(
+ "Associated PMCs",
+ coerce=str,
+ choices=committee_choices,
+ option_widget=wtforms.widgets.CheckboxInput(),
+ widget=wtforms.widgets.ListWidget(prefix_label=False),
+ description="Select the committees associated with this
key.",
+ )
+ submit = wtforms.SubmitField("Update associations")
+
+ form = await UpdateKeyCommitteesForm.create_form(
+ data=await quart.request.form if (quart.request.method ==
"POST") else None
+ )
- if not key:
- quart.abort(404, description="GPG key not found")
- key.committees.sort(key=lambda c: c.name)
+ if quart.request.method == "GET":
+ form.selected_committees.data = [c.name for c in
key.committees]
- authorised = False
- if key.apache_uid == session.uid:
- authorised = True
- else:
- user_affiliations = set(session.committees + session.projects)
- # async with db.session() as data:
- # key_committees = await data.execute(
- #
sqlmodel.select(models.KeyLink.committee_name).where(models.KeyLink.key_fingerprint
== fingerprint)
- # )
- # key_committee_names = {row[0] for row in key_committees.all()}
- key_committee_names = {c.name for c in key.committees}
- if user_affiliations.intersection(key_committee_names):
- authorised = True
+ if form and await form.validate_on_submit():
+ async with db.session() as data:
+ async with data.begin():
+ key = await data.public_signing_key(fingerprint=fingerprint,
_committees=True).get()
+ if not key:
+ quart.abort(404, description="GPG key not found")
- if not authorised:
- quart.abort(403, description="You are not authorised to view this key")
+ selected_committee_names = form.selected_committees.data or []
+ old_committee_names = {c.name for c in key.committees}
+
+ new_committees = await
data.committee(name_in=selected_committee_names).all()
+ key.committees = list(new_committees)
+ data.add(key)
+
+ affected_committee_names =
old_committee_names.union(set(selected_committee_names))
+ if affected_committee_names:
+ affected_committees = await
data.committee(name_in=list(affected_committee_names)).all()
+ for committee in affected_committees:
+ await autogenerate_keys_file(committee.name,
committee.is_podling, caller_data=data)
+
+ await quart.flash("Key committee associations updated
successfully.", "success")
+ return await session.redirect(details, fingerprint=fingerprint)
return await template.render(
+ # TODO: Rename to keys-details.html
"keys-show-gpg.html",
key=key,
+ form=form,
algorithms=routes.algorithms,
now=datetime.datetime.now(datetime.UTC),
asf_id=session.uid,
@@ -609,6 +636,31 @@ async def _get_keys_text(keys_url: str, render:
Callable[[str], Awaitable[str]])
raise base.ASFQuartException(f"Error fetching URL: {e}")
+async def _key_and_is_owner(
+ data: db.Session, session: routes.CommitterSession, fingerprint: str
+) -> tuple[models.PublicSigningKey, bool]:
+ key = await data.public_signing_key(fingerprint=fingerprint,
_committees=True).get()
+ if not key:
+ quart.abort(404, description="GPG key not found")
+ key.committees.sort(key=lambda c: c.name)
+
+ # Allow owners and committee members to view the key
+ authorised = False
+ is_owner = key.apache_uid == session.uid
+ if is_owner:
+ authorised = True
+ else:
+ user_affiliations = set(session.committees + session.projects)
+ key_committee_names = {c.name for c in key.committees}
+ if user_affiliations.intersection(key_committee_names):
+ authorised = True
+
+ if not authorised:
+ quart.abort(403, description="You are not authorised to view this key")
+
+ return key, is_owner
+
+
async def _keys_formatter(committee_name: str, data: db.Session) -> str:
committee = await data.committee(name=committee_name,
_public_signing_keys=True, _projects=True).demand(
base.ASFQuartException(f"Committee {committee_name} not found",
errorcode=404)
@@ -619,7 +671,7 @@ async def _keys_formatter(committee_name: str, data:
db.Session) -> str:
f"No keys found for committee {committee_name} to generate KEYS
file.", errorcode=404
)
- if not committee.projects:
+ if (not committee.projects) and (committee.name != "incubator"):
raise base.ASFQuartException(f"No projects found associated with
committee {committee_name}.", errorcode=404)
sorted_keys = sorted(committee.public_signing_keys, key=lambda k:
k.fingerprint)
diff --git a/atr/templates/keys-review.html b/atr/templates/keys-review.html
index 0edbd38..84149a3 100644
--- a/atr/templates/keys-review.html
+++ b/atr/templates/keys-review.html
@@ -42,7 +42,9 @@
<tbody>
<tr>
<th class="p-2 text-dark">Fingerprint</th>
- <td class="text-break">{{ key.fingerprint }}</td>
+ <td class="text-break">
+ <a href="{{ as_url(routes.keys.details,
fingerprint=key.fingerprint) }}">{{ key.fingerprint }}</a>
+ </td>
</tr>
<tr>
<th class="p-2 text-dark">Type</th>
diff --git a/atr/templates/keys-show-gpg.html b/atr/templates/keys-show-gpg.html
index 94cff5d..fe3e388 100644
--- a/atr/templates/keys-show-gpg.html
+++ b/atr/templates/keys-show-gpg.html
@@ -74,12 +74,30 @@
<td class="text-break">{{ key.apache_uid }}</td>
</tr>
<tr>
- <th class="p-2 text-dark">Associated PMCs</th>
- <td class="text-break">
- {% if key.committees %}
- {{ key.committees|map(attribute='name') |join(', ') }}
+ <th class="p-2 text-dark align-top">Associated PMCs</th>
+ <td class="text-break pt-2">
+ {% if form %}
+ <form method="post" novalidate>
+ {{ form.hidden_tag() }}
+ <div class="row">
+ {% for subfield in form.selected_committees %}
+ <div class="col-sm-12 col-md-6 col-lg-4">
+ <div class="form-check mb-2">
+ {{ forms.widget(subfield, classes="form-check-input")
}}
+ {{ forms.label(subfield, classes="form-check-label") }}
+ </div>
+ </div>
+ {% endfor %}
+ </div>
+ {{ forms.errors(form.selected_committees,
classes="invalid-feedback d-block") }}
+ <div class="mt-3">{{ form.submit(class_='btn btn-primary
btn-sm') }}</div>
+ </form>
{% else %}
- No PMCs associated
+ {% if key.committees %}
+ {{ key.committees|map(attribute='name') |join(', ') }}
+ {% else %}
+ No PMCs associated
+ {% endif %}
{% endif %}
</td>
</tr>
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]