Multi-Tenant Context Propagation#
Dify enforces tenant isolation through two complementary mechanisms that run in parallel during every app execution:
DifyRunContextβ a structured identity object threaded explicitly through the workflow graph, child engines, and node runtime classes.FileAccessScopeβ a PythonContextVarscoped to each app execution that gates all database lookups forUploadFileandToolFilerecords.
Both were progressively hardened across a series of refactors (primarily PR #32964, PR #39042, PR #39149, and PR #34044) that replaced implicit tenant/user derivation from session/user objects with explicit parameter passing.
DifyRunContext β Identity Carrier#
DifyRunContext is a Pydantic model with eight fields:
| Field | Type | Purpose |
|---|---|---|
tenant_id | str | Workspace/tenant boundary |
app_id | str | Owning application |
user_id | str | Invoking user (Account.id or EndUser.id) |
user_from | UserFrom | ACCOUNT or END_USER |
invoke_from | InvokeFrom | Entry point (DEBUGGER, SERVICE_API, WEB_APP, etc.) |
app_type | CreditUsageAppType | None | Optional top-level application type (e.g., CHATBOT, AGENT_V2, WORKFLOW) for credit usage attribution |
created_by | CreditUsageCreatedBy | None | Optional business feature that initiated the request (e.g., APP, AGENT_NODE, BUILD_DRAFT) for credit usage attribution |
trace_session_id | str | None | Optional observability trace ID |
build_dify_run_context() is the single factory. It wraps the model in a dict[str, Any] under the reserved key "_dify" (DIFY_RUN_CONTEXT_KEY), allowing callers to attach additional context keys without clobbering the identity payload.
InvokeFrom.runs_as_account() is the canonical check for whether a run is attributed to a workspace Account (DEBUGGER, EXPLORE) vs. an end user (WEB_APP, SERVICE_API, TRIGGER). This drives UserFrom resolution in all app runners .
Propagation Through Workflow Execution#
AppGenerator
ββ build_dify_run_context(tenant_id, app_id, user_id, ...)
ββ DifyGraphInitContext β DifyNodeFactory β Graph
ββ child engines (iteration/loop)
ββ same run_context via _WorkflowChildEngineBuilder
Graph initialization: WorkflowBasedAppRunner._init_graph() calls build_dify_run_context() and wraps the result in DifyGraphInitContext, which bridges the identity payload into graphon's GraphInitParams. DifyNodeFactory.from_graph_init_context() then creates all graph nodes within that scoped context .
Child engines: Iteration and loop nodes spawn independent child GraphEngine instances via _WorkflowChildEngineBuilder. These inherit the parent's run_context directly β no re-construction needed β so tenant_id and user_id flow automatically to all subgraph nodes .
Single-node debug runs: _get_graph_and_variable_pool_for_single_node_run() builds its own run_context from workflow.tenant_id and an explicit user_id argument . Before PR #34044, this path hardcoded user_id="", causing DatasetQuery.created_by to fail UUID validation in PostgreSQL inside iteration subgraphs. The fix threads the real user_id from application_generate_entity through _prepare_single_node_execution() into this method.
Context Resolution in Node Runtime#
Nodes access identity via resolve_dify_run_context(run_context) in node_runtime.py, which normalizes both raw Mapping and DifyRunContext inputs by keying on DIFY_RUN_CONTEXT_KEY. It raises ValueError if the key is absent, making missing context a hard failure rather than a silent bug.
Two key runtime classes use the resolved context for tenant/user scoping:
DifyFileReferenceFactoryβ passesrun_context.tenant_idtofile_factory.build_from_mapping(), ensuring every file reference resolves within the correct tenant's storage .DifyToolFileManagerβ passes bothrun_context.user_idandrun_context.tenant_idtoToolFileManager.create_file_by_raw(), attributing created tool files to the correct user and tenant .
Repository Tenant Isolation#
PR #39042 refactored all workflow execution and node execution repositories to accept tenant_id as an explicit constructor argument rather than deriving it from user objects.
DifyCoreRepositoryFactory exposes two factory methods β create_workflow_execution_repository() and create_workflow_node_execution_repository() β both requiring tenant_id: str alongside user, app_id, and triggered_from. The factory dynamically loads the configured repository implementation via import_string() and forwards tenant_id to it.
All app generators (workflow, advanced chat, pipeline) call these factories with tenant_id=app_model.tenant_id, so the tenant boundary is set from the app model at execution start and never re-derived from session state downstream.
File Access Tenant Scoping#
File lookups use a separate ContextVar-based mechanism. FileAccessScope is a frozen dataclass bound per execution via bind_file_access_scope(). It carries tenant_id as a mandatory outer boundary: DatabaseFileAccessController unconditionally appends UploadFile.tenant_id == scope.tenant_id to every SQL query, with additional user_id ownership filters for end-user contexts.
BaseAppGenerator._bind_file_access_scope() constructs the initial scope from tenant_id, user, and invoke_from at the start of every app execution. All app types β completion, chat, agent, advanced chat, workflow β pass through this base method.
Trial app cross-tenant fix (PR #39149): When a user explores another tenant's shared trial app, the upload API was previously creating files under the exploring user's own tenant. The fix added TrialAppFileUploadApi and TrialAppRemoteFileUploadApi endpoints at /trial-apps/<app_id>/files/upload that pass resource_tenant_id=app_model.tenant_id explicitly to FileService.upload_file(), ensuring uploaded files are owned by the app's tenant rather than the requesting user's tenant.
Key Files and References#
| File | Role |
|---|---|
api/core/app/entities/app_invoke_entities.py | DifyRunContext, build_dify_run_context(), UserFrom, InvokeFrom |
api/core/app/apps/workflow_app_runner.py | WorkflowBasedAppRunner._init_graph(), _get_graph_and_variable_pool_for_single_node_run() |
api/core/workflow/node_runtime.py | resolve_dify_run_context(), DifyFileReferenceFactory, DifyToolFileManager |
api/core/repositories/factory.py | DifyCoreRepositoryFactory with explicit tenant_id params |
api/core/app/file_access/scope.py | FileAccessScope, ContextVar, grant/check functions |
api/core/app/file_access/controller.py | DatabaseFileAccessController β SQL filter logic |
api/core/app/apps/base_app_generator.py | _bind_file_access_scope() β scope binding at app execution start |
api/controllers/console/explore/trial.py | TrialAppFileUploadApi, TrialAppRemoteFileUploadApi |
Notable PRs:
| PR | Summary |
|---|---|
| #32964 | Introduced DifyRunContext; delegated child engine creation to _WorkflowChildEngineBuilder |
| #34044 | Fixed empty user_id in iteration/loop debug subgraph runs; added parse_uuid_str_or_none() guard |
| #39042 | Refactored workflow repositories to accept tenant_id explicitly |
| #39149 | Scoped trial app file uploads to the app's owning tenant |