diff --git a/changes/12974.feature.md b/changes/12974.feature.md new file mode 100644 index 00000000000..0488a1dccec --- /dev/null +++ b/changes/12974.feature.md @@ -0,0 +1 @@ +Open the session, deployment and route scheduling-history scoped searches to non-admin callers, authorized against the requested scope through the RBAC scope chain. diff --git a/docs/manager/graphql-reference/supergraph.graphql b/docs/manager/graphql-reference/supergraph.graphql index 1147ee762a2..cca5aaeb445 100644 --- a/docs/manager/graphql-reference/supergraph.graphql +++ b/docs/manager/graphql-reference/supergraph.graphql @@ -15675,6 +15675,9 @@ enum RBACElementType ROLE @join__enumValue(graph: STRAWBERRY) AUDIT_LOG @join__enumValue(graph: STRAWBERRY) KERNEL_HISTORY @join__enumValue(graph: STRAWBERRY) + SESSION_HISTORY @join__enumValue(graph: STRAWBERRY) + DEPLOYMENT_HISTORY @join__enumValue(graph: STRAWBERRY) + ROUTE_HISTORY @join__enumValue(graph: STRAWBERRY) EVENT_LOG @join__enumValue(graph: STRAWBERRY) PROJECT_ADMIN_PAGE @join__enumValue(graph: STRAWBERRY) DOMAIN_ADMIN_PAGE @join__enumValue(graph: STRAWBERRY) diff --git a/docs/manager/graphql-reference/v2-schema.graphql b/docs/manager/graphql-reference/v2-schema.graphql index 360992b6c28..7548b87a54e 100644 --- a/docs/manager/graphql-reference/v2-schema.graphql +++ b/docs/manager/graphql-reference/v2-schema.graphql @@ -10646,6 +10646,9 @@ enum RBACElementType { ROLE AUDIT_LOG KERNEL_HISTORY + SESSION_HISTORY + DEPLOYMENT_HISTORY + ROUTE_HISTORY EVENT_LOG PROJECT_ADMIN_PAGE DOMAIN_ADMIN_PAGE diff --git a/docs/manager/rest-reference/openapi.json b/docs/manager/rest-reference/openapi.json index 4f5de481bdf..a4cba36c76c 100644 --- a/docs/manager/rest-reference/openapi.json +++ b/docs/manager/rest-reference/openapi.json @@ -15561,6 +15561,9 @@ "role", "audit_log", "kernel:history", + "session:history", + "deployment:history", + "route:history", "event_log", "project_admin_page", "domain_admin_page", @@ -49125,7 +49128,7 @@ } } ], - "description": "Search session scheduling histories scoped to a specific session.\n\n**Preconditions:**\n* Superadmin privilege required.\n" + "description": "Search session scheduling histories scoped to a specific session.\n\n**Preconditions:**\n* User privilege required.\n" } }, "/v2/scheduling-history/kernels/admin/search": { @@ -49250,7 +49253,7 @@ } } ], - "description": "Search deployment histories scoped to a specific deployment.\n\n**Preconditions:**\n* Superadmin privilege required.\n" + "description": "Search deployment histories scoped to a specific deployment.\n\n**Preconditions:**\n* User privilege required.\n" } }, "/v2/scheduling-history/routes/search": { @@ -49317,7 +49320,7 @@ } } ], - "description": "Search route histories scoped to a specific route.\n\n**Preconditions:**\n* Superadmin privilege required.\n" + "description": "Search route histories scoped to a specific route.\n\n**Preconditions:**\n* User privilege required.\n" } }, "/v2/service-catalogs/search": { diff --git a/src/ai/backend/common/data/permission/types.py b/src/ai/backend/common/data/permission/types.py index 2a7fafa3aef..5bc9fe4fd59 100644 --- a/src/ai/backend/common/data/permission/types.py +++ b/src/ai/backend/common/data/permission/types.py @@ -357,6 +357,7 @@ class ScopeType(enum.StrEnum): KERNEL = "kernel" DEPLOYMENT = "deployment" MODEL_DEPLOYMENT = "model_deployment" + ROUTING = "routing" VFOLDER = "vfolder" IMAGE = "image" ARTIFACT = "artifact" @@ -428,6 +429,9 @@ class RBACElementType(enum.StrEnum): ROLE = "role" AUDIT_LOG = "audit_log" KERNEL_HISTORY = "kernel:history" + SESSION_HISTORY = "session:history" + DEPLOYMENT_HISTORY = "deployment:history" + ROUTE_HISTORY = "route:history" EVENT_LOG = "event_log" # === Admin page access control === diff --git a/src/ai/backend/common/dto/manager/v2/rbac/types.py b/src/ai/backend/common/dto/manager/v2/rbac/types.py index 76373fdff7e..1a2555d4ece 100644 --- a/src/ai/backend/common/dto/manager/v2/rbac/types.py +++ b/src/ai/backend/common/dto/manager/v2/rbac/types.py @@ -110,6 +110,9 @@ class RBACElementTypeDTO(StrEnum): ROLE = "role" AUDIT_LOG = "audit_log" KERNEL_HISTORY = "kernel:history" + SESSION_HISTORY = "session:history" + DEPLOYMENT_HISTORY = "deployment:history" + ROUTE_HISTORY = "route:history" EVENT_LOG = "event_log" # Admin page access control diff --git a/src/ai/backend/manager/api/adapters/scheduling_history/adapter.py b/src/ai/backend/manager/api/adapters/scheduling_history/adapter.py index 26638b96c09..618cd99f7d9 100644 --- a/src/ai/backend/manager/api/adapters/scheduling_history/adapter.py +++ b/src/ai/backend/manager/api/adapters/scheduling_history/adapter.py @@ -32,6 +32,7 @@ SessionHistoryNode, ) from ai.backend.common.dto.manager.v2.scheduling_history.types import SubStepResultInfo +from ai.backend.common.identifier.deployment import DeploymentID from ai.backend.common.identifier.kernel_scheduling_history import KernelSchedulingHistoryID from ai.backend.common.identifier.replica import ReplicaID from ai.backend.common.types import KernelId, SessionId @@ -76,11 +77,6 @@ combine_conditions_or, negate_conditions, ) -from ai.backend.manager.repositories.scheduling_history.types import ( - DeploymentHistorySearchScope, - RouteHistorySearchScope, - SessionSchedulingHistorySearchScope, -) from ai.backend.manager.services.scheduling_history.actions.resolve_kernel_session import ( ResolveKernelSessionAction, ) @@ -258,10 +254,9 @@ async def session_scoped_search( input: AdminSearchSessionHistoriesInput, ) -> AdminSearchSessionHistoriesPayload: """Search session scheduling histories scoped to a session.""" - scope = SessionSchedulingHistorySearchScope(session_id=session_id) querier = self._build_session_querier(input) action_result = await self._processors.scheduling_history.search_session_scoped_history.wait_for_complete( - SearchSessionScopedHistoryAction(scope=scope, querier=querier) + SearchSessionScopedHistoryAction(session_id=SessionId(session_id), querier=querier) ) return AdminSearchSessionHistoriesPayload( items=[self._session_data_to_dto(h) for h in action_result.histories], @@ -647,10 +642,11 @@ async def deployment_scoped_search( input: AdminSearchDeploymentHistoriesInput, ) -> AdminSearchDeploymentHistoriesPayload: """Search deployment histories scoped to a deployment.""" - scope = DeploymentHistorySearchScope(deployment_id=deployment_id) querier = self._build_deployment_querier(input) action_result = await self._processors.scheduling_history.search_deployment_scoped_history.wait_for_complete( - SearchDeploymentScopedHistoryAction(scope=scope, querier=querier) + SearchDeploymentScopedHistoryAction( + deployment_id=DeploymentID(deployment_id), querier=querier + ) ) return AdminSearchDeploymentHistoriesPayload( items=[self._deployment_data_to_dto(h) for h in action_result.histories], @@ -810,11 +806,10 @@ async def route_scoped_search( input: AdminSearchRouteHistoriesInput, ) -> AdminSearchRouteHistoriesPayload: """Search route histories scoped to a route.""" - scope = RouteHistorySearchScope(route_id=ReplicaID(route_id)) querier = self._build_route_querier(input) action_result = ( await self._processors.scheduling_history.search_route_scoped_history.wait_for_complete( - SearchRouteScopedHistoryAction(scope=scope, querier=querier) + SearchRouteScopedHistoryAction(route_id=ReplicaID(route_id), querier=querier) ) ) return AdminSearchRouteHistoriesPayload( diff --git a/src/ai/backend/manager/api/gql/rbac/types/permission.py b/src/ai/backend/manager/api/gql/rbac/types/permission.py index e1cc366306d..0f9c066f908 100644 --- a/src/ai/backend/manager/api/gql/rbac/types/permission.py +++ b/src/ai/backend/manager/api/gql/rbac/types/permission.py @@ -240,6 +240,9 @@ async def scope( | RBACElementType.PROJECT_RESOURCE_POLICY | RBACElementType.AUDIT_LOG | RBACElementType.KERNEL_HISTORY + | RBACElementType.SESSION_HISTORY + | RBACElementType.DEPLOYMENT_HISTORY + | RBACElementType.ROUTE_HISTORY | RBACElementType.EVENT_LOG | RBACElementType.NOTIFICATION_RULE | RBACElementType.AGENT diff --git a/src/ai/backend/manager/api/rest/v2/scheduling_history/registry.py b/src/ai/backend/manager/api/rest/v2/scheduling_history/registry.py index 84282277470..ccd168a2a94 100644 --- a/src/ai/backend/manager/api/rest/v2/scheduling_history/registry.py +++ b/src/ai/backend/manager/api/rest/v2/scheduling_history/registry.py @@ -31,7 +31,7 @@ def register_v2_scheduling_history_routes( "POST", "/sessions/{session_id}/search", handler.admin_session_scoped_search, - middlewares=[superadmin_required], + middlewares=[auth_required], ) # Kernel history @@ -59,7 +59,7 @@ def register_v2_scheduling_history_routes( "POST", "/deployments/{deployment_id}/search", handler.admin_deployment_scoped_search, - middlewares=[superadmin_required], + middlewares=[auth_required], ) # Route history @@ -73,7 +73,7 @@ def register_v2_scheduling_history_routes( "POST", "/routes/{route_id}/search", handler.admin_route_scoped_search, - middlewares=[superadmin_required], + middlewares=[auth_required], ) return registry diff --git a/src/ai/backend/manager/models/alembic/versions/c8e2f41a67d5_add_scheduling_history_read_permissions.py b/src/ai/backend/manager/models/alembic/versions/c8e2f41a67d5_add_scheduling_history_read_permissions.py new file mode 100644 index 00000000000..e3e08aacf3f --- /dev/null +++ b/src/ai/backend/manager/models/alembic/versions/c8e2f41a67d5_add_scheduling_history_read_permissions.py @@ -0,0 +1,57 @@ +"""add scheduling history read permissions to roles that can read the scoped entity + +Revision ID: c8e2f41a67d5 +Revises: 5405ee0d8eed +Create Date: 2026-07-20 00:00:00.000000 + +""" + +import sqlalchemy as sa +from alembic import op + +# revision identifiers, used by Alembic. +revision = "c8e2f41a67d5" +down_revision = "5405ee0d8eed" +branch_labels = None +depends_on = None + +# (scope entity that already carries a READ grant, history entity to mirror it onto) +_HISTORY_GRANTS = [ + ("session", "session:history"), + ("model_deployment", "deployment:history"), + ("routing", "route:history"), +] + + +def upgrade() -> None: + conn = op.get_bind() + + # Reading an entity carries reading its scheduling history, so mirror every + # existing READ grant onto the history entity within the same scope. + # ON CONFLICT DO NOTHING keeps this idempotent for release-branch backports. + for scope_entity, history_entity in _HISTORY_GRANTS: + conn.execute( + sa.text(""" + INSERT INTO permissions (role_id, scope_type, scope_id, entity_type, operation) + SELECT DISTINCT p.role_id, p.scope_type, p.scope_id, :history_entity, 'read' + FROM permissions p + WHERE p.entity_type = :scope_entity + AND p.operation = 'read' + ON CONFLICT DO NOTHING + """), + {"scope_entity": scope_entity, "history_entity": history_entity}, + ) + + +def downgrade() -> None: + conn = op.get_bind() + + for _, history_entity in _HISTORY_GRANTS: + conn.execute( + sa.text(""" + DELETE FROM permissions + WHERE entity_type = :history_entity + AND operation = 'read' + """), + {"history_entity": history_entity}, + ) diff --git a/src/ai/backend/manager/services/scheduling_history/actions/search_deployment_scoped_history.py b/src/ai/backend/manager/services/scheduling_history/actions/search_deployment_scoped_history.py index 01cb472ea4b..6b7b625dd7f 100644 --- a/src/ai/backend/manager/services/scheduling_history/actions/search_deployment_scoped_history.py +++ b/src/ai/backend/manager/services/scheduling_history/actions/search_deployment_scoped_history.py @@ -3,33 +3,30 @@ 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, ScopeType +from ai.backend.common.identifier.deployment import DeploymentID +from ai.backend.manager.actions.action.scope import BaseScopeAction, BaseScopeActionResult from ai.backend.manager.actions.types import ActionOperationType from ai.backend.manager.data.deployment.types import DeploymentHistoryData +from ai.backend.manager.data.permission.types import RBACElementRef from ai.backend.manager.repositories.base import BatchQuerier -from ai.backend.manager.repositories.scheduling_history.types import ( - DeploymentHistorySearchScope, -) - -from .base import SchedulingHistoryAction @dataclass -class SearchDeploymentScopedHistoryAction(SchedulingHistoryAction): - """Action to search deployment history within a deployment scope. +class SearchDeploymentScopedHistoryAction(BaseScopeAction): + """Action to search the scheduling history of one deployment. - This is the scoped version used by entity-scoped APIs. - Scope is required and specifies which deployment to query history for. + The history is the entity being read and the deployment is the scope containing it, + so the RBAC scope chain authorizes the caller for reading history there. """ - scope: DeploymentHistorySearchScope + deployment_id: DeploymentID querier: BatchQuerier @override @classmethod def entity_type(cls) -> EntityType: - return EntityType.DEPLOYMENT_SCOPED_HISTORY + return EntityType.DEPLOYMENT_HISTORY @override @classmethod @@ -37,19 +34,35 @@ def operation_type(cls) -> ActionOperationType: return ActionOperationType.SEARCH @override - def entity_id(self) -> str | None: - return str(self.scope.deployment_id) + def scope_type(self) -> ScopeType: + return ScopeType.MODEL_DEPLOYMENT + + @override + def scope_id(self) -> str: + return str(self.deployment_id) + + @override + def target_element(self) -> RBACElementRef: + return RBACElementRef( + element_type=RBACElementType.MODEL_DEPLOYMENT, + element_id=str(self.deployment_id), + ) @dataclass -class SearchDeploymentScopedHistoryActionResult(BaseActionResult): - """Result of searching deployment history within scope.""" +class SearchDeploymentScopedHistoryActionResult(BaseScopeActionResult): + """Result of searching the scheduling history of one deployment.""" histories: list[DeploymentHistoryData] total_count: int has_next_page: bool has_previous_page: bool + deployment_id: DeploymentID + + @override + def scope_type(self) -> ScopeType: + return ScopeType.MODEL_DEPLOYMENT @override - def entity_id(self) -> str | None: - return None + def scope_id(self) -> str: + return str(self.deployment_id) diff --git a/src/ai/backend/manager/services/scheduling_history/actions/search_route_scoped_history.py b/src/ai/backend/manager/services/scheduling_history/actions/search_route_scoped_history.py index ee696c5021f..d33be9bd334 100644 --- a/src/ai/backend/manager/services/scheduling_history/actions/search_route_scoped_history.py +++ b/src/ai/backend/manager/services/scheduling_history/actions/search_route_scoped_history.py @@ -3,31 +3,30 @@ 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, ScopeType +from ai.backend.common.identifier.replica import ReplicaID +from ai.backend.manager.actions.action.scope import BaseScopeAction, BaseScopeActionResult from ai.backend.manager.actions.types import ActionOperationType from ai.backend.manager.data.deployment.types import RouteHistoryData +from ai.backend.manager.data.permission.types import RBACElementRef from ai.backend.manager.repositories.base import BatchQuerier -from ai.backend.manager.repositories.scheduling_history.types import RouteHistorySearchScope - -from .base import SchedulingHistoryAction @dataclass -class SearchRouteScopedHistoryAction(SchedulingHistoryAction): - """Action to search route history within a route scope. +class SearchRouteScopedHistoryAction(BaseScopeAction): + """Action to search the scheduling history of one route. - This is the scoped version used by entity-scoped APIs. - Scope is required and specifies which route to query history for. + The history is the entity being read and the route is the scope containing it, + so the RBAC scope chain authorizes the caller for reading history there. """ - scope: RouteHistorySearchScope + route_id: ReplicaID querier: BatchQuerier @override @classmethod def entity_type(cls) -> EntityType: - return EntityType.ROUTE_SCOPED_HISTORY + return EntityType.ROUTE_HISTORY @override @classmethod @@ -35,19 +34,35 @@ def operation_type(cls) -> ActionOperationType: return ActionOperationType.SEARCH @override - def entity_id(self) -> str | None: - return str(self.scope.route_id) + def scope_type(self) -> ScopeType: + return ScopeType.ROUTING + + @override + def scope_id(self) -> str: + return str(self.route_id) + + @override + def target_element(self) -> RBACElementRef: + return RBACElementRef( + element_type=RBACElementType.ROUTING, + element_id=str(self.route_id), + ) @dataclass -class SearchRouteScopedHistoryActionResult(BaseActionResult): - """Result of searching route history within scope.""" +class SearchRouteScopedHistoryActionResult(BaseScopeActionResult): + """Result of searching the scheduling history of one route.""" histories: list[RouteHistoryData] total_count: int has_next_page: bool has_previous_page: bool + route_id: ReplicaID + + @override + def scope_type(self) -> ScopeType: + return ScopeType.ROUTING @override - def entity_id(self) -> str | None: - return None + def scope_id(self) -> str: + return str(self.route_id) diff --git a/src/ai/backend/manager/services/scheduling_history/actions/search_session_scoped_history.py b/src/ai/backend/manager/services/scheduling_history/actions/search_session_scoped_history.py index 6e620e48f1c..62a31d748da 100644 --- a/src/ai/backend/manager/services/scheduling_history/actions/search_session_scoped_history.py +++ b/src/ai/backend/manager/services/scheduling_history/actions/search_session_scoped_history.py @@ -3,33 +3,30 @@ 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, ScopeType +from ai.backend.common.types import SessionId +from ai.backend.manager.actions.action.scope import BaseScopeAction, BaseScopeActionResult from ai.backend.manager.actions.types import ActionOperationType +from ai.backend.manager.data.permission.types import RBACElementRef from ai.backend.manager.data.session.types import SessionSchedulingHistoryData from ai.backend.manager.repositories.base import BatchQuerier -from ai.backend.manager.repositories.scheduling_history.types import ( - SessionSchedulingHistorySearchScope, -) - -from .base import SchedulingHistoryAction @dataclass -class SearchSessionScopedHistoryAction(SchedulingHistoryAction): - """Action to search session scheduling history within a session scope. +class SearchSessionScopedHistoryAction(BaseScopeAction): + """Action to search the scheduling history of one session. - This is the scoped version used by entity-scoped APIs. - Scope is required and specifies which session to query history for. + The history is the entity being read and the session is the scope containing it, + so the RBAC scope chain authorizes the caller for reading history there. """ - scope: SessionSchedulingHistorySearchScope + session_id: SessionId querier: BatchQuerier @override @classmethod def entity_type(cls) -> EntityType: - return EntityType.SESSION_SCOPED_HISTORY + return EntityType.SESSION_HISTORY @override @classmethod @@ -37,19 +34,35 @@ def operation_type(cls) -> ActionOperationType: return ActionOperationType.SEARCH @override - def entity_id(self) -> str | None: - return str(self.scope.session_id) + def scope_type(self) -> ScopeType: + return ScopeType.SESSION + + @override + def scope_id(self) -> str: + return str(self.session_id) + + @override + def target_element(self) -> RBACElementRef: + return RBACElementRef( + element_type=RBACElementType.SESSION, + element_id=str(self.session_id), + ) @dataclass -class SearchSessionScopedHistoryActionResult(BaseActionResult): - """Result of searching session scheduling history within scope.""" +class SearchSessionScopedHistoryActionResult(BaseScopeActionResult): + """Result of searching the scheduling history of one session.""" histories: list[SessionSchedulingHistoryData] total_count: int has_next_page: bool has_previous_page: bool + session_id: SessionId + + @override + def scope_type(self) -> ScopeType: + return ScopeType.SESSION @override - def entity_id(self) -> str | None: - return None + def scope_id(self) -> str: + return str(self.session_id) diff --git a/src/ai/backend/manager/services/scheduling_history/processors.py b/src/ai/backend/manager/services/scheduling_history/processors.py index e6dcf4b38dc..9a4a4bca515 100644 --- a/src/ai/backend/manager/services/scheduling_history/processors.py +++ b/src/ai/backend/manager/services/scheduling_history/processors.py @@ -48,7 +48,7 @@ class SchedulingHistoryProcessors(AbstractProcessorPackage): search_route_history: ActionProcessor[SearchRouteHistoryAction, SearchRouteHistoryActionResult] # Scoped processors (added in 26.2.0) - search_session_scoped_history: ActionProcessor[ + search_session_scoped_history: ScopeActionProcessor[ SearchSessionScopedHistoryAction, SearchSessionScopedHistoryActionResult ] resolve_kernel_session: ActionProcessor[ @@ -57,10 +57,10 @@ class SchedulingHistoryProcessors(AbstractProcessorPackage): search_kernel_scoped_history: ScopeActionProcessor[ SearchKernelScopedHistoryAction, SearchKernelScopedHistoryActionResult ] - search_deployment_scoped_history: ActionProcessor[ + search_deployment_scoped_history: ScopeActionProcessor[ SearchDeploymentScopedHistoryAction, SearchDeploymentScopedHistoryActionResult ] - search_route_scoped_history: ActionProcessor[ + search_route_scoped_history: ScopeActionProcessor[ SearchRouteScopedHistoryAction, SearchRouteScopedHistoryActionResult ] @@ -83,8 +83,10 @@ def __init__( self.search_route_history = ActionProcessor(service.search_route_history, action_monitors) # Scoped processors (added in 26.2.0) - self.search_session_scoped_history = ActionProcessor( - service.search_session_scoped_history, action_monitors + self.search_session_scoped_history = ScopeActionProcessor( + service.search_session_scoped_history, + monitors=action_monitors, + validators=[validators.rbac.scope], ) self.resolve_kernel_session = ActionProcessor( service.resolve_kernel_session, action_monitors @@ -94,11 +96,15 @@ def __init__( monitors=action_monitors, validators=[validators.rbac.scope], ) - self.search_deployment_scoped_history = ActionProcessor( - service.search_deployment_scoped_history, action_monitors + self.search_deployment_scoped_history = ScopeActionProcessor( + service.search_deployment_scoped_history, + monitors=action_monitors, + validators=[validators.rbac.scope], ) - self.search_route_scoped_history = ActionProcessor( - service.search_route_scoped_history, action_monitors + self.search_route_scoped_history = ScopeActionProcessor( + service.search_route_scoped_history, + monitors=action_monitors, + validators=[validators.rbac.scope], ) @override diff --git a/src/ai/backend/manager/services/scheduling_history/service.py b/src/ai/backend/manager/services/scheduling_history/service.py index 92349f0371c..ed5b1c57af4 100644 --- a/src/ai/backend/manager/services/scheduling_history/service.py +++ b/src/ai/backend/manager/services/scheduling_history/service.py @@ -1,6 +1,11 @@ from __future__ import annotations from ai.backend.manager.repositories.scheduling_history import SchedulingHistoryRepository +from ai.backend.manager.repositories.scheduling_history.types import ( + DeploymentHistorySearchScope, + RouteHistorySearchScope, + SessionSchedulingHistorySearchScope, +) from .actions.resolve_kernel_session import ( ResolveKernelSessionAction, @@ -123,7 +128,7 @@ async def search_session_scoped_history( """Searches session scheduling history within scope.""" result = await self._repository.search_session_scoped_history( querier=action.querier, - scope=action.scope, + scope=SessionSchedulingHistorySearchScope(session_id=action.session_id), ) return SearchSessionScopedHistoryActionResult( @@ -131,6 +136,7 @@ async def search_session_scoped_history( total_count=result.total_count, has_next_page=result.has_next_page, has_previous_page=result.has_previous_page, + session_id=action.session_id, ) async def resolve_kernel_session( @@ -166,7 +172,7 @@ async def search_deployment_scoped_history( """Searches deployment history within scope.""" result = await self._repository.search_deployment_scoped_history( querier=action.querier, - scope=action.scope, + scope=DeploymentHistorySearchScope(deployment_id=action.deployment_id), ) return SearchDeploymentScopedHistoryActionResult( @@ -174,6 +180,7 @@ async def search_deployment_scoped_history( total_count=result.total_count, has_next_page=result.has_next_page, has_previous_page=result.has_previous_page, + deployment_id=action.deployment_id, ) async def search_route_scoped_history( @@ -183,7 +190,7 @@ async def search_route_scoped_history( """Searches route history within scope.""" result = await self._repository.search_route_scoped_history( querier=action.querier, - scope=action.scope, + scope=RouteHistorySearchScope(route_id=action.route_id), ) return SearchRouteScopedHistoryActionResult( @@ -191,4 +198,5 @@ async def search_route_scoped_history( total_count=result.total_count, has_next_page=result.has_next_page, has_previous_page=result.has_previous_page, + route_id=action.route_id, ) diff --git a/tests/unit/manager/services/scheduling_history/test_scheduling_history_service.py b/tests/unit/manager/services/scheduling_history/test_scheduling_history_service.py index cd3ac7127d9..6ce515182a4 100644 --- a/tests/unit/manager/services/scheduling_history/test_scheduling_history_service.py +++ b/tests/unit/manager/services/scheduling_history/test_scheduling_history_service.py @@ -13,6 +13,7 @@ from dateutil.tz import tzutc from ai.backend.common.data.permission.types import EntityType, RBACElementType, ScopeType +from ai.backend.common.identifier.deployment import DeploymentID from ai.backend.common.identifier.kernel_scheduling_history import KernelSchedulingHistoryID from ai.backend.common.identifier.replica import ReplicaID from ai.backend.common.types import KernelId, SessionId @@ -250,14 +251,20 @@ async def test_scope_filters_by_deployment_id( has_next_page=False, has_previous_page=False, ) - scope = DeploymentHistorySearchScope(deployment_id=deployment_id) - - action = SearchDeploymentScopedHistoryAction(scope=scope, querier=querier) + action = SearchDeploymentScopedHistoryAction( + deployment_id=DeploymentID(deployment_id), querier=querier + ) result = await service.search_deployment_scoped_history(action) assert result.histories == [history_item] + assert action.target_element() == RBACElementRef( + element_type=RBACElementType.MODEL_DEPLOYMENT, element_id=str(deployment_id) + ) + assert action.entity_type() is EntityType.DEPLOYMENT_HISTORY + assert action.scope_type() is ScopeType.MODEL_DEPLOYMENT mock_repository.search_deployment_scoped_history.assert_awaited_once_with( - querier=querier, scope=scope + querier=querier, + scope=DeploymentHistorySearchScope(deployment_id=DeploymentID(deployment_id)), ) @@ -278,14 +285,18 @@ async def test_scope_filters_by_session_id( has_previous_page=False, ) ) - scope = SessionSchedulingHistorySearchScope(session_id=session_id) - - action = SearchSessionScopedHistoryAction(scope=scope, querier=querier) + action = SearchSessionScopedHistoryAction(session_id=SessionId(session_id), querier=querier) result = await service.search_session_scoped_history(action) assert result.histories == [history_item] + assert action.target_element() == RBACElementRef( + element_type=RBACElementType.SESSION, element_id=str(session_id) + ) + assert action.entity_type() is EntityType.SESSION_HISTORY + assert action.scope_type() is ScopeType.SESSION mock_repository.search_session_scoped_history.assert_awaited_once_with( - querier=querier, scope=scope + querier=querier, + scope=SessionSchedulingHistorySearchScope(session_id=SessionId(session_id)), ) @@ -328,14 +339,17 @@ async def test_scope_filters_by_route_id( has_next_page=False, has_previous_page=False, ) - scope = RouteHistorySearchScope(route_id=route_id) - - action = SearchRouteScopedHistoryAction(scope=scope, querier=querier) + action = SearchRouteScopedHistoryAction(route_id=ReplicaID(route_id), querier=querier) result = await service.search_route_scoped_history(action) assert result.histories == [history_item] + assert action.target_element() == RBACElementRef( + element_type=RBACElementType.ROUTING, element_id=str(route_id) + ) + assert action.entity_type() is EntityType.ROUTE_HISTORY + assert action.scope_type() is ScopeType.ROUTING mock_repository.search_route_scoped_history.assert_awaited_once_with( - querier=querier, scope=scope + querier=querier, scope=RouteHistorySearchScope(route_id=ReplicaID(route_id)) )