Roles & Permissions
Configure global roles, custom roles, and team-scoped permissions
Authorization in Clouisle stacks global roles and team gates. The frontend hides menus and buttons by permission, and the backend re-validates in API dependencies — "cannot see the entry point" is not a security boundary; the endpoint still returns 403.
For the full permission catalog and built-in role matrix, see the Permissions & Roles Reference. This page covers how to operate it, who has which permission, and what happens when it fails.
Permission Model
Permission Hierarchy
Identity layer (is_superuser bypasses everything; no permission or membership check)
└── Global layer (User → Role → Permission, matched by permission code; `*` matches anything)
Global-layer namespaces
├── Admin permissions (admin:*, requires admin:dashboard:access to enter the dashboard)
│ ├── admin:user:*, admin:role:*, admin:permission:*
│ ├── admin:team:*, admin:model:*, admin:app:*, admin:capability:*
│ ├── admin:knowledge-base:*, admin:settings:*, admin:sso:*
│ ├── admin:conversation:*, admin:memory:*, admin:notification:*
│ └── audit:read, audit:export
└── Resource permissions (team:*, agent:*, workflow:*, kb:*, tool:*, skill:*, apikey:*, conversation:*, memory:*)
Team gate layer (TeamMember.role: owner / admin / member / viewer)
└── Applied when the resource carries team_id: the user must be a member; owner/admin-only
operations are decided by TEAM_MANAGEMENT_PERMISSIONSThe two layers are an AND: the global layer supplies "what may be done", the team gate supplies "in which team and at what level". Team roles therefore grant no permission codes themselves, and admin-dashboard permissions can never come from a team role.
Permission Check Order
For a team-scoped request the backend runs, in order (see backend/app/api/team_access.py):
- Team exists: missing team →
team_not_found, HTTP 404. - Superuser shortcut:
is_superuser=Truepasses immediately, without role or membership checks. - Membership: not a member of that team →
not_team_member, HTTP 403. - Team admin gate: when the required permission is in
TEAM_MANAGEMENT_PERMISSIONS(or the caller explicitly requires it) and the membership role is notowner/admin→team_admin_required, HTTP 403. - Team viewer limit: membership role is
viewerand the required permission is not inVIEWER_ALLOWED_PERMISSIONS→operation_not_permitted, HTTP 403. - Global permission code: some global role of the user must contain the code (or the wildcard
*); otherwiseoperation_not_permitted, HTTP 403.
Global-layer and admin endpoints use PermissionChecker("code"): superusers pass straight through; regular users must match the code. * is an ordinary permission code that the check logic treats as "matches any code", and only the built-in Super Admin role holds it.
Permission Types
Dashboard / Admin permissions (admin: and audit: prefixes):
| Permission | Description |
|---|---|
admin:dashboard:access | Master switch for the admin dashboard (Dashboard, System, Resources, Monitoring groups) |
admin:user:read/create/update/delete | User management |
admin:role:read/create/update/delete | Role management |
admin:permission:read/create/update/delete | Permission viewing and maintenance |
admin:team:read/create/update/delete | System-wide team management (any team) |
admin:model:read/create/update/delete | Model management |
admin:app:read/create/update/delete/publish/duplicate | Cross-team agents and workflows |
admin:capability:read/create/update/delete/execute | Cross-team tools and skills |
admin:knowledge-base:read/test/create/update/delete | Knowledge base management from the admin |
admin:settings:read/update | View / modify site settings |
admin:sso:read/update | View / manage SSO |
admin:conversation:read/delete | Admin conversation management |
admin:memory:read/update/delete | Admin memory records |
admin:notification:create/delete | Admin notifications |
audit:read / audit:export | View / export audit logs |
Resource permissions (constrained by team data isolation):
| Permission | Description |
|---|---|
team:read/create/update/delete/manage | The team itself and its members |
agent:read/create/update/delete/publish/chat | Agent management |
workflow:read/create/update/delete/publish/run/execute | Workflow management |
kb:read/test/create/update/delete | Knowledge base management |
tool:read/create/update/delete/execute | Tool management |
skill:read/create/update/delete/execute | Skill management |
apikey:read/create/update/delete | API key management (per user, see below) |
conversation:read/delete | Conversation management |
memory:read/create/update/delete | Personal memory management |
In the admin dashboard, tools and skills are reached through Capabilities (/capabilities); there is no separate Tools navigation item.
Built-in Roles
At startup the backend idempotently initializes 5 global built-in roles (is_system_role = true) from SystemPermissions. They cannot be renamed or deleted, and their permission sets cannot be changed through the API.
| Role | Dashboard access | Authorization highlights |
|---|---|---|
| Super Admin | Yes (and bypasses all checks) | Holds the wildcard permission code *, satisfying any permission check |
| Admin | Yes | Dashboard read/write plus the full team-scoped resource set; without admin:role:create/update/delete, admin:permission:create/update/delete, admin:settings:update, admin:sso:update, admin:memory:update/delete |
| Team Admin | No | All Member permissions plus team:update and team:manage; granted by a startup migration to users whose membership role is owner/admin |
| Member | No | Creates, edits, and runs team resources and manages their own API keys; without team:update/manage, agent:delete/publish, workflow:delete/publish, tool:delete |
| Viewer | No | Read plus use/execute only; no write permission at all, and no apikey:* |
Super Admin
Name: Super Admin
Type: System built-in
is_superuser: true # the account also carries the superuser flag
Permissions:
- "*" # wildcard code, matches any permission checkThe first user registered in the system is automatically promoted to Super Admin and assigned this role. An is_superuser=True account passes team endpoints directly, but it still needs a TeamMember row to appear in member lists such as /teams/my.
Admin
Name: Admin
Type: System built-in
Permissions:
# Admin dashboard
- admin:dashboard:access
- admin:user:read/create/update/delete
- admin:role:read # read-only: cannot create/edit/delete roles
- admin:permission:read # read-only: cannot create/edit/delete permissions
- admin:team:read/create/update/delete
- admin:model:read/create/update/delete
- admin:capability:read/create/update/delete/execute
- admin:app:read/create/update/delete/publish/duplicate
- admin:knowledge-base:read/test/create/update/delete
- admin:settings:read
- admin:sso:read
- audit:read/export
- admin:conversation:read/delete
- admin:notification:create/delete
- admin:memory:read
# Team-scoped resource permissions (explicit codes, no wildcard)
- team:read/create/update/delete/manage
- agent:read/create/update/delete/publish/chat
- workflow:read/create/update/delete/publish/run/execute
- kb:read/test/create/update/delete
- tool:read/create/update/delete/execute
- skill:read/create/update/delete/execute
- apikey:read/create/update/delete
- conversation:read/deleteAdmin can manage every team and its members (team:manage), but team-management actions still require them to be owner/admin inside that team.
Team Admin
Name: Team Admin
Type: System built-in
Permissions: all Member permissions, plus
- team:update # change team name/description/avatar
- team:manage # add/change/remove membersThe startup migration migrate_team_admin_roles() grants this role to every user whose membership role is owner or admin in at least one team, and cleans up legacy team-scoped assignments. Consequently, before making someone a team admin they must already hold the global team:manage (usually by being given the Team Admin role first), otherwise the endpoint returns 403.
Member
Name: Member
Type: System built-in
Permissions:
- team:read
- agent:read/create/update/chat # no delete / publish
- workflow:read/create/update/run # no delete / publish / execute
- kb:read/test/create/update/delete
- tool:read/create/update/delete/execute
- skill:read/create/update/delete/execute
- apikey:read/create/update/delete
- conversation:read/deleteNote: Member holds tool:delete, but tool deletion requires team owner/admin (tool:delete belongs to TEAM_MANAGEMENT_PERMISSIONS), so a Member cannot actually delete team tools.
Viewer
Name: Viewer
Type: System built-in
Permissions:
- team:read
- agent:read/chat
- workflow:read/run
- kb:read/test
- tool:read/execute
- skill:read/execute
- conversation:readViewer is the only default role: first initialization points the site setting default_role_id at Viewer, so newly registered users (including SSO-provisioned ones) get it by default.
System role permissions are overwritten at startup
Every backend start runs sync_role_permissions(), which aligns the permission sets of the 4 non-superuser built-in roles to the list in code: permissions outside the list are removed and missing ones are re-added. Editing the database to grant a system role extra permissions is reverted on the next start.
The Two UI Surfaces
Two interfaces deal with teams and roles, and their permission requirements differ:
| Surface | Route | Required permission | What it can do |
|---|---|---|---|
| Admin team list (the team switcher's "Manage All Teams (Admin)") | /teams | admin:team:read (frontend route-permissions.ts) | List all teams, create a team (admin:team:create), edit a team (admin:team:update), delete a team (admin:team:delete), bulk delete |
| Team-scoped page ("Manage Current Team") | /app/team | The page itself only requires membership; the entry link appears only for is_superuser or a team owner/admin holding team:manage | View members, add members (team:manage + owner/admin), change roles (owner), remove, leave, transfer ownership, view authorized models (read-only), edit team info (team:update + owner/admin) |
Members and Viewers cannot see /teams (the frontend blocks on admin:team:read and the admin endpoints validate admin:team:*), so their only team entry is /app/team. The model tab on /app/team is read-only: it shows the team's authorized models and today's usage, stating that authorization is assigned by a system administrator.
Role Management
The dashboard Roles page (/roles) lists every role. That route requires admin:role:read — the built-in Admin role only has read, so only a Super Admin (or a custom role carrying admin:role:*) can create, modify, or delete custom roles.
Create a Custom Role
- Sign in as Super Admin and go to Admin → Roles.
- Click Create Role and fill in Name (globally unique) and Description.
- Check permission codes in the list (you can select all within a category).
- Save.
POST /api/v1/admin/rolesrequiresadmin:role:create.
Custom roles are global roles (is_system_role = false) and can be assigned as any user's global role, independent of team membership. A duplicate name returns role_with_name_exists.
Example:
Name: Content Manager
Description: Manages knowledge bases and documents, no dashboard access
Permissions:
- team:read
- kb:read/test/create/update/delete
- agent:read/chatUnknown permission codes passed at creation time are silently ignored, whereas replacing a permission set (PUT .../permissions) fails with permission_code_not_found for an unknown code. The two behave differently — keep that in mind when debugging.
Edit a Role
PUT /api/v1/admin/roles/{role_id} changes the name and description (admin:role:update); PUT /api/v1/admin/roles/{role_id}/permissions takes { "permissions": [...] } and replaces the whole set (not incremental). Both reject system roles:
- name/description →
cannot_modify_system_role - permission set →
cannot_modify_system_role_permissions
Delete a Role
DELETE /api/v1/admin/roles/{role_id} requires admin:role:delete. Two hard limits:
- System role →
cannot_delete_system_role - Still assigned to users →
role_in_use, withcountin the error payload
Deleting a role has no "migrate users to another role" option. To delete an in-use custom role you must first open Admin → Users and change that role on every affected user, then come back — otherwise the endpoint simply returns role_in_use. The list page supports selecting several custom roles and deleting them in bulk.
Custom Permissions
The permission catalog itself is maintainable (/permissions page, requires admin:permission:read):
| Operation | Endpoint | Permission |
|---|---|---|
List permissions (scope, search filters, page/page_size) | GET /api/v1/admin/permissions | admin:permission:read |
| List available scopes | GET /api/v1/admin/permissions/scopes | admin:permission:read |
Create a custom permission (is_system=false) | POST /api/v1/admin/permissions | admin:permission:create |
| Update a custom permission | PUT /api/v1/admin/permissions/{permission_id} | admin:permission:update |
| Delete a custom permission | DELETE /api/v1/admin/permissions/{permission_id} | admin:permission:delete |
System permissions (is_system=true, i.e. every code defined in SystemPermissions) cannot be modified or deleted: cannot_update_system_permission / cannot_delete_system_permission. A duplicate custom code returns permission_with_code_exists.
A newly created custom permission code is not checked anywhere by default — unless some backend code happens to test that exact string, it is just a record. Checking it on a role unlocks nothing.
User Permissions
View User Permissions
Admin → Users opens the user list (requires admin:user:read). The user detail/edit panel shows:
- The user's global roles (multi-select, the
rolesfield) - Team memberships and in-team roles (maintained from the teams pages)
- SSO connections,
auth_source,is_active, and theis_superuserflag
There is no aggregate "effective permissions" view: actual permissions are the union of permission codes from all global roles plus the team gates, so you compare manually against the Permissions Reference.
Change a User's Global Roles
- In Admin → Users, click Edit on the target user.
- Check/uncheck roles in the Role area (multi-select).
- Save. The frontend calls
PUT /api/v1/admin/users/{user_id}withroles: ["Admin", ...], requiringadmin:user:update.
Backend behaviour:
rolesreplaces the set: it clears first and then writes, so roles you omit are lost.- Role names must match exactly; unmatched names are ignored.
- Protection rule: if the user is still
owner/adminof some team and the new role set no longer containsteam:manage(or*), the request fails with 403operation_not_permitted(permission: team:manage). To strip a team administrator, first downgrade their membership role in every team.
Role changes apply to subsequent requests immediately (permissions are read per request); issued JWTs do not cache permissions. Changing your own or someone else's roles does not force a logout, but the next restricted request will be denied.
Roles cannot be set while creating a user: POST /api/v1/admin/users does not accept roles, so a new user has no global role until you assign one afterwards.
Grant Special Permissions
Not implemented / Roadmap. There is no per-user permission grant/revoke with scope and expiry. The endpoints
GET /users/{user_id}/permissions,/permissions/check, andPOST/DELETE /users/{user_id}/permissionsdo not exist. Permissions are assigned only through global roles (PUT /api/v1/admin/roles/{role_id}/permissions) and team memberships.
Team Permissions
Team Roles Are Gates, Not Grants
TeamMember.role has exactly 4 fixed values: owner, admin, member, viewer. It carries no permission codes; it answers only two questions: is this person a member of this team, and is their level high enough for a management action.
The legacy ScopedRoleAssignment (team-scoped role table) and check_scoped_permission design is retired: the model is still defined in code, but nothing creates or reads it, and the startup migration only deletes leftover rows. Team management capability therefore comes from the global Team Admin role, not from a team-scoped grant.
Two rules:
| Rule | Content |
|---|---|
TEAM_MANAGEMENT_PERMISSIONS | team:update, team:manage, team:delete, tool:delete — these actions require both the global permission code and owner/admin in that team |
VIEWER_ALLOWED_PERMISSIONS | A team viewer may only use: team:read, agent:read/agent:chat, workflow:read/workflow:run/workflow:execute, kb:read/kb:test, tool:read/tool:execute, skill:read/skill:execute, conversation:read; anything else is operation_not_permitted |
A team viewer and the global Viewer role are two different things: the former only means "lowest level inside this team" and grants no global Viewer codes; conversely, when adding a global Viewer to a team, use the viewer membership role.
Typical Team Role Combinations
Real capability = global role permissions ∩ team gate. The most common combinations (assuming the member already holds the matching global role):
| Capability | owner + Team Admin | admin + Team Admin | member + Member | viewer + Viewer |
|---|---|---|---|---|
| View team and members | ✓ | ✓ | ✓ | ✓ |
| Edit team info (name/description/avatar) | ✓ | ✓ | ✗ | ✗ |
| Add members | ✓ | ✓ | ✗ | ✗ |
| Change member roles | ✓ | ✗ | ✗ | ✗ |
| Remove members | ✓ | ✓ (not other admins/owner) | ✗ | ✗ |
| Transfer ownership | ✓ | ✗ | ✗ | ✗ |
| Delete the team | ✗ (use the dashboard admin:team:delete) | ✗ | ✗ | ✗ |
| Create/edit agents, workflows, knowledge bases | ✓ | ✓ | ✓ | ✗ |
| Delete agents / publish agents and workflows | ✓ | ✓ | ✗ | ✗ |
| Chat with agents, run workflows | ✓ | ✓ | ✓ | ✓ |
| Create/edit own tools and skills | ✓ | ✓ | ✓ | ✗ |
| Edit others' tools/skills | ✓ | ✓ | ✗ | ✗ |
| Delete tools | ✓ | ✓ | ✗ (team gate rejects) | ✗ |
| Search/test knowledge bases, upload/delete documents | ✓ | ✓ | ✓ | read-only search |
| Execute tools and skills | ✓ | ✓ | ✓ | ✓ |
| Manage API keys | Own keys only | Own keys only | Own keys only | ✗ |
| View audit logs / admin dashboard | Needs global audit:read / admin:dashboard:access | Same | ✗ | ✗ |
API keys are not team resources
An API key is owned by a person via user_id: for non-superusers the list endpoint forces user_id = current user, and ensure_api_key_owner() requires "owner or superuser" to read/write a single key. So a team owner/admin cannot see or revoke someone else's keys — only a superuser can "view all API keys". See the API Keys reference.
Full constraints on team-management actions live in Teams & Members.
Resource Visibility
Beyond the two gates, the resource's own visibility also blocks access:
Agent example:
Agent: Customer Support Agent
Owner: john.doe@example.com
Team: Support Team
Access control:
visibility=team → team members can read; writes need the owner or a team owner/admin
visibility=private → only the creator (or team owner/admin when the creator is absent)
Team members: Read, Chat
Other teams: No accessWorkflow example:
Workflow: Customer Inquiry Processing
Owner: jane.smith@example.com
Team: Support Team
Access control:
visibility=team → team members can read; writes need the owner or a team owner/admin
visibility=private → only the creator (or team owner/admin when the creator is absent)
Team members: Read, Run
Other teams: No accessShare Resources
Cross-team sharing exists only for tools:
- Custom tools can be shared with other teams via
POST /api/v1/admin/tools/{tool_id}/share(list withGET /api/v1/tools/{tool_id}/shares, unshare withDELETE /api/v1/tools/{tool_id}/share/{team_id}). - Agents / workflows / knowledge bases have no per-team sharing level: cross-team visibility comes from the resource's
visibility(Team / Public for agents), and access is otherwise team-isolated.
API Key Scopes
An API key carries a scopes list (default ["chat"]; an empty list means unrestricted), a rate_limit (requests per minute, 0 = unlimited), an expires_at, and can be bound to specific agents and workflows. Keys are managed on the /api-keys page or /app/api-keys, created via POST /api/v1/api-keys (requires apikey:create).
API Key: ak_...
Owner: integration@example.com
Scopes:
- chat
Rate limit: 1000 (requests/minute)
Expires at: 2027-02-11Scopes are an upper bound, not a role: the key's effective power is still constrained by the agents/workflows it can call, their configuration, and team isolation. See the API Keys reference.
Permission Auditing
View Permission Changes
Not implemented / Roadmap. There is no dedicated permission-change audit view, and the audit log has no
granted/revokedaction types.
Current audit coverage (each endpoint checked):
| Has audit entries | No audit entries |
|---|---|
Users: create_user, update_user, delete_user, activate_user, deactivate_user, force_password_change, … | Role create/update/delete (no audit calls in any /api/v1/admin/roles endpoint) |
Teams: create_team, update_team, delete_team, add_team_member, remove_team_member | Permission create/update/delete (no audit calls in any /api/v1/admin/permissions endpoint) |
Team model authorization: add_team_model, update_team_model, remove_team_model, batch add/remove | Member role changes, ownership transfer, leaving a team |
Review entries in Audit Logs (requires audit:read), filtered by resource and operation. See Audit Logs.
Permission Usage Reports
Not implemented / Roadmap. There are no per-user/team permission usage reports, unused-permission lists, or over-privilege detection.
Troubleshooting 403s
Permission Denied
Symptoms: a feature is unavailable, or the API returns 403 operation_not_permitted / team_admin_required / not_team_member.
Check in order:
- Is the global permission code missing? Go to Admin → Users → edit the user and check whether
rolesincludes a role carrying the target code (compare with the Permissions Reference). The error response carries apermissionfield naming the missing code. - Is the team gate the wrong level? When the action uses a management permission (
team:update/team:manage/team:delete/tool:delete) the user must be teamowner/admin; a teamvieweris always rejected for writes. - Is the user not a member?
not_team_member(403) means the user is not in that team at all. - Is it resource visibility? A
visibility=privateresource returnsagent_access_denied/tool_access_denied(403) to non-creators, regardless of role permissions. - Is it API key ownership? A key can only be read/written by its owner or a superuser; anyone else gets
permission_denied.
Over-Privileged Users
- In Users, check the target user's
rolesand compare each against the Permissions Reference. - In Roles, review each role's permission set — note that the built-in Admin role already covers all team resources, so stacking extra broad custom roles is usually unnecessary.
- Before downgrading, verify whether the user is still
owner/adminof any team; otherwisePUT /admin/users/{id}is rejected by theteam:manageprotection rule. - Review regularly. User and team-membership changes are audited, but role create/update/delete is not, so keep your own record of role edits.
Inconsistent Permission Behaviour
- Identify the layer: a team-scoped request is "global code + team gate"; only dashboard endpoints look at
admin:*. - Identify the source: users may hold several roles and permissions are unioned; the startup migration may also have added Team Admin.
- Remember system roles cannot be hand-edited: their permission sets are re-synced to the code list on every start, so direct database edits do nothing.
Best Practices
✅ Do:
- Carry job permissions in custom roles instead of adding more built-in roles
- Grant the Team Admin global role first, then set the person as team
admin(the reverse order is blocked with 403) - Use team
viewer+ global Viewer together for read-only team members - Create and rotate API keys per integration instead of sharing a personal account's key
- Review role members and team owner/admin lists quarterly
- Validate permission changes in a test environment before production
❌ Don't:
- Hand out
*(only Super Admin should hold it) - Try to stack permissions onto the built-in Admin role (it does not work; the startup sync reverts it)
- Treat "the frontend hides the button" as a security boundary
- Run several integrations from one shared account
API Access
Role and permission endpoints live under /api/v1/admin/roles and /api/v1/admin/permissions and require admin:role:* / admin:permission:*:
# List roles (page / page_size / search)
roles = api.get("/api/v1/admin/roles")
# Create a role
role = api.post("/api/v1/admin/roles", json={
"name": "Content Manager",
"description": "Manages knowledge bases",
"permissions": ["team:read", "kb:read", "kb:create", "kb:update", "kb:delete", "kb:test"],
})
# Replace a role's permissions wholesale (PUT, not PATCH)
api.put(f"/api/v1/admin/roles/{role_id}/permissions", json={
"permissions": ["kb:read", "kb:create"],
})
# List permissions, optionally filtered by scope
permissions = api.get("/api/v1/admin/permissions", params={"scope": ["admin", "team"]})
scopes = api.get("/api/v1/admin/permissions/scopes")Note: There are no per-user permission endpoints (
/users/{user_id}/permissions); user permissions can only be replaced wholesale through therolesfield.

Protection Rules
- System roles (the 5 built-in roles) and system permissions cannot be deleted or renamed, and their permission sets cannot be changed (the startup sync restores the code list).
- A custom role must be unassigned before deletion, otherwise
role_in_use. - Superuser accounts cannot be deleted (
cannot_delete_superuser) or deactivated (cannot_deactivate_superuser). - The default team cannot be deleted (
cannot_delete_default_team); a team owner cannot leave or be removed — transfer ownership first. - Permission changes apply to subsequent requests immediately; issued JWTs are not a permission cache.
Related Documentation
- Permissions & Roles Reference — the full permission catalog, built-in role matrix, and sidebar mapping
- Teams & Members — adding/changing/removing members, leaving, transferring ownership, and the team API
- User Management — creating, activating/deactivating, deleting users, and assigning roles
- Audit Logs — audit records of role and membership changes
How is this guide?