File Access Control#
Dify enforces workflow-layer file access control through the api/core/app/file_access/ module. The system gates database lookups for UploadFile and ToolFile records using a request-scoped ownership context carried as a Python ContextVar. Access is always tenant-scoped; end-user executions apply additional per-user ownership checks, with an execution-local grant mechanism for files surfaced through trusted knowledge retrieval paths.
The module's public API : DatabaseFileAccessController, FileAccessControllerProtocol, FileAccessScope, bind_file_access_scope, get_current_file_access_scope, grant_upload_file_access, grant_retriever_segment_access, is_retriever_segment_access_granted.
Core Data Structures#
FileAccessScope#
FileAccessScope is a frozen dataclass held in a ContextVar. Key fields:
| Field | Purpose |
|---|---|
tenant_id | Mandatory isolation boundary — always applied |
user_id | Identity of the invoking user |
user_from | ACCOUNT or END_USER |
invoke_from | Originating invocation path |
granted_upload_file_ids | Execution-local allowlist of UploadFile IDs |
granted_retriever_segment_ids | Execution-local allowlist of knowledge segment IDs |
The requires_user_ownership property returns True when user_from == END_USER, triggering stricter ownership filtering.
FileAccessControllerProtocol#
The protocol is the testable interface implemented by DatabaseFileAccessController. It defines:
apply_upload_file_filters/apply_tool_file_filters— append access-control predicates to SQLAlchemySelectstatementsget_upload_file/get_tool_file— load a single authorized record, returningNoneon access denial
Three-Layer Filtering Logic#
DatabaseFileAccessController.apply_upload_file_filters applies three conditions in order:
- Tenant scope — unconditionally adds
UploadFile.tenant_id == scope.tenant_id - User ownership — when
requires_user_ownership, addscreated_by_role == END_USER AND created_by == user_id - Execution-local grants — if
granted_upload_file_idsis non-empty, the ownership condition is OR-expanded withUploadFile.id IN (granted_ids)
ToolFile access follows the same tenant + user_id pattern, but has no grant list .
Execution-Local Grant Mechanism#
The grant system solves a concrete problem: knowledge base images are indexed as UploadFile records owned by the indexing account, but published apps execute as end-users who fail the per-user ownership check . Before the fix, this caused Invalid upload file errors when a Knowledge Retrieval node returned segments containing images — only reproducible in the published app, not Studio Preview (which runs as the account user).
The fix, landed in PR #36175 / PR #36195, added two grant functions to scope.py:
grant_retriever_segment_access(segment_ids)— allowlists segment IDs discovered during knowledge retrievalgrant_upload_file_access(upload_file_ids)— allowlists specificUploadFileIDs when attachment bindings are resolved
Both mutate the scope via an immutable replace() on the ContextVar, keeping the dataclass frozen .
is_retriever_segment_access_granted(segment_id) returns True for account users unconditionally, or when the segment ID is in the execution's allowlist.
Grant Call Sites#
| Location | What is granted |
|---|---|
dataset_retrieval.py L332 | Segment IDs from knowledge retrieval results |
dataset_retrieval.py L525 | Upload file IDs from segment attachment bindings |
retrieval_service.py L923, L969 | Upload file IDs for individual/batch attachment bindings |
node_runtime.py L379 | Upload file IDs just before attachment loader returns File refs |
node_runtime.py also enforces grants before loading: the DifyRetrieverAttachmentLoader returns [] if is_retriever_segment_access_granted rejects the segment or if the context-aware segment_access_checker (injected from node_factory.py) cannot find the segment in the workflow's current variable pool .
Scope Lifecycle#
bind_file_access_scope is a context manager that sets the ContextVar for the duration of an app execution and resets it on exit. BaseAppGenerator._bind_file_access_scope constructs the initial FileAccessScope from tenant_id, user (distinguishing Account vs EndUser), and invoke_from, then wraps the execution generator. All app types — completion, chat, agent, advanced chat, workflow — call this base method.
Key Files#
| File | Role |
|---|---|
api/core/app/file_access/scope.py | FileAccessScope, ContextVar, all grant/check functions |
api/core/app/file_access/controller.py | DatabaseFileAccessController — SQL filter logic |
api/core/app/file_access/protocols.py | FileAccessControllerProtocol — injectable interface |
api/core/app/file_access/__init__.py | Public re-exports |
api/core/app/apps/base_app_generator.py | Scope binding at app execution start |
api/core/rag/retrieval/dataset_retrieval.py | Segment + file grant calls during retrieval |
api/core/rag/datasource/retrieval_service.py | Attachment binding grant calls |
api/core/workflow/node_runtime.py | Attachment loader enforcement + final file grants |