feat(managementapi): enforce route bindings part 1/4 (S09 Enforcement plan: 17/20)

📚 Stacked MRs

This step is split into 4 stacked MRs to keep each within the review size limit (≈800 ideal). Each part targets the previous one (part 1 targets main) and the stack is reviewed and merged bottom-up. Part 1 of 4 of the S09 enforcement plan — Step 17: Management enforcement at the slugMiddleware seam, per S09 (authorization).

🔐 What this MR does

Adds the management-API authorization engine and runs it over every route's declared binding inside slugMiddleware, per the S09 (authorization) spec's Management API section: an unknown slug keeps its existence-hiding 404 before any authorization work, a denied binding renders through the management DenialRenderer, and the suspended-namespace write 403 is reachable only after an allow, so it cannot disclose a namespace's existence to a caller with no role.

  • scopeNamespace bindings check the namespace object with its organization ancestor, always derived from the resolved target namespace's own anchor, never from the caller's identity.
  • ⚠️ scopeRepository bindings ship as an explicit temporary stub in this part: an anonymous caller is masked through the evaluator's own gate, and every other caller passes through. Part 2 replaces the stub with the real repository-scope arm (repository pre-resolve, masked 404, 503 mapping). The stub is marked in code as a temporary allow-through.
  • scopeListingFilter (GET /repositories) is anonymous-gated and otherwise passes through: the listing's two-branch filter is #861 (closed)'s.
  • bindingOrgMemberOnly and every unrecognized binding kind or scope fail closed with the masked 404 and a WARN log.
  • validateBinding gains a scope-to-pattern ratchet: a repository-scoped binding requires {repository_name} in its pattern, a namespace-scoped one forbids it, and the listing-filter scope binds only the repository-collection route. All forty registered routes conform; the ratchet exists for the forty-first.
  • Wiring is an unconditional allow-all value in this part, so no production behavior changes until the final part lands the IAM/GLAZ gate; the Deps.Authorizer seam and its zero-value panic guard land here so every part in between stays green.
  • Internal tests pin the fail-closed arms, the resolve → authorize → suspension-gate ordering, and the ratchet rejections.

📏 Size

Above the 500-LOC guideline deliberately: the engine, its seam, and the tests for its fail-closed and ordering contracts are one predicate through one seam — cutting the internal tests away from the arms they pin would ship enforcement code without its tests.

🧪 e2e scenarios

No catalog under docs/testing/ covers the management API, so no scenario is added or affected; the enforcement contract is pinned by this stack's handler-level suites.

Related to #859 (closed)

Stacked MRs (review/merge bottom-up)

Edited by David Fernandez

Merge request reports

Loading
Loading