From d82b6fe48e8da3f7f69a23ffd4e3c4967c5f5567 Mon Sep 17 00:00:00 2001 From: "yunlu.wen" Date: Mon, 15 Jun 2026 13:51:30 +0800 Subject: [PATCH 1/7] guard openapi with rbac decorator --- api/controllers/openapi/app_dsl.py | 4 ++++ api/controllers/openapi/app_run.py | 3 +++ api/controllers/openapi/apps.py | 3 +++ api/controllers/openapi/human_input_form.py | 3 +++ api/controllers/openapi/workflow_events.py | 2 ++ 5 files changed, 15 insertions(+) diff --git a/api/controllers/openapi/app_dsl.py b/api/controllers/openapi/app_dsl.py index 8a8c62f28ca..a42a77dc35f 100644 --- a/api/controllers/openapi/app_dsl.py +++ b/api/controllers/openapi/app_dsl.py @@ -5,6 +5,7 @@ from typing import cast from flask_restx import Resource from sqlalchemy.orm import Session +from controllers.common.wraps import RBACPermission, RBACResourceScope, rbac_permission_required from controllers.openapi import openapi_ns from controllers.openapi._contract import accepts, returns from controllers.openapi._models import AppDslExportQuery, AppDslExportResponse, AppDslImportPayload @@ -33,6 +34,7 @@ class AppDslImportApi(Resource): Returns 400 when the import failed due to invalid DSL or a business error. """ + @rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_IMPORT_EXPORT_DSL, resource_required=False) @auth_router.guard_workspace( scope=Scope.WORKSPACE_WRITE, allowed_token_types=frozenset({TokenType.OAUTH_ACCOUNT}), @@ -121,6 +123,7 @@ class AppDslExportApi(Resource): receive a 403; enable the API in the console first if needed. """ + @rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_IMPORT_EXPORT_DSL) @auth_router.guard( scope=Scope.APPS_READ, allowed_token_types=frozenset({TokenType.OAUTH_ACCOUNT}), @@ -151,6 +154,7 @@ class AppDslCheckDependenciesApi(Resource): dependencies are satisfied. """ + @rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_IMPORT_EXPORT_DSL) @auth_router.guard( scope=Scope.APPS_READ, allowed_token_types=frozenset({TokenType.OAUTH_ACCOUNT}), diff --git a/api/controllers/openapi/app_run.py b/api/controllers/openapi/app_run.py index 76ddd166596..4f09631fce4 100644 --- a/api/controllers/openapi/app_run.py +++ b/api/controllers/openapi/app_run.py @@ -19,6 +19,7 @@ from werkzeug.exceptions import ( import services from controllers.common.fields import EventStreamResponse +from controllers.common.wraps import RBACPermission, RBACResourceScope, rbac_permission_required from controllers.openapi import openapi_ns from controllers.openapi._audit import emit_app_run from controllers.openapi._contract import accepts, returns @@ -136,6 +137,7 @@ _DISPATCH: dict[AppMode, Callable[[App, Any, AppRunRequest], Any]] = { @openapi_ns.route("/apps//run") class AppRunApi(Resource): + @rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_TEST_AND_RUN) @auth_router.guard(scope=Scope.APPS_RUN) @openapi_ns.response(200, "Run result (SSE stream)", openapi_ns.models[EventStreamResponse.__name__]) @accepts(body=AppRunRequest) @@ -167,6 +169,7 @@ class AppRunApi(Resource): @openapi_ns.route("/apps//tasks//stop") class AppRunTaskStopApi(Resource): + @rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_TEST_AND_RUN) @auth_router.guard(scope=Scope.APPS_RUN) @returns(200, TaskStopResponse, description="Task stopped") def post(self, app_id: str, task_id: str, *, auth_data: AuthData): diff --git a/api/controllers/openapi/apps.py b/api/controllers/openapi/apps.py index 84b1610d5f5..10b7fb0d5e1 100644 --- a/api/controllers/openapi/apps.py +++ b/api/controllers/openapi/apps.py @@ -21,6 +21,7 @@ from controllers.openapi._models import ( AppListRow, TagItem, ) +from controllers.common.wraps import RBACPermission, RBACResourceScope, rbac_permission_required from controllers.openapi.auth.composition import auth_router from controllers.openapi.auth.data import AuthData from controllers.service_api.app.error import AppUnavailableError @@ -86,6 +87,7 @@ def parameters_payload(app: App) -> dict: @openapi_ns.route("/apps//describe") class AppDescribeApi(AppReadResource): + @rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_VIEW_LAYOUT) @auth_router.guard(scope=Scope.APPS_READ, allowed_token_types=frozenset({TokenType.OAUTH_ACCOUNT})) @returns(200, AppDescribeResponse, description="App description") @accepts(query=AppDescribeQuery) @@ -136,6 +138,7 @@ class AppDescribeApi(AppReadResource): @openapi_ns.route("/apps") class AppListApi(Resource): + @rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_VIEW_LAYOUT, resource_required=False) @auth_router.guard_workspace(scope=Scope.APPS_READ, allowed_token_types=frozenset({TokenType.OAUTH_ACCOUNT})) @returns(200, AppListResponse, description="App list") @accepts(query=AppListQuery) diff --git a/api/controllers/openapi/human_input_form.py b/api/controllers/openapi/human_input_form.py index 995315150cc..6f04d111c52 100644 --- a/api/controllers/openapi/human_input_form.py +++ b/api/controllers/openapi/human_input_form.py @@ -16,6 +16,7 @@ from werkzeug.exceptions import BadRequest, NotFound from controllers.common.human_input import HumanInputFormSubmitPayload, stringify_form_default_values from controllers.common.schema import register_schema_models +from controllers.common.wraps import RBACPermission, RBACResourceScope, rbac_permission_required from controllers.openapi import openapi_ns from controllers.openapi._contract import accepts, returns from controllers.openapi._models import FormSubmitResponse, HumanInputFormDefinitionResponse @@ -57,6 +58,7 @@ def _ensure_form_is_allowed_for_openapi(form) -> None: @openapi_ns.route("/apps//form/human_input/") class OpenApiWorkflowHumanInputFormApi(Resource): + @rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_TEST_AND_RUN) @openapi_ns.response(200, "Form definition", openapi_ns.models[HumanInputFormDefinitionResponse.__name__]) @auth_router.guard(scope=Scope.APPS_RUN) def get(self, app_id: str, form_token: str, *, auth_data: AuthData): @@ -71,6 +73,7 @@ class OpenApiWorkflowHumanInputFormApi(Resource): service.ensure_form_active(form) return _jsonify_form_definition(form) + @rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_TEST_AND_RUN) @auth_router.guard(scope=Scope.APPS_RUN) @returns(200, FormSubmitResponse, description="Form submitted") @accepts(body=HumanInputFormSubmitPayload) diff --git a/api/controllers/openapi/workflow_events.py b/api/controllers/openapi/workflow_events.py index 61ebb3012dc..bbb9889cff4 100644 --- a/api/controllers/openapi/workflow_events.py +++ b/api/controllers/openapi/workflow_events.py @@ -19,6 +19,7 @@ from werkzeug.exceptions import NotFound, UnprocessableEntity from controllers.common.fields import EventStreamResponse from controllers.common.schema import query_params_from_model +from controllers.common.wraps import RBACPermission, RBACResourceScope, rbac_permission_required from controllers.openapi import openapi_ns from controllers.openapi.auth.composition import auth_router from controllers.openapi.auth.data import AuthData @@ -44,6 +45,7 @@ class WorkflowEventsQuery(BaseModel): @openapi_ns.route("/apps//tasks//events") class OpenApiWorkflowEventsApi(Resource): + @rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_TEST_AND_RUN) @openapi_ns.doc(params=query_params_from_model(WorkflowEventsQuery)) @openapi_ns.response(200, "SSE event stream", openapi_ns.models[EventStreamResponse.__name__]) @auth_router.guard(scope=Scope.APPS_RUN) From c38cba1f8c1713a06092c2fd8d4f1132d9d14e49 Mon Sep 17 00:00:00 2001 From: "yunlu.wen" Date: Wed, 17 Jun 2026 10:27:32 +0800 Subject: [PATCH 2/7] reorder decorators --- api/controllers/openapi/app_dsl.py | 6 +++--- api/controllers/openapi/app_run.py | 4 ++-- api/controllers/openapi/apps.py | 4 ++-- api/controllers/openapi/human_input_form.py | 4 ++-- api/controllers/openapi/workflow_events.py | 2 +- 5 files changed, 10 insertions(+), 10 deletions(-) diff --git a/api/controllers/openapi/app_dsl.py b/api/controllers/openapi/app_dsl.py index a42a77dc35f..f7890336d81 100644 --- a/api/controllers/openapi/app_dsl.py +++ b/api/controllers/openapi/app_dsl.py @@ -34,12 +34,12 @@ class AppDslImportApi(Resource): Returns 400 when the import failed due to invalid DSL or a business error. """ - @rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_IMPORT_EXPORT_DSL, resource_required=False) @auth_router.guard_workspace( scope=Scope.WORKSPACE_WRITE, allowed_token_types=frozenset({TokenType.OAUTH_ACCOUNT}), allowed_roles=frozenset({TenantAccountRole.EDITOR, TenantAccountRole.ADMIN, TenantAccountRole.OWNER}), ) + @rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_IMPORT_EXPORT_DSL, resource_required=False) @returns(200, Import, "Import completed") @returns(202, Import, "Import pending confirmation") @returns(400, Import, "Import failed") @@ -123,12 +123,12 @@ class AppDslExportApi(Resource): receive a 403; enable the API in the console first if needed. """ - @rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_IMPORT_EXPORT_DSL) @auth_router.guard( scope=Scope.APPS_READ, allowed_token_types=frozenset({TokenType.OAUTH_ACCOUNT}), allowed_roles=frozenset({TenantAccountRole.EDITOR, TenantAccountRole.ADMIN, TenantAccountRole.OWNER}), ) + @rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_IMPORT_EXPORT_DSL) @accepts(query=AppDslExportQuery) @returns(200, AppDslExportResponse, "Export successful") def get(self, app_id: str, *, auth_data: AuthData, query: AppDslExportQuery): @@ -154,12 +154,12 @@ class AppDslCheckDependenciesApi(Resource): dependencies are satisfied. """ - @rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_IMPORT_EXPORT_DSL) @auth_router.guard( scope=Scope.APPS_READ, allowed_token_types=frozenset({TokenType.OAUTH_ACCOUNT}), allowed_roles=frozenset({TenantAccountRole.EDITOR, TenantAccountRole.ADMIN, TenantAccountRole.OWNER}), ) + @rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_IMPORT_EXPORT_DSL) @returns(200, CheckDependenciesResult, "Dependencies checked") def get(self, app_id: str, *, auth_data: AuthData): app = cast(App, auth_data.app) diff --git a/api/controllers/openapi/app_run.py b/api/controllers/openapi/app_run.py index 4f09631fce4..7f214480110 100644 --- a/api/controllers/openapi/app_run.py +++ b/api/controllers/openapi/app_run.py @@ -137,8 +137,8 @@ _DISPATCH: dict[AppMode, Callable[[App, Any, AppRunRequest], Any]] = { @openapi_ns.route("/apps//run") class AppRunApi(Resource): - @rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_TEST_AND_RUN) @auth_router.guard(scope=Scope.APPS_RUN) + @rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_TEST_AND_RUN) @openapi_ns.response(200, "Run result (SSE stream)", openapi_ns.models[EventStreamResponse.__name__]) @accepts(body=AppRunRequest) def post(self, app_id: str, *, auth_data: AuthData, body: AppRunRequest): @@ -169,8 +169,8 @@ class AppRunApi(Resource): @openapi_ns.route("/apps//tasks//stop") class AppRunTaskStopApi(Resource): - @rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_TEST_AND_RUN) @auth_router.guard(scope=Scope.APPS_RUN) + @rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_TEST_AND_RUN) @returns(200, TaskStopResponse, description="Task stopped") def post(self, app_id: str, task_id: str, *, auth_data: AuthData): app_model, caller, caller_kind = auth_data.require_app_context() diff --git a/api/controllers/openapi/apps.py b/api/controllers/openapi/apps.py index 10b7fb0d5e1..4dfc147d7b6 100644 --- a/api/controllers/openapi/apps.py +++ b/api/controllers/openapi/apps.py @@ -87,8 +87,8 @@ def parameters_payload(app: App) -> dict: @openapi_ns.route("/apps//describe") class AppDescribeApi(AppReadResource): - @rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_VIEW_LAYOUT) @auth_router.guard(scope=Scope.APPS_READ, allowed_token_types=frozenset({TokenType.OAUTH_ACCOUNT})) + @rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_VIEW_LAYOUT) @returns(200, AppDescribeResponse, description="App description") @accepts(query=AppDescribeQuery) def get(self, app_id: str, *, auth_data: AuthData, query: AppDescribeQuery): @@ -138,8 +138,8 @@ class AppDescribeApi(AppReadResource): @openapi_ns.route("/apps") class AppListApi(Resource): - @rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_VIEW_LAYOUT, resource_required=False) @auth_router.guard_workspace(scope=Scope.APPS_READ, allowed_token_types=frozenset({TokenType.OAUTH_ACCOUNT})) + @rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_VIEW_LAYOUT, resource_required=False) @returns(200, AppListResponse, description="App list") @accepts(query=AppListQuery) def get(self, *, auth_data: AuthData, query: AppListQuery): diff --git a/api/controllers/openapi/human_input_form.py b/api/controllers/openapi/human_input_form.py index 6f04d111c52..51a0b49de20 100644 --- a/api/controllers/openapi/human_input_form.py +++ b/api/controllers/openapi/human_input_form.py @@ -58,9 +58,9 @@ def _ensure_form_is_allowed_for_openapi(form) -> None: @openapi_ns.route("/apps//form/human_input/") class OpenApiWorkflowHumanInputFormApi(Resource): - @rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_TEST_AND_RUN) @openapi_ns.response(200, "Form definition", openapi_ns.models[HumanInputFormDefinitionResponse.__name__]) @auth_router.guard(scope=Scope.APPS_RUN) + @rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_TEST_AND_RUN) def get(self, app_id: str, form_token: str, *, auth_data: AuthData): app_model, caller, caller_kind = auth_data.require_app_context() service = HumanInputService(db.engine) @@ -73,8 +73,8 @@ class OpenApiWorkflowHumanInputFormApi(Resource): service.ensure_form_active(form) return _jsonify_form_definition(form) - @rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_TEST_AND_RUN) @auth_router.guard(scope=Scope.APPS_RUN) + @rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_TEST_AND_RUN) @returns(200, FormSubmitResponse, description="Form submitted") @accepts(body=HumanInputFormSubmitPayload) def post(self, app_id: str, form_token: str, *, auth_data: AuthData, body: HumanInputFormSubmitPayload): diff --git a/api/controllers/openapi/workflow_events.py b/api/controllers/openapi/workflow_events.py index bbb9889cff4..7a4c657bd61 100644 --- a/api/controllers/openapi/workflow_events.py +++ b/api/controllers/openapi/workflow_events.py @@ -45,10 +45,10 @@ class WorkflowEventsQuery(BaseModel): @openapi_ns.route("/apps//tasks//events") class OpenApiWorkflowEventsApi(Resource): - @rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_TEST_AND_RUN) @openapi_ns.doc(params=query_params_from_model(WorkflowEventsQuery)) @openapi_ns.response(200, "SSE event stream", openapi_ns.models[EventStreamResponse.__name__]) @auth_router.guard(scope=Scope.APPS_RUN) + @rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_TEST_AND_RUN) def get(self, app_id: str, task_id: str, *, auth_data: AuthData): app_model, caller, caller_kind = auth_data.require_app_context() app_mode = AppMode.value_of(app_model.mode) From 39bf04e7fe1248a64c915306dee83885060475bf Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Wed, 17 Jun 2026 02:30:55 +0000 Subject: [PATCH 3/7] [autofix.ci] apply automated fixes --- api/controllers/openapi/apps.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/controllers/openapi/apps.py b/api/controllers/openapi/apps.py index 4dfc147d7b6..a6fa0288572 100644 --- a/api/controllers/openapi/apps.py +++ b/api/controllers/openapi/apps.py @@ -9,6 +9,7 @@ from flask_restx import Resource from werkzeug.exceptions import Conflict, NotFound, UnprocessableEntity from controllers.common.fields import Parameters +from controllers.common.wraps import RBACPermission, RBACResourceScope, rbac_permission_required 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 @@ -21,7 +22,6 @@ from controllers.openapi._models import ( AppListRow, TagItem, ) -from controllers.common.wraps import RBACPermission, RBACResourceScope, rbac_permission_required from controllers.openapi.auth.composition import auth_router from controllers.openapi.auth.data import AuthData from controllers.service_api.app.error import AppUnavailableError From 3c8d03d24f0789097381df3d6241430e8a60c80c Mon Sep 17 00:00:00 2001 From: "yunlu.wen" Date: Wed, 17 Jun 2026 11:00:56 +0800 Subject: [PATCH 4/7] handle enduser in decorator --- api/controllers/common/wraps.py | 34 ++++++++++++++++++++- api/controllers/openapi/app_run.py | 6 ++-- api/controllers/openapi/human_input_form.py | 6 ++-- api/controllers/openapi/workflow_events.py | 4 +-- 4 files changed, 41 insertions(+), 9 deletions(-) diff --git a/api/controllers/common/wraps.py b/api/controllers/common/wraps.py index c481f6eca94..ce9a3740d44 100644 --- a/api/controllers/common/wraps.py +++ b/api/controllers/common/wraps.py @@ -1,9 +1,41 @@ +from __future__ import annotations + from collections.abc import Callable from functools import wraps +from typing import TYPE_CHECKING from core.rbac import RBACPermission, RBACResourceScope -__all__ = ["RBACPermission", "RBACResourceScope", "rbac_permission_required"] +if TYPE_CHECKING: + from controllers.openapi.auth.data import AuthData + +__all__ = ["RBACPermission", "RBACResourceScope", "openapi_rbac_permission_required", "rbac_permission_required"] + + +def openapi_rbac_permission_required[**P, R]( + resource_type: RBACResourceScope, + scene: RBACPermission, + *, + resource_required: bool = True, +) -> Callable[[Callable[P, R]], Callable[P, R]]: + """RBAC guard for OpenAPI endpoints that may be called by either an Account or an EndUser. + """ + inner = rbac_permission_required(resource_type, scene, resource_required=resource_required) + + def decorator(view: Callable[P, R]) -> Callable[P, R]: + guarded = inner(view) + + @wraps(view) + def decorated(*args: P.args, **kwargs: P.kwargs) -> R: + auth_data: AuthData | None = kwargs.get("auth_data") + if auth_data is not None and auth_data.caller_kind == "end_user": + # we can skip rbac for enduser for now. + return view(*args, **kwargs) + return guarded(*args, **kwargs) + + return decorated + + return decorator def rbac_permission_required[**P, R]( diff --git a/api/controllers/openapi/app_run.py b/api/controllers/openapi/app_run.py index 7f214480110..1cec04844fb 100644 --- a/api/controllers/openapi/app_run.py +++ b/api/controllers/openapi/app_run.py @@ -19,7 +19,7 @@ from werkzeug.exceptions import ( import services from controllers.common.fields import EventStreamResponse -from controllers.common.wraps import RBACPermission, RBACResourceScope, rbac_permission_required +from controllers.common.wraps import RBACPermission, RBACResourceScope, openapi_rbac_permission_required from controllers.openapi import openapi_ns from controllers.openapi._audit import emit_app_run from controllers.openapi._contract import accepts, returns @@ -138,7 +138,7 @@ _DISPATCH: dict[AppMode, Callable[[App, Any, AppRunRequest], Any]] = { @openapi_ns.route("/apps//run") class AppRunApi(Resource): @auth_router.guard(scope=Scope.APPS_RUN) - @rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_TEST_AND_RUN) + @openapi_rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_TEST_AND_RUN) @openapi_ns.response(200, "Run result (SSE stream)", openapi_ns.models[EventStreamResponse.__name__]) @accepts(body=AppRunRequest) def post(self, app_id: str, *, auth_data: AuthData, body: AppRunRequest): @@ -170,7 +170,7 @@ class AppRunApi(Resource): @openapi_ns.route("/apps//tasks//stop") class AppRunTaskStopApi(Resource): @auth_router.guard(scope=Scope.APPS_RUN) - @rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_TEST_AND_RUN) + @openapi_rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_TEST_AND_RUN) @returns(200, TaskStopResponse, description="Task stopped") def post(self, app_id: str, task_id: str, *, auth_data: AuthData): app_model, caller, caller_kind = auth_data.require_app_context() diff --git a/api/controllers/openapi/human_input_form.py b/api/controllers/openapi/human_input_form.py index 51a0b49de20..6148c2ab8de 100644 --- a/api/controllers/openapi/human_input_form.py +++ b/api/controllers/openapi/human_input_form.py @@ -16,7 +16,7 @@ from werkzeug.exceptions import BadRequest, NotFound from controllers.common.human_input import HumanInputFormSubmitPayload, stringify_form_default_values from controllers.common.schema import register_schema_models -from controllers.common.wraps import RBACPermission, RBACResourceScope, rbac_permission_required +from controllers.common.wraps import RBACPermission, RBACResourceScope, openapi_rbac_permission_required from controllers.openapi import openapi_ns from controllers.openapi._contract import accepts, returns from controllers.openapi._models import FormSubmitResponse, HumanInputFormDefinitionResponse @@ -60,7 +60,7 @@ def _ensure_form_is_allowed_for_openapi(form) -> None: class OpenApiWorkflowHumanInputFormApi(Resource): @openapi_ns.response(200, "Form definition", openapi_ns.models[HumanInputFormDefinitionResponse.__name__]) @auth_router.guard(scope=Scope.APPS_RUN) - @rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_TEST_AND_RUN) + @openapi_rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_TEST_AND_RUN) def get(self, app_id: str, form_token: str, *, auth_data: AuthData): app_model, caller, caller_kind = auth_data.require_app_context() service = HumanInputService(db.engine) @@ -74,7 +74,7 @@ class OpenApiWorkflowHumanInputFormApi(Resource): return _jsonify_form_definition(form) @auth_router.guard(scope=Scope.APPS_RUN) - @rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_TEST_AND_RUN) + @openapi_rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_TEST_AND_RUN) @returns(200, FormSubmitResponse, description="Form submitted") @accepts(body=HumanInputFormSubmitPayload) def post(self, app_id: str, form_token: str, *, auth_data: AuthData, body: HumanInputFormSubmitPayload): diff --git a/api/controllers/openapi/workflow_events.py b/api/controllers/openapi/workflow_events.py index 7a4c657bd61..77f60224a48 100644 --- a/api/controllers/openapi/workflow_events.py +++ b/api/controllers/openapi/workflow_events.py @@ -19,7 +19,7 @@ from werkzeug.exceptions import NotFound, UnprocessableEntity from controllers.common.fields import EventStreamResponse from controllers.common.schema import query_params_from_model -from controllers.common.wraps import RBACPermission, RBACResourceScope, rbac_permission_required +from controllers.common.wraps import RBACPermission, RBACResourceScope, openapi_rbac_permission_required from controllers.openapi import openapi_ns from controllers.openapi.auth.composition import auth_router from controllers.openapi.auth.data import AuthData @@ -48,7 +48,7 @@ class OpenApiWorkflowEventsApi(Resource): @openapi_ns.doc(params=query_params_from_model(WorkflowEventsQuery)) @openapi_ns.response(200, "SSE event stream", openapi_ns.models[EventStreamResponse.__name__]) @auth_router.guard(scope=Scope.APPS_RUN) - @rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_TEST_AND_RUN) + @openapi_rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_TEST_AND_RUN) def get(self, app_id: str, task_id: str, *, auth_data: AuthData): app_model, caller, caller_kind = auth_data.require_app_context() app_mode = AppMode.value_of(app_model.mode) From 0ca14cd8ad0c79399a1401e5426a2865443d754f Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Wed, 17 Jun 2026 03:18:50 +0000 Subject: [PATCH 5/7] [autofix.ci] apply automated fixes --- api/controllers/common/wraps.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/api/controllers/common/wraps.py b/api/controllers/common/wraps.py index ce9a3740d44..c4fb5842431 100644 --- a/api/controllers/common/wraps.py +++ b/api/controllers/common/wraps.py @@ -18,8 +18,7 @@ def openapi_rbac_permission_required[**P, R]( *, resource_required: bool = True, ) -> Callable[[Callable[P, R]], Callable[P, R]]: - """RBAC guard for OpenAPI endpoints that may be called by either an Account or an EndUser. - """ + """RBAC guard for OpenAPI endpoints that may be called by either an Account or an EndUser.""" inner = rbac_permission_required(resource_type, scene, resource_required=resource_required) def decorator(view: Callable[P, R]) -> Callable[P, R]: From 547340ecca18a5d97ad162d2d9588aa19ce9da56 Mon Sep 17 00:00:00 2001 From: "yunlu.wen" Date: Wed, 17 Jun 2026 11:22:04 +0800 Subject: [PATCH 6/7] allow enduser access for app list/desc --- api/controllers/openapi/apps.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/api/controllers/openapi/apps.py b/api/controllers/openapi/apps.py index a6fa0288572..ade1140d50c 100644 --- a/api/controllers/openapi/apps.py +++ b/api/controllers/openapi/apps.py @@ -9,7 +9,7 @@ from flask_restx import Resource from werkzeug.exceptions import Conflict, NotFound, UnprocessableEntity from controllers.common.fields import Parameters -from controllers.common.wraps import RBACPermission, RBACResourceScope, rbac_permission_required +from controllers.common.wraps import RBACPermission, RBACResourceScope, openapi_rbac_permission_required 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 @@ -88,7 +88,7 @@ def parameters_payload(app: App) -> dict: @openapi_ns.route("/apps//describe") class AppDescribeApi(AppReadResource): @auth_router.guard(scope=Scope.APPS_READ, allowed_token_types=frozenset({TokenType.OAUTH_ACCOUNT})) - @rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_VIEW_LAYOUT) + @openapi_rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_VIEW_LAYOUT) @returns(200, AppDescribeResponse, description="App description") @accepts(query=AppDescribeQuery) def get(self, app_id: str, *, auth_data: AuthData, query: AppDescribeQuery): @@ -139,7 +139,7 @@ class AppDescribeApi(AppReadResource): @openapi_ns.route("/apps") class AppListApi(Resource): @auth_router.guard_workspace(scope=Scope.APPS_READ, allowed_token_types=frozenset({TokenType.OAUTH_ACCOUNT})) - @rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_VIEW_LAYOUT, resource_required=False) + @openapi_rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_VIEW_LAYOUT, resource_required=False) @returns(200, AppListResponse, description="App list") @accepts(query=AppListQuery) def get(self, *, auth_data: AuthData, query: AppListQuery): From c7ceaa5fe27f19b7ce082048c412cac4081b0d88 Mon Sep 17 00:00:00 2001 From: "yunlu.wen" Date: Wed, 17 Jun 2026 18:41:42 +0800 Subject: [PATCH 7/7] fix typecheck --- api/controllers/common/wraps.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/api/controllers/common/wraps.py b/api/controllers/common/wraps.py index c4fb5842431..e5dfb3d404d 100644 --- a/api/controllers/common/wraps.py +++ b/api/controllers/common/wraps.py @@ -2,7 +2,7 @@ from __future__ import annotations from collections.abc import Callable from functools import wraps -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, cast from core.rbac import RBACPermission, RBACResourceScope @@ -11,7 +11,7 @@ if TYPE_CHECKING: __all__ = ["RBACPermission", "RBACResourceScope", "openapi_rbac_permission_required", "rbac_permission_required"] - +# TODO(wylswz): refactor: make RBAC a pipeline step def openapi_rbac_permission_required[**P, R]( resource_type: RBACResourceScope, scene: RBACPermission, @@ -26,7 +26,7 @@ def openapi_rbac_permission_required[**P, R]( @wraps(view) def decorated(*args: P.args, **kwargs: P.kwargs) -> R: - auth_data: AuthData | None = kwargs.get("auth_data") + auth_data: AuthData | None = cast(AuthData | None, kwargs.get("auth_data")) if auth_data is not None and auth_data.caller_kind == "end_user": # we can skip rbac for enduser for now. return view(*args, **kwargs)