RBAC Initialization#
Overview#
Dify's enterprise RBAC system requires initialization at two lifecycle points:
1. Resource Creation#
When an App or Dataset is created, a three-step initialization sequence runs:
AppAccess/DatasetAccess.replace_whitelist(automatic_include_workspace_members=True)β seeds the access-control whitelist so the resource is visible to all authorized members.try_sync_creator_access_policy_member_bindingsβ wires the creator into any applicable access-policy group.initialize_created_app_rbac_access_task(async Celery task) β backfills RBAC member bindings for the new resource.
This sequence is only wired into the primary console create controllers. Every other creation entry point constructs the App/Dataset object directly and skips it, leaving the resource with no RBAC access record. Non-maintainer team members get 403 when opening those resources because the maintainer/creator short-circuit hides the gap from the creator.
2. Workspace Member Join#
When a new member joins a workspace, sync_joined_workspace_member_rbac_access_task scans all apps and datasets in the workspace to find those with automatic_include_workspace_members=True and adds the new member to their whitelists. This task only runs for new memberships (not role updates), when RBAC is enabled, for non-owner roles, and for active accounts (not pending).
Tracking issue: #39379 β opened 2026-07-21.
Where RBAC Init Is (and Isn't) Wired In#
Workspace Member Join Path#
| Entry point | RBAC initialized? | Code location |
|---|---|---|
TenantService.create_tenant_member | β Yes | account_service.py:1347β1369 |
When create_tenant_member creates a new membership (membership_created=True), RBAC is enabled, the role is not OWNER, and the account is not PENDING, it dispatches sync_joined_workspace_member_rbac_access_task. The task uses the operator_account_id (who invited the member) as the actor for RBAC API calls, falling back to the workspace owner if not provided.
Processing flow:
- Iterates through all apps and datasets in batches of 500 resources
- For each batch, fetches whitelist configs via
ResourceWhitelistConfigs.batch_get - Filters for resources with
automatic_include_workspace_members=True - Appends the new member to filtered resources' whitelists in batches of 500 using
AppAccess.append_whitelist_members_batchandDatasetAccess.append_whitelist_members_batch - Uses the
defaultpolicy ID for all access grants
App Creation Paths#
| Entry point | RBAC initialized? | Code location |
|---|---|---|
Console POST /apps | β Yes | app.py:724β733 |
Agent App create (POST /agent) | β No | roster.py:690β697 |
DSL import (import_app) | β No | app_dsl_service.py:272β295 |
| Agent App duplication | β No | roster_service.py:947β977 |
| Workflow β Advanced Chat/Workflow conversion | β No | workflow_converter.py:93β119 |
Console create calls AppService.create_app, which calls enterprise_rbac_service.try_sync_creator_access_policy_member_bindings , then calls RBACService.AppAccess.replace_whitelist with automatic_include_workspace_members=True, and dispatches initialize_created_app_rbac_access_task.
Agent App create (AgentAppListApi.post in roster.py) calls AppService.create_app β which includes try_sync_creator_access_policy_member_bindings β but does not call RBACService.AppAccess.replace_whitelist or dispatch initialize_created_app_rbac_access_task, leaving RBAC initialization incomplete.
DSL import (AppDslService._create_or_update_app) fires app_was_created and sets up the App model, but returns without any RBAC call . The same import_app code path is reused by inner_api/app/dsl.py and openapi/app_dsl.py β none of those endpoints initialize RBAC.
Agent duplication (AgentRosterService.duplicate_agent_app) delegates to AppService.create_app β which does call try_sync_creator_access_policy_member_bindings β but the controller that calls duplicate_agent_app does not separately dispatch initialize_created_app_rbac_access_task or call RBACService.AppAccess.replace_whitelist.
Workflow conversion (WorkflowConverter.convert_to_workflow) creates a bare App, commits, fires app_was_created, and returns with no RBAC calls.
Dataset Creation Paths#
| Entry point | RBAC initialized? | Notes |
|---|---|---|
| Console empty dataset | β Yes | controllers/console/datasets/datasets.py:615,633 |
Public API (service_api) create dataset | β Yes | controllers/service_api/dataset/dataset.py:551,557 |
File upload (save_document_without_dataset_id) | β Yes | Fixed in PR #40599 β now sets permission=ALL_TEAM and initializes RBAC properly |
| RAG pipeline empty dataset | β No | services/dataset_service.py:532 |
| External knowledge base | β Yes | services/external_knowledge_service.py:335 |
| RAG pipeline DSL import | β No | services/rag_pipeline/rag_pipeline_dsl_service.py:281,414 |
Additional Dataset Complication#
For datasets there is a second layer: check_dataset_permission reads the legacy dataset.permission / DatasetPermission model, which the RBAC access-config UI does not update. A knowledge base created via file upload defaults to permission=only_me and has no RBAC whitelist, so assigning "Partial team members" in the RBAC UI still fails β the legacy gate rejects first with You do not have permission to access this dataset.
The console empty-dataset create path forces permission=ALL_TEAM to make the legacy gate a pass-through; non-console paths do not.
RBAC_ENABLED guard: PR #41022 added an if not dify_config.RBAC_ENABLED guard around check_dataset_permission() in the dataset GET endpoint (api/controllers/console/datasets/datasets.py), ensuring the legacy permission check is bypassed when RBAC is enabled. Other call sites (8 in datasets_document.py) still invoke check_dataset_permission without this guard.
Root Cause#
The RBAC-on-create logic is duplicated inline inside multiple controllers rather than extracted into a shared helper. Each additional creation entry point added afterward silently omits the call.
Three open design questions from the tracking issue:
- Should all creation paths (App Γ5, Dataset Γ6) call a shared
apply_<resource>_creation_rbac()helper? ShouldResolved β PR #41022 implemented thecheck_dataset_permissionbe gated behindif not RBAC_ENABLED, or should non-console create paths forcepermission=ALL_TEAM?RBAC_ENABLEDguard in the dataset GET endpoint; the decision to gate the legacy check has been made at least for that code path.- Should
inner_api/openapiDSL-import endpoints also initialize RBAC?
Key Files#
| File | Role |
|---|---|
api/controllers/console/app/app.py | Primary console app creation with full RBAC init |
api/controllers/console/agent/roster.py | Agent app creation endpoint (AgentAppListApi.post) β missing RBAC init |
api/services/app_service.py | AppService.create_app β contains try_sync_creator_access_policy_member_bindings call |
api/services/app_dsl_service.py | DSL import β missing RBAC init |
api/services/agent/roster_service.py | AgentRosterService.duplicate_agent_app β partial init via create_app, missing whitelist/task |
api/services/workflow/workflow_converter.py | Workflow conversion β missing RBAC init |
api/services/account_service.py | TenantService.create_tenant_member β dispatches sync_joined_workspace_member_rbac_access_task for new memberships |
api/tasks/initialize_created_app_rbac_access_task.py | Celery tasks initialize_created_app_rbac_access_task and sync_joined_workspace_member_rbac_access_task |
api/services/enterprise/rbac_service.py | try_sync_creator_access_policy_member_bindings, RBACResourceType, AppAccess.replace_whitelist, ResourceWhitelistConfig, AppAccess.whitelist_config, DatasetAccess.whitelist_config, ResourceUserAccessPoliciesResponse with pagination, ResourceWhitelistConfigs.batch_get, AppAccess.append_whitelist_members_batch, DatasetAccess.append_whitelist_members_batch; defines legacy app permission sets _LEGACY_APP_OWNER_KEYS, _LEGACY_APP_ADMIN_KEYS, _LEGACY_APP_EDITOR_KEYS |
Related Issues#
- #39379 β Primary tracking issue with coverage matrix and reproduction steps
- #39037 β Earlier report: "some path miss rbac sync" (maintainer-filed)
- #37988 β Parent enhancement: resource-level permission consistency
- #39226 β Dataset-specific: syncing RBAC for
partial_members/only_mepermissions