Repository navigation
Authorization and RBAC
Canonical source:
docs/03-architecture/authorization-rbac.md— this page is rendered from it on every publish; edit the source.
ClawAI enforces authorization at the backend (frontend guards are cosmetic). The
base layer is Role-Based Access Control via two NestJS guards from shared-auth,
applied across all backend services. The Auth/RBAC/Plans/Quota flagship
(2026-05) layers on top of this:
- A new
USERrole (default for self-registration) alongside the legacy ADMIN/OPERATOR/VIEWER. New users register as ACTIVE on the default Free plan. -
Dynamic, DB-backed roles + permissions — a fixed
Permissioncode catalog (@claw/shared-types, 30 entries) granted to roles via theRolePermissiontable; admins create custom roles and toggle grants. Effective permissions = role grants ∩ plan feature-gates. -
Plans + daily token quota + per-plan model access, resolved fresh per
request by the shared
EntitlementsAdapter(Redis-cached, event-invalidated) so a plan/role change applies on the next request without re-issuing the JWT. - ADMIN bypasses quota + model-access + plan checks entirely.
See Flagship: Dynamic RBAC, Plans, Quota & Model Access below for the full model.
Full platform access. Manages users, connectors, routing policies, and system settings.
Typical users: IT administrators, team leads, platform owners.
Standard usage access. Can use chat, manage own memories and files, view connectors and routing decisions (read-only).
Typical users: Developers, analysts, regular team members.
Read-only access. Can view own chat threads but cannot create messages or modify anything.
Typical users: Auditors, observers, stakeholders needing visibility without write access.
| Resource | ADMIN | OPERATOR | VIEWER | USER |
|---|---|---|---|---|
| Users | Full CRUD | -- | -- | -- |
| System Settings | Full CRUD | -- | -- | -- |
| Connectors | Full CRUD | Read | -- | -- |
| Routing Policies | Full CRUD | Read | -- | -- |
| Chat Threads (own) | Full CRUD | Full CRUD | Read | Full CRUD |
| Chat Threads (all) | Full CRUD | -- | -- | -- |
| Chat Messages (own) | Full CRUD | Full CRUD | Read | Full CRUD |
| Memories (own) | Full CRUD | Full CRUD | Read | -- |
| Context Packs (own) | Full CRUD | Full CRUD | Read | -- |
| Files (own) | Full CRUD | Full CRUD | -- | -- |
| Workspace shell¹ | Full | Read | Read | Read |
| Provider app-configs¹ | Full CRUD | Read | Read | Read |
| Workspace own-connector flow¹ | Full | -- | -- | Full (own) |
| Audit Logs | Full access | Read | -- | -- |
| Usage Ledger | Full access | Read | -- | -- |
| Ollama Models | Full CRUD | Read | -- | -- |
| Model Catalog | Full access | Read + Pull | Read | -- |
| Health Status | Full access | Full access | Full access | Full access |
| Server Logs | Full access | -- | -- | -- |
| Client Logs | Full access | -- | -- | -- |
| Image Generation | Full access | Full access | -- | -- |
| File Generation | Full access | Full access | -- | -- |
¹ Backed by the new WORKSPACE_VIEW and WORKSPACE_APP_CONFIG_VIEW
permissions (added 2026-05-30). USER also receives WORKSPACE_CONNECT_OWN,
WORKSPACE_READ_OWN, WORKSPACE_SYNC_OWN, WORKSPACE_ACTION_OWN to use
their own connections; write/delete of provider-app-configs and policy
mutations stay locked to ADMIN_WORKSPACE_AUTOMATION_MANAGE. See
Workspace partial-relax pattern
below.
Applied to every endpoint of every service that uses it. Part of the shared-auth package.
Request -> AuthGuard
|
+-- Check @Public() decorator -> Skip if present
|
+-- Extract JWT from Authorization header
|
+-- Verify signature (JWT_SECRET)
|
+-- Verify expiry
|
+-- Decode payload -> Attach to request.user
|
+-- Pass to next guard/handler
Bypass: Endpoints decorated with @Public() skip authentication entirely. Used for:
POST /auth/loginPOST /auth/refresh-
GET /health(all services) -
POST /ollama/generate(internal)
Applied to endpoints that require specific roles. Reads the @Roles() decorator.
Request -> RolesGuard
|
+-- Read @Roles() metadata from endpoint
|
+-- If no @Roles() -> Allow all authenticated users
|
+-- Compare request.user.role against allowed roles
|
+-- If role matches -> Allow
|
+-- If role does not match -> 403 Forbidden
// Skip authentication entirely
@Public()
// Require specific roles
@Roles(UserRole.ADMIN)
@Roles(UserRole.ADMIN, UserRole.OPERATOR)
// Extract current user from request
@CurrentUser() user: JwtPayloadBeyond role-based access, ClawAI enforces resource ownership at the service layer:
| Resource | Ownership Check |
|---|---|
| Chat Threads | thread.userId === currentUser.id |
| Chat Messages | Via thread ownership |
| Memories | memory.userId === currentUser.id |
| Context Packs | pack.userId === currentUser.id |
| Files | file.userId === currentUser.id |
Controller: Extract userId from @CurrentUser()
-> Service: Verify resource.userId === currentUser.id
-> If mismatch: throw BusinessException(FORBIDDEN_*_ACCESS, 403)
-> If match: proceed with operation
ADMIN users can bypass ownership checks for administrative purposes (e.g., viewing any user's threads for compliance review).
| Status | Code | When |
|---|---|---|
| 401 | UNAUTHORIZED | Missing, invalid, or expired JWT |
| 403 | FORBIDDEN | Role not allowed for endpoint |
| 403 | FORBIDDEN_THREAD_ACCESS | Accessing another user's thread |
| 403 | FORBIDDEN_MEMORY_ACCESS | Accessing another user's memory |
| 403 | FORBIDDEN_CONTEXT_PACK_ACCESS | Accessing another user's context pack |
| 403 | FORBIDDEN_FILE_ACCESS | Accessing another user's file |
- No role escalation: Users cannot modify their own role. Only another ADMIN can change roles.
- No self-deactivation: Users cannot deactivate their own account.
- Guards are global: AuthGuard is registered at the application level, not per-module. Every endpoint is protected by default.
- Defense in depth: Both guard-level (role check) and service-level (ownership check) enforcement exist. Even if a guard is misconfigured, the service layer validates ownership.
- Audit trail: All 403 responses are logged. Repeated forbidden access attempts can be detected in audit logs.
This section documents the SaaS-ification layer added in 2026-05.
-
Role(slug unique,isSystem,isAssignable) +RolePermission(one row per granted permission) live inclaw_auth. Seeded system roles: ADMIN (all permissions) and USER (own-scoped set).User.roleIdFKs the role; the JWT carries the role slug only (sub,email,role) — entitlements are not embedded. -
Permissionis a fixed code catalog in@claw/shared-types(packages/shared-types/src/enums/permission.enum.ts). Categories:CHAT_*,MEMORY_*,CONTEXT_PACK_*,WORKSPACE_*(including the new read-onlyWORKSPACE_VIEWandWORKSPACE_APP_CONFIG_VIEW— see Workspace partial-relax permission pattern),MODEL_USE_ALLOWED,ROUTER_USE,COMPARE_USE,JUDGE_USE,FILES_USE,RESEARCH_USE,AGENT_USE,MODELS_CATALOG_VIEW,VIEW_DASHBOARD, and theADMIN_*management permissions. - Admin role/permission management (auth-service
rolesmodule,@Roles(ADMIN)):GET/POST/PATCH/DELETE /api/v1/admin/roles,GET /api/v1/admin/roles/permissions(catalog),PUT /api/v1/admin/roles/:id/permissions(set grants). System roles cannot be deleted; ADMIN cannot be stripped ofADMIN_PERMISSIONS_MANAGE(lockout guard).
-
Plan(name/slug/prices/dailyTokenQuota/feature gates: allowCompareMode, allowJudgeMode, allowWorkspaces, allowMemory, allowContextPacks),PlanModelAccess(provider/model + allowAsPrimary/Fallback/Judge/InCompare),UserPlanAssignment,TokenUsageLedger. Seeded plans: free / pro / team. - Admin plan management (auth-service
plansmodule,@Roles(ADMIN)) under/api/v1/admin/plans— CRUD, activate/deactivate, set-default, reorder,PUT /:id/model-access, assign-user. -
Quota is a Redis-atomic reserve→finalize→release counter keyed
quota:{userId}:{YYYY-MM-DD}(TTL to end of day) backed by the durableTokenUsageLedger. chat-service reserves before the LLM call and finalizes after.
- auth-service is the source of truth.
GET /internal/users/:id/entitlements(@Public, service-to-service) returns{ role, isAdmin, permissions[], plan, allowedModels[], allowedProviders[], quota{dailyLimit,used,remaining,unlimited} }. - The shared
@claw/shared-entitlementsEntitlementsAdapterresolves this fresh per request (Redis-cached, event-invalidated on plan/role/quota changes), plus helpershasPermission,hasPlanFeature,isModelAllowedForUsage. - The frontend uses the auth-guarded
GET /api/v1/auth/me/entitlements(same aggregate, scoped to the caller) to render/planand/usage.
| Layer | Enforcement |
|---|---|
chat-service AccessControlService
|
403 MODEL_NOT_ALLOWED_FOR_PLAN on a forbidden manual model; 429 quota.dailyLimitExceeded when the daily budget is exhausted. Fail-open on an entitlements outage (auth remains the hard backstop). ADMIN/unlimited pass. |
| routing-service AUTO path |
applyPlanModelGate(decision, allowedModels) drops off-plan candidates and promotes the first allowed fallback; empty allowedModels = allow-all (preserves the v1 hot path). |
routing-service RoutingController
|
@Roles(ADMIN, OPERATOR) on the whole controller — RoutingDecision rows carry messageContent but no userId, so decisions/detail/:id is admin/operator-only to prevent a cross-user prompt leak. |
The ADMIN role bypasses Quota, ModelAccess and Plan checks entirely (no plan
assignment required). Chosen over a synthetic "unrestricted plan".
The workspace surface ships with a deliberately two-tier permission scope so
USER can self-serve their own connectors without giving them write access to
admin-managed infrastructure (provider-app-configs, policies, webhook replay).
The split was introduced 2026-05-30 because the legacy admin-only gate hid the
whole /workspace/* route tree from USER even though we needed them to browse
providers and connect their own GitHub/Slack/etc. account.
The two new narrow permissions:
| Permission | Granted to USER? | What it unlocks |
|---|---|---|
WORKSPACE_VIEW |
Yes | Read-only access to the workspace shell — the /workspace, /workspace/connections, and provider-listing pages. |
WORKSPACE_APP_CONFIG_VIEW |
Yes | Browse admin-created provider-app-configs (sanitised as ProviderAppConfigPublic — hasSecret: boolean only; never encryptedSecret). Required so the connect-own-account flow can pick which provider app to use. |
The existing *_OWN workspace permissions then let USER actually use the
shell: WORKSPACE_CONNECT_OWN (create a connector for themselves),
WORKSPACE_READ_OWN, WORKSPACE_SYNC_OWN, WORKSPACE_ACTION_OWN. Every
mutation that affects admin-managed state (creating/updating/deleting
provider-app-configs, suggestion rules, policies, replaying webhooks) stays
gated by ADMIN_WORKSPACE_AUTOMATION_MANAGE + @Roles(ADMIN).
This is the partial-relax pattern: rather than introducing a single coarse
WORKSPACE_USER permission, two narrow permissions are added so admins can
take each capability away independently in the role→permission matrix without
having to re-engineer the gate. The same pattern is the template for future
"USER can read but not write" surfaces.
ClawAI Wiki · source-controlled under wiki/ · generated/mirrored from the current repository knowledge layer.
ClawAI Wiki
Product
- Product-Vision
- Flagship-Features
- Business-Overview
- Requirements-Register
- Drift-Log
- Runtime-Managed-Connector-Providers
- Threads
- ADR-Index
Architecture
- System-at-a-Glance
- Architecture-Map
- System-Architecture
- Message-Flow
- End-to-End-Data-Flow
- Routing-Engine
- Event-Bus
- Security-Architecture
Platform
- Backend-Services
- Frontend-Architecture
- Shared-Packages
- Database-Reference
- API-Reference
- Provider-Catalog
- Memory-and-Context-Architecture
- File-Support-Architecture
- Research-and-Web-Crawling
Runtime & Ops
- Docker-Guide
- Nginx-Reference
- CI-CD-Pipeline
- Logging-and-Observability
- Operational-Runbooks
- Troubleshooting
Quality
Engineering OS
- AI-Native-Engineering-OS
- Generated-Manifests
- Rules-Catalog
- Skills-Catalog
- Context-Layer
- Engineering-Memory
- Tooling-and-Automation
- Scripts-and-Operations
- ESLint-and-Architecture-Enforcement
- Extended-Work-Skills
Agents
Reference