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 c2b31d8  Add an API endpoint to delete an SSH key
c2b31d8 is described below

commit c2b31d8fc31f818ec7dfa775f859ac77f655487f
Author: Sean B. Palmer <[email protected]>
AuthorDate: Tue Jul 15 20:41:27 2025 +0100

    Add an API endpoint to delete an SSH key
---
 atr/blueprints/api/api.py | 97 ++++++++++++++++++++++++++++-------------------
 atr/models/api.py         | 60 +++++++++++++++++------------
 atr/routes/keys.py        |  8 ++++
 3 files changed, 101 insertions(+), 64 deletions(-)

diff --git a/atr/blueprints/api/api.py b/atr/blueprints/api/api.py
index 88a8ddb..c0c310c 100644
--- a/atr/blueprints/api/api.py
+++ b/atr/blueprints/api/api.py
@@ -300,46 +300,6 @@ async def public_keys(query_args: models.api.KeysQuery) -> 
DictResponse:
         ).model_dump(), 200
 
 
[email protected]("/keys/ssh/add", methods=["POST"])
[email protected]
-@quart_schema.security_scheme([{"BearerAuth": []}])
-@quart_schema.validate_request(models.api.KeysSshAddArgs)
-@quart_schema.validate_response(models.api.KeysSshAddResults, 201)
-async def keys_ssh_add(data: models.api.KeysSshAddArgs) -> DictResponse:
-    """Add an SSH key for a user."""
-    asf_uid = _jwt_asf_uid()
-    fingerprint = await keys.ssh_key_add(data.text, asf_uid)
-    return models.api.KeysSshAddResults(
-        endpoint="/keys/ssh/add",
-        fingerprint=fingerprint,
-    ).model_dump(), 201
-
-
[email protected]("/keys/ssh/list")
-@quart_schema.validate_querystring(models.api.KeysSshListQuery)
-async def keys_ssh_list(query_args: models.api.KeysSshListQuery) -> 
DictResponse:
-    """Paged list of developer SSH public keys."""
-    _pagination_args_validate(query_args)
-    via = sql.validate_instrumented_attribute
-    async with db.session() as data:
-        statement = (
-            sqlmodel.select(sql.SSHKey)
-            .limit(query_args.limit)
-            .offset(query_args.offset)
-            .order_by(via(sql.SSHKey.fingerprint).asc())
-        )
-        paged_keys = (await data.execute(statement)).scalars().all()
-
-        count_stmt = 
sqlalchemy.select(sqlalchemy.func.count(via(sql.SSHKey.fingerprint)))
-        count = (await data.execute(count_stmt)).scalar_one()
-
-        return models.api.KeysSshListResults(
-            endpoint="/keys/ssh/list",
-            data=paged_keys,
-            count=count,
-        ).model_dump(), 200
-
-
 # TODO: Call this release/paths
 @api.BLUEPRINT.route("/list/<project>/<version>")
 @api.BLUEPRINT.route("/list/<project>/<version>/<revision>")
@@ -567,6 +527,63 @@ async def revisions_project_version(project: str, version: 
str) -> DictResponse:
     ).model_dump(), 200
 
 
[email protected]("/ssh/add", methods=["POST"])
[email protected]
+@quart_schema.security_scheme([{"BearerAuth": []}])
+@quart_schema.validate_request(models.api.SshAddArgs)
+@quart_schema.validate_response(models.api.SshAddResults, 201)
+async def ssh_add(data: models.api.SshAddArgs) -> DictResponse:
+    """Add an SSH key for a user."""
+    asf_uid = _jwt_asf_uid()
+    fingerprint = await keys.ssh_key_add(data.text, asf_uid)
+    return models.api.SshAddResults(
+        endpoint="/ssh/add",
+        fingerprint=fingerprint,
+    ).model_dump(), 201
+
+
[email protected]("/ssh/delete", methods=["POST"])
[email protected]
+@quart_schema.security_scheme([{"BearerAuth": []}])
+@quart_schema.validate_request(models.api.SshDeleteArgs)
+@quart_schema.validate_response(models.api.SshDeleteResults, 201)
+async def ssh_delete(data: models.api.SshDeleteArgs) -> DictResponse:
+    """Delete an SSH key for a user."""
+    asf_uid = _jwt_asf_uid()
+    await keys.ssh_key_delete(data.fingerprint, asf_uid)
+    return models.api.SshDeleteResults(
+        endpoint="/ssh/delete",
+        success="SSH key deleted",
+    ).model_dump(), 201
+
+
[email protected]("/ssh/list/<asf_uid>")
+@quart_schema.validate_querystring(models.api.SshListQuery)
+async def ssh_list(asf_uid: str, query_args: models.api.SshListQuery) -> 
DictResponse:
+    """List of developer SSH public keys."""
+    _simple_check(asf_uid)
+    _pagination_args_validate(query_args)
+    via = sql.validate_instrumented_attribute
+    async with db.session() as data:
+        statement = (
+            sqlmodel.select(sql.SSHKey)
+            .where(sql.SSHKey.asf_uid == asf_uid)
+            .limit(query_args.limit)
+            .offset(query_args.offset)
+            .order_by(via(sql.SSHKey.fingerprint).asc())
+        )
+        paged_keys = (await data.execute(statement)).scalars().all()
+
+        count_stmt = 
sqlalchemy.select(sqlalchemy.func.count(via(sql.SSHKey.fingerprint)))
+        count = (await data.execute(count_stmt)).scalar_one()
+
+        return models.api.SshListResults(
+            endpoint="/ssh/list",
+            data=paged_keys,
+            count=count,
+        ).model_dump(), 200
+
+
 @api.BLUEPRINT.route("/tasks")
 @quart_schema.validate_querystring(models.api.TasksQuery)
 async def tasks(query_args: models.api.TasksQuery) -> DictResponse:
