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-releases.git
The following commit(s) were added to refs/heads/main by this push:
new 4c3a067 Make the form to add an OpenPGP key more type safe
4c3a067 is described below
commit 4c3a06735c2cd629d3b4c5149ab423f353152a6a
Author: Sean B. Palmer <[email protected]>
AuthorDate: Tue Nov 11 18:33:14 2025 +0000
Make the form to add an OpenPGP key more type safe
---
atr/form.py | 1 +
atr/get/keys.py | 31 ++++++++++++++++++-
atr/post/keys.py | 42 ++++++++++++++++++++++++--
atr/shared/keys.py | 89 +++++++++---------------------------------------------
4 files changed, 85 insertions(+), 78 deletions(-)
diff --git a/atr/form.py b/atr/form.py
index d8fa7d2..0a2e5da 100644
--- a/atr/form.py
+++ b/atr/form.py
@@ -382,6 +382,7 @@ def to_int(v: Any) -> int:
def to_str_list(v: Any) -> list[str]:
+ # TODO: Might need to handle the empty case
if isinstance(v, list):
return [str(item) for item in v]
if isinstance(v, str):
diff --git a/atr/get/keys.py b/atr/get/keys.py
index 1dc725a..a9eb88d 100644
--- a/atr/get/keys.py
+++ b/atr/get/keys.py
@@ -21,6 +21,9 @@ import quart
import atr.blueprints.get as get
import atr.db as db
+import atr.form as form
+import atr.htm as htm
+import atr.post as post
import atr.shared as shared
import atr.storage as storage
import atr.template as template
@@ -31,7 +34,33 @@ import atr.web as web
@get.committer("/keys/add")
async def add(session: web.Committer) -> str:
"""Add a new public signing key to the user's account."""
- return await shared.keys.add(session)
+ async with storage.write() as write:
+ participant_of_committees = await write.participant_of_committees()
+
+ committee_choices = [(c.name, c.display_name or c.name) for c in
participant_of_committees]
+
+ page = htm.Block()
+ page.p[htm.a(href=util.as_url(keys), class_="atr-back-link")["← Back to
Manage keys"],]
+ page.div(class_="my-4")[
+ htm.h1(class_="mb-4")["Add your OpenPGP key"],
+ htm.p["Add your public key to use for signing release artifacts."],
+ ]
+ form.render_block(
+ page,
+ model_cls=shared.keys.AddOpenPGPKeyForm,
+ action=util.as_url(post.keys.add),
+ submit_label="Add OpenPGP key",
+ cancel_url=util.as_url(keys),
+ defaults={
+ "selected_committees": committee_choices,
+ },
+ )
+
+ return await template.blank(
+ "Add your OpenPGP key",
+ content=page.collect(),
+ description="Add your public signing key to your ATR account.",
+ )
@get.committer("/keys/details/<fingerprint>")
diff --git a/atr/post/keys.py b/atr/post/keys.py
index 0fcad5e..36054cb 100644
--- a/atr/post/keys.py
+++ b/atr/post/keys.py
@@ -20,6 +20,8 @@ import quart
import atr.blueprints.post as post
import atr.get as get
+import atr.htm as htm
+import atr.log as log
import atr.models.sql as sql
import atr.shared as shared
import atr.storage as storage
@@ -30,9 +32,45 @@ import atr.web as web
@post.committer("/keys/add")
-async def add(session: web.Committer) -> str:
[email protected](shared.keys.AddOpenPGPKeyForm)
+async def add(session: web.Committer, add_openpgp_key_form:
shared.keys.AddOpenPGPKeyForm) -> web.WerkzeugResponse:
"""Add a new public signing key to the user's account."""
- return await shared.keys.add(session)
+ try:
+ key_text = add_openpgp_key_form.public_key
+ selected_committee_names = add_openpgp_key_form.selected_committees
+
+ async with storage.write() as write:
+ wafc = write.as_foundation_committer()
+ ocr: outcome.Outcome[types.Key] = await
wafc.keys.ensure_stored_one(key_text)
+ key = ocr.result_or_raise()
+
+ for selected_committee_name in selected_committee_names:
+ wacp = write.as_committee_participant(selected_committee_name)
+ oc: outcome.Outcome[types.LinkedCommittee] = await
wacp.keys.associate_fingerprint(
+ key.key_model.fingerprint
+ )
+ oc.result_or_raise()
+
+ fingerprint_upper = key.key_model.fingerprint.upper()
+ if key.status == types.KeyStatus.PARSED:
+ details_url = util.as_url(get.keys.details,
fingerprint=key.key_model.fingerprint)
+ p = htm.p[
+ f"OpenPGP key {fingerprint_upper} was already in the
database. ",
+ htm.a(href=details_url)["View key details"],
+ ".",
+ ]
+ await quart.flash(str(p), "warning")
+ else:
+ await quart.flash(f"OpenPGP key {fingerprint_upper} added
successfully.", "success")
+
+ except web.FlashError as e:
+ log.warning("FlashError adding OpenPGP key: %s", e)
+ await quart.flash(str(e), "error")
+ except Exception as e:
+ log.exception("Error adding OpenPGP key:")
+ await quart.flash(f"An unexpected error occurred: {e!s}", "error")
+
+ return await session.redirect(get.keys.keys)
@post.committer("/keys/delete")
diff --git a/atr/shared/keys.py b/atr/shared/keys.py
index b576eca..8da536f 100644
--- a/atr/shared/keys.py
+++ b/atr/shared/keys.py
@@ -23,15 +23,15 @@ from collections.abc import Awaitable, Callable, Sequence
import aiohttp
import asfquart.base as base
+import pydantic
import quart
import werkzeug.datastructures as datastructures
import wtforms
import atr.db as db
+import atr.form as form
import atr.forms as forms
import atr.get as get
-import atr.htm as htm
-import atr.log as log
import atr.models.sql as sql
import atr.shared as shared
import atr.storage as storage
@@ -43,18 +43,22 @@ import atr.util as util
import atr.web as web
-class AddOpenPGPKeyForm(forms.Typed):
- public_key = forms.textarea(
+class AddOpenPGPKeyForm(form.Form):
+ public_key: str = form.label(
"Public OpenPGP key",
- placeholder="Paste your ASCII-armored public OpenPGP key here...",
- description="Your public key should be in ASCII-armored format,
starting with"
- ' "-----BEGIN PGP PUBLIC KEY BLOCK-----"',
+ 'Your public key should be in ASCII-armored format, starting with
"-----BEGIN PGP PUBLIC KEY BLOCK-----"',
+ widget=form.Widget.TEXTAREA,
)
- selected_committees = forms.checkboxes(
+ selected_committees: form.StrList = form.label(
"Associate key with committees",
- description="Select the committees with which to associate your key.",
+ "Select the committees with which to associate your key.",
)
- submit = forms.submit("Add OpenPGP key")
+
+ @pydantic.model_validator(mode="after")
+ def validate_at_least_one_committee(self) -> "AddOpenPGPKeyForm":
+ if not self.selected_committees:
+ raise ValueError("You must select at least one committee to
associate with this key")
+ return self
class AddSSHKeyForm(forms.Typed):
@@ -134,71 +138,6 @@ class UploadKeyFormBase(forms.Typed):
return True
-async def add(session: web.Committer) -> str:
- """Add a new public signing key to the user's account."""
- key_info = None
-
- async with storage.write() as write:
- participant_of_committees = await write.participant_of_committees()
-
- committee_choices: forms.Choices = [(c.name, c.display_name or c.name) for
c in participant_of_committees]
-
- form = await AddOpenPGPKeyForm.create_form(
- data=(await quart.request.form) if (quart.request.method == "POST")
else None
- )
- forms.choices(form.selected_committees, committee_choices)
-
- if await form.validate_on_submit():
- try:
- key_text: str = util.unwrap(form.public_key.data)
- selected_committee_names: list[str] =
util.unwrap(form.selected_committees.data)
-
- async with storage.write() as write:
- wafc = write.as_foundation_committer()
- ocr: outcome.Outcome[types.Key] = await
wafc.keys.ensure_stored_one(key_text)
- key = ocr.result_or_raise()
-
- for selected_committee_name in selected_committee_names:
- # TODO: Should this be committee member or committee
participant?
- # Also, should we emit warnings and continue here?
- wacp =
write.as_committee_participant(selected_committee_name)
- oc: outcome.Outcome[types.LinkedCommittee] = await
wacp.keys.associate_fingerprint(
- key.key_model.fingerprint
- )
- oc.result_or_raise()
-
- fingerprint_upper = key.key_model.fingerprint.upper()
- if key.status == types.KeyStatus.PARSED:
- details_url = util.as_url(get.keys.details,
fingerprint=key.key_model.fingerprint)
- p = htm.p[
- f"OpenPGP key {fingerprint_upper} was already in the
database. ",
- htm.a(href=details_url)["View key details"],
- ".",
- ]
- await quart.flash(str(p), "warning")
- else:
- await quart.flash(f"OpenPGP key {fingerprint_upper} added
successfully.", "success")
- # Clear form data on success by creating a new empty form instance
- form = await AddOpenPGPKeyForm.create_form()
- forms.choices(form.selected_committees, committee_choices)
-
- except web.FlashError as e:
- log.warning("FlashError adding OpenPGP key: %s", e)
- await quart.flash(str(e), "error")
- except Exception as e:
- log.exception("Error adding OpenPGP key:")
- await quart.flash(f"An unexpected error occurred: {e!s}", "error")
-
- return await template.render(
- "keys-add.html",
- asf_id=session.uid,
- user_committees=participant_of_committees,
- form=form,
- key_info=key_info,
- algorithms=shared.algorithms,
- )
-
-
async def details(session: web.Committer, fingerprint: str) -> str |
web.WerkzeugResponse:
"""Display details for a specific OpenPGP key."""
fingerprint = fingerprint.lower()
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]