Repository navigation
feat(auth): RequireRoles middleware — fail closed, no permissive fallback #145
Description
Problem
The README and AGENTS.md (design decision #11) advertise "Hasura-style access control" with per-table, per-role column- and row-level permissions. The data plane for this is in place — internal/policy/ evaluates JWT-claim-templated policies against WAVEHOUSE_POLICY KV — but there is no RequireRoles middleware that gates routes on a role list.
What exists today in internal/api/middleware.go:
JWTMiddlewareparses the token and validates the signature.extractClaim()reads the role string out of the configured claim path (defaultrole).- The role is placed in
r.Context()underContextKeyRole.
What does not exist: any per-route check that says "this endpoint requires role X / Y / Z." Handlers can pull RoleFromContext(r) and gate themselves, but there's no enforcement primitive, no central policy, and no fail-closed default for routes that forget to gate. Shipping alpha with the public docs claiming RBAC enforcement, while the only enforcement is whatever the policy evaluator (or individual handlers) happen to do, is a credibility gap.
Proposed Solution
Build the middleware fail-closed from day one:
- New
RequireRoles(roles ...string) func(http.Handler) http.Handlerininternal/api/middleware.go. - On entry: extract role from context (must already have been populated by
JWTMiddleware). - If the role is missing/empty, or doesn't match any of the allowed roles, return
403withwriteJSONError. - If
auth.dev_mode: true, bypass entirely (explicit configured intent, not a fallback). - If
auth.enabled: false, also bypass (consistent with the rest of the auth stack). - Log a structured
WARNon every denial (reason,role_observed,roles_allowed,route) so operators see misconfiguration immediately.
Then apply it to the routes that need it. Suggested initial scope:
/v1/query(raw SQL) — admin role only./v1/dlq/*— admin role only./v1/policy/*— admin role only./v1/pipes/*(mutations) — admin role only; reads can stay open or use the named-pipe's own role gate.- Ingest, structured query, SSE/WS — continue to rely on policy evaluator for per-table/per-row checks (this middleware is a coarser outer ring, not a replacement).
Tests
For each route: token-with-allowed-role → 200, token-with-wrong-role → 403, no-token (auth on) → 401, no-token (auth off) → 200, no-token (dev mode) → 200. Plus a fuzz-y "every role list permutation" table-driven test for the middleware itself.
Breaking change implications
For deployments that have auth.enabled: true today but no auth.role_claim configured (or a claim path that doesn't resolve), routes that newly require a role will start returning 403. Document under "Breaking changes" in CHANGELOG with the exact config remediation. Confirm against current deployments before tagging alpha.
Sequencing
Blocks v0.1.0-alpha.1 (#149). The README RBAC pitch and the actual enforcement need to match before public launch.
Related
- AGENTS.md design decision docs: add initial Astro-based documentation site #7 (optional auth with JWKS) and Implement asynchronous metrics scrapers for embedded NATS and Pebble #11 (Hasura-style access control).
internal/policy/— the per-row/per-column engine that already exists.internal/api/middleware.go:122-126— where the role currently lands in context.
Metadata
Metadata
Assignees
Labels
Type
Projects
- StatusShow more project fieldsDone
Activity
coderabbitai commentedon May 15, 2026
🔗 Related PRs
#123 - fix(api): drop CORS credentials + skip same-origin decoration [merged]
📝 Issue Planner
Check the box below or use the
@coderabbitai plancommand to generate an implementation plan and prompts that you can use with your favorite coding assistant.🧪 Issue enrichment is currently in open beta.
You can configure auto-planning by selecting labels in the issue_enrichment configuration.
To disable automatic issue enrichment, add the following to your
.coderabbit.yaml:💬 Have feedback or questions? Drop into our discord!
chore: claude code native improvements (#147)
refactor(ingest)!: insert-only pipeline; mutations via /v1/query (#164)
feat(rbac)!: fail-closed authorization + default_role public access (#…
feat(api): structured WARN logging on authorization denials (#189)