From b881109d5c0c32c0f51053543d8a33f7f7a3f586 Mon Sep 17 00:00:00 2001 From: Sanghun Lee Date: Fri, 6 Mar 2026 02:27:19 +0900 Subject: [PATCH 1/2] feat(BA-4876): add repository/service/action for permission update MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - PermissionUpdaterSpec in updaters.py (scope_type, scope_id, entity_type, operation — role_id excluded) - UpdatePermissionAction / UpdatePermissionActionResult - Service method update_permission() - Repository method with resilience policy - DB Source method using execute_updater - Processor wiring in PermissionControllerProcessors Co-Authored-By: Claude Opus 4.6 --- .../db_source/db_source.py | 22 +++++++++++++ .../permission_controller/repository.py | 16 ++++++++++ .../permission_controller/updaters.py | 32 ++++++++++++++++++- .../permission_contoller/actions/__init__.py | 3 ++ .../actions/update_permission.py | 32 +++++++++++++++++++ .../permission_contoller/processors.py | 7 ++++ .../services/permission_contoller/service.py | 13 ++++++++ 7 files changed, 124 insertions(+), 1 deletion(-) create mode 100644 src/ai/backend/manager/services/permission_contoller/actions/update_permission.py diff --git a/src/ai/backend/manager/repositories/permission_controller/db_source/db_source.py b/src/ai/backend/manager/repositories/permission_controller/db_source/db_source.py index 400afb579c5..7f2849a9ecf 100644 --- a/src/ai/backend/manager/repositories/permission_controller/db_source/db_source.py +++ b/src/ai/backend/manager/repositories/permission_controller/db_source/db_source.py @@ -162,6 +162,28 @@ async def delete_permission( raise ObjectNotFound(f"Permission with ID {purger.pk_value} does not exist.") return result.row + async def update_permission( + self, + updater: Updater[PermissionRow], + ) -> PermissionRow: + """ + Update a permission. + + Args: + updater: Updater with permission ID and fields to update + + Returns: + Updated permission row + + Raises: + ObjectNotFound: If permission does not exist + """ + async with self._db.begin_session() as db_session: + result = await execute_updater(db_session, updater) + if result is None: + raise ObjectNotFound(f"Permission with ID {updater.pk_value} does not exist.") + return result.row + async def delete_object_permission( self, purger: Purger[ObjectPermissionRow], diff --git a/src/ai/backend/manager/repositories/permission_controller/repository.py b/src/ai/backend/manager/repositories/permission_controller/repository.py index c37261cd6af..3584a0fe98e 100644 --- a/src/ai/backend/manager/repositories/permission_controller/repository.py +++ b/src/ai/backend/manager/repositories/permission_controller/repository.py @@ -118,6 +118,22 @@ async def delete_permission( row = await self._db_source.delete_permission(purger) return row.to_data() + @permission_controller_repository_resilience.apply() + async def update_permission( + self, + updater: Updater[PermissionRow], + ) -> PermissionData: + """ + Update a permission in the database. + + Returns the updated permission data. + + Raises: + ObjectNotFound: If permission does not exist. + """ + row = await self._db_source.update_permission(updater) + return row.to_data() + @permission_controller_repository_resilience.apply() async def create_object_permission( self, diff --git a/src/ai/backend/manager/repositories/permission_controller/updaters.py b/src/ai/backend/manager/repositories/permission_controller/updaters.py index 9cbf210cf27..10879a14a33 100644 --- a/src/ai/backend/manager/repositories/permission_controller/updaters.py +++ b/src/ai/backend/manager/repositories/permission_controller/updaters.py @@ -4,7 +4,13 @@ from typing import Any, override from ai.backend.manager.data.permission.status import RoleStatus -from ai.backend.manager.data.permission.types import RoleSource +from ai.backend.manager.data.permission.types import ( + EntityType, + OperationType, + RoleSource, + ScopeType, +) +from ai.backend.manager.models.rbac_models.permission.permission import PermissionRow from ai.backend.manager.models.rbac_models.role import RoleRow from ai.backend.manager.repositories.base.updater import UpdaterSpec from ai.backend.manager.types import OptionalState, TriState @@ -32,3 +38,27 @@ def build_values(self) -> dict[str, Any]: self.status.update_dict(to_update, "status") self.description.update_dict(to_update, "description") return to_update + + +@dataclass +class PermissionUpdaterSpec(UpdaterSpec[PermissionRow]): + """UpdaterSpec for permission updates.""" + + scope_type: OptionalState[ScopeType] = field(default_factory=OptionalState.nop) + scope_id: OptionalState[str] = field(default_factory=OptionalState.nop) + entity_type: OptionalState[EntityType] = field(default_factory=OptionalState.nop) + operation: OptionalState[OperationType] = field(default_factory=OptionalState.nop) + + @property + @override + def row_class(self) -> type[PermissionRow]: + return PermissionRow + + @override + def build_values(self) -> dict[str, Any]: + to_update: dict[str, Any] = {} + self.scope_type.update_dict(to_update, "scope_type") + self.scope_id.update_dict(to_update, "scope_id") + self.entity_type.update_dict(to_update, "entity_type") + self.operation.update_dict(to_update, "operation") + return to_update diff --git a/src/ai/backend/manager/services/permission_contoller/actions/__init__.py b/src/ai/backend/manager/services/permission_contoller/actions/__init__.py index cc0aec80b93..ae45ab92261 100644 --- a/src/ai/backend/manager/services/permission_contoller/actions/__init__.py +++ b/src/ai/backend/manager/services/permission_contoller/actions/__init__.py @@ -18,6 +18,7 @@ SearchUsersAssignedToRoleAction, SearchUsersAssignedToRoleActionResult, ) +from .update_permission import UpdatePermissionAction, UpdatePermissionActionResult from .update_role import UpdateRoleAction, UpdateRoleActionResult from .update_role_permissions import ( UpdateRolePermissionsAction, @@ -47,6 +48,8 @@ "SearchPermissionsActionResult", "SearchUsersAssignedToRoleAction", "SearchUsersAssignedToRoleActionResult", + "UpdatePermissionAction", + "UpdatePermissionActionResult", "UpdateRoleAction", "UpdateRoleActionResult", "UpdateRolePermissionsAction", diff --git a/src/ai/backend/manager/services/permission_contoller/actions/update_permission.py b/src/ai/backend/manager/services/permission_contoller/actions/update_permission.py new file mode 100644 index 00000000000..5e053d74aef --- /dev/null +++ b/src/ai/backend/manager/services/permission_contoller/actions/update_permission.py @@ -0,0 +1,32 @@ +from dataclasses import dataclass +from typing import override + +from ai.backend.manager.actions.action import BaseActionResult +from ai.backend.manager.actions.types import ActionOperationType +from ai.backend.manager.data.permission.permission import PermissionData +from ai.backend.manager.models.rbac_models.permission.permission import PermissionRow +from ai.backend.manager.repositories.base.updater import Updater +from ai.backend.manager.services.permission_contoller.actions.permission import PermissionAction + + +@dataclass +class UpdatePermissionAction(PermissionAction): + updater: Updater[PermissionRow] + + @override + def entity_id(self) -> str | None: + return str(self.updater.pk_value) + + @override + @classmethod + def operation_type(cls) -> ActionOperationType: + return ActionOperationType.UPDATE + + +@dataclass +class UpdatePermissionActionResult(BaseActionResult): + data: PermissionData + + @override + def entity_id(self) -> str | None: + return str(self.data.id) if self.data else None diff --git a/src/ai/backend/manager/services/permission_contoller/processors.py b/src/ai/backend/manager/services/permission_contoller/processors.py index e5c0462db4f..c3f45f53603 100644 --- a/src/ai/backend/manager/services/permission_contoller/processors.py +++ b/src/ai/backend/manager/services/permission_contoller/processors.py @@ -55,6 +55,10 @@ SearchScopesAction, SearchScopesActionResult, ) +from .actions.update_permission import ( + UpdatePermissionAction, + UpdatePermissionActionResult, +) from .service import PermissionControllerService @@ -83,6 +87,7 @@ class PermissionControllerProcessors(AbstractProcessorPackage): ] search_permissions: ActionProcessor[SearchPermissionsAction, SearchPermissionsActionResult] create_permission: ActionProcessor[CreatePermissionAction, CreatePermissionActionResult] + update_permission: ActionProcessor[UpdatePermissionAction, UpdatePermissionActionResult] delete_permission: ActionProcessor[DeletePermissionAction, DeletePermissionActionResult] def __init__( @@ -111,6 +116,7 @@ def __init__( ) self.search_permissions = ActionProcessor(service.search_permissions, action_monitors) self.create_permission = ActionProcessor(service.create_permission, action_monitors) + self.update_permission = ActionProcessor(service.update_permission, action_monitors) self.delete_permission = ActionProcessor(service.delete_permission, action_monitors) @override @@ -133,5 +139,6 @@ def supported_actions(self) -> list[ActionSpec]: SearchElementAssociationsAction.spec(), SearchPermissionsAction.spec(), CreatePermissionAction.spec(), + UpdatePermissionAction.spec(), DeletePermissionAction.spec(), ] diff --git a/src/ai/backend/manager/services/permission_contoller/service.py b/src/ai/backend/manager/services/permission_contoller/service.py index 957748f7043..be40a7a1f2b 100644 --- a/src/ai/backend/manager/services/permission_contoller/service.py +++ b/src/ai/backend/manager/services/permission_contoller/service.py @@ -80,6 +80,10 @@ SearchUsersAssignedToRoleAction, SearchUsersAssignedToRoleActionResult, ) +from ai.backend.manager.services.permission_contoller.actions.update_permission import ( + UpdatePermissionAction, + UpdatePermissionActionResult, +) from ai.backend.manager.services.permission_contoller.actions.update_role import ( UpdateRoleAction, UpdateRoleActionResult, @@ -129,6 +133,15 @@ async def delete_permission( result = await self._repository.delete_permission(action.purger) return DeletePermissionActionResult(data=result) + async def update_permission( + self, action: UpdatePermissionAction + ) -> UpdatePermissionActionResult: + """ + Updates an existing permission in the repository. + """ + result = await self._repository.update_permission(action.updater) + return UpdatePermissionActionResult(data=result) + async def create_object_permission( self, action: CreateObjectPermissionAction ) -> CreateObjectPermissionActionResult: From 61d4330e60d691b12fed82910a0f9917fd547c80 Mon Sep 17 00:00:00 2001 From: Sanghun Lee Date: Fri, 6 Mar 2026 02:44:21 +0900 Subject: [PATCH 2/2] changelog: add news fragment for PR #9721 Co-Authored-By: Claude Opus 4.6 --- changes/9721.feature.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 changes/9721.feature.md diff --git a/changes/9721.feature.md b/changes/9721.feature.md new file mode 100644 index 00000000000..23272a47c16 --- /dev/null +++ b/changes/9721.feature.md @@ -0,0 +1 @@ +Add service/repository layer for permission update