[story #126] docs(api): OpenAPI spec for RBAC middleware contract

This commit is contained in:
2026-06-11 03:10:49 +00:00
parent 69488d2ef5
commit b3e07695b4
+249
View File
@@ -0,0 +1,249 @@
openapi: 3.0.3
info:
title: InboxOS RBAC Middleware API Contract
description: |
API contract and middleware interface specification for the RBAC & Auth
Layer (INB-1.5-C).
This document defines:
1. The **auth endpoints** (`/auth/me`) that the frontend calls to discover
the current user's role and permissions.
2. The **middleware interface contract** that the backend routing layer
MUST implement so that every request is checked for the required role
before the handler runs.
All endpoints are scoped to an **authenticated** principal; unauthenticated
requests are rejected with `401` before role checks occur.
**Traces to:**
- Epic: [EPIC-INB-1.5-C #111](https://git.cloudinit.dev/jc-capital/inboxos/issues/111)
- Initiative: INTV-5 #109
- Architecture: `docs/architecture/v1.md` §4.5 (RBAC trust boundary)
- Data model: `docs/schemas.md` §Users / Roles tables
**Status:** Spec-only (S1). Implementation in S2 (`be/INB-1.5-C-rbac-impl`).
version: "1.5.0-s1"
contact:
name: InboxOS Backend Engineering
url: https://git.cloudinit.dev/jc-capital/inboxos
servers:
- url: "https://{host}/api/v1"
description: InboxOS self-hosted instance
variables:
host:
default: "inboxos.local"
description: Operator-defined ingress host (Caddy / reverse-proxy)
security:
- BearerAuth: []
tags:
- name: Auth
description: Authentication and role discovery.
- name: Meta
description: Health, metrics, and spec discovery.
paths:
/auth/me:
get:
summary: Current user identity and role
description: |
Returns the authenticated user's identifier, assigned role, and
effective permission list. Used by the frontend to decide which
view to render (Mailbox Owner vs Admin/Operator) and which navigation
items to show.
The `role` field is one of `owner`, `admin`, or `operator`.
The `permissions` array is derived from the role and is read-only.
**Latency target:** p95 < 50 ms (NFR-1).
operationId: getAuthMe
tags: [Auth]
responses:
"200":
description: Current user identity.
content:
application/json:
schema:
$ref: "#/components/schemas/AuthIdentity"
"401":
$ref: "#/components/responses/Unauthorized"
"429":
$ref: "#/components/responses/TooManyRequests"
"500":
$ref: "#/components/responses/InternalError"
/health:
get:
summary: Health check
description: |
Liveness/readiness probe for the auth layer. Does **not** require
authentication.
operationId: getHealth
tags: [Meta]
security: []
responses:
"200":
description: Healthy.
content:
application/json:
schema:
type: object
required: [status]
properties:
status:
type: string
enum: [ok]
components:
securitySchemes:
BearerAuth:
type: http
scheme: bearer
bearerFormat: JWT
description: |
JWT issued by the InboxOS auth layer. The `sub` claim is the user
identifier; the `roles` claim is an array of strings. The RBAC
middleware inspects this claim against the route's required role.
schemas:
AuthIdentity:
type: object
required: [user_id, email, role, permissions]
properties:
user_id:
type: string
description: Stable user identifier.
example: "usr_2vPqN3"
email:
type: string
format: email
description: User's registered email address.
example: "owner@example.com"
role:
type: string
enum: [owner, admin, operator]
description: Primary role assigned to the user.
example: "owner"
permissions:
type: array
items:
type: string
description: Effective permissions derived from role.
example: ["mailbox:read", "mailbox:write"]
issued_at:
type: string
format: date-time
description: JWT issuance timestamp.
example: "2026-06-10T09:42:00Z"
expires_at:
type: string
format: date-time
description: JWT expiration timestamp.
example: "2026-06-10T10:42:00Z"
MiddlewareContract:
type: object
description: |
**Interface contract (not a REST endpoint).**
Every request entering the InboxOS API layer MUST pass through the
RBAC middleware before the route handler is invoked. The middleware
contract is:
1. **Extract token:** Parse `Authorization: Bearer <token>` header.
If missing or malformed → `401 Unauthorized`.
2. **Validate token:** Verify signature and expiry. If invalid →
`401 Unauthorized`.
3. **Resolve role:** Decode `roles` claim. If absent → `403 Forbidden`
with code `ROLE_REQUIRED`.
4. **Check route requirement:** Each route declares a required role
(or `null` for public routes). If the user's role is not in the
allowed set → `403 Forbidden` with code `OWNER_ACCESS_REQUIRED`,
`ADMIN_ACCESS_REQUIRED`, or `OPERATOR_ACCESS_REQUIRED` as
appropriate.
5. **Inject context:** Set `request.user` (user_id, email, role,
permissions) so handlers can perform additional authorisation if
needed.
6. **Latency budget:** Steps 1–5 MUST complete in < 50 ms per request
(NFR-1). Measurement is via `inboxos_rbac_duration_seconds`
histogram.
**Implementation note:** The middleware is a pure function with no
external network calls in the hot path; role-to-permission mapping is
cached in-process with a 60 s TTL.
required: [name, version, steps]
properties:
name:
type: string
example: "inboxos-rbac-middleware"
version:
type: string
example: "1.5.0"
steps:
type: array
items:
type: object
required: [order, action, failure_response]
properties:
order:
type: integer
action:
type: string
failure_response:
type: object
properties:
status:
type: integer
code:
type: string
Error:
type: object
required: [error, code]
properties:
error:
type: string
code:
type: string
example: "ROLE_REQUIRED"
details:
type: object
nullable: true
responses:
Unauthorized:
description: Missing or invalid Bearer token.
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
example:
error: "Authentication required. Provide a valid Bearer token in the Authorization header."
code: "UNAUTHORIZED"
TooManyRequests:
description: Rate limit exceeded.
headers:
Retry-After:
description: Seconds until the quota resets.
schema:
type: integer
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
example:
error: "Rate limit exceeded. Retry after 60 seconds."
code: "RATE_LIMITED"
InternalError:
description: Unexpected server-side failure. Logged with `run_id` for triage.
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
example:
error: "Internal server error. Run ID: 2026-06-10T14-00-00Z-abc123. Contact operator."
code: "INTERNAL_ERROR"
details:
run_id: "2026-06-10T14-00-00Z-abc123"
incident_url: "https://git.cloudinit.dev/jc-capital/inboxos/issues/new"