Skip to main content

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 MongoDB users.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):

PatternPurpose
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:

PatternPurpose
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:

ParameterTypeRequiredDescription
(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:

StatusCodeDescription
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:

ParameterTypeRequiredDescription
(none)β€”β€”β€”

Request Body:

{
"name": "custom_support",
"description": "Optional human-readable description"
}

Response 201:

{
"success": true,
"data": {
"message": "Role created successfully",
"name": "custom_support"
}
}

Errors:

StatusCodeDescription
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:

ParameterTypeRequiredDescription
(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:

StatusCodeDescription
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:

ParameterTypeRequiredDescription
(none)β€”β€”β€”

Request Body: None.

Response 200:

{
"success": true,
"data": {
"message": "Role deleted successfully"
}
}

Errors:

StatusCodeDescription
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:

ParameterTypeRequiredDescription
(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:

StatusCodeDescription
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:

ParameterTypeRequiredDescription
(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:

StatusCodeDescription
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:

ParameterTypeRequiredDescription
aliasstringYesIdentity provider alias (e.g. duo-sso).

Request Body: None.

Response 200:

{
"success": true,
"data": {
"ok": true
}
}

Errors:

StatusCodeDescription
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:

ParameterTypeRequiredDescription
(none)β€”β€”β€”

Request Body: None.

Response 200:

{
"permissions": {
"admin_ui": ["view", "audit.view"],
"rag": ["kb.query", "kb.ingest"],
"skill": ["view", "invoke"]
}
}

Errors:

StatusCodeDescription
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:

ParameterTypeRequiredDescription
(none)β€”β€”β€”

Request Body: None.

Response 200 (authenticated):

{
"role": "admin",
"email": "alice@example.com"
}

Response 200 (no session):

{
"role": "user"
}

Errors:

StatusCodeDescription
(rare)500MongoDB 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:

ParameterTypeRequiredDescription
(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:

StatusCodeDescription
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:

ParameterTypeRequiredDescription
(none)β€”β€”β€”

Request Body:

{
"content": "# Updated policy logic\npackage caipe\n..."
}

Response 200:

{
"success": true,
"data": {
"message": "Policy updated successfully"
}
}

Errors:

StatusCodeDescription
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:

ParameterTypeRequiredDescription
actionstringYesMust be reset.

Request Body: None.

Response 200:

{
"success": true,
"data": {
"message": "Policy reset to default from file"
}
}

Errors:

StatusCodeDescription
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:

ParameterTypeRequiredDescription
(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:

StatusCodeDescription
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:

ParameterTypeRequiredDescription
fromstring (ISO 8601)NoStart of time range; default now βˆ’ 24h.
tostring (ISO 8601)NoEnd of time range; default now.
componentstringNoFilter by component (admin_ui, rag, mcp, …).
capabilitystringNoFilter by capability string.
subject_hashstringNoFilter by subject hash.
outcomestringNoallow or deny (case-insensitive).
pageintegerNoPage number, default 1, must be β‰₯ 1.
limitintegerNoPage 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:

StatusCodeDescription
400VALIDATION_ERRORInvalid ISO dates, from > to, bad outcome, or invalid page / limit.
401(none)No session or missing email; missing accessToken (Authentication required).
403admin_ui#audit.viewKeycloak denied (You do not have permission to perform this action.).
403CEL_DENIEDCEL 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:

ParameterTypeDefaultDescription
typeauth | tool_action | agent_delegation(all)Filter by event type.
fromISO 8601-24hStart of date range.
toISO 8601nowEnd of date range.
outcomeallow | deny | success | error(all)Filter by outcome.
agent_namestring(all)Filter by agent name (exact match).
tool_namestring(all)Filter by tool name (exact match).
user_emailstring(all)Filter by user email (case-insensitive substring).
componentstring(all)Filter by component (e.g., admin_ui, dynamic_agents).
correlation_idstring(all)Filter by correlation / trace id.
pageinteger β‰₯ 11Page number.
limit1–20050Results 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:

StatusCodeDescription
400VALIDATION_ERRORInvalid filter values (type, outcome, dates, page, limit).
401(none)No session or missing email.
403admin_ui#audit.viewKeycloak / RBAC denied.
503MONGODB_NOT_CONFIGUREDMongoDB 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:

NameTypeRequiredDescription
(none)β€”β€”β€”

Response (200):

{
"gates": {
"users": true,
"roles": false,
"slack": false
}
}

Errors:

CodeDescription
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.


AreaLocation
Realm roles & IdP mappersui/src/lib/rbac/keycloak-admin.ts
Permission check & effective permissionsui/src/lib/rbac/keycloak-authz.ts
Types (RbacResource, RbacScope, audit)ui/src/lib/rbac/types.ts
Session, admin gates, requireRbacPermissionui/src/lib/api-middleware.ts
Python audit loggerai_platform_engineering/utils/audit_logger.py
Python audit callback handlerai_platform_engineering/utils/audit_callback.py
BFF dual-write (auth β†’ audit_events)ui/src/lib/rbac/audit.ts
Unified audit API routeui/src/app/api/admin/audit-events/route.ts
Admin tab visibility gatesui/src/app/api/rbac/admin-tab-gates/route.ts
Unified audit UI componentui/src/components/admin/UnifiedAuditTab.tsx