feat: namespace details endpoint (S09 permission verdicts plan: 7/10)

What

Adds GET /api/v1/{slug}/namespace: any authenticated member of the organization that owns the namespace can read its slug and created_at — no role assignment needed. A caller from another organization gets the same 404 an unknown slug gets, so the endpoint never reveals whether a slug exists. No credential means a 401.

The membership check runs only when the boot enforces authorization; an unenforced boot serves the route without deciding anything, like every other management route (the spec's boot posture). Denials are counted (denials_total{org_membership}) and logged with the denying arm's reason.

Spec amendments (same MR)

  • id is dropped from the body: ADR-022 keeps the namespace UUID internal ("never exposed externally"), so it wins over the spec's repository-id precedent, per operator decision. The field set is additive (ADR-009), so id can return later behind an ADR-022 amendment.
  • The observability section gains org_membership and the previously omitted escaped_separator, and organization_ancestor_skipped_total's definition widens to cover the gate's derivation, which runs no tuple read.

Size (guardrail 18)

+920/−53 across 27 files. The overage is mostly tests: 131 lines are source, 684 are the tests that pin the gate's arms, and 105 are contract files (OpenAPI, Bruno, spec). Splitting would not help: the handler, its gate, and their contract tests only make sense reviewed together, and the plan already forecasts this as the one step past the 500-LOC guardrail.

Notes

  • e2e catalogs (guardrail 12): no scenario added; the endpoint's entry lands with Step 8 (verdicts + cache headers), which also stamps the /namespace 200's cache-header $refs.
  • docs/dev/bruno.md: deliberately deviates from open sibling !2224 (merged)'s paragraph; whichever lands second merges it.
  • Posture asymmetry: Enforcing() == false also covers the interim no-iam: boot, which already allow-alls every management route; Step 4's verdict seam fails its third arm closed. Deliberate.

Spec coverage

Spec: docs/specs/S09-authorization.md. Rows owned by other steps or the enforcement plan say so; unlisted criteria are unchanged from the enforcement plan's coverage.

# Criterion Tests
AC-1 Anonymous deny Enforcement plan; this step adds TestAuthorizeRoute_AnonymousOrgMembershipNeverCompares.
AC-3 Read deny masks Enforcement plan; this route: TestNamespaceDetail_CrossOrganizationCallerGetsTheUnknownSlugNotFound.
AC-7 Target-anchored ancestor Enforcement plan; the comparison reads the resolved anchor (same test).
AC-14 Management point checks TestAuthorizeRoute_EveryBoundRouteEnforcesItsPermission: org_member_only is allowlisted; an unlisted route carrying the kind fails the sweep.
AC-20 Boot posture Enforcement plan + Step 4; this step: TestNamespaceDetail_UnenforcedPostureDecidesNothing, TestEvaluatorEnforcing, TestAllowAllDecisionEvaluatorReportsUnenforced.
AC-21 Namespace endpoint gate TestNamespaceDetailIntegration_GateArms, TestNamespaceDetail_MemberOfTheOwningOrganizationReadsTheNamespace, TestNamespaceDetail_CrossOrganizationCallerGetsTheUnknownSlugNotFound, TestNamespaceDetail_NonOrganizationAnchorDenies, TestNamespaceDetail_EmptyEntityIDDenies, TestNamespaceDetail_RequestWithNoIdentityHolderDenies, TestNamespaceDetail_FederatedOriginDenies, TestAuthorizeRoute_OrgMemberOnlyFailsClosed, TestNewRouter_BindingsMatchTheGoldenTable. Verdict half: Step 8.
AC-24 Opt-out is inert Steps 5, 6, 8; this step pins the body's key set (slug/created_at).
E-4 Management 404 identical to the genuine one TestNamespaceDetail_CrossOrganizationCallerGetsTheUnknownSlugNotFound (full header set + body), TestNamespaceDetailIntegration_GateArms, TestNamespaceDetail_ResponsesMatchOpenAPIContract.
E-5 No denial names the permission/role/policy TestNamespaceDetail_NonOrganizationAnchorDenies; TestNamespaceDetail_DenialLogsWhichArmDenied pins the reason reaches the log, not the response.
E-6 No denial echoes request-derived values Cross-org 404 byte-identical to unknown-slug, headers included.
E-9 One 404 for unknown slug and cross-org caller Same two tests as E-4.
S-1 Existence masking is shape-sensitive Same two tests (status, headers, body bytes).
S-2 Every denial is enveloped The three deny-arm tests under AC-21.
S-3 Fail closed everywhere TestAuthorizeRoute_OrgMemberOnlyFailsClosed plus the deny-arm tests.
S-4 Trusts nothing it did not derive Anchor from the resolved namespace; a matching id under the wrong Origin still denies (TestNamespaceDetail_FederatedOriginDenies).
S-7 Verdicts advisory and organization-bound Binding half is this step; advisory half is Steps 5, 6, 8.

Related to #670 (closed)

Edited by João Pereira

Merge request reports

Loading
Loading