This repository has been archived on 2026-11-08. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
platform-ci/platform_ci/delivery/authorization.py
forge-recoveryandClaude Sonnet 5.5 fd4689fd19
KofTwentyTwo/ci-policy: ci / fork-guard (pull_request) Skipped
KofTwentyTwo/ci-policy: ci / go-0 (pull_request) Skipped
KofTwentyTwo/ci-policy: ci / go-nix-1 (pull_request) Skipped
KofTwentyTwo/ci-policy: ci / gradle-2 (pull_request) Skipped
KofTwentyTwo/ci-policy: ci / gradle-3 (pull_request) Skipped
KofTwentyTwo/ci-policy: ci / java-docker-4 (pull_request) Skipped
KofTwentyTwo/ci-policy: ci / java-maven-5 (pull_request) Skipped
KofTwentyTwo/ci-policy: ci / manifests-6 (pull_request) Skipped
KofTwentyTwo/ci-policy: ci / nix-7 (pull_request) Skipped
KofTwentyTwo/ci-policy: ci / node-8 (pull_request) Skipped
KofTwentyTwo/ci-policy: ci / python-9 (pull_request) Skipped
KofTwentyTwo/ci-policy: ci / rust-10 (pull_request) Skipped
KofTwentyTwo/ci-policy: ci / rust-11 (pull_request) Skipped
KofTwentyTwo/ci-policy: ci / rust-12 (pull_request) Skipped
KofTwentyTwo/ci-policy: ci / source-config-13 (pull_request) Skipped
KofTwentyTwo/ci-policy: ci / source-config-14 (pull_request) Skipped
KofTwentyTwo/ci-policy: ci / image-build-0 (pull_request) Skipped
KofTwentyTwo/ci-policy: ci / image-build-1 (pull_request) Skipped
KofTwentyTwo/ci-policy: ci / image-build-2 (pull_request) Skipped
KofTwentyTwo/ci-policy: ci / image-build-3 (pull_request) Skipped
KofTwentyTwo/ci-policy: ci / image-build-4 (pull_request) Skipped
KofTwentyTwo/ci-policy: ci / image-build-5 (pull_request) Skipped
KofTwentyTwo/ci-policy: ci / image-build-6 (pull_request) Skipped
KofTwentyTwo/ci-policy: ci / image-build-7 (pull_request) Skipped
KofTwentyTwo/ci-policy: ci / not-enrolled (pull_request) Skipped
KofTwentyTwo/ci-policy: ci / content-validate-0 (pull_request) Skipped
KofTwentyTwo/ci-policy: ci / standards-0 (pull_request) Skipped
KofTwentyTwo/ci-policy: ci / node-matrix-0 (pull_request) Skipped
KofTwentyTwo/ci-policy: ci / source-config-15 (pull_request) Blocked by required conditions
KofTwentyTwo/ci-policy: Shared source and config CI / verify (pull_request) Blocked by required conditions
KofTwentyTwo/ci-policy: Shared source and config CI / secrets (pull_request) Blocked by required conditions
KofTwentyTwo/ci-policy: Shared source and config CI / gate (pull_request) Waiting for needed jobs
KofTwentyTwo/ci-policy: ci / ci-result (pull_request) Waiting for needed jobs
KofTwentyTwo/ci-policy: native / fork-deny (pull_request) Skipped
KofTwentyTwo/ci-policy: native / build-macos-0 (pull_request) Skipped
KofTwentyTwo/ci-policy: native / build-macos-1 (pull_request) Skipped
KofTwentyTwo/ci-policy: native / build-macos-2 (pull_request) Skipped
KofTwentyTwo/ci-policy: native / build-macos-3 (pull_request) Skipped
KofTwentyTwo/ci-policy: native / build-windows-4 (pull_request) Skipped
KofTwentyTwo/ci-policy: native / build-windows-5 (pull_request) Skipped
KofTwentyTwo/ci-policy: native / build-windows-6 (pull_request) Skipped
KofTwentyTwo/ci-policy: native / build-windows-7 (pull_request) Skipped
KofTwentyTwo/ci-policy: native / dotnet-8 (pull_request) Skipped
KofTwentyTwo/ci-policy: native / dotnet-9 (pull_request) Skipped
KofTwentyTwo/ci-policy: native / dotnet-10 (pull_request) Skipped
KofTwentyTwo/ci-policy: native / dotnet-11 (pull_request) Skipped
KofTwentyTwo/ci-policy: native / swift-12 (pull_request) Skipped
KofTwentyTwo/ci-policy: native / swift-13 (pull_request) Skipped
KofTwentyTwo/ci-policy: native / swift-14 (pull_request) Skipped
KofTwentyTwo/ci-policy: native / swift-15 (pull_request) Skipped
KofTwentyTwo/ci-policy: native / swift-16 (pull_request) Skipped
KofTwentyTwo/ci-policy: native / swift-17 (pull_request) Skipped
KofTwentyTwo/ci-policy: native / swift-18 (pull_request) Skipped
KofTwentyTwo/ci-policy: native / swift-19 (pull_request) Skipped
KofTwentyTwo/ci-policy: native / native-result (pull_request) Canceled after 3s
feat(ci): reuse verified builds before merge verification
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-05 03:54:30 +00:00

140 lines
6.1 KiB
Python

