From 5ee06905ae33d24e183292b48a50819aaf887c1b Mon Sep 17 00:00:00 2001 From: Sanghun Lee Date: Fri, 13 Mar 2026 10:52:31 +0900 Subject: [PATCH 01/11] feat(BA-3692): Add RBAC base classes for model serving actions - Add ModelServiceScopeAction for scope-based actions (create, list, search) - Add ModelServiceSingleEntityAction for single-entity actions (get, update, delete) - Add corresponding result classes - Change entity_type from MODEL_SERVICE to MODEL_DEPLOYMENT per RBAC requirements Co-Authored-By: Claude Sonnet 4.5 --- .../services/model_serving/actions/base.py | 34 ++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) diff --git a/src/ai/backend/manager/services/model_serving/actions/base.py b/src/ai/backend/manager/services/model_serving/actions/base.py index 46e25e78432..62da759b28d 100644 --- a/src/ai/backend/manager/services/model_serving/actions/base.py +++ b/src/ai/backend/manager/services/model_serving/actions/base.py @@ -2,10 +2,42 @@ from ai.backend.common.data.permission.types import EntityType from ai.backend.manager.actions.action import BaseAction +from ai.backend.manager.actions.action.scope import BaseScopeAction, BaseScopeActionResult +from ai.backend.manager.actions.action.single_entity import ( + BaseSingleEntityAction, + BaseSingleEntityActionResult, +) +from ai.backend.manager.actions.action.types import FieldData class ModelServiceAction(BaseAction): @override @classmethod def entity_type(cls) -> EntityType: - return EntityType.MODEL_SERVICE + return EntityType.MODEL_DEPLOYMENT + + +class ModelServiceScopeAction(BaseScopeAction): + @override + @classmethod + def entity_type(cls) -> EntityType: + return EntityType.MODEL_DEPLOYMENT + + +class ModelServiceScopeActionResult(BaseScopeActionResult): + pass + + +class ModelServiceSingleEntityAction(BaseSingleEntityAction): + @override + @classmethod + def entity_type(cls) -> EntityType: + return EntityType.MODEL_DEPLOYMENT + + @override + def field_data(self) -> FieldData | None: + return None + + +class ModelServiceSingleEntityActionResult(BaseSingleEntityActionResult): + pass From de1c21a3989f7dfdbdb104d35ece70479023c092 Mon Sep 17 00:00:00 2001 From: Sanghun Lee Date: Fri, 13 Mar 2026 10:56:24 +0900 Subject: [PATCH 02/11] feat(BA-3692): Refactor model serving actions to use RBAC base classes Apply RBAC validator integration pattern to model serving actions: Scope Actions (ProjectScope): - CreateModelServiceAction - create operations within project scope - ListModelServiceAction - list operations within project scope - SearchServicesAction - search operations within project scope Single Entity Actions: - GetModelServiceInfoAction - read specific model service - DeleteModelServiceAction - delete specific model service - ModifyEndpointAction - update specific endpoint - UpdateRouteAction - update specific route (DEPLOYMENT_ROUTE entity type) - DeleteRouteAction - delete specific route (DEPLOYMENT_ROUTE entity type) Each action now implements required RBAC methods: - Scope actions: scope_type(), scope_id(), target_element() - Single entity actions: target_entity_id(), target_element() All actions follow the pattern from group service processors. Co-Authored-By: Claude Sonnet 4.5 --- .../actions/create_model_service.py | 38 ++++++++++++++----- .../actions/delete_model_service.py | 30 +++++++++------ .../model_serving/actions/delete_route.py | 31 +++++++++------ .../actions/get_model_service_info.py | 26 ++++++++----- .../actions/list_model_service.py | 38 ++++++++++++++----- .../model_serving/actions/modify_endpoint.py | 26 ++++++++----- .../model_serving/actions/search_services.py | 38 ++++++++++++++----- .../model_serving/actions/update_route.py | 31 +++++++++------ 8 files changed, 175 insertions(+), 83 deletions(-) diff --git a/src/ai/backend/manager/services/model_serving/actions/create_model_service.py b/src/ai/backend/manager/services/model_serving/actions/create_model_service.py index aa486356a72..a27391eda36 100644 --- a/src/ai/backend/manager/services/model_serving/actions/create_model_service.py +++ b/src/ai/backend/manager/services/model_serving/actions/create_model_service.py @@ -4,32 +4,50 @@ from dataclasses import dataclass from typing import override -from ai.backend.manager.actions.action import BaseActionResult +from ai.backend.common.data.permission.types import RBACElementType, ScopeType from ai.backend.manager.actions.types import ActionOperationType from ai.backend.manager.data.model_serving.creator import ModelServiceCreator from ai.backend.manager.data.model_serving.types import ServiceInfo -from ai.backend.manager.services.model_serving.actions.base import ModelServiceAction +from ai.backend.manager.data.permission.types import RBACElementRef +from ai.backend.manager.services.model_serving.actions.base import ( + ModelServiceScopeAction, + ModelServiceScopeActionResult, +) @dataclass -class CreateModelServiceAction(ModelServiceAction): +class CreateModelServiceAction(ModelServiceScopeAction): request_user_id: uuid.UUID creator: ModelServiceCreator - - @override - def entity_id(self) -> str | None: - return None + _project_id: uuid.UUID @override @classmethod def operation_type(cls) -> ActionOperationType: return ActionOperationType.CREATE + @override + def scope_type(self) -> ScopeType: + return ScopeType.PROJECT + + @override + def scope_id(self) -> str: + return str(self._project_id) + + @override + def target_element(self) -> RBACElementRef: + return RBACElementRef(RBACElementType.PROJECT, str(self._project_id)) + @dataclass -class CreateModelServiceActionResult(BaseActionResult): +class CreateModelServiceActionResult(ModelServiceScopeActionResult): data: ServiceInfo + _project_id: uuid.UUID + + @override + def scope_type(self) -> ScopeType: + return ScopeType.PROJECT @override - def entity_id(self) -> str | None: - return str(self.data.endpoint_id) + def scope_id(self) -> str: + return str(self._project_id) diff --git a/src/ai/backend/manager/services/model_serving/actions/delete_model_service.py b/src/ai/backend/manager/services/model_serving/actions/delete_model_service.py index 4d34171a432..ec14b6b5d34 100644 --- a/src/ai/backend/manager/services/model_serving/actions/delete_model_service.py +++ b/src/ai/backend/manager/services/model_serving/actions/delete_model_service.py @@ -2,29 +2,37 @@ from dataclasses import dataclass from typing import override -from ai.backend.manager.actions.action import BaseActionResult +from ai.backend.common.data.permission.types import RBACElementType from ai.backend.manager.actions.types import ActionOperationType -from ai.backend.manager.services.model_serving.actions.base import ModelServiceAction +from ai.backend.manager.data.permission.types import RBACElementRef +from ai.backend.manager.services.model_serving.actions.base import ( + ModelServiceSingleEntityAction, + ModelServiceSingleEntityActionResult, +) @dataclass -class DeleteModelServiceAction(ModelServiceAction): +class DeleteModelServiceAction(ModelServiceSingleEntityAction): service_id: uuid.UUID - @override - def entity_id(self) -> str | None: - return None - @override @classmethod def operation_type(cls) -> ActionOperationType: return ActionOperationType.DELETE + @override + def target_entity_id(self) -> str: + return str(self.service_id) + + @override + def target_element(self) -> RBACElementRef: + return RBACElementRef(RBACElementType.MODEL_DEPLOYMENT, str(self.service_id)) + @dataclass -class DeleteModelServiceActionResult(BaseActionResult): - success: bool +class DeleteModelServiceActionResult(ModelServiceSingleEntityActionResult): + service_id: uuid.UUID @override - def entity_id(self) -> str | None: - return None + def target_entity_id(self) -> str: + return str(self.service_id) diff --git a/src/ai/backend/manager/services/model_serving/actions/delete_route.py b/src/ai/backend/manager/services/model_serving/actions/delete_route.py index 65e84bcd076..84cdd2d8f2d 100644 --- a/src/ai/backend/manager/services/model_serving/actions/delete_route.py +++ b/src/ai/backend/manager/services/model_serving/actions/delete_route.py @@ -2,14 +2,17 @@ from dataclasses import dataclass from typing import override -from ai.backend.common.data.permission.types import EntityType -from ai.backend.manager.actions.action import BaseActionResult +from ai.backend.common.data.permission.types import EntityType, RBACElementType from ai.backend.manager.actions.types import ActionOperationType -from ai.backend.manager.services.model_serving.actions.base import ModelServiceAction +from ai.backend.manager.data.permission.types import RBACElementRef +from ai.backend.manager.services.model_serving.actions.base import ( + ModelServiceSingleEntityAction, + ModelServiceSingleEntityActionResult, +) @dataclass -class DeleteRouteAction(ModelServiceAction): +class DeleteRouteAction(ModelServiceSingleEntityAction): service_id: uuid.UUID route_id: uuid.UUID @@ -18,20 +21,24 @@ class DeleteRouteAction(ModelServiceAction): def entity_type(cls) -> EntityType: return EntityType.DEPLOYMENT_ROUTE - @override - def entity_id(self) -> str | None: - return None - @override @classmethod def operation_type(cls) -> ActionOperationType: return ActionOperationType.DELETE + @override + def target_entity_id(self) -> str: + return str(self.route_id) + + @override + def target_element(self) -> RBACElementRef: + return RBACElementRef(RBACElementType.DEPLOYMENT_ROUTE, str(self.route_id)) + @dataclass -class DeleteRouteActionResult(BaseActionResult): - success: bool +class DeleteRouteActionResult(ModelServiceSingleEntityActionResult): + route_id: uuid.UUID @override - def entity_id(self) -> str | None: - return None + def target_entity_id(self) -> str: + return str(self.route_id) diff --git a/src/ai/backend/manager/services/model_serving/actions/get_model_service_info.py b/src/ai/backend/manager/services/model_serving/actions/get_model_service_info.py index 0ddca5393dd..b8c18b1d4ef 100644 --- a/src/ai/backend/manager/services/model_serving/actions/get_model_service_info.py +++ b/src/ai/backend/manager/services/model_serving/actions/get_model_service_info.py @@ -2,30 +2,38 @@ from dataclasses import dataclass from typing import override -from ai.backend.manager.actions.action import BaseActionResult +from ai.backend.common.data.permission.types import RBACElementType from ai.backend.manager.actions.types import ActionOperationType from ai.backend.manager.data.model_serving.types import ServiceInfo -from ai.backend.manager.services.model_serving.actions.base import ModelServiceAction +from ai.backend.manager.data.permission.types import RBACElementRef +from ai.backend.manager.services.model_serving.actions.base import ( + ModelServiceSingleEntityAction, + ModelServiceSingleEntityActionResult, +) @dataclass -class GetModelServiceInfoAction(ModelServiceAction): +class GetModelServiceInfoAction(ModelServiceSingleEntityAction): service_id: uuid.UUID - @override - def entity_id(self) -> str | None: - return None - @override @classmethod def operation_type(cls) -> ActionOperationType: return ActionOperationType.GET + @override + def target_entity_id(self) -> str: + return str(self.service_id) + + @override + def target_element(self) -> RBACElementRef: + return RBACElementRef(RBACElementType.MODEL_DEPLOYMENT, str(self.service_id)) + @dataclass -class GetModelServiceInfoActionResult(BaseActionResult): +class GetModelServiceInfoActionResult(ModelServiceSingleEntityActionResult): data: ServiceInfo @override - def entity_id(self) -> str | None: + def target_entity_id(self) -> str: return str(self.data.endpoint_id) diff --git a/src/ai/backend/manager/services/model_serving/actions/list_model_service.py b/src/ai/backend/manager/services/model_serving/actions/list_model_service.py index e9de3b44605..2ffa439ac43 100644 --- a/src/ai/backend/manager/services/model_serving/actions/list_model_service.py +++ b/src/ai/backend/manager/services/model_serving/actions/list_model_service.py @@ -2,31 +2,49 @@ from dataclasses import dataclass from typing import override -from ai.backend.manager.actions.action import BaseActionResult +from ai.backend.common.data.permission.types import RBACElementType, ScopeType from ai.backend.manager.actions.types import ActionOperationType from ai.backend.manager.data.model_serving.types import CompactServiceInfo -from ai.backend.manager.services.model_serving.actions.base import ModelServiceAction +from ai.backend.manager.data.permission.types import RBACElementRef +from ai.backend.manager.services.model_serving.actions.base import ( + ModelServiceScopeAction, + ModelServiceScopeActionResult, +) @dataclass -class ListModelServiceAction(ModelServiceAction): +class ListModelServiceAction(ModelServiceScopeAction): session_owener_id: uuid.UUID name: str | None - - @override - def entity_id(self) -> str | None: - return None + _project_id: uuid.UUID @override @classmethod def operation_type(cls) -> ActionOperationType: return ActionOperationType.SEARCH + @override + def scope_type(self) -> ScopeType: + return ScopeType.PROJECT + + @override + def scope_id(self) -> str: + return str(self._project_id) + + @override + def target_element(self) -> RBACElementRef: + return RBACElementRef(RBACElementType.PROJECT, str(self._project_id)) + @dataclass -class ListModelServiceActionResult(BaseActionResult): +class ListModelServiceActionResult(ModelServiceScopeActionResult): data: list[CompactServiceInfo] + _project_id: uuid.UUID + + @override + def scope_type(self) -> ScopeType: + return ScopeType.PROJECT @override - def entity_id(self) -> str | None: - return None + def scope_id(self) -> str: + return str(self._project_id) diff --git a/src/ai/backend/manager/services/model_serving/actions/modify_endpoint.py b/src/ai/backend/manager/services/model_serving/actions/modify_endpoint.py index 02669292fbd..30fa0da3d12 100644 --- a/src/ai/backend/manager/services/model_serving/actions/modify_endpoint.py +++ b/src/ai/backend/manager/services/model_serving/actions/modify_endpoint.py @@ -2,34 +2,42 @@ from dataclasses import dataclass from typing import override -from ai.backend.manager.actions.action import BaseActionResult +from ai.backend.common.data.permission.types import RBACElementType from ai.backend.manager.actions.types import ActionOperationType from ai.backend.manager.data.model_serving.types import EndpointData +from ai.backend.manager.data.permission.types import RBACElementRef from ai.backend.manager.models.endpoint import EndpointRow from ai.backend.manager.repositories.base.updater import Updater -from ai.backend.manager.services.model_serving.actions.base import ModelServiceAction +from ai.backend.manager.services.model_serving.actions.base import ( + ModelServiceSingleEntityAction, + ModelServiceSingleEntityActionResult, +) @dataclass -class ModifyEndpointAction(ModelServiceAction): +class ModifyEndpointAction(ModelServiceSingleEntityAction): endpoint_id: uuid.UUID updater: Updater[EndpointRow] - @override - def entity_id(self) -> str | None: - return None - @override @classmethod def operation_type(cls) -> ActionOperationType: return ActionOperationType.UPDATE + @override + def target_entity_id(self) -> str: + return str(self.endpoint_id) + + @override + def target_element(self) -> RBACElementRef: + return RBACElementRef(RBACElementType.MODEL_DEPLOYMENT, str(self.endpoint_id)) + @dataclass -class ModifyEndpointActionResult(BaseActionResult): +class ModifyEndpointActionResult(ModelServiceSingleEntityActionResult): success: bool data: EndpointData | None @override - def entity_id(self) -> str | None: + def target_entity_id(self) -> str: return str(self.data.id) if self.data is not None else None diff --git a/src/ai/backend/manager/services/model_serving/actions/search_services.py b/src/ai/backend/manager/services/model_serving/actions/search_services.py index f9b314bebd2..e4079c46acd 100644 --- a/src/ai/backend/manager/services/model_serving/actions/search_services.py +++ b/src/ai/backend/manager/services/model_serving/actions/search_services.py @@ -2,37 +2,55 @@ from dataclasses import dataclass, field from typing import override -from ai.backend.manager.actions.action import BaseActionResult +from ai.backend.common.data.permission.types import RBACElementType, ScopeType from ai.backend.manager.actions.types import ActionOperationType from ai.backend.manager.data.model_serving.types import ServiceSearchItem +from ai.backend.manager.data.permission.types import RBACElementRef from ai.backend.manager.repositories.base.types import QueryCondition -from ai.backend.manager.services.model_serving.actions.base import ModelServiceAction +from ai.backend.manager.services.model_serving.actions.base import ( + ModelServiceScopeAction, + ModelServiceScopeActionResult, +) @dataclass -class SearchServicesAction(ModelServiceAction): +class SearchServicesAction(ModelServiceScopeAction): session_owner_id: uuid.UUID + _project_id: uuid.UUID conditions: list[QueryCondition] = field(default_factory=list) offset: int = 0 limit: int = 20 - @override - def entity_id(self) -> str | None: - return None - @override @classmethod def operation_type(cls) -> ActionOperationType: return ActionOperationType.SEARCH + @override + def scope_type(self) -> ScopeType: + return ScopeType.PROJECT + + @override + def scope_id(self) -> str: + return str(self._project_id) + + @override + def target_element(self) -> RBACElementRef: + return RBACElementRef(RBACElementType.PROJECT, str(self._project_id)) + @dataclass -class SearchServicesActionResult(BaseActionResult): +class SearchServicesActionResult(ModelServiceScopeActionResult): items: list[ServiceSearchItem] total_count: int offset: int limit: int + _project_id: uuid.UUID + + @override + def scope_type(self) -> ScopeType: + return ScopeType.PROJECT @override - def entity_id(self) -> str | None: - return None + def scope_id(self) -> str: + return str(self._project_id) diff --git a/src/ai/backend/manager/services/model_serving/actions/update_route.py b/src/ai/backend/manager/services/model_serving/actions/update_route.py index de19dbc08f4..e6a6417691f 100644 --- a/src/ai/backend/manager/services/model_serving/actions/update_route.py +++ b/src/ai/backend/manager/services/model_serving/actions/update_route.py @@ -2,14 +2,17 @@ from dataclasses import dataclass from typing import override -from ai.backend.common.data.permission.types import EntityType -from ai.backend.manager.actions.action import BaseActionResult +from ai.backend.common.data.permission.types import EntityType, RBACElementType from ai.backend.manager.actions.types import ActionOperationType -from ai.backend.manager.services.model_serving.actions.base import ModelServiceAction +from ai.backend.manager.data.permission.types import RBACElementRef +from ai.backend.manager.services.model_serving.actions.base import ( + ModelServiceSingleEntityAction, + ModelServiceSingleEntityActionResult, +) @dataclass -class UpdateRouteAction(ModelServiceAction): +class UpdateRouteAction(ModelServiceSingleEntityAction): service_id: uuid.UUID route_id: uuid.UUID traffic_ratio: float @@ -19,20 +22,24 @@ class UpdateRouteAction(ModelServiceAction): def entity_type(cls) -> EntityType: return EntityType.DEPLOYMENT_ROUTE - @override - def entity_id(self) -> str | None: - return None - @override @classmethod def operation_type(cls) -> ActionOperationType: return ActionOperationType.UPDATE + @override + def target_entity_id(self) -> str: + return str(self.route_id) + + @override + def target_element(self) -> RBACElementRef: + return RBACElementRef(RBACElementType.DEPLOYMENT_ROUTE, str(self.route_id)) + @dataclass -class UpdateRouteActionResult(BaseActionResult): - success: bool +class UpdateRouteActionResult(ModelServiceSingleEntityActionResult): + route_id: uuid.UUID @override - def entity_id(self) -> str | None: - return None + def target_entity_id(self) -> str: + return str(self.route_id) From 31383361d4814bf4c9b082629010971259df57d3 Mon Sep 17 00:00:00 2001 From: Sanghun Lee Date: Fri, 13 Mar 2026 10:58:58 +0900 Subject: [PATCH 03/11] feat(BA-3692): Wire RBAC validators to model serving processors MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Update model_serving processors to use specialized processor types with RBAC validation: Scope actions (with RBAC): - create_model_service → ScopeActionProcessor with scope validator - list_model_service → ScopeActionProcessor with scope validator - search_services → ScopeActionProcessor with scope validator Single entity actions (with RBAC): - get_model_service_info → SingleEntityActionProcessor with single_entity validator - delete_model_service → SingleEntityActionProcessor with single_entity validator - modify_endpoint → SingleEntityActionProcessor with single_entity validator - update_route → SingleEntityActionProcessor with single_entity validator - delete_route → SingleEntityActionProcessor with single_entity validator Internal/system actions (no RBAC): - dry_run_model_service, list_errors, clear_error, force_sync, generate_token, validate_model_service → plain ActionProcessor Co-Authored-By: Claude Sonnet 4.5 --- .../model_serving/processors/model_serving.py | 70 ++++++++++++++----- 1 file changed, 51 insertions(+), 19 deletions(-) diff --git a/src/ai/backend/manager/services/model_serving/processors/model_serving.py b/src/ai/backend/manager/services/model_serving/processors/model_serving.py index 1a382e7ff8f..f8d336b1b99 100644 --- a/src/ai/backend/manager/services/model_serving/processors/model_serving.py +++ b/src/ai/backend/manager/services/model_serving/processors/model_serving.py @@ -2,6 +2,8 @@ from ai.backend.manager.actions.monitors.monitor import ActionMonitor from ai.backend.manager.actions.processor import ActionProcessor +from ai.backend.manager.actions.processor.scope import ScopeActionProcessor +from ai.backend.manager.actions.processor.single_entity import SingleEntityActionProcessor from ai.backend.manager.actions.types import AbstractProcessorPackage, ActionSpec from ai.backend.manager.actions.validators import ActionValidators from ai.backend.manager.services.model_serving.actions.clear_error import ( @@ -66,21 +68,30 @@ class ModelServingProcessors(AbstractProcessorPackage): - create_model_service: ActionProcessor[CreateModelServiceAction, CreateModelServiceActionResult] - list_model_service: ActionProcessor[ListModelServiceAction, ListModelServiceActionResult] - delete_model_service: ActionProcessor[DeleteModelServiceAction, DeleteModelServiceActionResult] - dry_run_model_service: ActionProcessor[DryRunModelServiceAction, DryRunModelServiceActionResult] - get_model_service_info: ActionProcessor[ + # Scope actions (with RBAC) + create_model_service: ScopeActionProcessor[ + CreateModelServiceAction, CreateModelServiceActionResult + ] + list_model_service: ScopeActionProcessor[ListModelServiceAction, ListModelServiceActionResult] + search_services: ScopeActionProcessor[SearchServicesAction, SearchServicesActionResult] + + # Single entity actions (with RBAC) + get_model_service_info: SingleEntityActionProcessor[ GetModelServiceInfoAction, GetModelServiceInfoActionResult ] + delete_model_service: SingleEntityActionProcessor[ + DeleteModelServiceAction, DeleteModelServiceActionResult + ] + modify_endpoint: SingleEntityActionProcessor[ModifyEndpointAction, ModifyEndpointActionResult] + update_route: SingleEntityActionProcessor[UpdateRouteAction, UpdateRouteActionResult] + delete_route: SingleEntityActionProcessor[DeleteRouteAction, DeleteRouteActionResult] + + # Internal/system actions (no RBAC) + dry_run_model_service: ActionProcessor[DryRunModelServiceAction, DryRunModelServiceActionResult] list_errors: ActionProcessor[ListErrorsAction, ListErrorsActionResult] clear_error: ActionProcessor[ClearErrorAction, ClearErrorActionResult] force_sync: ActionProcessor[ForceSyncAction, ForceSyncActionResult] - update_route: ActionProcessor[UpdateRouteAction, UpdateRouteActionResult] - delete_route: ActionProcessor[DeleteRouteAction, DeleteRouteActionResult] generate_token: ActionProcessor[GenerateTokenAction, GenerateTokenActionResult] - modify_endpoint: ActionProcessor[ModifyEndpointAction, ModifyEndpointActionResult] - search_services: ActionProcessor[SearchServicesAction, SearchServicesActionResult] validate_model_service: ActionProcessor[ ValidateModelServiceAction, ValidateModelServiceActionResult ] @@ -91,21 +102,42 @@ def __init__( action_monitors: list[ActionMonitor], validators: ActionValidators, ) -> None: - self.create_model_service = ActionProcessor(service.create, action_monitors) - self.list_model_service = ActionProcessor(service.list_serve, action_monitors) - self.delete_model_service = ActionProcessor(service.delete, action_monitors) - self.dry_run_model_service = ActionProcessor(service.dry_run, action_monitors) - self.get_model_service_info = ActionProcessor( - service.get_model_service_info, action_monitors + # Scope actions with RBAC validator + self.create_model_service = ScopeActionProcessor( + service.create, action_monitors, validators=[validators.rbac.scope] + ) + self.list_model_service = ScopeActionProcessor( + service.list_serve, action_monitors, validators=[validators.rbac.scope] + ) + self.search_services = ScopeActionProcessor( + service.search_services, action_monitors, validators=[validators.rbac.scope] ) + + # Single entity actions with RBAC validator + self.get_model_service_info = SingleEntityActionProcessor( + service.get_model_service_info, + action_monitors, + validators=[validators.rbac.single_entity], + ) + self.delete_model_service = SingleEntityActionProcessor( + service.delete, action_monitors, validators=[validators.rbac.single_entity] + ) + self.modify_endpoint = SingleEntityActionProcessor( + service.modify_endpoint, action_monitors, validators=[validators.rbac.single_entity] + ) + self.update_route = SingleEntityActionProcessor( + service.update_route, action_monitors, validators=[validators.rbac.single_entity] + ) + self.delete_route = SingleEntityActionProcessor( + service.delete_route, action_monitors, validators=[validators.rbac.single_entity] + ) + + # Internal/system actions without RBAC + self.dry_run_model_service = ActionProcessor(service.dry_run, action_monitors) self.list_errors = ActionProcessor(service.list_errors, action_monitors) self.clear_error = ActionProcessor(service.clear_error, action_monitors) self.force_sync = ActionProcessor(service.force_sync_with_app_proxy, action_monitors) - self.update_route = ActionProcessor(service.update_route, action_monitors) - self.delete_route = ActionProcessor(service.delete_route, action_monitors) self.generate_token = ActionProcessor(service.generate_token, action_monitors) - self.modify_endpoint = ActionProcessor(service.modify_endpoint, action_monitors) - self.search_services = ActionProcessor(service.search_services, action_monitors) self.validate_model_service = ActionProcessor( service.validate_model_service, action_monitors ) From 31bc4bb06aa4a50d9ae5c3b3c9fbc59787a04bfb Mon Sep 17 00:00:00 2001 From: Sanghun Lee Date: Fri, 13 Mar 2026 11:06:02 +0900 Subject: [PATCH 04/11] fix(BA-3692): Fix action result signatures and entity type references - Add _project_id parameter to scope action results (Create, List, Search) - Fix single entity action results to use entity ID instead of success flag - Update modify_endpoint to include endpoint_id in result - Use EntityType.MODEL_DEPLOYMENT instead of non-existent DEPLOYMENT_ROUTE - Use RBACElementType.MODEL_DEPLOYMENT with service_id for route operations All mypy errors in model_serving/* are now resolved. Co-Authored-By: Claude Sonnet 4.5 --- .../model_serving/actions/delete_route.py | 4 ++-- .../model_serving/actions/modify_endpoint.py | 3 ++- .../model_serving/actions/update_route.py | 4 ++-- .../model_serving/services/model_serving.py | 19 ++++++++++++------- 4 files changed, 18 insertions(+), 12 deletions(-) diff --git a/src/ai/backend/manager/services/model_serving/actions/delete_route.py b/src/ai/backend/manager/services/model_serving/actions/delete_route.py index 84cdd2d8f2d..0f3bc696d0b 100644 --- a/src/ai/backend/manager/services/model_serving/actions/delete_route.py +++ b/src/ai/backend/manager/services/model_serving/actions/delete_route.py @@ -19,7 +19,7 @@ class DeleteRouteAction(ModelServiceSingleEntityAction): @override @classmethod def entity_type(cls) -> EntityType: - return EntityType.DEPLOYMENT_ROUTE + return EntityType.MODEL_DEPLOYMENT @override @classmethod @@ -32,7 +32,7 @@ def target_entity_id(self) -> str: @override def target_element(self) -> RBACElementRef: - return RBACElementRef(RBACElementType.DEPLOYMENT_ROUTE, str(self.route_id)) + return RBACElementRef(RBACElementType.MODEL_DEPLOYMENT, str(self.service_id)) @dataclass diff --git a/src/ai/backend/manager/services/model_serving/actions/modify_endpoint.py b/src/ai/backend/manager/services/model_serving/actions/modify_endpoint.py index 30fa0da3d12..cb66341a95a 100644 --- a/src/ai/backend/manager/services/model_serving/actions/modify_endpoint.py +++ b/src/ai/backend/manager/services/model_serving/actions/modify_endpoint.py @@ -35,9 +35,10 @@ def target_element(self) -> RBACElementRef: @dataclass class ModifyEndpointActionResult(ModelServiceSingleEntityActionResult): + endpoint_id: uuid.UUID success: bool data: EndpointData | None @override def target_entity_id(self) -> str: - return str(self.data.id) if self.data is not None else None + return str(self.endpoint_id) diff --git a/src/ai/backend/manager/services/model_serving/actions/update_route.py b/src/ai/backend/manager/services/model_serving/actions/update_route.py index e6a6417691f..cde4d864304 100644 --- a/src/ai/backend/manager/services/model_serving/actions/update_route.py +++ b/src/ai/backend/manager/services/model_serving/actions/update_route.py @@ -20,7 +20,7 @@ class UpdateRouteAction(ModelServiceSingleEntityAction): @override @classmethod def entity_type(cls) -> EntityType: - return EntityType.DEPLOYMENT_ROUTE + return EntityType.MODEL_DEPLOYMENT @override @classmethod @@ -33,7 +33,7 @@ def target_entity_id(self) -> str: @override def target_element(self) -> RBACElementRef: - return RBACElementRef(RBACElementType.DEPLOYMENT_ROUTE, str(self.route_id)) + return RBACElementRef(RBACElementType.MODEL_DEPLOYMENT, str(self.service_id)) @dataclass diff --git a/src/ai/backend/manager/services/model_serving/services/model_serving.py b/src/ai/backend/manager/services/model_serving/services/model_serving.py index 6806cae025d..dbcefd0051a 100644 --- a/src/ai/backend/manager/services/model_serving/services/model_serving.py +++ b/src/ai/backend/manager/services/model_serving/services/model_serving.py @@ -377,7 +377,7 @@ async def create(self, action: CreateModelServiceAction) -> CreateModelServiceAc endpoint_id = endpoint_data.id return CreateModelServiceActionResult( - ServiceInfo( + data=ServiceInfo( endpoint_id=endpoint_id, model_id=endpoint_spec.model, extra_mounts=[m.vfid.folder_id for m in endpoint_spec.extra_mounts], @@ -389,7 +389,8 @@ async def create(self, action: CreateModelServiceAction) -> CreateModelServiceAc service_endpoint=None, is_public=action.creator.open_to_public, runtime_variant=action.creator.runtime_variant, - ) + ), + _project_id=action._project_id, ) async def list_serve(self, action: ListModelServiceAction) -> ListModelServiceActionResult: @@ -413,7 +414,8 @@ async def list_serve(self, action: ListModelServiceAction) -> ListModelServiceAc is_public=endpoint.open_to_public, ) for endpoint in endpoints - ] + ], + _project_id=action._project_id, ) async def search_services(self, action: SearchServicesAction) -> SearchServicesActionResult: @@ -427,6 +429,7 @@ async def search_services(self, action: SearchServicesAction) -> SearchServicesA total_count=result.total_count, offset=action.offset, limit=action.limit, + _project_id=action._project_id, ) async def check_user_access(self) -> None: @@ -462,7 +465,7 @@ async def delete(self, action: DeleteModelServiceAction) -> DeleteModelServiceAc # Update endpoint lifecycle await self._repository.update_endpoint_lifecycle(service_id, lifecycle_stage, replicas) - return DeleteModelServiceActionResult(success=True) + return DeleteModelServiceActionResult(service_id=service_id) async def dry_run(self, action: DryRunModelServiceAction) -> DryRunModelServiceActionResult: # TODO: Seperate background task definition and trigger into different layer @@ -746,7 +749,7 @@ async def update_route(self, action: UpdateRouteAction) -> UpdateRouteActionResu updated_endpoint_data.id ) - return UpdateRouteActionResult(success=True) + return UpdateRouteActionResult(route_id=action.route_id) async def delete_route(self, action: DeleteRouteAction) -> DeleteRouteActionResult: # Validate access @@ -783,7 +786,7 @@ async def delete_route(self, action: DeleteRouteAction) -> DeleteRouteActionResu # Decrease endpoint replicas await self._repository.decrease_endpoint_replicas(action.service_id) - return DeleteRouteActionResult(success=True) + return DeleteRouteActionResult(route_id=action.route_id) async def generate_token(self, action: GenerateTokenAction) -> GenerateTokenActionResult: # Validate access @@ -886,7 +889,9 @@ async def modify_endpoint(self, action: ModifyEndpointAction) -> ModifyEndpointA await self._deployment_controller.mark_lifecycle_needed( DeploymentLifecycleType.CHECK_REPLICA, ) - return ModifyEndpointActionResult(success=result.success, data=result.data) + return ModifyEndpointActionResult( + endpoint_id=action.endpoint_id, success=result.success, data=result.data + ) async def validate_model_service( self, action: ValidateModelServiceAction From 4624c1f56513b8dc9a34d4511c4e42d41148a362 Mon Sep 17 00:00:00 2001 From: Sanghun Lee Date: Fri, 13 Mar 2026 11:07:45 +0900 Subject: [PATCH 05/11] changelog: add news fragment for PR #10033 --- changes/10033.feature.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 changes/10033.feature.md diff --git a/changes/10033.feature.md b/changes/10033.feature.md new file mode 100644 index 00000000000..f6094860920 --- /dev/null +++ b/changes/10033.feature.md @@ -0,0 +1 @@ +Apply RBAC permission validators to model deployment service actions From 635a3e94b6ff4f8870802662847b74730a8fe4e4 Mon Sep 17 00:00:00 2001 From: Sanghun Lee Date: Fri, 13 Mar 2026 12:50:48 +0900 Subject: [PATCH 06/11] fix(BA-3692): Fix action signatures and scope types for list/search operations - Changed ListModelServiceAction and SearchServicesAction to use USER scope instead of PROJECT scope - Removed _project_id field and replaced with _user_id in action results - Fixed handler to not access non-existent .success attribute on action results - Removed unused result variables in update_route and delete_route handlers These changes fix the mypy errors in PR #10033: 1. Missing positional argument "_project_id" - fixed by removing it and using USER scope 2. ActionResult has no attribute "success" - fixed by using SuccessResponseModel(success=True) directly Co-Authored-By: Claude Sonnet 4.5 --- src/ai/backend/manager/api/rest/service/handler.py | 8 ++++---- .../model_serving/actions/list_model_service.py | 13 ++++++------- .../model_serving/actions/search_services.py | 13 ++++++------- .../model_serving/services/model_serving.py | 4 ++-- 4 files changed, 18 insertions(+), 20 deletions(-) diff --git a/src/ai/backend/manager/api/rest/service/handler.py b/src/ai/backend/manager/api/rest/service/handler.py index d60bb354a25..db45174dfb6 100644 --- a/src/ai/backend/manager/api/rest/service/handler.py +++ b/src/ai/backend/manager/api/rest/service/handler.py @@ -469,9 +469,9 @@ async def update_route( traffic_ratio=params.traffic_ratio, ) - result = await self._model_serving.update_route.wait_for_complete(action) + await self._model_serving.update_route.wait_for_complete(action) - resp = SuccessResponseModel(success=result.success) + resp = SuccessResponseModel(success=True) return APIResponse.build(HTTPStatus.OK, resp) # ------------------------------------------------------------------ @@ -494,9 +494,9 @@ async def delete_route( route_id=path_params.route_id, ) - result = await self._model_serving.delete_route.wait_for_complete(action) + await self._model_serving.delete_route.wait_for_complete(action) - resp = SuccessResponseModel(success=result.success) + resp = SuccessResponseModel(success=True) return APIResponse.build(HTTPStatus.OK, resp) # ------------------------------------------------------------------ diff --git a/src/ai/backend/manager/services/model_serving/actions/list_model_service.py b/src/ai/backend/manager/services/model_serving/actions/list_model_service.py index 2ffa439ac43..af528273fa6 100644 --- a/src/ai/backend/manager/services/model_serving/actions/list_model_service.py +++ b/src/ai/backend/manager/services/model_serving/actions/list_model_service.py @@ -16,7 +16,6 @@ class ListModelServiceAction(ModelServiceScopeAction): session_owener_id: uuid.UUID name: str | None - _project_id: uuid.UUID @override @classmethod @@ -25,26 +24,26 @@ def operation_type(cls) -> ActionOperationType: @override def scope_type(self) -> ScopeType: - return ScopeType.PROJECT + return ScopeType.USER @override def scope_id(self) -> str: - return str(self._project_id) + return str(self.session_owener_id) @override def target_element(self) -> RBACElementRef: - return RBACElementRef(RBACElementType.PROJECT, str(self._project_id)) + return RBACElementRef(RBACElementType.USER, str(self.session_owener_id)) @dataclass class ListModelServiceActionResult(ModelServiceScopeActionResult): data: list[CompactServiceInfo] - _project_id: uuid.UUID + _user_id: uuid.UUID @override def scope_type(self) -> ScopeType: - return ScopeType.PROJECT + return ScopeType.USER @override def scope_id(self) -> str: - return str(self._project_id) + return str(self._user_id) diff --git a/src/ai/backend/manager/services/model_serving/actions/search_services.py b/src/ai/backend/manager/services/model_serving/actions/search_services.py index e4079c46acd..6612d4cd4e1 100644 --- a/src/ai/backend/manager/services/model_serving/actions/search_services.py +++ b/src/ai/backend/manager/services/model_serving/actions/search_services.py @@ -16,7 +16,6 @@ @dataclass class SearchServicesAction(ModelServiceScopeAction): session_owner_id: uuid.UUID - _project_id: uuid.UUID conditions: list[QueryCondition] = field(default_factory=list) offset: int = 0 limit: int = 20 @@ -28,15 +27,15 @@ def operation_type(cls) -> ActionOperationType: @override def scope_type(self) -> ScopeType: - return ScopeType.PROJECT + return ScopeType.USER @override def scope_id(self) -> str: - return str(self._project_id) + return str(self.session_owner_id) @override def target_element(self) -> RBACElementRef: - return RBACElementRef(RBACElementType.PROJECT, str(self._project_id)) + return RBACElementRef(RBACElementType.USER, str(self.session_owner_id)) @dataclass @@ -45,12 +44,12 @@ class SearchServicesActionResult(ModelServiceScopeActionResult): total_count: int offset: int limit: int - _project_id: uuid.UUID + _user_id: uuid.UUID @override def scope_type(self) -> ScopeType: - return ScopeType.PROJECT + return ScopeType.USER @override def scope_id(self) -> str: - return str(self._project_id) + return str(self._user_id) diff --git a/src/ai/backend/manager/services/model_serving/services/model_serving.py b/src/ai/backend/manager/services/model_serving/services/model_serving.py index dbcefd0051a..500f4bae9e0 100644 --- a/src/ai/backend/manager/services/model_serving/services/model_serving.py +++ b/src/ai/backend/manager/services/model_serving/services/model_serving.py @@ -415,7 +415,7 @@ async def list_serve(self, action: ListModelServiceAction) -> ListModelServiceAc ) for endpoint in endpoints ], - _project_id=action._project_id, + _user_id=action.session_owener_id, ) async def search_services(self, action: SearchServicesAction) -> SearchServicesActionResult: @@ -429,7 +429,7 @@ async def search_services(self, action: SearchServicesAction) -> SearchServicesA total_count=result.total_count, offset=action.offset, limit=action.limit, - _project_id=action._project_id, + _user_id=action.session_owner_id, ) async def check_user_access(self) -> None: From 73f9b6afbcb5504eed88227f9a46eb2a0b8de7ef Mon Sep 17 00:00:00 2001 From: Sanghun Lee Date: Fri, 13 Mar 2026 20:41:59 +0900 Subject: [PATCH 07/11] refactor: exclude search actions from RBAC validator application Search actions are excluded from RBAC validator scope per BA-2946. Search results are already filtered by scope through the existing SearchScope mechanism. Co-Authored-By: Claude Opus 4.6 --- .../model_serving/actions/search_services.py | 37 +++++-------------- .../model_serving/processors/model_serving.py | 6 +-- .../model_serving/services/model_serving.py | 1 - 3 files changed, 12 insertions(+), 32 deletions(-) diff --git a/src/ai/backend/manager/services/model_serving/actions/search_services.py b/src/ai/backend/manager/services/model_serving/actions/search_services.py index 6612d4cd4e1..f9b314bebd2 100644 --- a/src/ai/backend/manager/services/model_serving/actions/search_services.py +++ b/src/ai/backend/manager/services/model_serving/actions/search_services.py @@ -2,54 +2,37 @@ from dataclasses import dataclass, field from typing import override -from ai.backend.common.data.permission.types import RBACElementType, ScopeType +from ai.backend.manager.actions.action import BaseActionResult from ai.backend.manager.actions.types import ActionOperationType from ai.backend.manager.data.model_serving.types import ServiceSearchItem -from ai.backend.manager.data.permission.types import RBACElementRef from ai.backend.manager.repositories.base.types import QueryCondition -from ai.backend.manager.services.model_serving.actions.base import ( - ModelServiceScopeAction, - ModelServiceScopeActionResult, -) +from ai.backend.manager.services.model_serving.actions.base import ModelServiceAction @dataclass -class SearchServicesAction(ModelServiceScopeAction): +class SearchServicesAction(ModelServiceAction): session_owner_id: uuid.UUID conditions: list[QueryCondition] = field(default_factory=list) offset: int = 0 limit: int = 20 + @override + def entity_id(self) -> str | None: + return None + @override @classmethod def operation_type(cls) -> ActionOperationType: return ActionOperationType.SEARCH - @override - def scope_type(self) -> ScopeType: - return ScopeType.USER - - @override - def scope_id(self) -> str: - return str(self.session_owner_id) - - @override - def target_element(self) -> RBACElementRef: - return RBACElementRef(RBACElementType.USER, str(self.session_owner_id)) - @dataclass -class SearchServicesActionResult(ModelServiceScopeActionResult): +class SearchServicesActionResult(BaseActionResult): items: list[ServiceSearchItem] total_count: int offset: int limit: int - _user_id: uuid.UUID - - @override - def scope_type(self) -> ScopeType: - return ScopeType.USER @override - def scope_id(self) -> str: - return str(self._user_id) + def entity_id(self) -> str | None: + return None diff --git a/src/ai/backend/manager/services/model_serving/processors/model_serving.py b/src/ai/backend/manager/services/model_serving/processors/model_serving.py index f8d336b1b99..58fc34aa59c 100644 --- a/src/ai/backend/manager/services/model_serving/processors/model_serving.py +++ b/src/ai/backend/manager/services/model_serving/processors/model_serving.py @@ -73,7 +73,7 @@ class ModelServingProcessors(AbstractProcessorPackage): CreateModelServiceAction, CreateModelServiceActionResult ] list_model_service: ScopeActionProcessor[ListModelServiceAction, ListModelServiceActionResult] - search_services: ScopeActionProcessor[SearchServicesAction, SearchServicesActionResult] + search_services: ActionProcessor[SearchServicesAction, SearchServicesActionResult] # Single entity actions (with RBAC) get_model_service_info: SingleEntityActionProcessor[ @@ -109,9 +109,7 @@ def __init__( self.list_model_service = ScopeActionProcessor( service.list_serve, action_monitors, validators=[validators.rbac.scope] ) - self.search_services = ScopeActionProcessor( - service.search_services, action_monitors, validators=[validators.rbac.scope] - ) + self.search_services = ActionProcessor(service.search_services, action_monitors) # Single entity actions with RBAC validator self.get_model_service_info = SingleEntityActionProcessor( diff --git a/src/ai/backend/manager/services/model_serving/services/model_serving.py b/src/ai/backend/manager/services/model_serving/services/model_serving.py index 500f4bae9e0..e063477a380 100644 --- a/src/ai/backend/manager/services/model_serving/services/model_serving.py +++ b/src/ai/backend/manager/services/model_serving/services/model_serving.py @@ -429,7 +429,6 @@ async def search_services(self, action: SearchServicesAction) -> SearchServicesA total_count=result.total_count, offset=action.offset, limit=action.limit, - _user_id=action.session_owner_id, ) async def check_user_access(self) -> None: From b61bc615e8be9a2efce10de37decb7bd51683382 Mon Sep 17 00:00:00 2001 From: Sanghun Lee Date: Sat, 14 Mar 2026 02:32:31 +0900 Subject: [PATCH 08/11] fix(test): use real ActionValidators instance in model serving processor fixtures Co-Authored-By: Claude Opus 4.6 --- tests/component/model_serving/conftest.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tests/component/model_serving/conftest.py b/tests/component/model_serving/conftest.py index f1b54ad93cd..5190b03e7d5 100644 --- a/tests/component/model_serving/conftest.py +++ b/tests/component/model_serving/conftest.py @@ -7,6 +7,7 @@ from ai.backend.common.bgtask.bgtask import BackgroundTaskManager from ai.backend.common.events.hub.hub import EventHub from ai.backend.manager.actions.validators import ActionValidators +from ai.backend.manager.actions.validators.rbac import RBACValidators from ai.backend.manager.api.rest.routing import RouteRegistry from ai.backend.manager.api.rest.service.handler import ServiceHandler from ai.backend.manager.api.rest.service.registry import register_service_routes @@ -68,7 +69,9 @@ def model_serving_processors( revision_generator_registry=revision_gen, ) return ModelServingProcessors( - service=service, action_monitors=[], validators=MagicMock(spec=ActionValidators) + service=service, + action_monitors=[], + validators=ActionValidators(rbac=MagicMock(spec=RBACValidators)), ) From 4c51fae1e810211ebc026a355e0037c276ca58b6 Mon Sep 17 00:00:00 2001 From: Sanghun Lee Date: Sat, 14 Mar 2026 03:56:06 +0900 Subject: [PATCH 09/11] revert(BA-3692): Restore ListModelServiceAction to main version ListModelServiceAction does not need RBAC scope migration as it is a search action excluded from RBAC validator application. Co-Authored-By: Claude Opus 4.6 --- .../actions/list_model_service.py | 37 +++++-------------- 1 file changed, 10 insertions(+), 27 deletions(-) diff --git a/src/ai/backend/manager/services/model_serving/actions/list_model_service.py b/src/ai/backend/manager/services/model_serving/actions/list_model_service.py index af528273fa6..e9de3b44605 100644 --- a/src/ai/backend/manager/services/model_serving/actions/list_model_service.py +++ b/src/ai/backend/manager/services/model_serving/actions/list_model_service.py @@ -2,48 +2,31 @@ from dataclasses import dataclass from typing import override -from ai.backend.common.data.permission.types import RBACElementType, ScopeType +from ai.backend.manager.actions.action import BaseActionResult from ai.backend.manager.actions.types import ActionOperationType from ai.backend.manager.data.model_serving.types import CompactServiceInfo -from ai.backend.manager.data.permission.types import RBACElementRef -from ai.backend.manager.services.model_serving.actions.base import ( - ModelServiceScopeAction, - ModelServiceScopeActionResult, -) +from ai.backend.manager.services.model_serving.actions.base import ModelServiceAction @dataclass -class ListModelServiceAction(ModelServiceScopeAction): +class ListModelServiceAction(ModelServiceAction): session_owener_id: uuid.UUID name: str | None + @override + def entity_id(self) -> str | None: + return None + @override @classmethod def operation_type(cls) -> ActionOperationType: return ActionOperationType.SEARCH - @override - def scope_type(self) -> ScopeType: - return ScopeType.USER - - @override - def scope_id(self) -> str: - return str(self.session_owener_id) - - @override - def target_element(self) -> RBACElementRef: - return RBACElementRef(RBACElementType.USER, str(self.session_owener_id)) - @dataclass -class ListModelServiceActionResult(ModelServiceScopeActionResult): +class ListModelServiceActionResult(BaseActionResult): data: list[CompactServiceInfo] - _user_id: uuid.UUID - - @override - def scope_type(self) -> ScopeType: - return ScopeType.USER @override - def scope_id(self) -> str: - return str(self._user_id) + def entity_id(self) -> str | None: + return None From bb80cdaea5442d83a80efe3a748814f2f8bacd42 Mon Sep 17 00:00:00 2001 From: Sanghun Lee Date: Sat, 14 Mar 2026 04:33:49 +0900 Subject: [PATCH 10/11] fix(BA-3692): Exclude list action from RBAC validator and fix test conftest - Revert list_model_service from ScopeActionProcessor to ActionProcessor (no RBAC) - Remove invalid _user_id kwarg from ListModelServiceActionResult - Use real RBACValidators instance in conftest instead of MagicMock(spec=...) Co-Authored-By: Claude Opus 4.6 --- .../model_serving/processors/model_serving.py | 6 ++---- .../services/model_serving/services/model_serving.py | 1 - tests/component/model_serving/conftest.py | 11 ++++++++++- 3 files changed, 12 insertions(+), 6 deletions(-) diff --git a/src/ai/backend/manager/services/model_serving/processors/model_serving.py b/src/ai/backend/manager/services/model_serving/processors/model_serving.py index 58fc34aa59c..b312643e990 100644 --- a/src/ai/backend/manager/services/model_serving/processors/model_serving.py +++ b/src/ai/backend/manager/services/model_serving/processors/model_serving.py @@ -72,7 +72,7 @@ class ModelServingProcessors(AbstractProcessorPackage): create_model_service: ScopeActionProcessor[ CreateModelServiceAction, CreateModelServiceActionResult ] - list_model_service: ScopeActionProcessor[ListModelServiceAction, ListModelServiceActionResult] + list_model_service: ActionProcessor[ListModelServiceAction, ListModelServiceActionResult] search_services: ActionProcessor[SearchServicesAction, SearchServicesActionResult] # Single entity actions (with RBAC) @@ -106,9 +106,7 @@ def __init__( self.create_model_service = ScopeActionProcessor( service.create, action_monitors, validators=[validators.rbac.scope] ) - self.list_model_service = ScopeActionProcessor( - service.list_serve, action_monitors, validators=[validators.rbac.scope] - ) + self.list_model_service = ActionProcessor(service.list_serve, action_monitors) self.search_services = ActionProcessor(service.search_services, action_monitors) # Single entity actions with RBAC validator diff --git a/src/ai/backend/manager/services/model_serving/services/model_serving.py b/src/ai/backend/manager/services/model_serving/services/model_serving.py index e063477a380..d1c8e3be1d2 100644 --- a/src/ai/backend/manager/services/model_serving/services/model_serving.py +++ b/src/ai/backend/manager/services/model_serving/services/model_serving.py @@ -415,7 +415,6 @@ async def list_serve(self, action: ListModelServiceAction) -> ListModelServiceAc ) for endpoint in endpoints ], - _user_id=action.session_owener_id, ) async def search_services(self, action: SearchServicesAction) -> SearchServicesActionResult: diff --git a/tests/component/model_serving/conftest.py b/tests/component/model_serving/conftest.py index 5190b03e7d5..a04c5369058 100644 --- a/tests/component/model_serving/conftest.py +++ b/tests/component/model_serving/conftest.py @@ -8,6 +8,10 @@ from ai.backend.common.events.hub.hub import EventHub from ai.backend.manager.actions.validators import ActionValidators from ai.backend.manager.actions.validators.rbac import RBACValidators +from ai.backend.manager.actions.validators.rbac.scope import ScopeActionRBACValidator +from ai.backend.manager.actions.validators.rbac.single_entity import ( + SingleEntityActionRBACValidator, +) from ai.backend.manager.api.rest.routing import RouteRegistry from ai.backend.manager.api.rest.service.handler import ServiceHandler from ai.backend.manager.api.rest.service.registry import register_service_routes @@ -71,7 +75,12 @@ def model_serving_processors( return ModelServingProcessors( service=service, action_monitors=[], - validators=ActionValidators(rbac=MagicMock(spec=RBACValidators)), + validators=ActionValidators( + rbac=RBACValidators( + scope=MagicMock(spec=ScopeActionRBACValidator), + single_entity=MagicMock(spec=SingleEntityActionRBACValidator), + ), + ), ) From e069fc7c41c07f581b305914d0c11a0acffc223b Mon Sep 17 00:00:00 2001 From: Sanghun Lee Date: Sat, 14 Mar 2026 04:53:26 +0900 Subject: [PATCH 11/11] fix(BA-3692): Fix unit tests for updated RBAC action signatures - Add _project_id to CreateModelServiceAction/Result test data - Replace success field with route_id/service_id in ActionResult assertions - Add shared conftest with mock_action_validators fixture - Replace MagicMock(spec=ActionValidators) with real dataclass instances (MagicMock spec doesn't expose dataclass instance fields) - Add python_test_utils to BUILD for conftest.py Co-Authored-By: Claude Opus 4.6 --- .../services/model_serving/actions/BUILD | 4 ++++ .../model_serving/actions/conftest.py | 20 +++++++++++++++++++ .../actions/test_create_auto_scaling_rule.py | 3 ++- .../actions/test_create_model_service.py | 13 ++++++++++-- .../actions/test_delete_auto_scaling_rule.py | 3 ++- .../actions/test_delete_model_service.py | 5 +++-- .../actions/test_dry_run_model_service.py | 9 ++++++--- .../actions/test_generate_token.py | 3 ++- .../actions/test_get_model_service_info.py | 3 ++- .../model_serving/actions/test_list_errors.py | 3 ++- .../actions/test_list_model_service.py | 3 ++- .../test_model_serving_crud_actions.py | 7 ++++--- .../actions/test_modify_auto_scaling_rule.py | 3 ++- .../actions/test_scale_service_replicas.py | 3 ++- .../actions/test_search_services.py | 3 ++- .../actions/test_update_route.py | 5 +++-- 16 files changed, 69 insertions(+), 21 deletions(-) create mode 100644 tests/unit/manager/services/model_serving/actions/conftest.py diff --git a/tests/unit/manager/services/model_serving/actions/BUILD b/tests/unit/manager/services/model_serving/actions/BUILD index 57341b1358b..9ae5c8877ec 100644 --- a/tests/unit/manager/services/model_serving/actions/BUILD +++ b/tests/unit/manager/services/model_serving/actions/BUILD @@ -1,3 +1,7 @@ python_tests( name="tests", ) + +python_test_utils( + name="testutils", +) diff --git a/tests/unit/manager/services/model_serving/actions/conftest.py b/tests/unit/manager/services/model_serving/actions/conftest.py new file mode 100644 index 00000000000..93c6a7d8b02 --- /dev/null +++ b/tests/unit/manager/services/model_serving/actions/conftest.py @@ -0,0 +1,20 @@ +from unittest.mock import MagicMock + +import pytest + +from ai.backend.manager.actions.validators import ActionValidators +from ai.backend.manager.actions.validators.rbac import RBACValidators +from ai.backend.manager.actions.validators.rbac.scope import ScopeActionRBACValidator +from ai.backend.manager.actions.validators.rbac.single_entity import ( + SingleEntityActionRBACValidator, +) + + +@pytest.fixture +def mock_action_validators() -> ActionValidators: + return ActionValidators( + rbac=RBACValidators( + scope=MagicMock(spec=ScopeActionRBACValidator), + single_entity=MagicMock(spec=SingleEntityActionRBACValidator), + ), + ) diff --git a/tests/unit/manager/services/model_serving/actions/test_create_auto_scaling_rule.py b/tests/unit/manager/services/model_serving/actions/test_create_auto_scaling_rule.py index a7a723a1edc..f2a50a86af7 100644 --- a/tests/unit/manager/services/model_serving/actions/test_create_auto_scaling_rule.py +++ b/tests/unit/manager/services/model_serving/actions/test_create_auto_scaling_rule.py @@ -74,11 +74,12 @@ def auto_scaling_processors( self, mock_action_monitor: MagicMock, auto_scaling_service: AutoScalingService, + mock_action_validators: ActionValidators, ) -> ModelServingAutoScalingProcessors: return ModelServingAutoScalingProcessors( service=auto_scaling_service, action_monitors=[mock_action_monitor], - validators=MagicMock(spec=ActionValidators), + validators=mock_action_validators, ) @pytest.fixture diff --git a/tests/unit/manager/services/model_serving/actions/test_create_model_service.py b/tests/unit/manager/services/model_serving/actions/test_create_model_service.py index 6d05a109795..56dc9bbdb72 100644 --- a/tests/unit/manager/services/model_serving/actions/test_create_model_service.py +++ b/tests/unit/manager/services/model_serving/actions/test_create_model_service.py @@ -234,11 +234,12 @@ def model_serving_processors( self, mock_action_monitor: MagicMock, model_serving_service: ModelServingService, + mock_action_validators: ActionValidators, ) -> ModelServingProcessors: return ModelServingProcessors( service=model_serving_service, action_monitors=[mock_action_monitor], - validators=MagicMock(spec=ActionValidators), + validators=mock_action_validators, ) @pytest.fixture @@ -379,6 +380,7 @@ def mock_create_endpoint_validated(self, mocker: Any, mock_repositories: Any) -> extra_mounts=[], ), ), + _project_id=uuid.UUID("00000000-0000-0000-0000-000000000002"), ), CreateModelServiceActionResult( data=ServiceInfo( @@ -394,6 +396,7 @@ def mock_create_endpoint_validated(self, mocker: Any, mock_repositories: Any) -> is_public=False, runtime_variant=RuntimeVariant.CUSTOM, ), + _project_id=uuid.UUID("00000000-0000-0000-0000-000000000002"), ), ), ScenarioBase.failure( @@ -436,6 +439,7 @@ def mock_create_endpoint_validated(self, mocker: Any, mock_repositories: Any) -> extra_mounts=[], ), ), + _project_id=uuid.UUID("00000000-0000-0000-0000-000000000002"), ), Exception, # insufficient resources ), @@ -479,6 +483,7 @@ def mock_create_endpoint_validated(self, mocker: Any, mock_repositories: Any) -> extra_mounts=[], ), ), + _project_id=uuid.UUID("00000000-0000-0000-0000-000000000002"), ), InvalidAPIParameters, ), @@ -522,6 +527,7 @@ def mock_create_endpoint_validated(self, mocker: Any, mock_repositories: Any) -> extra_mounts=[], ), ), + _project_id=uuid.UUID("00000000-0000-0000-0000-000000000002"), ), CreateModelServiceActionResult( data=ServiceInfo( @@ -537,6 +543,7 @@ def mock_create_endpoint_validated(self, mocker: Any, mock_repositories: Any) -> is_public=True, runtime_variant=RuntimeVariant.CUSTOM, ), + _project_id=uuid.UUID("00000000-0000-0000-0000-000000000002"), ), ), ], @@ -735,11 +742,12 @@ def model_serving_processors( self, mock_action_monitor: MagicMock, model_serving_service: ModelServingService, + mock_action_validators: ActionValidators, ) -> ModelServingProcessors: return ModelServingProcessors( service=model_serving_service, action_monitors=[mock_action_monitor], - validators=MagicMock(spec=ActionValidators), + validators=mock_action_validators, ) @pytest.fixture @@ -841,6 +849,7 @@ def action_with_api_request_values(self) -> CreateModelServiceAction: extra_mounts=[], ), ), + _project_id=uuid.UUID("00000000-0000-0000-0000-000000000002"), ) async def test_service_definition_overrides_applied( diff --git a/tests/unit/manager/services/model_serving/actions/test_delete_auto_scaling_rule.py b/tests/unit/manager/services/model_serving/actions/test_delete_auto_scaling_rule.py index 07614bb8da0..26783cd3878 100644 --- a/tests/unit/manager/services/model_serving/actions/test_delete_auto_scaling_rule.py +++ b/tests/unit/manager/services/model_serving/actions/test_delete_auto_scaling_rule.py @@ -69,11 +69,12 @@ def auto_scaling_processors( self, mock_action_monitor: MagicMock, auto_scaling_service: AutoScalingService, + mock_action_validators: ActionValidators, ) -> ModelServingAutoScalingProcessors: return ModelServingAutoScalingProcessors( service=auto_scaling_service, action_monitors=[mock_action_monitor], - validators=MagicMock(spec=ActionValidators), + validators=mock_action_validators, ) @pytest.fixture diff --git a/tests/unit/manager/services/model_serving/actions/test_delete_model_service.py b/tests/unit/manager/services/model_serving/actions/test_delete_model_service.py index e30d67ee20d..e3e3851f038 100644 --- a/tests/unit/manager/services/model_serving/actions/test_delete_model_service.py +++ b/tests/unit/manager/services/model_serving/actions/test_delete_model_service.py @@ -158,11 +158,12 @@ def model_serving_processors( self, mock_action_monitor: MagicMock, model_serving_service: ModelServingService, + mock_action_validators: ActionValidators, ) -> ModelServingProcessors: return ModelServingProcessors( service=model_serving_service, action_monitors=[mock_action_monitor], - validators=MagicMock(spec=ActionValidators), + validators=mock_action_validators, ) @pytest.fixture @@ -222,7 +223,7 @@ def mock_check_user_access(self, mocker: Any, model_serving_service: Any) -> Asy service_id=uuid.UUID("cccccccc-dddd-eeee-ffff-111111111111"), ), DeleteModelServiceActionResult( - success=True, + service_id=uuid.UUID("cccccccc-dddd-eeee-ffff-111111111111"), ), ), ScenarioBase.failure( diff --git a/tests/unit/manager/services/model_serving/actions/test_dry_run_model_service.py b/tests/unit/manager/services/model_serving/actions/test_dry_run_model_service.py index aad82f76b9d..ae1741b094a 100644 --- a/tests/unit/manager/services/model_serving/actions/test_dry_run_model_service.py +++ b/tests/unit/manager/services/model_serving/actions/test_dry_run_model_service.py @@ -204,11 +204,12 @@ def model_serving_processors( self, mock_action_monitor: MagicMock, model_serving_service: ModelServingService, + mock_action_validators: ActionValidators, ) -> ModelServingProcessors: return ModelServingProcessors( service=model_serving_service, action_monitors=[mock_action_monitor], - validators=MagicMock(spec=ActionValidators), + validators=mock_action_validators, ) @pytest.fixture @@ -694,11 +695,12 @@ def model_serving_processors( self, mock_action_monitor: MagicMock, model_serving_service: ModelServingService, + mock_action_validators: ActionValidators, ) -> ModelServingProcessors: return ModelServingProcessors( service=model_serving_service, action_monitors=[mock_action_monitor], - validators=MagicMock(spec=ActionValidators), + validators=mock_action_validators, ) @pytest.fixture @@ -998,11 +1000,12 @@ def model_serving_processors( self, mock_action_monitor: MagicMock, model_serving_service: ModelServingService, + mock_action_validators: ActionValidators, ) -> ModelServingProcessors: return ModelServingProcessors( service=model_serving_service, action_monitors=[mock_action_monitor], - validators=MagicMock(spec=ActionValidators), + validators=mock_action_validators, ) @pytest.fixture diff --git a/tests/unit/manager/services/model_serving/actions/test_generate_token.py b/tests/unit/manager/services/model_serving/actions/test_generate_token.py index dbbe32fd8a6..68fa163a223 100644 --- a/tests/unit/manager/services/model_serving/actions/test_generate_token.py +++ b/tests/unit/manager/services/model_serving/actions/test_generate_token.py @@ -161,11 +161,12 @@ def model_serving_processors( self, mock_action_monitor: MagicMock, model_serving_service: ModelServingService, + mock_action_validators: ActionValidators, ) -> ModelServingProcessors: return ModelServingProcessors( service=model_serving_service, action_monitors=[mock_action_monitor], - validators=MagicMock(spec=ActionValidators), + validators=mock_action_validators, ) @pytest.fixture diff --git a/tests/unit/manager/services/model_serving/actions/test_get_model_service_info.py b/tests/unit/manager/services/model_serving/actions/test_get_model_service_info.py index 47be4444294..0b1f549a03a 100644 --- a/tests/unit/manager/services/model_serving/actions/test_get_model_service_info.py +++ b/tests/unit/manager/services/model_serving/actions/test_get_model_service_info.py @@ -160,11 +160,12 @@ def model_serving_processors( self, mock_action_monitor: MagicMock, model_serving_service: ModelServingService, + mock_action_validators: ActionValidators, ) -> ModelServingProcessors: return ModelServingProcessors( service=model_serving_service, action_monitors=[mock_action_monitor], - validators=MagicMock(spec=ActionValidators), + validators=mock_action_validators, ) @pytest.fixture diff --git a/tests/unit/manager/services/model_serving/actions/test_list_errors.py b/tests/unit/manager/services/model_serving/actions/test_list_errors.py index 0655a9f8e91..f4e4138e6f2 100644 --- a/tests/unit/manager/services/model_serving/actions/test_list_errors.py +++ b/tests/unit/manager/services/model_serving/actions/test_list_errors.py @@ -160,11 +160,12 @@ def model_serving_processors( self, mock_action_monitor: MagicMock, model_serving_service: ModelServingService, + mock_action_validators: ActionValidators, ) -> ModelServingProcessors: return ModelServingProcessors( service=model_serving_service, action_monitors=[mock_action_monitor], - validators=MagicMock(spec=ActionValidators), + validators=mock_action_validators, ) @pytest.fixture diff --git a/tests/unit/manager/services/model_serving/actions/test_list_model_service.py b/tests/unit/manager/services/model_serving/actions/test_list_model_service.py index b191f088c84..7c8532e834f 100644 --- a/tests/unit/manager/services/model_serving/actions/test_list_model_service.py +++ b/tests/unit/manager/services/model_serving/actions/test_list_model_service.py @@ -160,11 +160,12 @@ def model_serving_processors( self, mock_action_monitor: MagicMock, model_serving_service: ModelServingService, + mock_action_validators: ActionValidators, ) -> ModelServingProcessors: return ModelServingProcessors( service=model_serving_service, action_monitors=[mock_action_monitor], - validators=MagicMock(spec=ActionValidators), + validators=mock_action_validators, ) @pytest.fixture diff --git a/tests/unit/manager/services/model_serving/actions/test_model_serving_crud_actions.py b/tests/unit/manager/services/model_serving/actions/test_model_serving_crud_actions.py index 4d8bd839243..5866acf4af5 100644 --- a/tests/unit/manager/services/model_serving/actions/test_model_serving_crud_actions.py +++ b/tests/unit/manager/services/model_serving/actions/test_model_serving_crud_actions.py @@ -180,11 +180,12 @@ def model_serving_processors( self, mock_action_monitor: MagicMock, model_serving_service: ModelServingService, + mock_action_validators: ActionValidators, ) -> ModelServingProcessors: return ModelServingProcessors( service=model_serving_service, action_monitors=[mock_action_monitor], - validators=MagicMock(spec=ActionValidators), + validators=mock_action_validators, ) @pytest.fixture @@ -488,7 +489,7 @@ async def test_healthy_route_deletion_success( action = DeleteRouteAction(service_id=service_id, route_id=route_id) result = await model_serving_processors.delete_route.wait_for_complete(action) - assert result.success is True + assert result.route_id == route_id mock_destroy_session.assert_called_once_with( mock_session_row, forced=False, @@ -539,7 +540,7 @@ async def test_sessionless_route_deletes_without_session_destruction( action = DeleteRouteAction(service_id=service_id, route_id=route_id) result = await model_serving_processors.delete_route.wait_for_complete(action) - assert result.success is True + assert result.route_id == route_id mock_destroy_session.assert_not_called() mock_decrease_endpoint_replicas.assert_called_once_with(service_id) diff --git a/tests/unit/manager/services/model_serving/actions/test_modify_auto_scaling_rule.py b/tests/unit/manager/services/model_serving/actions/test_modify_auto_scaling_rule.py index e202a71a859..b64d3eb7a67 100644 --- a/tests/unit/manager/services/model_serving/actions/test_modify_auto_scaling_rule.py +++ b/tests/unit/manager/services/model_serving/actions/test_modify_auto_scaling_rule.py @@ -80,11 +80,12 @@ def auto_scaling_processors( self, mock_action_monitor: MagicMock, auto_scaling_service: AutoScalingService, + mock_action_validators: ActionValidators, ) -> ModelServingAutoScalingProcessors: return ModelServingAutoScalingProcessors( service=auto_scaling_service, action_monitors=[mock_action_monitor], - validators=MagicMock(spec=ActionValidators), + validators=mock_action_validators, ) @pytest.fixture diff --git a/tests/unit/manager/services/model_serving/actions/test_scale_service_replicas.py b/tests/unit/manager/services/model_serving/actions/test_scale_service_replicas.py index a0edf660a70..0e67a29c1b7 100644 --- a/tests/unit/manager/services/model_serving/actions/test_scale_service_replicas.py +++ b/tests/unit/manager/services/model_serving/actions/test_scale_service_replicas.py @@ -68,11 +68,12 @@ def auto_scaling_processors( self, mock_action_monitor: MagicMock, auto_scaling_service: AutoScalingService, + mock_action_validators: ActionValidators, ) -> ModelServingAutoScalingProcessors: return ModelServingAutoScalingProcessors( service=auto_scaling_service, action_monitors=[mock_action_monitor], - validators=MagicMock(spec=ActionValidators), + validators=mock_action_validators, ) @pytest.fixture diff --git a/tests/unit/manager/services/model_serving/actions/test_search_services.py b/tests/unit/manager/services/model_serving/actions/test_search_services.py index 24b45700c88..dd7b5fb14d0 100644 --- a/tests/unit/manager/services/model_serving/actions/test_search_services.py +++ b/tests/unit/manager/services/model_serving/actions/test_search_services.py @@ -165,11 +165,12 @@ def model_serving_processors( self, mock_action_monitor: MagicMock, model_serving_service: ModelServingService, + mock_action_validators: ActionValidators, ) -> ModelServingProcessors: return ModelServingProcessors( service=model_serving_service, action_monitors=[mock_action_monitor], - validators=MagicMock(spec=ActionValidators), + validators=mock_action_validators, ) @pytest.fixture diff --git a/tests/unit/manager/services/model_serving/actions/test_update_route.py b/tests/unit/manager/services/model_serving/actions/test_update_route.py index d50c3a903f3..73a277d68b0 100644 --- a/tests/unit/manager/services/model_serving/actions/test_update_route.py +++ b/tests/unit/manager/services/model_serving/actions/test_update_route.py @@ -159,11 +159,12 @@ def model_serving_processors( self, mock_action_monitor: MagicMock, model_serving_service: ModelServingService, + mock_action_validators: ActionValidators, ) -> ModelServingProcessors: return ModelServingProcessors( service=model_serving_service, action_monitors=[mock_action_monitor], - validators=MagicMock(spec=ActionValidators), + validators=mock_action_validators, ) @pytest.fixture @@ -243,7 +244,7 @@ def mock_notify_endpoint_route_update_to_appproxy( route_id=uuid.UUID("11111111-1111-1111-1111-111111111111"), traffic_ratio=0.7, ), - UpdateRouteActionResult(success=True), + UpdateRouteActionResult(route_id=uuid.UUID("11111111-1111-1111-1111-111111111111")), ), ], )