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 275da57 Move the workflow SSH key writer to the storage interface
275da57 is described below
commit 275da57376584bd81197d7743bc4d1d06eb9086c
Author: Sean B. Palmer <[email protected]>
AuthorDate: Thu Sep 4 15:17:28 2025 +0100
Move the workflow SSH key writer to the storage interface
---
atr/blueprints/api/api.py | 32 +++---------
atr/storage/__init__.py | 5 ++
atr/storage/writers/__init__.py | 3 +-
atr/storage/writers/ssh.py | 109 ++++++++++++++++++++++++++++++++++++++++
4 files changed, 123 insertions(+), 26 deletions(-)
diff --git a/atr/blueprints/api/api.py b/atr/blueprints/api/api.py
index 2ba6e5a..ee6c187 100644
--- a/atr/blueprints/api/api.py
+++ b/atr/blueprints/api/api.py
@@ -19,7 +19,6 @@
import base64
import hashlib
import pathlib
-import time
from typing import Any
import aiofiles.os
@@ -356,37 +355,20 @@ async def jwt_create(data: models.api.JwtCreateArgs) ->
DictResponse:
@quart_schema.validate_request(models.api.JwtGithubArgs)
async def jwt_github(data: models.api.JwtGithubArgs) -> DictResponse:
"""
- Create a JWT from a GitHub OIDC JWT.
-
- The payload must include a valid GitHub OIDC JWT.
+ Register an SSH key sent with a corroborating GitHub OIDC JWT.
"""
log.info(f"SSH key: {data.ssh_key}")
- # TODO: This is a placeholder for the actual implementation
payload = await jwtoken.verify_github_oidc(data.jwt)
asf_uid = await ldap.github_to_apache(payload["actor_id"])
-
project = await interaction.trusted_project(payload["repository"],
payload["workflow_ref"])
-
- # TODO: This needs to go in the storage interface
- # And it needs to create an audit event for logging
- now = int(time.time())
- # Twenty minutes to upload all files
- ttl = 20 * 60
- expires = now + ttl
- fingerprint = keys.key_ssh_fingerprint(data.ssh_key)
- async with db.session() as db_data:
- wsk = sql.WorkflowSSHKey(
- fingerprint=fingerprint,
- key=data.ssh_key,
- project_name=project.name,
- asf_uid=asf_uid,
- github_uid=payload["actor"],
- github_nid=payload["actor_id"],
- expires=expires,
+ async with
storage.write_as_committee_member(util.unwrap(project.committee).name, asf_uid)
as wacm:
+ fingerprint, expires = await wacm.ssh.add_workflow_key(
+ payload["actor"],
+ payload["actor_id"],
+ project.name,
+ data.ssh_key,
)
- db_data.add(wsk)
- await db_data.commit()
return models.api.JwtGithubResults(
endpoint="/jwt/github",
diff --git a/atr/storage/__init__.py b/atr/storage/__init__.py
index 4f27492..5b23bdd 100644
--- a/atr/storage/__init__.py
+++ b/atr/storage/__init__.py
@@ -136,6 +136,7 @@ class WriteAsGeneralPublic(WriteAs):
def __init__(self, write: Write, data: db.Session):
self.checks = writers.checks.GeneralPublic(write, self, data)
self.keys = writers.keys.GeneralPublic(write, self, data)
+ self.ssh = writers.ssh.GeneralPublic(write, self, data)
self.tokens = writers.tokens.GeneralPublic(write, self, data)
@@ -144,6 +145,7 @@ class WriteAsFoundationCommitter(WriteAsGeneralPublic):
# TODO: We need a definitive list of ASF UIDs
self.checks = writers.checks.FoundationCommitter(write, self, data)
self.keys = writers.keys.FoundationCommitter(write, self, data)
+ self.ssh = writers.ssh.FoundationCommitter(write, self, data)
self.tokens = writers.tokens.FoundationCommitter(write, self, data)
@@ -152,6 +154,7 @@ class
WriteAsCommitteeParticipant(WriteAsFoundationCommitter):
self.__committee_name = committee_name
self.checks = writers.checks.CommitteeParticipant(write, self, data,
committee_name)
self.keys = writers.keys.CommitteeParticipant(write, self, data,
committee_name)
+ self.ssh = writers.ssh.CommitteeParticipant(write, self, data,
committee_name)
self.tokens = writers.tokens.CommitteeParticipant(write, self, data,
committee_name)
@property
@@ -165,6 +168,7 @@ class WriteAsCommitteeMember(WriteAsCommitteeParticipant):
self.checks = writers.checks.CommitteeMember(write, self, data,
committee_name)
self.distributions = writers.distributions.CommitteeMember(write,
self, data, committee_name)
self.keys = writers.keys.CommitteeMember(write, self, data,
committee_name)
+ self.ssh = writers.ssh.CommitteeMember(write, self, data,
committee_name)
self.tokens = writers.tokens.CommitteeMember(write, self, data,
committee_name)
@property
@@ -177,6 +181,7 @@ class WriteAsFoundationAdmin(WriteAsCommitteeMember):
self.__committee_name = committee_name
# self.checks = writers.checks.FoundationAdmin(write, self, data,
committee_name)
self.keys = writers.keys.FoundationAdmin(write, self, data,
committee_name)
+ # self.ssh = writers.ssh.FoundationAdmin(write, self, data,
committee_name)
# self.tokens = writers.tokens.FoundationAdmin(write, self, data,
committee_name)
@property
diff --git a/atr/storage/writers/__init__.py b/atr/storage/writers/__init__.py
index f3613b0..fd4caa3 100644
--- a/atr/storage/writers/__init__.py
+++ b/atr/storage/writers/__init__.py
@@ -18,6 +18,7 @@
import atr.storage.writers.checks as checks
import atr.storage.writers.distributions as distributions
import atr.storage.writers.keys as keys
+import atr.storage.writers.ssh as ssh
import atr.storage.writers.tokens as tokens
-__all__ = ["checks", "distributions", "keys", "tokens"]
+__all__ = ["checks", "distributions", "keys", "ssh", "tokens"]
diff --git a/atr/storage/writers/ssh.py b/atr/storage/writers/ssh.py
new file mode 100644
index 0000000..646f41d
--- /dev/null
+++ b/atr/storage/writers/ssh.py
@@ -0,0 +1,109 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+# Removing this will cause circular imports
+from __future__ import annotations
+
+import time
+
+import atr.db as db
+import atr.models.sql as sql
+import atr.routes.keys as keys
+import atr.storage as storage
+
+
+class GeneralPublic:
+ def __init__(
+ self,
+ write: storage.Write,
+ write_as: storage.WriteAsGeneralPublic,
+ data: db.Session,
+ ):
+ self.__write = write
+ self.__write_as = write_as
+ self.__data = data
+ self.__asf_uid = write.authorisation.asf_uid
+
+
+class FoundationCommitter(GeneralPublic):
+ def __init__(self, write: storage.Write, write_as:
storage.WriteAsFoundationCommitter, data: db.Session):
+ super().__init__(write, write_as, data)
+ self.__write = write
+ self.__write_as = write_as
+ self.__data = data
+ asf_uid = write.authorisation.asf_uid
+ if asf_uid is None:
+ raise storage.AccessError("No ASF UID")
+ self.__asf_uid = asf_uid
+
+
+class CommitteeParticipant(FoundationCommitter):
+ def __init__(
+ self,
+ write: storage.Write,
+ write_as: storage.WriteAsCommitteeParticipant,
+ data: db.Session,
+ committee_name: str,
+ ):
+ super().__init__(write, write_as, data)
+ self.__write = write
+ self.__write_as = write_as
+ self.__data = data
+ asf_uid = write.authorisation.asf_uid
+ if asf_uid is None:
+ raise storage.AccessError("No ASF UID")
+ self.__asf_uid = asf_uid
+ self.__committee_name = committee_name
+
+ async def add_workflow_key(self, github_uid: str, github_nid: int,
project_name: str, key: str) -> tuple[str, int]:
+ # TODO: This needs to create an audit event for logging
+ now = int(time.time())
+ # Twenty minutes to upload all files
+ ttl = 20 * 60
+ expires = now + ttl
+ fingerprint = keys.key_ssh_fingerprint(key)
+ wsk = sql.WorkflowSSHKey(
+ fingerprint=fingerprint,
+ key=key,
+ project_name=project_name,
+ asf_uid=self.__asf_uid,
+ github_uid=github_uid,
+ github_nid=github_nid,
+ expires=expires,
+ )
+ self.__data.add(wsk)
+ await self.__data.commit()
+ return fingerprint, expires
+
+
+class CommitteeMember(CommitteeParticipant):
+ def __init__(
+ self,
+ write: storage.Write,
+ write_as: storage.WriteAsCommitteeMember,
+ data: db.Session,
+ committee_name: str,
+ ):
+ super().__init__(write, write_as, data, committee_name)
+ self.__write = write
+ self.__write_as = write_as
+ self.__data = data
+ asf_uid = write.authorisation.asf_uid
+ if asf_uid is None:
+ raise storage.AccessError("No ASF UID")
+ self.__asf_uid = asf_uid
+ self.__committee_name = committee_name
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]