"""Publication requests and operator-owned access bindings.
A source configuration may only REQUEST a delivery by naming an artifact, a format and a logical destination. It cannot
carry a URL, credential, registry, owner, repository or signer. The concrete target comes from an `AccessBinding` that the
operator provisioned for the authenticated identity; an identity is authorization data and never selects behavior.
"""
import re
from collections.abc import Mapping
from dataclasses import dataclass
from typing import cast
from platform_ci.delivery.errors import AuthorizationError, UnsupportedFormatError
MAVEN_FORMAT = "maven"
NPM_FORMAT = "npm"
CONTAINER_FORMAT = "container"
GITOPS_DIGEST_FORMAT = "gitops-digest"
PUBLICATION_FORMATS = frozenset({MAVEN_FORMAT, NPM_FORMAT, CONTAINER_FORMAT})
DEPLOYMENT_FORMATS = frozenset({GITOPS_DIGEST_FORMAT})
_REQUEST_KEYS = frozenset({"component", "artifact", "format", "platform", "destination", "channel"})
_LOGICAL_NAME = re.compile(r"[a-z0-9][a-z0-9._-]*")
_OWNER = re.compile(r"[A-Za-z0-9][A-Za-z0-9_.-]*")
_IMAGE = re.compile(r"[a-z0-9][a-z0-9._-]*(/[a-z0-9][a-z0-9._-]*)+")
@dataclass(frozen=True)
class PublicationRequest:
"""A source-declared request for one artifact to reach one logical destination. It contains no secret or address."""
name: str
component: str
artifact: str
format: str
platform: str
destination: str
channel: str
@dataclass(frozen=True)
class PackageTarget:
"""A package registry owner, for Maven and npm delivery."""
owner: str
@dataclass(frozen=True)
class ImageTarget:
"""A container registry image path."""
image: str
@dataclass(frozen=True)
class GitOpsTarget:
"""One kustomization file on one branch of one GitOps repository, and the images it may pin (kustomize name to registry image)."""
repository: str
repository_id: int
branch: str
path: str
images: tuple[tuple[str, str], ...]
DestinationTarget = PackageTarget | ImageTarget | GitOpsTarget
@dataclass(frozen=True)
class AccessBinding:
"""An operator-owned grant: this identity may deliver these formats to this logical destination on these channels."""
identity: str
destination: str
formats: frozenset[str]
channels: frozenset[str]
target: DestinationTarget
require_provenance: bool = False
require_signature_verification: bool = False
def parse_publication_request(name: str, document: object) -> PublicationRequest:
"""Validate one request from source configuration; any unknown field, including a credential-like one, is refused."""
if not isinstance(document, dict):
raise UnsupportedFormatError(f"publication {name!r} must be an object")
fields = cast(dict[str, object], document)
extra = set(fields) - _REQUEST_KEYS
if extra:
raise AuthorizationError(f"publication {name!r} may not set {sorted(extra)}; destinations and credentials come only from operator bindings")
missing = _REQUEST_KEYS - set(fields)
if missing:
raise AuthorizationError(f"publication {name!r} is missing {sorted(missing)}")
values = {key: fields[key] for key in _REQUEST_KEYS}
if not all(isinstance(value, str) and _LOGICAL_NAME.fullmatch(value) for value in values.values()):
raise AuthorizationError(f"publication {name!r} fields must be logical names")
request = PublicationRequest(name=name, component=cast(str, values["component"]), artifact=cast(str, values["artifact"]), format=cast(str, values["format"]), platform=cast(str, values["platform"]), destination=cast(str, values["destination"]), channel=cast(str, values["channel"]))
require_supported_format(request.format, PUBLICATION_FORMATS | DEPLOYMENT_FORMATS)
return request
def require_supported_format(format_name: str, supported: frozenset[str]) -> None:
"""Fail explicitly for a format no adapter implements; there is no fallback and no best-effort guess."""
if format_name not in supported:
raise UnsupportedFormatError(f"unsupported delivery format {format_name!r}; supported: {sorted(supported)}")
def authorize(bindings: tuple[AccessBinding, ...], identity: str, destination: str, format_name: str, channel: str) -> AccessBinding:
"""Return the single binding that grants this exact identity, destination, format and channel, or refuse."""
matches = [binding for binding in bindings if binding.identity == identity and binding.destination == destination]
if not matches:
raise AuthorizationError(f"identity is not bound to destination {destination!r}")
if len(matches) > 1:
raise AuthorizationError(f"destination {destination!r} has ambiguous bindings for this identity")
binding = matches[0]
if format_name not in binding.formats:
raise AuthorizationError(f"destination {destination!r} is not authorized for format {format_name!r}")
if channel not in binding.channels:
raise AuthorizationError(f"destination {destination!r} is not authorized for channel {channel!r}")
_require_target_matches_format(binding.target, format_name)
return binding
def _require_target_matches_format(target: DestinationTarget, format_name: str) -> None:
"""Refuse a mis-provisioned binding whose target cannot carry the format, and malformed targets."""
if format_name in (MAVEN_FORMAT, NPM_FORMAT):
if not isinstance(target, PackageTarget) or not _OWNER.fullmatch(target.owner):
raise AuthorizationError(f"binding target is not a package owner for {format_name}")
elif format_name == CONTAINER_FORMAT:
if not isinstance(target, ImageTarget) or not _IMAGE.fullmatch(target.image):
raise AuthorizationError("binding target is not a container image")
elif format_name == GITOPS_DIGEST_FORMAT:
if not isinstance(target, GitOpsTarget) or not target.images or not all(_IMAGE.fullmatch(image) for _, image in target.images):
raise AuthorizationError("binding target is not a GitOps file with enrolled images")
else:
raise UnsupportedFormatError(f"unsupported delivery format {format_name!r}")
def image_map(target: GitOpsTarget) -> Mapping[str, str]:
"""Return the enrolled kustomize-name to registry-image mapping of a GitOps target."""
return dict(target.images)