diff --git a/api/controllers/openapi/app_dsl.py b/api/controllers/openapi/app_dsl.py index 8a8c62f28ca..f7890336d81 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 @@ -38,6 +39,7 @@ class AppDslImportApi(Resource): 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") @@ -126,6 +128,7 @@ class AppDslExportApi(Resource): 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): @@ -156,6 +159,7 @@ class AppDslCheckDependenciesApi(Resource): 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 76ddd166596..1cec04844fb 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, 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 @@ -137,6 +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) + @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): @@ -168,6 +170,7 @@ class AppRunApi(Resource): @openapi_ns.route("/apps//tasks//stop") class AppRunTaskStopApi(Resource): @auth_router.guard(scope=Scope.APPS_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/apps.py b/api/controllers/openapi/apps.py index 768f62d2620..c1b9b5eed06 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, 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 @@ -124,6 +125,7 @@ def build_app_describe_response(app: App, fields: set[str] | None) -> AppDescrib @openapi_ns.route("/apps//describe") class AppDescribeApi(AppReadResource): @auth_router.guard(scope=Scope.APPS_READ, allowed_token_types=frozenset({TokenType.OAUTH_ACCOUNT})) + @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): @@ -135,6 +137,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})) + @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): diff --git a/api/controllers/openapi/human_input_form.py b/api/controllers/openapi/human_input_form.py index e5930ea8fa2..ac4c33962de 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 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, openapi_rbac_permission_required from controllers.openapi import openapi_ns from controllers.openapi._contract import accepts, returns from controllers.openapi._errors import HumanInputFormNotFound, RecipientSurfaceMismatch @@ -63,6 +64,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) + @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) @@ -76,6 +78,7 @@ class OpenApiWorkflowHumanInputFormApi(Resource): return _jsonify_form_definition(form) @auth_router.guard(scope=Scope.APPS_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 61ebb3012dc..77f60224a48 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, 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 @@ -47,6 +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) + @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)