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]