User Roles and Permissions#
Dify uses a dual-layer permission model: a legacy workspace role system (always active) and an optional enterprise RBAC system (gated by RBAC_ENABLED). The two layers must stay aligned across backend config and frontend env vars, or access denials and management UI errors will occur.
Role Model#
Legacy Roles (TenantAccountRole)#
Five roles are defined in api/models/account.py:
| Role | Value |
|---|---|
| Owner | owner |
| Admin | admin |
| Editor | editor |
| Normal | normal |
| Dataset Operator | dataset_operator |
Roles are stored in the TenantAccountJoin table. TenantService provides the main read paths: get_user_role, has_roles, and check_member_permission.
Workspace Ownership#
- One workspace has exactly one
ownerat a time. - Ownership transfer is console-only via
OwnerTransfer; the OpenAPIPATCH /workspaces/{id}/members/{mid}endpoint explicitly rejectsownerrole assignments. - Transfer requires email verification via
SendOwnerTransferEmailApi+OwnerTransferCheckApibefore the final transfer call. - When RBAC is enabled, ownership transfer also updates the RBAC binding via
RBACService.MemberRoles.replace.
RBAC System (Enterprise)#
When RBAC_ENABLED=true, every legacy role maps to a builtin RBAC role with a matching role_tag. The bridge is AccountService._resolve_legacy_role_id(). The RBAC service itself lives in api/services/enterprise/rbac_service.py.
Configuration Flags (Must Be Aligned)#
| Layer | Flag | Location | Default |
|---|---|---|---|
| Backend | RBAC_ENABLED | api/configs/enterprise/__init__.py | false |
| Frontend | NEXT_PUBLIC_RBAC_ENABLED | web/env.ts:96 | false |
Mismatching these flags causes the frontend to call RBAC endpoints (e.g., /workspaces/current/rbac/...) that the backend doesn't serve (returning 404), or vice versa β the root cause of the v3.11 management backend error.
Legacy Permission Fallback#
When RBAC is disabled, _legacy_my_permissions() in rbac_service.py serves a static permission-key map keyed by tenant role, providing the same permission_keys shape that the RBAC API would return . The _LEGACY_MY_PERMISSIONS dict maps each TenantAccountRole to workspace, app, dataset, and agent permission-key lists.
Permission Key Enforcement#
Backend#
When RBAC is enabled, check_member_permission reads workspace.member.manage and workspace.role.manage from RBACService.MyPermissions rather than checking the legacy role hierarchy.
The central enforcement entry point is enforce_rbac_checks(), which short-circuits if RBAC_ENABLED=False, grants access if the requesting account matches the resource owner_id, and otherwise calls RBACService.CheckAccess.check().
Frontend#
The frontend reads the user's workspacePermissionKeys from useAppContext and checks them with hasPermission(). App and dataset capabilities are derived via getAppACLCapabilities() and getDatasetACLCapabilities(), which also check if the current user is the resource maintainer β granting full access if they hold app.create_and_management or dataset.create_and_management in their workspace keys. The canAccessConfig flag is only enabled when isRbacEnabled=true .
maintainer Column (Backfill Migration)#
The migration a7c4e9d2f681 (June 2026) added a maintainer column to both apps and datasets tables and backfilled it from created_by. This enables the RBAC layer to short-circuit on resource ownership without a separate join.
Member Invitation & Lifecycle#
Key service: RegisterService.invite_new_member().
Flow:
- No existing account β create with
AccountStatus.PENDING, createTenantAccountJoin, send invite email. - Existing account, no
TenantAccountJoin, AND (account PENDING OR RBAC enabled) β create member join. - Active account + RBAC disabled β
AccountAlreadyInTenantError(see Known Bugs).
When checking for an existing account, the invitation system uses case-insensitive email matching via get_account_by_email_with_case_fallback. The lookup first tries an exact match, then falls back to comparing normalized_email, ensuring that inviting User@Example.com will correctly find an existing account stored as user@example.com (or any other casing variation). This prevents duplicate accounts and is especially important for SSO-provisioned accounts, which may store email addresses with the identity provider's original casing.
When RBAC is enabled, invite_new_member also calls RBACService.MemberRoles.replace and assigns the NORMAL legacy role at the TenantAccountJoin level (the RBAC role carries the actual permission).
Known Bugs#
[v1.15.0] Inviting an existing workspace owner silently fails (Issue #38048)#
Root cause: The invite_new_member condition at line 2095 only creates a TenantAccountJoin for active accounts when RBAC_ENABLED=true. With RBAC off, the email is sent but the user is never added to the workspace. Fix: PR #37479 + follow-up PR #38101.
[v1.15.0] Re-inviting a removed member shows "workspace not found" (Issue #38073)#
Bug 1: After removal, the re-invited account has no current workspace, causing the login check to fail. Fix: PR #38087.
Bug 2: The /activate endpoint lacked session validation β an admin's open browser could consume the invite token. Fix: PR #39438 (July 2026) β the endpoint now validates that the logged-in session belongs to the token's account; mismatches return 403 InvitationAccountMismatchError.
[v3.11] 404 on RBAC management endpoints (Issue #38272)#
Root cause: Frontend/backend RBAC_ENABLED flag mismatch. Setting NEXT_PUBLIC_RBAC_ENABLED=false and clearing the browser cache resolves the symptom for non-enterprise deployments. Enterprise users should contact support.
UUID vs. String Key Mismatch in Agent Roster Permissions (PRs #42095, #42151)#
ResourcePermissionSnapshot.permission_keys_by_resource_ids() always builds its result dict with str(resource_id) keys. Two call sites in roster.py previously passed agent.id (a UUID object) as the lookup key, so .get(agent.id) always returned [], silently stripping all permission keys from agent list and detail responses for all users including workspace owners. Fix: coerce lookup key to str(agent.id) at both call sites.
Key Files#
| File | Purpose |
|---|---|
api/models/account.py | TenantAccountRole enum, TenantAccountJoin model |
api/services/account_service.py | TenantService, RegisterService.invite_new_member, RBAC integration |
api/services/enterprise/rbac_service.py | RBACService, _LEGACY_MY_PERMISSIONS, ResourcePermissionSnapshot |
api/controllers/console/workspace/members.py | Member invite, role update, ownership transfer endpoints |
api/controllers/openapi/workspaces.py | OpenAPI workspace/member endpoints (bearer auth) |
web/utils/permission.ts | hasPermission, getAppACLCapabilities, getDatasetACLCapabilities |
api/migrations/β¦/a7c4e9d2f681_add_resource_maintainers.py | Adds maintainer column to apps and datasets |