Files
suaveplan/openspec/archive/2026-05-26-api-rbac/proposal.md
2026-05-27 23:29:53 +01:00

3.2 KiB

@suaveplan/api-rbac — scope + rule-based authorisation middleware

Package: @suaveplan/api-rbac Status: PROPOSED Type: new-package Stage: 8 — see umbrella openspec/changes/api-plugin-foundation/

Why

Per design doc §6 step 5, the per-request pipeline includes an RBAC gate that compares the route's declared auth against the resolved principal.scopes. Plugins declare scopes in their manifest (§3) and assign them per route via router.get("/x", { auth: "billing:read" }, …). Centralising the gate in a single middleware keeps "forgot the auth middleware" out of the class-of-bug list, and gives operators a single place to wire complex policy via @suaveplan/rules.

What changes

  • ADDED: createRbacMiddleware({ rules }) — produces a Middleware that consumes RequestContext.principal and the route's auth option. If auth is a string scope name, performs a default scope check (principal.scopes.includes(scope)); if auth is a RuleSpec, delegates evaluation to @suaveplan/rules. 403 on deny, pass-through on allow, 401 if auth is required and the principal is null (non-public route with no resolved principal).
  • ADDED: RouteAuthSpec — discriminated union: { kind: "scope", scope: string } | { kind: "rule", rule: RuleSpec } | { kind: "public" }. The router translates the route option's auth value into this shape before invoking the middleware.
  • ADDED: Manifest-scope registration helper registerPluginScopes(registry, manifest) — adds every scope from manifest.scopes[] to the host's ScopeRegistry. Conflicts (two plugins claiming the same scope name) raise RBAC_SCOPE_CONFLICT.
  • ADDED: api.rbac.denials_total{plugin, route, reason} counter — incremented on every deny. reason is missing-scope or rule-deny or no-principal.
  • ADDED: Error codes (RBAC_*): MISSING_SCOPE, RULE_DENY, NO_PRINCIPAL, SCOPE_CONFLICT.

Out of scope

  • Session resolution (that's api-auth-* adapters, design doc §2.3).
  • Tenant resolution (that's api-tenant-resolver-* adapters).
  • Rate limiting (that's api-rate-limit).
  • The router itself — this is a Middleware consumed by any router adapter.

Source mapping

Genesis-original. Source of design intent: docs/superpowers/specs/2026-05-13-api-plugin-foundation-design.md.

Requirement Design doc section
Per-request RBAC gate §6 step 5
Plugin-declared scopes auto-registered §3 (scopes block), §5
Rule-policy delegation to @suaveplan/rules §6 step 5
api.rbac.denials_total counter §12.3 (host-emitted), §12.4

Verification approach

  • src/middleware/middleware.test.ts — allow / deny matrices for scope, rule, and public route specs.
  • src/middleware/middleware.spec-conformance.test.ts — pins observability: denials emit api.rbac.denials_total with the correct reason label, asserted against a real in-memory MeterProvider.
  • src/registry/registry.test.ts — duplicate-scope registration raises RBAC_SCOPE_CONFLICT.
  • tests/dep-graph.test.ts — runtime deps (api-types, error, logger, rules).

Migration impact

None — additive only. api-host wires this middleware at boot.