Raxx · internal docs

internal · gated ↑ index

RBAC V2 — API Contract

Status: Design
Date: 2026-05-09 UTC
Owner: software-architect
Refs: design.md, ADR-0054, ADR-0055

All endpoints are on the Console Flask app (console.raxx.app) unless noted. All timestamps UTC. All requests require a valid Console session cookie. All responses are JSON.


Authentication contract

Every endpoint below requires: 1. A valid console_session cookie (validated by validate_session()). 2. The applicable RBAC role (listed per endpoint). 3. No endpoint accepts API keys or bearer tokens — session cookie only.

403 responses follow this shape:

{
  "error": "forbidden",
  "required_role": "<role_name>"
}

GET /api/rbac/me

Returns the current operator's effective roles and permissions.

RBAC gate: Any valid session (no role required; returns the caller's own data).

Query parameters: none

Response 200:

{
  "admin_id": "uuid",
  "groups": [
    {"id": "uuid", "name": "raxx-platform-admins"}
  ],
  "roles": [
    {"name": "console-token-admin", "app": "console", "via_group": "raxx-platform-admins"},
    {"name": "console-token-user",  "app": "console", "via_group": "raxx-platform-admins", "inherited_from": "console-token-admin"}
  ],
  "permissions": ["console:tokens:read", "console:tokens:rotate", "console:tokens:delete"],
  "ticket_grants": [
    {
      "id": "grant-uuid",
      "role_name": "raptor-audit-support",
      "ticket_id": "FreeScout:888",
      "customer_id": 42,
      "expires_at_utc": null
    }
  ],
  "break_glass_active": false,
  "cached_at_utc": "2026-05-09T12:00:00Z"
}

Caching: The resolved role/permission set is cached in the session record. cached_at_utc reflects when the cache was last computed. The /me endpoint always returns the current cache; it does not force a recompute. To invalidate the cache (after a grant/revoke), the grant writer calls the session invalidation service.


POST /api/rbac/grants

Grant a group membership or direct role to a user.

RBAC gate: console-invite-admin

Request body:

{
  "target_user_id": "admin-uuid",
  "grant_type": "group",
  "group_id": "group-uuid"
}

Or for a direct role grant (break-glass pattern only):

{
  "target_user_id": "admin-uuid",
  "grant_type": "role",
  "role_id": "role-uuid",
  "justification": "Emergency incident response — outage in progress",
  "expires_in_seconds": 3600
}

Validation: - target_user_id must exist in admins. - Caller must hold console-invite-admin. - Self-grant check: if target_user_id == g.admin_id AND the caller does not already hold the target role/group transitively → 422 "self_escalation_prohibited". - Break-glass role grants: justification is required (minimum 20 chars). expires_in_seconds max 14400 (4h). - If the target group is break-glass: triggers passkey re-authentication challenge before completing.

Pre-write audit: rbac_grants_audit INSERT fires before the group/role assignment. If the audit INSERT fails, the grant is not applied (returns 500).

Response 201:

{
  "grant_id": "audit-row-uuid",
  "target_user_id": "admin-uuid",
  "group_id": "group-uuid",
  "granted_at_utc": "2026-05-09T12:00:00Z"
}

Side effects: - Invalidates session cache for target_user_id. - If role is in the audit role family (raptor-audit-*): writes a customer_audit_events row with action=operator.rbac.grant. - If break-glass group grant: fires Slack alert to ops@raxx.app immediately.


DELETE /api/rbac/grants/{grant_id}

Revoke a group membership, direct role grant, or ticket-scoped grant.

RBAC gate: console-invite-admin (or the target user revoking their own ticket grant)

Path parameter: grant_id — the id from rbac_grants_audit.

Response 200:

{
  "grant_id": "audit-row-uuid",
  "revoked_at_utc": "2026-05-09T12:30:00Z"
}

Validation: - Self-revocation is permitted only for ticket-scoped grants the caller holds. - Revoking a break-glass grant requires passkey re-authentication.

Side effects: same as POST — audit row, session invalidation, optional customer notification row.


POST /api/rbac/grants/ticket-scoped

Grant a ticket-scoped temporary role to a user, bound to a specific customer + FreeScout ticket.

RBAC gate: console-invite-admin

Request body:

{
  "target_user_id": "admin-uuid",
  "role_name": "raptor-audit-support",
  "ticket_id": "FreeScout:888",
  "customer_id": 42,
  "expires_in_seconds": null
}

Validation: - ticket_id must resolve to an OPEN ticket in FreeScout via live API check. - customer_id must exist in Raptor's users table. - role_name must be in the set of allowed ticket-scopeable roles: ["raptor-audit-support"] (v1 scope). Other roles may be added by operator config without code ship. - expires_in_seconds is optional; if null the grant expires only on ticket close.

Response 201:

{
  "ticket_grant_id": "ticket-grant-uuid",
  "role_name": "raptor-audit-support",
  "ticket_id": "FreeScout:888",
  "customer_id": 42,
  "expires_at_utc": null,
  "granted_at_utc": "2026-05-09T12:00:00Z"
}

Auto-revoke: A background job (or FreeScout webhook — see OQ-2 in design.md) monitors ticket state. On ticket RESOLVED or CLOSED, all active rbac_ticket_grants rows for that ticket_id are revoked (soft delete: revoked_at_utc set, revoke_reason=ticket_closed). A rbac_grants_audit row is inserted (event_type: ticket_expire). Session cache for affected users is invalidated.


GET /api/rbac/permissions/check

Check whether the current session holds a specific permission, optionally scoped to a resource.

RBAC gate: Any valid session.

Query parameters: - permission (required): e.g., raptor:audit:read-support - resource_id (optional): e.g., customer_id for dim-3 scoped checks - ticket_id (optional): for ticket-scoped checks

Response 200:

{
  "allowed": true,
  "permission": "raptor:audit:read-support",
  "resolved_via": "ticket_grant",
  "ticket_grant_id": "ticket-grant-uuid"
}

Or:

{
  "allowed": false,
  "permission": "raptor:audit:read-support",
  "reason": "no_ticket_grant"
}

Notes: - This endpoint is designed for the Raptor audit reader (SC-A8) to call as a pre-check before serving audit data. - It does NOT perform the ticket status re-validation against FreeScout; that is the caller's responsibility for dim-3 requests. This endpoint only reports whether the user has the role. - Responses are not cached; every call is a live DB lookup.


GET /api/rbac/grants/audit

Paginated audit log of all grant and revoke events.

RBAC gate: console-audit-user

Query parameters: - target_user_id (optional): filter by affected user - event_type (optional): one of grant, revoke, ticket_grant, ticket_expire, break_glass_grant, break_glass_expire - from_utc / to_utc (optional): ISO timestamps; default last 30 days - page / per_page (optional): pagination; max per_page 100

Response 200:

{
  "total": 143,
  "page": 1,
  "per_page": 50,
  "events": [
    {
      "id": "uuid",
      "event_type": "ticket_grant",
      "target_user_id": "admin-uuid",
      "target_user_email_hint": "a...@raxx.app",
      "group_name": null,
      "role_name": "raptor-audit-support",
      "ticket_id": "FreeScout:888",
      "customer_id": 42,
      "justification": null,
      "granted_by": "admin-uuid",
      "granted_by_email_hint": "k...@raxx.app",
      "expires_at_utc": null,
      "created_at_utc": "2026-05-09T12:00:00Z"
    }
  ]
}

Notes: - email_hint fields show first character + domain only (not full email) in the response. Full email is available in the DB for authorized lookups. - This endpoint feeds the Console Grants Audit timeline page.


GET /api/rbac/roles

List all defined roles with their permissions.

RBAC gate: console-audit-user

Response 200: Array of role objects, each with name, app, description, permissions[], inherited_from[].


GET /api/rbac/groups

List all groups with their roles and member counts.

RBAC gate: console-audit-user


Error shapes

HTTP error key Meaning
401 unauthenticated No valid session cookie
403 forbidden Valid session but insufficient role
422 self_escalation_prohibited Caller attempted to grant themselves a role they don't hold
422 cycle_detected Proposed role inheritance edge would create a cycle
422 ticket_not_open Ticket-scoped grant rejected because FreeScout ticket is not OPEN
429 break_glass_rate_limited Too many break-glass sessions opened in 24h
503 freescout_unavailable FreeScout check for ticket status failed; dim-3 fails closed