[story #126] docs(api): OpenAPI spec for RBAC middleware contract
This commit is contained in:
@@ -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"
|
||||
Reference in New Issue
Block a user