feat(rbac): agent RBAC/ACL scenes and resource-locator check bundles (#41768)

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
This commit is contained in:
Xiyuan Chenandautofix-ci[bot] authored and GitHub committed 2026-09-07 04:39:43 +00:00
1 parent 60e983c493
commit 86134ea99e
152 files changed
+5360 -3405

No files matched your search

+6 -14
View File
@@ -6,12 +6,12 @@ from flask_restx import Resource
from sqlalchemy.orm import Session
from werkzeug.exceptions import Forbidden
from controllers.common.wraps import RBACPermission, RBACResourceScope
from controllers.common.rbac import PlainApp, RBACCheck, RBACPermission, Workspace
from controllers.openapi import openapi_ns
from controllers.openapi._contract import accepts, returns
from controllers.openapi._models import AppDslExportQuery, AppDslExportResponse, AppDslImportPayload
from controllers.openapi.auth.composition import auth_router
from controllers.openapi.auth.data import AuthData, RBACRequirement
from controllers.openapi.auth.data import AuthData
from extensions.ext_database import db
from libs.oauth_bearer import Scope, TokenType
from models import Account, App
@@ -40,11 +40,7 @@ class AppDslImportApi(Resource):
scope=Scope.WORKSPACE_WRITE,
allowed_token_types=frozenset({TokenType.OAUTH_ACCOUNT}),
allowed_roles=frozenset({TenantAccountRole.EDITOR, TenantAccountRole.ADMIN, TenantAccountRole.OWNER}),
rbac=RBACRequirement(
resource_type=RBACResourceScope.APP,
scene=RBACPermission.APP_IMPORT_EXPORT_DSL,
resource_required=False,
),
rbac=RBACCheck(RBACPermission.APP_IMPORT_EXPORT_DSL, Workspace()),
)
@returns(200, Import, "Import completed")
@returns(202, Import, "Import pending confirmation")
@@ -100,11 +96,7 @@ class AppDslImportConfirmApi(Resource):
scope=Scope.WORKSPACE_WRITE,
allowed_token_types=frozenset({TokenType.OAUTH_ACCOUNT}),
allowed_roles=frozenset({TenantAccountRole.EDITOR, TenantAccountRole.ADMIN, TenantAccountRole.OWNER}),
rbac=RBACRequirement(
resource_type=RBACResourceScope.APP,
scene=RBACPermission.APP_IMPORT_EXPORT_DSL,
resource_required=False,
),
rbac=RBACCheck(RBACPermission.APP_IMPORT_EXPORT_DSL, Workspace()),
)
@returns(200, Import, "Import confirmed")
@returns(400, Import, "Import failed")
@@ -144,7 +136,7 @@ class AppDslExportApi(Resource):
scope=Scope.APPS_READ,
allowed_token_types=frozenset({TokenType.OAUTH_ACCOUNT}),
allowed_roles=frozenset({TenantAccountRole.EDITOR, TenantAccountRole.ADMIN, TenantAccountRole.OWNER}),
rbac=RBACRequirement(resource_type=RBACResourceScope.APP, scene=RBACPermission.APP_IMPORT_EXPORT_DSL),
rbac=RBACCheck(RBACPermission.APP_IMPORT_EXPORT_DSL, PlainApp()),
)
@accepts(query=AppDslExportQuery)
@returns(200, AppDslExportResponse, "Export successful")
@@ -176,7 +168,7 @@ class AppDslCheckDependenciesApi(Resource):
scope=Scope.APPS_READ,
allowed_token_types=frozenset({TokenType.OAUTH_ACCOUNT}),
allowed_roles=frozenset({TenantAccountRole.EDITOR, TenantAccountRole.ADMIN, TenantAccountRole.OWNER}),
rbac=RBACRequirement(resource_type=RBACResourceScope.APP, scene=RBACPermission.APP_IMPORT_EXPORT_DSL),
rbac=RBACCheck(RBACPermission.APP_IMPORT_EXPORT_DSL, PlainApp()),
)
@returns(200, CheckDependenciesResult, "Dependencies checked")
def get(self, app_id: str, *, auth_data: AuthData):
+4 -4
View File
@@ -20,14 +20,14 @@ from werkzeug.exceptions import (
import services
from controllers.common.fields import EventStreamResponse
from controllers.common.wraps import RBACPermission, RBACResourceScope
from controllers.common.rbac import PlainApp, RBACCheck, RBACPermission
from controllers.console.app.wraps import with_session
from controllers.openapi import openapi_ns
from controllers.openapi._audit import emit_app_run
from controllers.openapi._contract import accepts, returns
from controllers.openapi._models import AppRunRequest, TaskStopResponse
from controllers.openapi.auth.composition import auth_router
from controllers.openapi.auth.data import AuthData, RBACRequirement
from controllers.openapi.auth.data import AuthData
from controllers.service_api.app.error import (
AppUnavailableError,
CompletionRequestError,
@@ -148,7 +148,7 @@ _DISPATCH: dict[AppMode, Callable[[App, Any, AppRunRequest, Session], Any]] = {
class AppRunApi(Resource):
@auth_router.guard(
scope=Scope.APPS_RUN,
rbac=RBACRequirement(resource_type=RBACResourceScope.APP, scene=RBACPermission.APP_TEST_AND_RUN),
rbac=RBACCheck(RBACPermission.APP_TEST_AND_RUN, PlainApp()),
)
@openapi_ns.response(200, "Run result (SSE stream)", openapi_ns.models[EventStreamResponse.__name__])
@accepts(body=AppRunRequest)
@@ -184,7 +184,7 @@ class AppRunApi(Resource):
class AppRunTaskStopApi(Resource):
@auth_router.guard(
scope=Scope.APPS_RUN,
rbac=RBACRequirement(resource_type=RBACResourceScope.APP, scene=RBACPermission.APP_TEST_AND_RUN),
rbac=RBACCheck(RBACPermission.APP_TEST_AND_RUN, PlainApp()),
)
@returns(200, TaskStopResponse, description="Task stopped")
def post(self, app_id: str, task_id: str, *, auth_data: AuthData):
+3 -3
View File
@@ -12,8 +12,8 @@ from werkzeug.exceptions import Conflict, NotFound, UnprocessableEntity
from configs import dify_config
from controllers.common.app_access import AppAccessFilter, resolve_app_access_filter
from controllers.common.fields import Parameters
from controllers.common.rbac import PlainApp, RBACCheck, RBACPermission
from controllers.common.session import with_session
from controllers.common.wraps import RBACPermission, RBACResourceScope
from controllers.openapi import openapi_ns
from controllers.openapi._contract import accepts, returns
from controllers.openapi._input_schema import EMPTY_INPUT_SCHEMA, build_input_schema, resolve_app_config
@@ -27,7 +27,7 @@ from controllers.openapi._models import (
AppListRow,
)
from controllers.openapi.auth.composition import auth_router
from controllers.openapi.auth.data import AuthData, CallerKind, RBACRequirement
from controllers.openapi.auth.data import AuthData, CallerKind
from controllers.service_api.app.error import AppUnavailableError
from core.app.app_config.common.parameters_mapping import get_parameters_from_feature_dict
from libs.oauth_bearer import Scope, TokenType
@@ -135,7 +135,7 @@ class AppDescribeApi(AppReadResource):
@auth_router.guard(
scope=Scope.APPS_READ,
allowed_token_types=frozenset({TokenType.OAUTH_ACCOUNT}),
rbac=RBACRequirement(resource_type=RBACResourceScope.APP, scene=RBACPermission.APP_VIEW_LAYOUT),
rbac=RBACCheck(RBACPermission.APP_VIEW_LAYOUT, PlainApp()),
)
@returns(200, AppDescribeResponse, description="App description")
@accepts(query=AppDescribeQuery)
+4 -12
View File
@@ -6,7 +6,7 @@ from enum import StrEnum
from pydantic import BaseModel, ConfigDict, Field
from werkzeug.exceptions import InternalServerError
from core.rbac import RBACPermission, RBACResourceScope
from controllers.common.rbac import RBACCheck
from libs.oauth_bearer import Scope, TokenType
from models.account import Account, Tenant, TenantAccountRole
from models.model import App, EndUser
@@ -25,23 +25,15 @@ class ExternalIdentity(BaseModel):
issuer: str | None = None
class RBACRequirement(BaseModel):
model_config = ConfigDict(frozen=True)
resource_type: RBACResourceScope
scene: RBACPermission
resource_required: bool = True
class RequestContext(BaseModel):
model_config = ConfigDict(frozen=True)
model_config = ConfigDict(frozen=True, arbitrary_types_allowed=True)
token_type: TokenType
scope: Scope | None = None
path_params: dict[str, str]
workspace_membership: bool = False
allowed_roles: frozenset[TenantAccountRole] | None = None
rbac: RBACRequirement | None = None
rbac: RBACCheck | None = None
class AuthData(BaseModel):
@@ -58,7 +50,7 @@ class AuthData(BaseModel):
path_params: dict[str, str] = Field(default_factory=dict)
allowed_roles: frozenset[TenantAccountRole] | None = None
rbac: RBACRequirement | None = None
rbac: RBACCheck | None = None
app: App | None = None
tenant: Tenant | None = None
+6 -6
View File
@@ -17,11 +17,11 @@ from flask_login import user_logged_in
from werkzeug.exceptions import Forbidden, NotFound, Unauthorized
from configs import dify_config
from controllers.common.rbac import RBACCheck
from controllers.openapi._audit import emit_wrong_surface
from controllers.openapi.auth.data import (
AuthData,
ExternalIdentity,
RBACRequirement,
RequestContext,
)
from controllers.openapi.auth.flow import When
@@ -61,7 +61,7 @@ class AuthPipeline:
scope: Scope | None,
workspace_membership: bool = False,
allowed_roles: frozenset[TenantAccountRole] | None = None,
rbac: RBACRequirement | None = None,
rbac: RBACCheck | None = None,
) -> Any:
req_ctx = RequestContext(
token_type=identity.token_type,
@@ -135,7 +135,7 @@ class PipelineRouter:
require_valid_enterprise_license: bool = False,
workspace_membership: bool = False,
allowed_roles: frozenset[TenantAccountRole] | None = None,
rbac: RBACRequirement | None = None,
rbac: RBACCheck | None = None,
) -> Callable:
return self._make_decorator(
scope=scope,
@@ -155,7 +155,7 @@ class PipelineRouter:
edition: frozenset[DeploymentEdition] | None = None,
require_valid_enterprise_license: bool = False,
allowed_roles: frozenset[TenantAccountRole] | None = None,
rbac: RBACRequirement | None = None,
rbac: RBACCheck | None = None,
) -> Callable:
return self._make_decorator(
scope=scope,
@@ -176,7 +176,7 @@ class PipelineRouter:
require_valid_enterprise_license: bool,
workspace_membership: bool,
allowed_roles: frozenset[TenantAccountRole] | None,
rbac: RBACRequirement | None,
rbac: RBACCheck | None,
) -> Callable:
def decorator(view: Callable) -> Callable:
@wraps(view)
@@ -210,7 +210,7 @@ class PipelineRouter:
require_valid_enterprise_license: bool,
workspace_membership: bool = False,
allowed_roles: frozenset[TenantAccountRole] | None = None,
rbac: RBACRequirement | None = None,
rbac: RBACCheck | None = None,
) -> Any:
# 404 not 403 — this edition doesn't expose the feature at all
if edition is not None and dify_config.DEPLOYMENT_EDITION not in edition:
+3 -5
View File
@@ -4,7 +4,7 @@ from flask import request
from werkzeug.exceptions import Forbidden, NotFound, UnprocessableEntity
from configs import dify_config
from controllers.common.wraps import enforce_rbac_access
from controllers.common.rbac import enforce_rbac_checks
from controllers.openapi.auth.data import AuthData, CallerKind
from extensions.ext_database import db
from libs.oauth_bearer import Scope, TokenType
@@ -62,12 +62,10 @@ def check_rbac_permission(data: AuthData) -> None:
return
if data.account_id is None or data.tenant is None:
raise Forbidden("rbac context missing")
enforce_rbac_access(
enforce_rbac_checks(
tenant_id=str(data.tenant.id),
account_id=str(data.account_id),
resource_type=req.resource_type,
scene=req.scene,
resource_required=req.resource_required,
checks=[req],
path_args=dict(data.path_params),
)
+4 -4
View File
@@ -15,14 +15,14 @@ from flask_restx import Resource
from werkzeug.exceptions import BadRequest
from controllers.common.human_input import HumanInputFormSubmitPayload, stringify_form_default_values
from controllers.common.rbac import PlainApp, RBACCheck, RBACPermission
from controllers.common.schema import register_schema_models
from controllers.common.wraps import RBACPermission, RBACResourceScope
from controllers.openapi import openapi_ns
from controllers.openapi._contract import accepts, returns
from controllers.openapi._errors import HumanInputFormNotFound, RecipientSurfaceMismatch
from controllers.openapi._models import FormSubmitResponse, HumanInputFormDefinitionResponse
from controllers.openapi.auth.composition import auth_router
from controllers.openapi.auth.data import AuthData, CallerKind, RBACRequirement
from controllers.openapi.auth.data import AuthData, CallerKind
from core.workflow.human_input_policy import (
HumanInputSurface,
is_recipient_type_allowed_for_surface,
@@ -65,7 +65,7 @@ class OpenApiWorkflowHumanInputFormApi(Resource):
@openapi_ns.response(200, "Form definition", openapi_ns.models[HumanInputFormDefinitionResponse.__name__])
@auth_router.guard(
scope=Scope.APPS_RUN,
rbac=RBACRequirement(resource_type=RBACResourceScope.APP, scene=RBACPermission.APP_TEST_AND_RUN),
rbac=RBACCheck(RBACPermission.APP_TEST_AND_RUN, PlainApp()),
)
def get(self, app_id: str, form_token: str, *, auth_data: AuthData):
app_model, _caller, _caller_kind = auth_data.require_app_context()
@@ -84,7 +84,7 @@ class OpenApiWorkflowHumanInputFormApi(Resource):
class OpenApiWorkflowHumanInputFormSubmitApi(Resource):
@auth_router.guard(
scope=Scope.APPS_RUN,
rbac=RBACRequirement(resource_type=RBACResourceScope.APP, scene=RBACPermission.APP_TEST_AND_RUN),
rbac=RBACCheck(RBACPermission.APP_TEST_AND_RUN, PlainApp()),
)
@returns(200, FormSubmitResponse, description="Form submitted")
@accepts(body=HumanInputFormSubmitPayload)
+3 -3
View File
@@ -18,11 +18,11 @@ from sqlalchemy.orm import sessionmaker
from werkzeug.exceptions import NotFound, UnprocessableEntity
from controllers.common.fields import EventStreamResponse
from controllers.common.rbac import PlainApp, RBACCheck, RBACPermission
from controllers.common.schema import query_params_from_model
from controllers.common.wraps import RBACPermission, RBACResourceScope
from controllers.openapi import openapi_ns
from controllers.openapi.auth.composition import auth_router
from controllers.openapi.auth.data import AuthData, CallerKind, RBACRequirement
from controllers.openapi.auth.data import AuthData, CallerKind
from core.app.apps.advanced_chat.app_generator import AdvancedChatAppGenerator
from core.app.apps.base_app_generator import BaseAppGenerator
from core.app.apps.common.workflow_response_converter import WorkflowResponseConverter
@@ -49,7 +49,7 @@ class OpenApiWorkflowEventsApi(Resource):
@openapi_ns.response(200, "SSE event stream", openapi_ns.models[EventStreamResponse.__name__])
@auth_router.guard(
scope=Scope.APPS_RUN,
rbac=RBACRequirement(resource_type=RBACResourceScope.APP, scene=RBACPermission.APP_TEST_AND_RUN),
rbac=RBACCheck(RBACPermission.APP_TEST_AND_RUN, PlainApp()),
)
def get(self, app_id: str, task_id: str, *, auth_data: AuthData):
app_model, caller, caller_kind = auth_data.require_app_context()