RBAC & Roles API
Swagger-style reference for CAIPE UI UI Backend API routes that manage Keycloak realm roles, IdP group β role mappings, authorization policies (MongoDB + CEL), RBAC audit (MongoDB), and current-user role/permission introspection.
All paths are relative to the Next.js app (typically /api/...). Unless noted, responses from withErrorHandler + successResponse use the envelope { "success": true, "data": ... }. Errors use { "success": false, "error": "...", "code": "..." } when thrown as ApiError.
RBAC model (overview)β
Keycloak realm rolesβ
Platform roles are assigned in Keycloak (often via IdP group mappers). Common built-in / protected names include:
adminβ Full admin; OIDC group or MongoDBusers.metadata.role === "admin"can elevate the UI Backend API session.chat_userβ Standard chat user.team_memberβ Team-scoped collaboration.kb_adminβ Knowledge-base administration (global; complements per-KB roles).offline_access,uma_authorization,default-roles-<realm>β Keycloak/OIDC plumbing; listed by the admin roles API and must not be deleted via the UI Backend API.
The UI Backend API also recognizes denied in type definitions for explicit lockout scenarios.
Per-KB rolesβ
Fine-grained KB access uses realm role name conventions (JWT realm_access.roles):
| Pattern | Purpose |
|---|---|
kb_reader:<kb-id> | Read/query a specific KB |
kb_ingestor:<kb-id> | Ingest into a specific KB |
kb_admin:<kb-id> | Full admin for that KB |
Wildcards such as kb_reader:* may be used where policy allows. Global admin / kb_admin typically bypass per-KB checks downstream (RAG server + team ownership from MongoDB).
Per-agent rolesβ
Dynamic agent access combines Keycloak resources/scopes with realm roles:
| Pattern | Purpose |
|---|---|
agent_user:<agent-id> | Use (view/invoke) a specific agent |
agent_admin:<agent-id> | Configure/delete that agent |
Wildcards like agent_user:* may apply platform-wide at that scope per deployment policy.
Policy evaluation layerβ
The BFF no longer supports a supplementary CEL deny layer. After Keycloak Authorization Services grants a permission, the request proceeds unless a resource-specific OpenFGA/ReBAC check or service-side guard denies it. Model new resource policy as OpenFGA relationships and audited ReBAC change sets.
Keycloak AuthZ resources & scopesβ
Typed resources include admin_ui, slack, rag, sub_agent, tool, skill, and mcp. Scopes include view, create, update, delete, invoke, admin, configure, ingest, query, audit.view, kb.admin, kb.ingest, kb.query, and tool-specific scopes. The permissions endpoint returns effective resource β [scopes] from an RPT (response_mode=permissions).
Realm Roles (CRUD)β
GET /api/admin/rolesβ
Auth: Session (admin) | Since: v1.0
Lists all realm roles in the configured Keycloak realm via the Admin REST API. Caller must have UI Backend API admin role (OIDC or MongoDB fallback).
Query Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
| (none) | β | β | β |
Request Body: None.
Response 200:
{
"success": true,
"data": {
"roles": [
{
"id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"name": "admin",
"description": "Platform administrators",
"composite": false,
"clientRole": false,
"containerId": "caipe"
},
{
"id": "b2c3d4e5-f6a7-8901-bcde-f12345678901",
"name": "kb_reader:platform-docs",
"description": "Read access to platform-docs KB",
"composite": false,
"clientRole": false,
"containerId": "caipe"
}
],
"total": 2
}
}
Errors:
| Status | Code | Description |
|---|---|---|
| 401 | (none) | No valid session (Unauthorized). |
| 403 | (none) | Not admin (Admin access required - must be member of admin group). |
| 500 | (none) | Keycloak admin token or list failure (message from handler). |
POST /api/admin/rolesβ
Auth: Session (admin) | Since: v1.0
Creates a new realm role in Keycloak.
Query Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
| (none) | β | β | β |
Request Body:
{
"name": "custom_support",
"description": "Optional human-readable description"
}
Response 201:
{
"success": true,
"data": {
"message": "Role created successfully",
"name": "custom_support"
}
}
Errors:
| Status | Code | Description |
|---|---|---|
| 400 | (none) | Missing or empty name (Role name is required). |
| 401 | (none) | No valid session. |
| 403 | (none) | Not admin. |
| 500 | (none) | Keycloak create failed (e.g. duplicate role). |
GET /api/admin/roles/{name}β
Auth: Session (admin) | Since: v1.0
Returns a single realm role by name. {name} is URL-encoded (e.g. kb_reader%3Aplatform-docs).
Query Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
| (none) | β | β | β |
Request Body: None.
Response 200:
{
"success": true,
"data": {
"role": {
"id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"name": "admin",
"description": "Platform administrators",
"composite": false,
"clientRole": false,
"containerId": "caipe"
}
}
}
Errors:
| Status | Code | Description |
|---|---|---|
| 401 | (none) | No valid session. |
| 403 | (none) | Not admin. |
| 404 | (none) | Role not found. |
| 500 | (none) | Keycloak error. |
DELETE /api/admin/roles/{name}β
Auth: Session (admin) | Since: v1.0
Deletes a realm role by name. Built-in roles cannot be deleted: admin, chat_user, team_member, kb_admin, offline_access, uma_authorization, default-roles-caipe.
Query Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
| (none) | β | β | β |
Request Body: None.
Response 200:
{
"success": true,
"data": {
"message": "Role deleted successfully"
}
}
Errors:
| Status | Code | Description |
|---|---|---|
| 400 | (none) | Target is a built-in role (Cannot delete built-in role). |
| 401 | (none) | No valid session. |
| 403 | (none) | Not admin. |
| 500 | (none) | Keycloak delete error. |
Role Mappings (IdP group to role)β
GET /api/admin/role-mappingsβ
Auth: Session (admin) | Since: v1.0
Lists all identity providers and their OIDC mappers, flattened with an idpAlias field on each mapper for UI use.
Query Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
| (none) | β | β | β |
Request Body: None.
Response 200:
{
"success": true,
"data": {
"mappers": [
{
"id": "mapper-uuid-1",
"name": "duo-sso-engineering-to-team_member",
"identityProviderAlias": "duo-sso",
"identityProviderMapper": "oidc-advanced-role-idp-mapper",
"config": {
"syncMode": "INHERIT",
"are.claim.values.regex": "false",
"claims": "[{\"key\":\"groups\",\"value\":\"engineering\"}]",
"role": "team_member"
},
"idpAlias": "duo-sso"
}
],
"idpAliases": [
{
"alias": "duo-sso",
"displayName": "Duo SSO",
"providerId": "oidc"
}
]
}
}
Errors:
| Status | Code | Description |
|---|---|---|
| 401 | (none) | No valid session. |
| 403 | (none) | Not admin. |
| 500 | (none) | Keycloak admin API failure. |
POST /api/admin/role-mappingsβ
Auth: Session (admin) | Since: v1.0
Creates an OIDC advanced role IdP mapper that assigns a realm role when the IdP tokenβs groups claim contains an exact group name.
Query Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
| (none) | β | β | β |
Request Body:
{
"idpAlias": "duo-sso",
"groupName": "caipe-admins",
"roleName": "admin"
}
Response 201:
{
"success": true,
"data": {
"id": "new-mapper-uuid",
"name": "duo-sso-caipe-admins-to-admin",
"identityProviderAlias": "duo-sso",
"identityProviderMapper": "oidc-advanced-role-idp-mapper",
"config": {
"syncMode": "INHERIT",
"are.claim.values.regex": "false",
"claims": "[{\"key\":\"groups\",\"value\":\"caipe-admins\"}]",
"role": "admin"
}
}
}
Errors:
| Status | Code | Description |
|---|---|---|
| 400 | (none) | Invalid or empty idpAlias, groupName, or roleName. |
| 401 | (none) | No valid session. |
| 403 | (none) | Not admin. |
| 500 | (none) | Keycloak mapper creation failed. |
DELETE /api/admin/role-mappings/{id}β
Auth: Session (admin) | Since: v1.0
Deletes an IdP mapper by mapper id and IdP alias (query param).
Query Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
alias | string | Yes | Identity provider alias (e.g. duo-sso). |
Request Body: None.
Response 200:
{
"success": true,
"data": {
"ok": true
}
}
Errors:
| Status | Code | Description |
|---|---|---|
| 400 | (none) | Missing alias (alias query parameter is required). |
| 401 | (none) | No valid session. |
| 403 | (none) | Not admin. |
| 500 | (none) | Keycloak delete failed. |
RBAC Permissions (current user)β
GET /api/rbac/permissionsβ
Auth: Session with access token | Since: v1.0
Returns the callerβs effective Keycloak Authorization permissions as a map of resource name β scope list (from UMA ticket grant with response_mode=permissions). Used for capability-based UI (e.g. useRbacPermissions). Does not use the success / data envelope.
Query Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
| (none) | β | β | β |
Request Body: None.
Response 200:
{
"permissions": {
"admin_ui": ["view", "audit.view"],
"rag": ["kb.query", "kb.ingest"],
"skill": ["view", "invoke"]
}
}
Errors:
| Status | Code | Description |
|---|---|---|
| 401 | (none) | { "error": "Unauthorized" } β no session or no accessToken. |
| 503 | (none) | { "error": "Failed to retrieve permissions" } β Keycloak or network failure. |
GET /api/auth/roleβ
Auth: Optional session | Since: v1.0
Returns a coarse UI role string: admin or user (default). Uses session.role from OIDC; if not admin, may promote to admin when MongoDB users has metadata.role === "admin". Unauthenticated callers still receive 200 with role: "user" (no email).
Query Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
| (none) | β | β | β |
Request Body: None.
Response 200 (authenticated):
{
"role": "admin",
"email": "alice@example.com"
}
Response 200 (no session):
{
"role": "user"
}
Errors:
| Status | Code | Description |
|---|---|---|
| (rare) | 500 | MongoDB errors are logged; response typically still 200 with OIDC-derived role. |
Authorization Policiesβ
GET /api/policiesβ
Auth: Session (admin view) | Since: v1.0
Loads the default named policy document (name: "default") from MongoDB. Requires authentication (all authenticated users can read). Requires MongoDB.
Query Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
| (none) | β | β | β |
Request Body: None.
Response 200 (document exists):
{
"success": true,
"data": {
"name": "default",
"content": "# CEL / policy rules...\npackage caipe\n...",
"is_system": true,
"updated_at": "2026-03-25T12:00:00.000Z",
"updated_by": "alice@example.com",
"exists": true
}
}
Response 200 (no document yet):
{
"success": true,
"data": {
"name": "default",
"content": "",
"is_system": true,
"exists": false
}
}
Errors:
| Status | Code | Description |
|---|---|---|
| 401 | (none) | No valid session. |
| 403 | (none) | Not authenticated. |
| 503 | (none) | MongoDB not configured. |
| 500 | (none) | Unexpected error. |
PUT /api/policiesβ
Auth: Session (admin) | Since: v1.0
Upserts the default policy body in MongoDB. Requires MongoDB.
Query Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
| (none) | β | β | β |
Request Body:
{
"content": "# Updated policy logic\npackage caipe\n..."
}
Response 200:
{
"success": true,
"data": {
"message": "Policy updated successfully"
}
}
Errors:
| Status | Code | Description |
|---|---|---|
| 400 | (none) | content missing or not a string. |
| 401 | (none) | No valid session. |
| 403 | (none) | Not admin. |
| 503 | (none) | MongoDB not configured. |
POST /api/policiesβ
Auth: Session (admin) | Since: v1.0
Reset default policy from the first readable policy.lp on disk (seed paths: POLICY_SEED_PATH, /app/policy.lp, ./policy.lp, etc.). Query parameter action=reset is required.
Query Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
action | string | Yes | Must be reset. |
Request Body: None.
Response 200:
{
"success": true,
"data": {
"message": "Policy reset to default from file"
}
}
Errors:
| Status | Code | Description |
|---|---|---|
| 400 | (none) | action not reset (Only action=reset is supported). |
| 401 | (none) | No valid session. |
| 403 | (none) | Not admin. |
| 404 | (none) | No policy.lp found on disk. |
| 503 | (none) | MongoDB not configured. |
GET /api/policies/seedβ
Auth: Session (authenticated) | Since: v1.0
If no default policy exists in MongoDB, inserts one from the first found policy.lp file. If a default policy already exists, returns without changing data. Requires MongoDB.
Query Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
| (none) | β | β | β |
Request Body: None.
Response 200 (seeded):
{
"success": true,
"data": {
"seeded": true,
"message": "Seeded default policy from /app/policy.lp"
}
}
Response 200 (already present):
{
"success": true,
"data": {
"seeded": false,
"message": "Default policy already exists"
}
}
Response 200 (no file):
{
"success": true,
"data": {
"seeded": false,
"message": "No policy.lp found. Set POLICY_SEED_PATH or mount at /app/policy.lp"
}
}
Errors:
| Status | Code | Description |
|---|---|---|
| 401 | (none) | No valid session. |
| 503 | (none) | MongoDB not configured. |
RBAC Auditβ
GET /api/admin/rbac-auditβ
Auth: Session + Keycloak permission admin_ui#audit.view | Since: v1.0
Reads paginated authorization decision records from MongoDB collection authorization_decision_records. Uses getServerSession and requireRbacPermission (Keycloak UMA decision mode, then optional CEL). Scoped by session.org as tenant_id when present. Response is plain JSON (no success / data wrapper).
Query Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
from | string (ISO 8601) | No | Start of time range; default now β 24h. |
to | string (ISO 8601) | No | End of time range; default now. |
component | string | No | Filter by component (admin_ui, rag, mcp, β¦). |
capability | string | No | Filter by capability string. |
subject_hash | string | No | Filter by subject hash. |
outcome | string | No | allow or deny (case-insensitive). |
page | integer | No | Page number, default 1, must be β₯ 1. |
limit | integer | No | Page size, default 50, 1β200. |
Request Body: None.
Response 200:
{
"records": [
{
"ts": "2026-03-25T14:30:00.000Z",
"tenant_id": "org-123",
"subject_hash": "sha256:abcd...",
"actor_hash": "sha256:ef01...",
"capability": "admin_ui#audit.view",
"component": "admin_ui",
"resource_ref": "/api/admin/rbac-audit",
"outcome": "allow",
"reason_code": "OK",
"pdp": "keycloak",
"correlation_id": "req-9f3c2a1b"
},
{
"ts": "2026-03-25T14:29:55.000Z",
"tenant_id": "org-123",
"subject_hash": "sha256:abcd...",
"capability": "rag#kb.query",
"component": "rag",
"outcome": "deny",
"reason_code": "DENY_NO_CAPABILITY",
"pdp": "keycloak",
"correlation_id": "req-8e2b1a0c"
}
],
"total": 1240,
"page": 1,
"limit": 50
}
Errors:
| Status | Code | Description |
|---|---|---|
| 400 | VALIDATION_ERROR | Invalid ISO dates, from > to, bad outcome, or invalid page / limit. |
| 401 | (none) | No session or missing email; missing accessToken (Authentication required). |
| 403 | admin_ui#audit.view | Keycloak denied (You do not have permission to perform this action.). |
| 403 | CEL_DENIED | CEL policy denied (Policy denied (CEL)). |
| 503 | (none) | MongoDB not configured (MONGODB_NOT_CONFIGURED body) or PDP unavailable (Authorization service unavailable β access denied (fail-closed)). |
| 500 | (none) | Unhandled server error. |
GET /api/admin/audit-events β Unified Audit Events (FR-037)β
Paginated query across all audit event types (auth decisions, tool actions, agent delegations) stored in the audit_events MongoDB collection.
Auth: Requires valid session + requireRbacPermission(admin_ui, audit.view).
Query Parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
type | auth | tool_action | agent_delegation | (all) | Filter by event type. |
from | ISO 8601 | -24h | Start of date range. |
to | ISO 8601 | now | End of date range. |
outcome | allow | deny | success | error | (all) | Filter by outcome. |
agent_name | string | (all) | Filter by agent name (exact match). |
tool_name | string | (all) | Filter by tool name (exact match). |
user_email | string | (all) | Filter by user email (case-insensitive substring). |
component | string | (all) | Filter by component (e.g., admin_ui, dynamic_agents). |
correlation_id | string | (all) | Filter by correlation / trace id. |
page | integer β₯ 1 | 1 | Page number. |
limit | 1β200 | 50 | Results per page. |
Response (200):
{
"records": [
{
"ts": "2026-03-25T18:30:00.000Z",
"type": "tool_action",
"tenant_id": "default",
"subject_hash": "sha256:abc123...",
"user_email": "alice@example.com",
"action": "argocd_list_applications",
"agent_name": "argocd",
"tool_name": "argocd_list_applications",
"outcome": "success",
"duration_ms": 1234.56,
"correlation_id": "trace-abc-def",
"context_id": "conv-123",
"component": "dynamic_agents",
"source": "dynamic_agents"
}
],
"total": 500,
"page": 1,
"limit": 50
}
Errors:
| Status | Code | Description |
|---|---|---|
| 400 | VALIDATION_ERROR | Invalid filter values (type, outcome, dates, page, limit). |
| 401 | (none) | No session or missing email. |
| 403 | admin_ui#audit.view | Keycloak / RBAC denied. |
| 503 | MONGODB_NOT_CONFIGURED | MongoDB not configured. |
Admin tab visibilityβ
Admin UI tabs are gated by deterministic BFF logic, not editable CEL. Baseline
tabs (users, teams, skills, metrics, health) are visible to signed-in
users; administrative tabs require the admin session/realm role or bootstrap
admin email. Feature flags (feedbackEnabled,
auditLogsEnabled, actionAuditEnabled) are still ANDed with the matching tab.
GET /api/rbac/admin-tab-gatesβ
Description: Returns { gates: Record<tab_key, boolean> } for all known admin tabs (users, teams, roles, identity_group_sync, slack, skills, feedback, stats, metrics, health, audit_logs, action_audit, openfga).
Authorization: Valid NextAuth session with user.email. Unauthenticated β 401.
Parameters:
| Name | Type | Required | Description |
|---|---|---|---|
| (none) | β | β | β |
Response (200):
{
"gates": {
"users": true,
"roles": false,
"slack": false
}
}
Errors:
| Code | Description |
|---|---|
401 | { "error": "Unauthorized" } |
Notes: The retired Policy tab, admin_tab_policies collection, and
/api/rbac/admin-tab-policies endpoint are no longer part of the admin surface.
Related implementationβ
| Area | Location |
|---|---|
| Realm roles & IdP mappers | ui/src/lib/rbac/keycloak-admin.ts |
| Permission check & effective permissions | ui/src/lib/rbac/keycloak-authz.ts |
Types (RbacResource, RbacScope, audit) | ui/src/lib/rbac/types.ts |
Session, admin gates, requireRbacPermission | ui/src/lib/api-middleware.ts |
| Python audit logger | ai_platform_engineering/utils/audit_logger.py |
| Python audit callback handler | ai_platform_engineering/utils/audit_callback.py |
| BFF dual-write (auth β audit_events) | ui/src/lib/rbac/audit.ts |
| Unified audit API route | ui/src/app/api/admin/audit-events/route.ts |
| Admin tab visibility gates | ui/src/app/api/rbac/admin-tab-gates/route.ts |
| Unified audit UI component | ui/src/components/admin/UnifiedAuditTab.tsx |