Draft: feat(authz): OCI-pull authorization assembly middleware
Draft — demo-able, not ready to merge. !950 (merged), !951 (merged), and !953 (merged) have all merged to main; this MR is now a clean two-commit stack on top of it (the assembly, then the wiring). The reviewable content is internal/authz/oci.go + the cmd/artifact-registry/wire_oci.go/wire_iam.go/wire_glaz.go/wire.go composition wiring.
What
The S28 authorization decision layer for the OCI /v2/ pull surface, now wired into the real request path — ReadRelationships → GLAZ Check runs on every OCI pull, not just in unit tests.
The assembly (internal/authz/oci.go)
Per request, OCIMiddleware:
- Parses
/v2/→(slug, repository, operation); mapsGET/HEADon manifests/blobs →read_artifact, tags →read_repository. - Resolves the repository + visibility (
ResolveRepository→FindByID); no row → 404. - Builds the principal from
auth.Identity— authenticated →ReadRelationshipsfor the resource + ancestors (using the resolvedPrincipalUUIDReadRelationshipsechoes back, when at least one tuple exists); anonymous → synthesized-tuple path. - Calls GLAZ
Check; allow → next, deny → 404 (read) or a follow-up read check → 403/404 (non-read), per ADR-021's existence-leak split. - Fails closed (503) on any infra error.
Tested: the path parser, action mapping, and visibility mapping (oci_internal_test.go).
The wiring (cmd/artifact-registry/wire_oci.go, wire_iam.go, wire_glaz.go, wire.go)
OCIMiddleware now wraps the /v2/ dispatcher directly inside wireOCI/wireOCIWithStore, reusing the IAM and GLAZ clients wireIAM/wireGLAZ already construct (exposed via new wiring.iamClient/glazClient fields) rather than dialing either backend a second time.
Wrapping happens locally at the mount point instead of through the internal/authz.Register seam: that seam is read at server.New() construction time, which runs before wireServices builds these clients — registering there would need them before they exist. Wrapping where every dependency is already in scope sidesteps that ordering conflict entirely. Falls back to mounting the dispatcher unwrapped when the iam: config block is absent, matching pre-authz behavior for configs without IAM configured.
Open before it can ship (all marked TODO(S28) in code)
- Route-parser divergence (most important).
authzcan't importinternal/format/oci(oci → server → authzcycle), so the/v2/parser is inlined. For an authz path a parser that diverges from the dispatcher's is a security risk — the grammar should be extracted to a leaf package both import. - Organization ancestor —
namespace.Resolutiongives repo + namespace only; does theReadRelationshipsancestor set need a separate org id (ADR-022)? - Anonymous tuple shape and visibility attribute vocabulary — GLAZ-contract details blocked on Auth Platform confirmation (#321 tracks the related logging gap). The principal string for an authenticated caller is resolved from
ReadRelationships' echoedPrincipalwhen tuples exist; the zero-tuple case still has nothing to resolve from and needs the same confirmation.
Not in this draft
End-to-end middleware tests (need an identity-injection seam) and the push-path actions (create_artifact/delete_*).
Related to gitlab-org/gitlab#605903 (closed)