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 4301d9b  Allow admins to disassociate a committee with all of its keys
4301d9b is described below

commit 4301d9b737f5567047872788c77e536faa28b31f
Author: Sean B. Palmer <[email protected]>
AuthorDate: Mon May 26 19:24:56 2025 +0100

    Allow admins to disassociate a committee with all of its keys
---
 atr/blueprints/admin/admin.py            | 63 ++++++++++++++++++++++++++++++++
 atr/templates/delete-committee-keys.html | 36 ++++++++++++++++++
 2 files changed, 99 insertions(+)

diff --git a/atr/blueprints/admin/admin.py b/atr/blueprints/admin/admin.py
index 9c5cb8d..743c4d3 100644
--- a/atr/blueprints/admin/admin.py
+++ b/atr/blueprints/admin/admin.py
@@ -31,6 +31,7 @@ import asfquart.base as base
 import asfquart.session
 import httpx
 import quart
+import sqlalchemy.orm as orm
 import werkzeug.wrappers.response as response
 import wtforms
 
@@ -47,6 +48,16 @@ import atr.util as util
 _LOGGER: Final = logging.getLogger(__name__)
 
 
+class DeleteCommitteeKeysForm(util.QuartFormTyped):
+    committee_name = wtforms.SelectField("Committee", 
validators=[wtforms.validators.InputRequired()])
+    confirm_delete = wtforms.StringField(
+        "Confirmation",
+        validators=[wtforms.validators.InputRequired(), 
wtforms.validators.Regexp("^DELETE KEYS$")],
+        render_kw={"placeholder": "DELETE KEYS"},
+    )
+    submit = wtforms.SubmitField("Delete all keys for selected committee")
+
+
 class DeleteReleaseForm(util.QuartFormTyped):
     """Form for deleting releases."""
 
@@ -122,6 +133,58 @@ async def admin_data(model: str = "Committee") -> str:
         )
 
 
[email protected]("/delete-committee-keys", methods=["GET", "POST"])
+async def admin_delete_committee_keys() -> str | response.Response:
+    form = await DeleteCommitteeKeysForm.create_form()
+    async with db.session() as data:
+        all_committees = await 
data.committee(_public_signing_keys=True).order_by(models.Committee.name).all()
+        committees_with_keys = [c for c in all_committees if 
c.public_signing_keys]
+    form.committee_name.choices = [(c.name, c.display_name) for c in 
committees_with_keys]
+
+    if await form.validate_on_submit():
+        committee_name = form.committee_name.data
+        async with db.session() as data:
+            committee_query = data.committee(name=committee_name)
+            via = models.validate_instrumented_attribute
+            committee_query.query = committee_query.query.options(
+                
orm.selectinload(via(models.Committee.public_signing_keys)).selectinload(
+                    via(models.PublicSigningKey.committees)
+                )
+            )
+            committee = await committee_query.get()
+
+            if not committee:
+                await quart.flash(f"Committee '{committee_name}' not found.", 
"error")
+                return 
quart.redirect(quart.url_for("admin.admin_delete_committee_keys"))
+
+            keys_to_check = list(committee.public_signing_keys)
+            if not keys_to_check:
+                await quart.flash(f"Committee '{committee_name}' has no 
keys.", "info")
+                return 
quart.redirect(quart.url_for("admin.admin_delete_committee_keys"))
+
+            num_removed = len(committee.public_signing_keys)
+            committee.public_signing_keys.clear()
+            await data.flush()
+
+            unused_deleted = 0
+            for key_obj in keys_to_check:
+                if not key_obj.committees:
+                    await data.delete(key_obj)
+                    unused_deleted += 1
+
+            await data.commit()
+            await quart.flash(
+                f"Removed {num_removed} key links for '{committee_name}'. 
Deleted {unused_deleted} unused keys.",
+                "success",
+            )
+        return 
quart.redirect(quart.url_for("admin.admin_delete_committee_keys"))
+
+    elif quart.request.method == "POST":
+        await quart.flash("Form validation failed. Select committee and type 
DELETE KEYS.", "warning")
+
+    return await template.render("delete-committee-keys.html", form=form)
+
+
 @admin.BLUEPRINT.route("/delete-release", methods=["GET", "POST"])
 async def admin_delete_release() -> str | response.Response:
     """Page to delete selected releases and their associated data and files."""
diff --git a/atr/templates/delete-committee-keys.html 
b/atr/templates/delete-committee-keys.html
new file mode 100644
index 0000000..3d33141
--- /dev/null
+++ b/atr/templates/delete-committee-keys.html
@@ -0,0 +1,36 @@
+{% extends "layouts/base.html" %}
+
+{% import "macros/forms.html" as forms %}
+
+{% block title %}
+  Delete committee keys
+{% endblock title %}
+
+{% block content %}
+  <div class="container mx-auto p-4">
+    <h1 class="mb-4">Delete all keys for a committee</h1>
+
+    <form method="post"
+          action="{{ url_for('admin.admin_delete_committee_keys') }}"
+          class="atr-canary py-4 px-5 border rounded">
+      {{ form.csrf_token }}
+
+      <div class="mb-3">
+        {{ forms.label(form.committee_name) }}
+        {{ forms.widget(form.committee_name, classes="form-select", 
id=form.committee_name.id) }}
+        {{ forms.errors(form.committee_name) }}
+        {{ forms.description(form.committee_name) }}
+      </div>
+
+      <div class="mb-3">
+        {{ forms.label(form.confirm_delete) }}
+        {{ forms.widget(form.confirm_delete, classes="form-control", 
id=form.confirm_delete.id) }}
+        {{ forms.errors(form.confirm_delete) }}
+        {{ forms.description(form.confirm_delete) }}
+      </div>
+
+      <div class="mt-4">{{ form.submit(class_="btn btn-danger") }}</div>
+    </form>
+
+  </div>
+{% endblock content %}


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to