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]

Reply via email to