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]

Reply via email to