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 6ff8c8e Add an API endpoint to upload a KEYS file
6ff8c8e is described below
commit 6ff8c8e65c93d6515e1bd00fbc0f334dc86b041f
Author: Sean B. Palmer <[email protected]>
AuthorDate: Wed Jul 16 19:19:39 2025 +0100
Add an API endpoint to upload a KEYS file
---
atr/blueprints/api/api.py | 51 +++++++++++++++++++++++++++++++++++++++--------
atr/db/__init__.py | 2 +-
atr/models/api.py | 25 ++++++++++++++++++++++-
atr/models/schema.py | 4 ++++
4 files changed, 72 insertions(+), 10 deletions(-)
diff --git a/atr/blueprints/api/api.py b/atr/blueprints/api/api.py
index 394415a..d3ebe9d 100644
--- a/atr/blueprints/api/api.py
+++ b/atr/blueprints/api/api.py
@@ -298,16 +298,14 @@ async def keys_endpoint(query_args: models.api.KeysQuery)
-> DictResponse:
@quart_schema.validate_response(models.api.KeysAddResults, 200)
async def keys_add(data: models.api.KeysAddArgs) -> DictResponse:
asf_uid = _jwt_asf_uid()
- selected_committee_names = []
- if data.committees:
- selected_committee_names[:] = data.committees.split(",")
+ selected_committee_names = data.committees
async with db.session() as db_data:
- member_of_committees = await
interaction.user_committees_member(asf_uid, caller_data=db_data)
+ participant_of_committees = await
interaction.user_committees_member(asf_uid, caller_data=db_data)
selected_committees = await
db_data.committee(name_in=selected_committee_names).all()
committee_is_podling = {c.name: c.is_podling for c in
selected_committees}
for committee in selected_committees:
- if committee not in member_of_committees:
+ if committee not in participant_of_committees:
raise exceptions.BadRequest(f"You are not a member of
committee {committee.name}")
added_keys = await interaction.key_user_add(asf_uid, data.key,
selected_committee_names)
fingerprints = []
@@ -337,9 +335,9 @@ async def keys_delete(data: models.api.KeysDeleteArgs) ->
DictResponse:
asf_uid = _jwt_asf_uid()
fingerprint = data.fingerprint.lower()
async with db.session() as db_data:
- key = await db_data.public_signing_key(fingerprint=fingerprint,
apache_uid=asf_uid, _committees=True).demand(
- exceptions.NotFound()
- )
+ key = await db_data.public_signing_key(fingerprint=fingerprint,
apache_uid=asf_uid, _committees=True).get()
+ if key is None:
+ raise ValueError(f"Key {fingerprint} not found")
await db_data.delete(key)
for committee in key.committees:
await keys.autogenerate_keys_file(committee.name,
committee.is_podling)
@@ -378,6 +376,43 @@ async def keys_get(fingerprint: str) -> DictResponse:
).model_dump(), 200
[email protected]("/keys/upload", methods=["POST"])
[email protected]
+@quart_schema.security_scheme([{"BearerAuth": []}])
+@quart_schema.validate_request(models.api.KeysUploadArgs)
+@quart_schema.validate_response(models.api.KeysUploadResults, 200)
+async def keys_upload(data: models.api.KeysUploadArgs) -> DictResponse:
+ asf_uid = _jwt_asf_uid()
+ filetext = data.filetext
+ selected_committee_names = data.committees
+ async with db.session() as db_data:
+ participant_of_committees = await
interaction.user_committees_participant(asf_uid, caller_data=db_data)
+ participant_of_committee_names = [c.name for c in
participant_of_committees]
+ for committee_name in selected_committee_names:
+ if committee_name not in participant_of_committee_names:
+ raise exceptions.BadRequest(f"You are not a participant of
committee {committee_name}")
+ # TODO: Does this export KEYS files?
+ # Appearently it does not
+ # This needs fixing in keys.py too
+ results, success_count, error_count, submitted_committees = await
interaction.upload_keys(
+ participant_of_committee_names, filetext, selected_committee_names
+ )
+
+ # TODO: Should push this much further upstream
+ import logging
+
+ for result in results:
+ logging.info(result)
+ results = [models.api.KeysUploadSubset(**result) for result in results]
+ return models.api.KeysUploadResults(
+ endpoint="/keys/upload",
+ results=results,
+ success_count=success_count,
+ error_count=error_count,
+ submitted_committees=submitted_committees,
+ ).model_dump(), 200
+
+
@api.BLUEPRINT.route("/keys/user/<asf_uid>")
@quart_schema.validate_response(models.api.KeysUserResults, 200)
async def keys_user(asf_uid: str) -> DictResponse:
diff --git a/atr/db/__init__.py b/atr/db/__init__.py
index 00f568a..d7e50f3 100644
--- a/atr/db/__init__.py
+++ b/atr/db/__init__.py
@@ -241,7 +241,7 @@ class Session(sqlalchemy.ext.asyncio.AsyncSession):
if is_defined(has_participant):
query = query.where(
via(sql.Committee.committee_members).contains(has_participant)
- or via(sql.Committee.committers).contains(has_participant)
+ | via(sql.Committee.committers).contains(has_participant)
)
if _child_committees:
diff --git a/atr/models/api.py b/atr/models/api.py
index 336ab2e..14cc85c 100644
--- a/atr/models/api.py
+++ b/atr/models/api.py
@@ -116,7 +116,7 @@ class KeysResults(schema.Strict):
class KeysAddArgs(schema.Strict):
asfuid: str
key: str
- committees: str
+ committees: list[str]
class KeysAddResults(schema.Strict):
@@ -144,6 +144,27 @@ class KeysGetResults(schema.Strict):
key: sql.PublicSigningKey
+class KeysUploadArgs(schema.Strict):
+ filetext: str
+ committees: list[str]
+
+
+class KeysUploadSubset(schema.Lax):
+ status: Literal["success", "error"]
+ key_id: str
+ fingerprint: str
+ user_id: str
+ email: str
+
+
+class KeysUploadResults(schema.Strict):
+ endpoint: Literal["/keys/upload"] = schema.Field(alias="endpoint")
+ results: Sequence[KeysUploadSubset]
+ success_count: int
+ error_count: int
+ submitted_committees: list[str]
+
+
class KeysUserResults(schema.Strict):
endpoint: Literal["/keys/user"] = schema.Field(alias="endpoint")
keys: Sequence[sql.PublicSigningKey]
@@ -329,6 +350,7 @@ Results = Annotated[
| KeysDeleteResults
| KeysGetResults
| KeysCommitteeResults
+ | KeysUploadResults
| KeysUserResults
| ListResults
| ProjectResults
@@ -379,6 +401,7 @@ validate_keys_add = validator(KeysAddResults)
validate_keys_committee = validator(KeysCommitteeResults)
validate_keys_delete = validator(KeysDeleteResults)
validate_keys_get = validator(KeysGetResults)
+validate_keys_upload = validator(KeysUploadResults)
validate_keys_user = validator(KeysUserResults)
validate_list = validator(ListResults)
validate_project = validator(ProjectResults)
diff --git a/atr/models/schema.py b/atr/models/schema.py
index 37427f0..e2ecf82 100644
--- a/atr/models/schema.py
+++ b/atr/models/schema.py
@@ -24,6 +24,10 @@ import pydantic
Field = pydantic.Field
+class Lax(pydantic.BaseModel):
+ model_config = pydantic.ConfigDict(extra="allow", strict=False,
validate_assignment=True)
+
+
class Strict(pydantic.BaseModel):
model_config = pydantic.ConfigDict(extra="forbid", strict=True,
validate_assignment=True)
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]