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]

Reply via email to