This is an automated email from the ASF dual-hosted git repository. arm pushed a commit to branch gha-distributions in repository https://gitbox.apache.org/repos/asf/tooling-trusted-releases.git
commit d284a336f55c4dc4ce5d049ba2ae2a124fab6de5 Author: Alastair McFarlane <[email protected]> AuthorDate: Thu Jan 8 16:04:46 2026 +0000 Add new SSH register endpoint for distributions --- atr/api/__init__.py | 26 ++++++++++++++++++++++++++ atr/db/interaction.py | 18 ++++++++++++++++++ atr/models/api.py | 15 +++++++++++++++ atr/storage/writers/distributions.py | 2 +- atr/tasks/gha.py | 3 ++- 5 files changed, 62 insertions(+), 2 deletions(-) diff --git a/atr/api/__init__.py b/atr/api/__init__.py index eb3b6cf..87e641c 100644 --- a/atr/api/__init__.py +++ b/atr/api/__init__.py @@ -251,6 +251,32 @@ async def committees_list() -> DictResponse: ).model_dump(), 200 [email protected]("/distribute/ssh/register", methods=["POST"]) +@quart_schema.validate_request(models.api.DistributeSshRegisterArgs) +async def distribute_ssh_register(data: models.api.DistributeSshRegisterArgs) -> DictResponse: + """ + Register an SSH key sent with a corroborating Trusted Publisher JWT, + validating the requested version is in the correct phase. + """ + payload, asf_uid, project = await interaction.trusted_jwt_for_version( + data.publisher, data.jwt, interaction.TrustedProjectPhase(data.phase), data.version + ) + 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, + ) + + return models.api.DistributeSshRegisterResults( + endpoint="/distribute/ssh/register", + fingerprint=fingerprint, + project=project.name, + expires=expires, + ).model_dump(), 200 + + @api.route("/distribution/record", methods=["POST"]) @jwtoken.require @quart_schema.security_scheme([{"BearerAuth": []}]) diff --git a/atr/db/interaction.py b/atr/db/interaction.py index f8026f8..1bd097c 100644 --- a/atr/db/interaction.py +++ b/atr/db/interaction.py @@ -179,6 +179,24 @@ async def trusted_jwt(publisher: str, jwt: str, phase: TrustedProjectPhase) -> t return payload, asf_uid, project +async def trusted_jwt_for_version( + publisher: str, jwt: str, phase: TrustedProjectPhase, version_name: str +) -> tuple[dict[str, Any], str, sql.Project]: + payload, asf_uid, project = await trusted_jwt(publisher, jwt, phase) + async with db.session() as db_data: + release = await db_data.release(project_name=project.name, version=version_name).get() + if not release: + raise InteractionError(f"Release {version} does not exist in project {project.name}") + if phase == TrustedProjectPhase.COMPOSE and release.phase != sql.ReleasePhase.RELEASE_CANDIDATE_DRAFT: + raise InteractionError(f"Release {version} is not in compose phase") + if phase == TrustedProjectPhase.VOTE and release.phase != sql.ReleasePhase.RELEASE_CANDIDATE: + raise InteractionError(f"Release {version} is not in vote phase") + if phase == TrustedProjectPhase.FINISH and release.phase != sql.ReleasePhase.RELEASE_PREVIEW: + raise InteractionError(f"Release {version} is not in finish phase") + + return payload, asf_uid, project + + async def has_failing_checks(release: sql.Release, revision_number: str, caller_data: db.Session | None = None) -> bool: async with db.ensure_session(caller_data) as data: query = ( diff --git a/atr/models/api.py b/atr/models/api.py index 716fbce..06e5122 100644 --- a/atr/models/api.py +++ b/atr/models/api.py @@ -67,6 +67,21 @@ class CommitteesListResults(schema.Strict): committees: Sequence[sql.Committee] +class DistributeSshRegisterArgs(schema.Strict): + publisher: str = schema.example("user") + jwt: str = schema.example("eyJhbGciOiJIUzI1[...]mMjLiuyu5CSpyHI=") + ssh_key: str = schema.example("ssh-ed25519 AAAAC3NzaC1lZDI1NTEgH5C9okWi0dh25AAAAIOMqqnkVzrm0SdG6UOoqKLsabl9GKJl") + phase: str = schema.Field(strict=False, default="compose", json_schema_extra={"examples": ["compose", "finish"]}) + version: str = schema.example("0.0.1") + + +class DistributeSshRegisterResults(schema.Strict): + endpoint: Literal["/distribute/ssh/register"] = schema.alias("endpoint") + fingerprint: str = schema.example("SHA256:0123456789abcdef0123456789abcdef01234567") + project: str = schema.example("example") + expires: int = schema.example(1713547200) + + class DistributionRecordArgs(schema.Strict): project: str = schema.example("example") version: str = schema.example("0.0.1") diff --git a/atr/storage/writers/distributions.py b/atr/storage/writers/distributions.py index a0748d4..91839f8 100644 --- a/atr/storage/writers/distributions.py +++ b/atr/storage/writers/distributions.py @@ -115,7 +115,7 @@ class CommitteeMember(CommitteeParticipant): task_type=sql.TaskType.DISTRIBUTION_WORKFLOW, task_args=gha.DistributionWorkflow( name=release_name, - # "distribution-owner-namespace": owner_namespace, # TODO: Put into workflow + namespace=owner_namespace or "", package=package, version=version, project_name=project_name, diff --git a/atr/tasks/gha.py b/atr/tasks/gha.py index 669d469..cd9a458 100644 --- a/atr/tasks/gha.py +++ b/atr/tasks/gha.py @@ -46,6 +46,7 @@ class DistributionWorkflow(schema.Strict): owner: str = schema.description("Github owner of the repository") repo: str = schema.description("Repository in which to start the workflow") ref: str = schema.description("Git ref to trigger the workflow") + namespace: str = schema.description("Namespace to distribute to") package: str = schema.description("Package to distribute") version: str = schema.description("Version to distribute") project_name: str = schema.description("Project name in ATR") @@ -108,7 +109,7 @@ async def trigger_workflow(args: DistributionWorkflow) -> results.Results | None "committee.name", "release", sql_platform, - "", # TODO: Needs set in args + namespace=args.namespace, package=args.package, version=args.version, staging=True, --------------------------------------------------------------------- To unsubscribe, e-mail: [email protected] For additional commands, e-mail: [email protected]
