feat(authz): add the format-agnostic authorization Assembly
1 of 3 in the !960 (closed) split (see !960 (closed) for the original combined draft). Depends on nothing; the next two — the OCI RouteParser and the dispatcher wiring — stack on top of this branch.
What
The format-agnostic authorization decision layer, generalized before any format plugs into it:
RouteParseris the seam each format owns: it recovers the ADR-021 action and target repository from a request in its own path grammar (OCI's/v2/, npm's package layout, Maven's coordinate layout, whichever lands first).Assemblyis the shared decision logic every format's middleware reuses: resolve the repository, fetch the principal's role-assignment tuples, ask GLAZCheck, map the verdict onto next/403/404 per ADR-021's existence-leak split.
Closed beta: private repositories only
ADR-021 was rescoped to private-visibility-only for the closed beta by gitlab-com/content-sites/handbook!20467 (merged). This MR implements that rescoped version: every action needs a role from GLAZ, there is no visibility check, and an anonymous caller is denied outright (404) before any lookup runs.
This layer has no governing spec in docs/specs/ — S09 still lists "Never returns 403" as an acceptance criterion, which predates the ADR-021 rescope and no longer applies. ADR-021 (as rescoped) is the document this MR implements.
Tests
middleware_test.go exercises Assembly directly via a stub RouteParser, independent of any one format's grammar:
TestAssembly_ScopeAndRouteOutcomes— theRouteOutcome→ next/403/404 mapping.TestNewAssembly_PanicsOnNilDep— every constructor dependency.TestAssembly_EveryActionReachesGLAZ— every action reaches GLAZ; there's no shortcut for any of them.TestAssembly_AnonymousCallerDenied,TestAssembly_AllowedActionCallsNext,TestAssembly_DenialStatusMapping,TestAssembly_ResolutionAndInfrastructureFailures— the anonymous gate, the allow path, the 403/404 denial split, and the resolution/infrastructure fail-closed paths.
Known gaps, deferred to a follow-up MR
- The IAM relationships lookup doesn't include the organization as a third ancestor object (ADR-021).
namespaces.entity_idisn't a UUID, so this needs its own namespace-data-model change; tracked with a TODO intuples. - JWT forwarding to IAM (
iam.ContextWithToken) has no caller yet.
Not in this MR
No format's RouteParser — that's OCI's in the next MR in the stack, wired into the dispatcher in the one after that.
Related to gitlab-org/gitlab#605903 (closed)