diff --git a/atr/models/api.py b/atr/models/api.py
index 02e63b6..171b3f6 100644
--- a/atr/models/api.py
+++ b/atr/models/api.py
@@ -118,26 +118,6 @@ class KeysResults(schema.Strict):
     count: int
 
 
-class KeysSshAddArgs(schema.Strict):
-    text: str
-
-
-class KeysSshAddResults(schema.Strict):
-    endpoint: Literal["/keys/ssh/add"] = schema.Field(alias="endpoint")
-    fingerprint: str
-
-
-class KeysSshListQuery:
-    offset: int = 0
-    limit: int = 20
-
-
-class KeysSshListResults(schema.Strict):
-    endpoint: Literal["/keys/ssh/list"] = schema.Field(alias="endpoint")
-    data: Sequence[sql.SSHKey]
-    count: int
-
-
 class ProjectResults(schema.Strict):
     endpoint: Literal["/project"] = schema.Field(alias="endpoint")
     project: sql.Project
@@ -215,6 +195,36 @@ class RevisionsResults(schema.Strict):
     revisions: Sequence[sql.Revision]
 
 
+class SshAddArgs(schema.Strict):
+    text: str
+
+
+class SshAddResults(schema.Strict):
+    endpoint: Literal["/ssh/add"] = schema.Field(alias="endpoint")
+    fingerprint: str
+
+
+class SshDeleteArgs(schema.Strict):
+    fingerprint: str
+
+
+class SshDeleteResults(schema.Strict):
+    endpoint: Literal["/ssh/delete"] = schema.Field(alias="endpoint")
+    success: str
+
+
[email protected]
+class SshListQuery:
+    offset: int = 0
+    limit: int = 20
+
+
+class SshListResults(schema.Strict):
+    endpoint: Literal["/ssh/list"] = schema.Field(alias="endpoint")
+    data: Sequence[sql.SSHKey]
+    count: int
+
+
 @dataclasses.dataclass
 class TasksQuery:
     limit: int = 20
@@ -280,8 +290,6 @@ Results = Annotated[
     | JwtResults
     | KeyResults
     | KeysResults
-    | KeysSshAddResults
-    | KeysSshListResults
     | ListResults
     | ProjectResults
     | ProjectReleasesResults
@@ -293,6 +301,9 @@ Results = Annotated[
     | ReleasesVersionResults
     | ReleasesRevisionsResults
     | RevisionsResults
+    | SshAddResults
+    | SshDeleteResults
+    | SshListResults
     | TasksResults
     | VoteResolveResults
     | VoteStartResults
@@ -324,8 +335,6 @@ validate_draft_delete = validator(DraftDeleteResults)
 validate_jwt = validator(JwtResults)
 validate_key = validator(KeyResults)
 validate_keys = validator(KeysResults)
-validate_keys_ssh_add = validator(KeysSshAddResults)
-validate_keys_ssh_list = validator(KeysSshListResults)
 validate_list = validator(ListResults)
 validate_project = validator(ProjectResults)
 validate_project_releases = validator(ProjectReleasesResults)
@@ -337,6 +346,9 @@ validate_releases_project = 
validator(ReleasesProjectResults)
 validate_releases_version = validator(ReleasesVersionResults)
 validate_releases_revisions = validator(ReleasesRevisionsResults)
 validate_revisions = validator(RevisionsResults)
+validate_ssh_add = validator(SshAddResults)
+validate_ssh_delete = validator(SshDeleteResults)
+validate_ssh_list = validator(SshListResults)
 validate_tasks = validator(TasksResults)
 validate_vote_resolve = validator(VoteResolveResults)
 validate_vote_start = validator(VoteStartResults)
diff --git a/atr/routes/keys.py b/atr/routes/keys.py
index 8b23123..48073e6 100644
--- a/atr/routes/keys.py
+++ b/atr/routes/keys.py
@@ -35,6 +35,7 @@ import asfquart as asfquart
 import asfquart.base as base
 import quart
 import werkzeug.datastructures as datastructures
+import werkzeug.exceptions as exceptions
 import werkzeug.wrappers.response as response
 import wtforms
 
@@ -483,6 +484,13 @@ async def ssh_key_add(key: str, asf_uid: str) -> str:
     return fingerprint
 
 
+async def ssh_key_delete(fingerprint: str, asf_uid: str) -> None:
+    async with db.session() as data:
+        ssh_key = await data.ssh_key(fingerprint=fingerprint, 
asf_uid=asf_uid).demand(exceptions.NotFound())
+        await data.delete(ssh_key)
+        await data.commit()
+
+
 @routes.committer("/keys/update-committee-keys/<committee_name>", 
methods=["POST"])
 async def update_committee_keys(session: routes.CommitterSession, 
committee_name: str) -> response.Response:
     """Generate and save the KEYS file for a specific committee."""


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to