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 18d3e27 Use structured forms for adding and deleting keys
18d3e27 is described below
commit 18d3e27b1065ce0774810aa9190041ccf18028e7
Author: Sean B. Palmer <[email protected]>
AuthorDate: Wed Apr 2 15:28:19 2025 +0100
Use structured forms for adding and deleting keys
---
atr/routes/draft.py | 2 +-
atr/routes/keys.py | 65 ++++++++++--
atr/templates/includes/sidebar.html | 4 +-
atr/templates/keys-add.html | 190 +++++++++++++++++++++---------------
atr/templates/keys-review.html | 16 ++-
5 files changed, 183 insertions(+), 94 deletions(-)
diff --git a/atr/routes/draft.py b/atr/routes/draft.py
index f708e2c..508fd24 100644
--- a/atr/routes/draft.py
+++ b/atr/routes/draft.py
@@ -167,7 +167,7 @@ async def add_file(session: routes.CommitterSession,
project_name: str, version_
await _upload_files(project_name, version_name, file_name,
file_data)
return await session.redirect(
- review, success="File(s) added successfully",
project_name=project_name, version_name=version_name
+ review, success="File or files added successfully",
project_name=project_name, version_name=version_name
)
except Exception as e:
logging.exception("Error adding file(s):")
diff --git a/atr/routes/keys.py b/atr/routes/keys.py
index 13c9bb6..9dd86ed 100644
--- a/atr/routes/keys.py
+++ b/atr/routes/keys.py
@@ -35,6 +35,7 @@ import quart
import werkzeug.datastructures as datastructures
import werkzeug.wrappers.response as response
import wtforms
+from wtforms import widgets
import atr.db as db
import atr.db.models as models
@@ -42,6 +43,10 @@ import atr.routes as routes
import atr.util as util
+class DeleteKeyForm(util.QuartFormTyped):
+ submit = wtforms.SubmitField("Delete key")
+
+
class AddSSHKeyForm(util.QuartFormTyped):
key = wtforms.StringField("SSH key", widget=wtforms.widgets.TextArea())
submit = wtforms.SubmitField("Add SSH key")
@@ -247,20 +252,55 @@ async def add(session: routes.CommitterSession) -> str:
project_list = session.committees + session.projects
user_committees = await data.committee(name_in=project_list).all()
- if quart.request.method == "POST":
+ committee_choices = [(c.name, c.display_name or c.name) for c in
user_committees]
+
+ class AddGpgKeyForm(util.QuartFormTyped):
+ public_key = wtforms.TextAreaField(
+ "Public GPG key",
validators=[wtforms.validators.InputRequired("Public key is required")]
+ )
+ selected_committees = wtforms.SelectMultipleField(
+ "Associate key with committees",
+ validators=[wtforms.validators.InputRequired("You must select at
least one committee")],
+ coerce=str,
+ choices=committee_choices,
+ option_widget=widgets.CheckboxInput(),
+ widget=widgets.ListWidget(prefix_label=False),
+ )
+ submit = wtforms.SubmitField("Add GPG key")
+
+ form = await AddGpgKeyForm.create_form(data=await quart.request.form if
quart.request.method == "POST" else None)
+
+ if await form.validate_on_submit():
try:
- key_info = await key_add_post(session, quart.request,
user_committees)
+ public_key_data: str = util.unwrap(form.public_key.data)
+ selected_committees_data: list[str] =
util.unwrap(form.selected_committees.data)
+
+ invalid_committees = [
+ committee
+ for committee in selected_committees_data
+ if (committee not in session.committees) and (committee not in
session.projects)
+ ]
+ if invalid_committees:
+ raise routes.FlashError(f"Invalid PMC selection: {',
'.join(invalid_committees)}")
+
+ key_info = await key_user_add(session.uid, public_key_data,
selected_committees_data)
+ if key_info:
+ await quart.flash(f"GPG key {key_info.get('fingerprint', '')}
added successfully.", "success")
+ # Clear form data on success by creating a new empty form instance
+ form = await AddGpgKeyForm.create_form()
+
except routes.FlashError as e:
- logging.exception("FlashError:")
+ logging.warning("FlashError adding GPG key: %s", e)
await quart.flash(str(e), "error")
except Exception as e:
- logging.exception("Exception:")
- await quart.flash(f"Exception: {e}", "error")
+ logging.exception("Exception adding GPG key:")
+ await quart.flash(f"An unexpected error occurred: {e!s}", "error")
return await quart.render_template(
"keys-add.html",
asf_id=session.uid,
user_committees=user_committees,
+ form=form,
key_info=key_info,
algorithms=routes.algorithms,
)
@@ -268,11 +308,15 @@ async def add(session: routes.CommitterSession) -> str:
@routes.committer("/keys/delete", methods=["POST"])
async def delete(session: routes.CommitterSession) -> response.Response:
- """Delete a public signing key from the user's account."""
- form = await routes.get_form(quart.request)
- fingerprint = form.get("fingerprint")
+ """Delete a public signing key or SSH key from the user's account."""
+ form = await DeleteKeyForm.create_form(data=await quart.request.form)
+
+ if not await form.validate_on_submit():
+ return await session.redirect(review, error="Invalid request for key
deletion.")
+
+ fingerprint = (await quart.request.form).get("fingerprint")
if not fingerprint:
- return await session.redirect(review, error="No key fingerprint
provided")
+ return await session.redirect(review, error="Missing key fingerprint
for deletion.")
async with db.session() as data:
async with data.begin():
@@ -305,6 +349,8 @@ async def review(session: routes.CommitterSession) -> str:
status_message = quart.request.args.get("status_message")
status_type = quart.request.args.get("status_type")
+ delete_form = await DeleteKeyForm.create_form()
+
return await quart.render_template(
"keys-review.html",
asf_id=session.uid,
@@ -314,6 +360,7 @@ async def review(session: routes.CommitterSession) -> str:
status_message=status_message,
status_type=status_type,
now=datetime.datetime.now(datetime.UTC),
+ delete_form=delete_form,
)
diff --git a/atr/templates/includes/sidebar.html
b/atr/templates/includes/sidebar.html
index e756a6b..86834f4 100644
--- a/atr/templates/includes/sidebar.html
+++ b/atr/templates/includes/sidebar.html
@@ -85,13 +85,13 @@
<a href="{{ as_url(routes.keys.review) }}">Your signing keys</a>
</li>
<li>
- <a href="{{ as_url(routes.keys.add) }}">Add your signing key</a>
+ <a href="{{ as_url(routes.keys.add) }}">Add your GPG key</a>
</li>
<li>
<a href="{{ as_url(routes.keys.upload) }}">Upload a KEYS file</a>
</li>
<li>
- <a href="{{ as_url(routes.keys.ssh_add) }}">Add SSH key</a>
+ <a href="{{ as_url(routes.keys.ssh_add) }}">Add your SSH key</a>
</li>
</ul>
diff --git a/atr/templates/keys-add.html b/atr/templates/keys-add.html
index 5df0843..0dd5fbe 100644
--- a/atr/templates/keys-add.html
+++ b/atr/templates/keys-add.html
@@ -1,98 +1,132 @@
{% extends "layouts/base.html" %}
{% block title %}
- Add signing key ~ ATR
+ Add your GPG key ~ ATR
{% endblock title %}
{% block description %}
- Add a public signing key to your account.
+ Add your public signing key to your ATR account.
{% endblock description %}
{% block content %}
- <h1>Add signing key</h1>
- <p class="intro">Add your public key to use for signing release
artifacts.</p>
+ <div class="my-4">
+ <h1 class="mb-4">Add your GPG key</h1>
- <div class="user-info">
- <p>
- Welcome, <strong>{{ asf_id }}</strong>! You are authenticated as an ASF
committer.
- </p>
- </div>
-
- {% if key_info %}
- <h2>Key results</h2>
- <div class="mt-3 p-3 bg-light rounded">
- <h3 class="mt-0">Success: Added Key</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>
- <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>
- <dt class="col-sm-3 fw-bold">Key Data</dt>
- <dd class="col-sm-9 mb-2">
- <pre class="mb-0">{{ key_info.data }}</pre>
- </dd>
- </dl>
- </div>
- {% endif %}
+ <p>Add your public key to use for signing release artifacts.</p>
+ {% if form.errors %}<div class="alert alert-danger">Please correct the
errors below:</div>{% endif %}
- <form method="post" class="striking">
- <div class="mb-4">
- <div class="mb-3">
- <label for="public_key" class="form-label">Public key:</label>
- </div>
- <textarea id="public_key"
- name="public_key"
- class="form-control mb-2"
- rows="8"
- required
- placeholder="Paste your public key here (in ASCII-armored
format)"
- aria-describedby="key-help"></textarea>
- <small id="key-help" class="form-text text-muted">
- Your public key should be in ASCII-armored format, starting with
"-----BEGIN PGP PUBLIC KEY BLOCK-----"
- </small>
- </div>
+ <form method="post"
+ class="striking py-4 px-5"
+ action="{{ as_url(routes.keys.add) }}"
+ novalidate>
+ {{ form.hidden_tag() if form.hidden_tag }}
- {% if user_committees %}
<div class="mb-4">
- <div class="mb-3">
- <label class="form-label">Associate with projects:</label>
+ <div class="row mb-3 pb-3 border-bottom{% if form.public_key.errors %}
has-danger{% endif %}">
+ <div class="col-md-3 text-md-end fw-medium pt-2">{{
form.public_key.label }}</div>
+ <div class="col-md-9">
+ {{ form.public_key(class_='form-control font-monospace', rows=10,
placeholder='Paste your ASCII-armored public GPG key here...') }}
+ <small class="form-text text-muted">
+ Your public key should be in ASCII-armored format, starting with
"-----BEGIN PGP PUBLIC KEY BLOCK-----"
+ </small>
+ {% if form.public_key.errors %}
+ <div class="invalid-feedback d-block">
+ {% for error in form.public_key.errors %}{{ error }}{% endfor
%}
+ </div>
+ {% endif %}
+ </div>
</div>
- <div class="d-flex flex-wrap gap-3 mb-2">
- {% for committee in user_committees|sort(attribute='name') %}
- <div class="form-check d-flex align-items-center gap-2">
- <input type="checkbox"
- class="form-check-input"
- id="committee_{{ committee.name }}"
- name="selected_committees"
- value="{{ committee.name }}" />
- <label class="form-check-label mb-0" for="committee_{{
committee.name }}">{{ committee.display_name }}</label>
+
+ <div class="row mb-3 pb-3 border-bottom{% if
form.selected_committees.errors %} has-danger{% endif %}">
+ <div class="col-md-3 text-md-end fw-medium pt-2">{{
form.selected_committees.label }}</div>
+ <div class="col-md-9">
+ <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">
+ {{ subfield(class_='form-check-input') }}
+ {{ subfield.label(class_='form-check-label') }}
+ </div>
+ </div>
+ {% endfor %}
+ </div>
+ <div class="mt-2 mb-2">
+ <button type="button"
+ id="toggleCommitteesBtn"
+ class="btn btn-sm btn-outline-secondary">Select
all</button>
</div>
- {% endfor %}
+ <small class="form-text text-muted">Select the committees you are
a member of.</small>
+ {% if form.selected_committees.errors %}
+ <div class="invalid-feedback d-block">
+ {% for error in form.selected_committees.errors %}{{ error
}}{% endfor %}
+ </div>
+ {% endif %}
+ </div>
</div>
- <small class="form-text text-muted">You must associate your key with
at least one PMC of which you are a member.</small>
</div>
- {% else %}
- <div class="text-danger mt-1">
- <p>You must be a member of at least one PMC to add a signing key.</p>
+
+ <div class="mt-4">
+ {{ form.submit(class_='btn btn-primary') }}
+ <a href="{{ as_url(routes.keys.review) }}"
+ class="btn btn-link text-secondary">Cancel</a>
</div>
- {% endif %}
+ </form>
- <button type="submit" class="btn btn-primary">Add key</button>
- </form>
+ {% if key_info and key_info.status == 'success' %}
+ <div class="mt-5">
+ <h2 class="mb-3 fs-5">Key details added:</h2>
+ <div class="p-3 bg-light border rounded">
+ <p>
+ <strong>Key ID:</strong> {{ key_info.key_id }}
+ <br />
+ <strong>Fingerprint:</strong> <code>{{ key_info.fingerprint
}}</code>
+ <br />
+ <strong>User ID:</strong> {{ key_info.user_id }}
+ <br />
+ <strong>Created:</strong> {{
key_info.creation_date.strftime("%Y-%m-%d") }}
+ <br />
+ <strong>Expires:</strong> {{
key_info.expiration_date.strftime("%Y-%m-%d") if key_info.expiration_date else
'Never' }}
+ </p>
+ </div>
+ </div>
+ {% endif %}
+ </div>
{% endblock content %}
+
+{% block javascripts %}
+ {{ super() }}
+ <script>
+ document.addEventListener("DOMContentLoaded", () => {
+ const btn = document.getElementById("toggleCommitteesBtn");
+ const checkboxes =
document.querySelectorAll("input[name='selected_committees']");
+
+ if (!btn || checkboxes.length === 0) return;
+
+ function updateButtonText() {
+ let allChecked = true;
+ checkboxes.forEach(cb => {
+ if (!cb.checked) allChecked = false;
+ });
+ btn.textContent = allChecked ? "Select none" : "Select all";
+ }
+
+ btn.addEventListener("click", () => {
+ let allChecked = true;
+ checkboxes.forEach(cb => {
+ if (!cb.checked) allChecked = false;
+ });
+ const shouldCheck = !allChecked;
+ checkboxes.forEach(cb => {
+ cb.checked = shouldCheck;
+ });
+ updateButtonText();
+ });
+
+ checkboxes.forEach(cb => {
+ cb.addEventListener("change", updateButtonText);
+ });
+
+ updateButtonText();
+ });
+ </script>
+{% endblock javascripts %}
diff --git a/atr/templates/keys-review.html b/atr/templates/keys-review.html
index 41fbbb0..baefb3a 100644
--- a/atr/templates/keys-review.html
+++ b/atr/templates/keys-review.html
@@ -83,9 +83,13 @@
<pre class="mt-3">{{ key.ascii_armored_key }}</pre>
</details>
- <form method="post" action="{{ as_url(routes.keys.delete) }}"
class="mt-3">
+ <form method="post"
+ action="{{ as_url(routes.keys.delete) }}"
+ class="mt-3"
+ onsubmit="return confirm('Are you sure you want to delete
this GPG key?');">
+ {{ delete_form.hidden_tag() }}
<input type="hidden" name="fingerprint" value="{{
key.fingerprint }}" />
- <button type="submit" class="btn btn-danger">Delete key</button>
+ {{ delete_form.submit(class_='btn btn-danger', value='Delete
key') }}
</form>
</div>
{% endfor %}
@@ -125,9 +129,13 @@
<pre class="mt-3">{{ key.key }}</pre>
</details>
- <form method="post" action="{{ as_url(routes.keys.delete) }}"
class="mt-3">
+ <form method="post"
+ action="{{ as_url(routes.keys.delete) }}"
+ class="mt-3"
+ onsubmit="return confirm('Are you sure you want to delete
this SSH key?');">
+ {{ delete_form.hidden_tag() }}
<input type="hidden" name="fingerprint" value="{{
key.fingerprint }}" />
- <button type="submit" class="btn btn-danger">Delete key</button>
+ {{ delete_form.submit(class_='btn btn-danger', value='Delete
key') }}
</form>
</div>
{% endfor %}
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]