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.
scopeNamespacebindings check the namespace object with its organization ancestor, always derived from the resolved target namespace's own anchor, never from the caller's identity.⚠️ scopeRepositorybindings 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.bindingOrgMemberOnlyand every unrecognized binding kind or scope fail closed with the masked 404 and a WARN log.validateBindinggains 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.Authorizerseam 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)
- feat(managementapi): enforce route bindings par... (!2139 - merged) • David Fernandez • 19.4
👈 - feat(managementapi): enforce the repository-sco... (!2140 - merged) • David Fernandez • 19.4
- test(managementapi): binding enforcement suite ... (!2141 - merged) • David Fernandez • 19.4
- feat(managementapi): gate the evaluator on IAM/... (!2142 - merged) • David Fernandez • 19.4