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 a44adfc Add a URL input tab to the KEYS upload form
a44adfc is described below
commit a44adfcdb9c8d17cad48a7d0d5bf4adcebd39bec
Author: Sean B. Palmer <[email protected]>
AuthorDate: Tue Jun 17 15:34:27 2025 +0100
Add a URL input tab to the KEYS upload form
---
atr/routes/keys.py | 97 +++++++++++++++++++++++++++++++++---------
atr/templates/keys-upload.html | 47 ++++++++++++++++++--
2 files changed, 122 insertions(+), 22 deletions(-)
diff --git a/atr/routes/keys.py b/atr/routes/keys.py
index ab86ce0..127497c 100644
--- a/atr/routes/keys.py
+++ b/atr/routes/keys.py
@@ -26,11 +26,12 @@ import logging
import logging.handlers
import pathlib
import textwrap
-from collections.abc import Sequence
+from collections.abc import Awaitable, Callable, Sequence
import aiofiles.os
import asfquart as asfquart
import asfquart.base as base
+import httpx
import quart
import sqlmodel
import werkzeug.datastructures as datastructures
@@ -68,6 +69,56 @@ class UpdateCommitteeKeysForm(util.QuartFormTyped):
submit = wtforms.SubmitField("Update KEYS file")
+class UploadKeyFormBase(util.QuartFormTyped):
+ key = wtforms.FileField(
+ "KEYS file",
+ validators=[wtforms.validators.Optional()],
+ description=(
+ "Upload a KEYS file containing multiple PGP public keys."
+ " The file should contain keys in ASCII-armored format, starting
with"
+ ' "-----BEGIN PGP PUBLIC KEY BLOCK-----".'
+ ),
+ )
+ keys_url = wtforms.URLField(
+ "KEYS file URL",
+ validators=[wtforms.validators.Optional(), wtforms.validators.URL()],
+ render_kw={"placeholder": "Enter URL to KEYS file"},
+ description="Enter a URL to a KEYS file. This will be fetched by the
server.",
+ )
+ submit = wtforms.SubmitField("Upload KEYS file")
+ selected_committees = wtforms.SelectMultipleField(
+ "Associate keys with committees",
+ choices=[(c.name, c.display_name) for c in [] if (not
util.committee_is_standing(c.name))],
+ coerce=str,
+ option_widget=wtforms.widgets.CheckboxInput(),
+ widget=wtforms.widgets.ListWidget(prefix_label=False),
+ validators=[wtforms.validators.InputRequired("You must select at least
one committee")],
+ description=(
+ "Select the committees with which to associate these keys. You
must be a member of the selected committees."
+ ),
+ )
+
+ async def validate(self, extra_validators: dict | None = None) -> bool:
+ """Ensure that either a file is uploaded or a URL is provided, but not
both."""
+ if not await super().validate(extra_validators):
+ return False
+ if not self.key.data and not self.keys_url.data:
+ msg = "Either a file or a URL is required."
+ if self.key.errors and isinstance(self.key.errors, list):
+ self.key.errors.append(msg)
+ else:
+ self.key.errors = [msg]
+ return False
+ if self.key.data and self.keys_url.data:
+ msg = "Provide either a file or a URL, not both."
+ if self.key.errors and isinstance(self.key.errors, list):
+ self.key.errors.append(msg)
+ else:
+ self.key.errors = [msg]
+ return False
+ return True
+
+
@routes.committer("/keys/add", methods=["GET", "POST"])
async def add(session: routes.CommitterSession) -> str:
"""Add a new public signing key to the user's account."""
@@ -426,7 +477,7 @@ async def update_committee_keys(session:
routes.CommitterSession, committee_name
return await session.redirect(keys)
[email protected]("/keys/upload", methods=["GET", "POST"])
[email protected]("/keys/upload", methods=["GET", "POST"],
measure_performance=False)
async def upload(session: routes.CommitterSession) -> str:
"""Upload a KEYS file containing multiple GPG keys."""
# Get committees for all projects the user is a member of
@@ -434,17 +485,7 @@ async def upload(session: routes.CommitterSession) -> str:
project_list = session.committees + session.projects
user_committees = await data.committee(name_in=project_list).all()
- class UploadKeyForm(util.QuartFormTyped):
- key = wtforms.FileField(
- "KEYS file",
- validators=[wtforms.validators.InputRequired("A KEYS file is
required")],
- description=(
- "Upload a KEYS file containing multiple PGP public keys."
- " The file should contain keys in ASCII-armored format,
starting with"
- ' "-----BEGIN PGP PUBLIC KEY BLOCK-----".'
- ),
- )
- submit = wtforms.SubmitField("Upload KEYS file")
+ class UploadKeyForm(UploadKeyFormBase):
selected_committees = wtforms.SelectMultipleField(
"Associate keys with committees",
choices=[(c.name, c.display_name) for c in user_committees if (not
util.committee_is_standing(c.name))],
@@ -487,9 +528,18 @@ async def upload(session: routes.CommitterSession) -> str:
)
if await form.validate_on_submit():
- key_file = form.key.data
- if not isinstance(key_file, datastructures.FileStorage):
- return await render(error="Invalid file upload")
+ keys_text = ""
+ if form.key.data:
+ key_file = form.key.data
+ if not isinstance(key_file, datastructures.FileStorage):
+ return await render(error="Invalid file upload")
+ keys_content = await asyncio.to_thread(key_file.read)
+ keys_text = keys_content.decode("utf-8", errors="replace")
+ elif form.keys_url.data:
+ keys_text = await _get_keys_text(form.keys_url.data, render)
+
+ if not keys_text:
+ return await render(error="No KEYS data found.")
# Get selected committee list from the form
selected_committees = form.selected_committees.data
@@ -497,9 +547,6 @@ async def upload(session: routes.CommitterSession) -> str:
return await render(error="You must select at least one committee")
# This is a KEYS file of multiple GPG keys
# We need to parse it and add each key to the user's account
- keys_content = await asyncio.to_thread(key_file.read)
- keys_text = keys_content.decode("utf-8", errors="replace")
-
try:
upload_results, success_count, error_count, submitted_committees =
await interaction.upload_keys(
project_list, keys_text, selected_committees
@@ -522,6 +569,18 @@ async def upload(session: routes.CommitterSession) -> str:
return await render()
+async def _get_keys_text(keys_url: str, render: Callable[[str],
Awaitable[str]]) -> str:
+ try:
+ async with httpx.AsyncClient() as client:
+ response = await client.get(keys_url, follow_redirects=True)
+ response.raise_for_status()
+ return response.text
+ except httpx.HTTPStatusError as e:
+ raise base.ASFQuartException(f"Error fetching URL:
{e.response.status_code} {e.response.reason_phrase}")
+ except httpx.RequestError as e:
+ raise base.ASFQuartException(f"Error fetching URL: {e}")
+
+
async def _write_keys_file(
project: models.Project,
base_finished_dir: pathlib.Path,
diff --git a/atr/templates/keys-upload.html b/atr/templates/keys-upload.html
index 34743e0..7d36a86 100644
--- a/atr/templates/keys-upload.html
+++ b/atr/templates/keys-upload.html
@@ -153,9 +153,50 @@
<div class="row mb-3 pb-3 border-bottom">
{{ forms.label(form.key, col="md2") }}
<div class="col-md-9">
- {{ forms.widget(form.key, id=form.key.id) }}
- {{ forms.errors(form.key, classes="invalid-feedback d-block") }}
- {{ forms.description(form.key, classes="form-text text-muted mt-2")
}}
+ <ul class="nav nav-tabs" id="keysUploadTab" role="tablist">
+ <li class="nav-item" role="presentation">
+ <button class="nav-link active"
+ id="file-upload-tab"
+ data-bs-toggle="tab"
+ data-bs-target="#file-upload-pane"
+ type="button"
+ role="tab"
+ aria-controls="file-upload-pane"
+ aria-selected="true">Upload from file</button>
+ </li>
+ <li class="nav-item" role="presentation">
+ <button class="nav-link"
+ id="url-upload-tab"
+ data-bs-toggle="tab"
+ data-bs-target="#url-upload-pane"
+ type="button"
+ role="tab"
+ aria-controls="url-upload-pane"
+ aria-selected="false">Upload from URL</button>
+ </li>
+ </ul>
+ <div class="tab-content" id="keysUploadTabContent">
+ <div class="tab-pane fade show active"
+ id="file-upload-pane"
+ role="tabpanel"
+ aria-labelledby="file-upload-tab">
+ <div class="pt-3">
+ {{ forms.widget(form.key, id=form.key.id) }}
+ {{ forms.errors(form.key, classes="invalid-feedback d-block")
}}
+ {{ forms.description(form.key, classes="form-text text-muted
mt-2") }}
+ </div>
+ </div>
+ <div class="tab-pane fade"
+ id="url-upload-pane"
+ role="tabpanel"
+ aria-labelledby="url-upload-tab">
+ <div class="pt-3">
+ {{ forms.widget(form.keys_url, classes="form-control") }}
+ {{ forms.errors(form.keys_url, classes="invalid-feedback
d-block") }}
+ {{ forms.description(form.keys_url, classes="form-text
text-muted mt-2") }}
+ </div>
+ </div>
+ </div>
</div>
</div>
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]