Skip to content

Commit b4f8119

Browse files
fregataaclaude
andcommitted
refactor(BA-5765): unify purge actions into RBAC-enforced PurgeVFolderV2Action
Merge PurgeVFolderV2RBACAction into PurgeVFolderV2Action by switching its base to VFolderSingleEntityAction and removing user_id field. The service method now uses current_user() for host permission checks instead of receiving user_id through the action. Drop the redundant purge_v2_rbac service/processor path and vfolder_v2_rbac.py module. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 35a531b commit b4f8119

4 files changed

Lines changed: 35 additions & 38 deletions

File tree

src/ai/backend/manager/api/adapters/vfolder.py

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -412,8 +412,8 @@ async def restore(self, vfolder_id: UUID) -> RestoreVFolderPayload:
412412

413413
async def purge(self, vfolder_id: UUID) -> PurgeVFolderPayload:
414414
"""Permanently delete a vfolder. RBAC enforced."""
415-
action = PurgeVFolderV2RBACAction(vfolder_id=vfolder_id)
416-
await self._processors.vfolder.purge_v2_rbac.wait_for_complete(action)
415+
action = PurgeVFolderV2Action(vfolder_id=vfolder_id)
416+
await self._processors.vfolder.purge_v2.wait_for_complete(action)
417417
return PurgeVFolderPayload(id=vfolder_id)
418418

419419
async def deploy(
@@ -504,11 +504,8 @@ async def bulk_delete(self, input: BulkDeleteVFoldersInput) -> BulkDeleteVFolder
504504

505505
async def bulk_purge(self, input: BulkPurgeVFoldersInput) -> BulkPurgeVFoldersPayload:
506506
"""Permanently purge multiple vfolders."""
507-
me = current_user()
508-
if me is None:
509-
raise UnreachableError("User context is not available")
510507
for vfolder_id in input.ids:
511-
action = PurgeVFolderV2Action(user_id=me.user_id, vfolder_id=vfolder_id)
508+
action = PurgeVFolderV2Action(vfolder_id=vfolder_id)
512509
await self._processors.vfolder.purge_v2.wait_for_complete(action)
513510
return BulkPurgeVFoldersPayload(purged_count=len(input.ids))
514511

src/ai/backend/manager/services/vfolder/actions/vfolder_v2.py

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
"""V2 vfolder actions — user_id based, no keypair_resource_policy."""
1+
"""V2 vfolder actions — RBAC-enforced via SingleEntityActionProcessor."""
22

33
import uuid
44
from dataclasses import dataclass
@@ -13,6 +13,10 @@
1313
from ai.backend.manager.actions.action.scope import BaseScopeAction
1414
from ai.backend.manager.actions.types import ActionOperationType
1515
from ai.backend.manager.data.permission.types import RBACElementRef
16+
from ai.backend.manager.services.vfolder.actions.base import (
17+
VFolderSingleEntityAction,
18+
VFolderSingleEntityActionResult,
19+
)
1620

1721

1822
@dataclass
@@ -60,32 +64,24 @@ def entity_id(self) -> str | None:
6064

6165

6266
@dataclass
63-
class PurgeVFolderV2Action(BaseScopeAction):
64-
user_id: uuid.UUID
67+
class PurgeVFolderV2Action(VFolderSingleEntityAction):
68+
"""Permanently purge a vfolder by ID with RBAC enforcement."""
69+
6570
vfolder_id: uuid.UUID
6671

6772
@override
68-
@classmethod
69-
def entity_type(cls) -> EntityType:
70-
return EntityType.VFOLDER
73+
def entity_id(self) -> str | None:
74+
return str(self.vfolder_id)
7175

7276
@override
7377
@classmethod
7478
def operation_type(cls) -> ActionOperationType:
7579
return ActionOperationType.PURGE
7680

7781
@override
78-
def entity_id(self) -> str | None:
82+
def target_entity_id(self) -> str:
7983
return str(self.vfolder_id)
8084

81-
@override
82-
def scope_type(self) -> ScopeType:
83-
return ScopeType.USER
84-
85-
@override
86-
def scope_id(self) -> str:
87-
return str(self.user_id)
88-
8985
@override
9086
def target_element(self) -> RBACElementRef:
9187
return RBACElementRef(
@@ -95,9 +91,13 @@ def target_element(self) -> RBACElementRef:
9591

9692

9793
@dataclass
98-
class PurgeVFolderV2ActionResult(BaseActionResult):
94+
class PurgeVFolderV2ActionResult(VFolderSingleEntityActionResult):
9995
vfolder_id: uuid.UUID
10096

10197
@override
10298
def entity_id(self) -> str | None:
10399
return str(self.vfolder_id)
100+
101+
@override
102+
def target_entity_id(self) -> str:
103+
return str(self.vfolder_id)

src/ai/backend/manager/services/vfolder/processors/vfolder.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -165,7 +165,7 @@ class VFolderProcessors(AbstractProcessorPackage):
165165
CreateUploadSessionV2Action, CreateUploadSessionV2ActionResult
166166
]
167167
delete_v2: ActionProcessor[DeleteVFolderV2Action, DeleteVFolderV2ActionResult]
168-
purge_v2: ActionProcessor[PurgeVFolderV2Action, PurgeVFolderV2ActionResult]
168+
purge_v2: SingleEntityActionProcessor[PurgeVFolderV2Action, PurgeVFolderV2ActionResult]
169169
clone_v2: ActionProcessor[CloneVFolderV2Action, CloneVFolderV2ActionResult]
170170
delete_v2_rbac: SingleEntityActionProcessor[
171171
DeleteVFolderV2RBACAction, DeleteVFolderV2RBACActionResult
@@ -262,7 +262,9 @@ def __init__(
262262
service.create_upload_session_v2, action_monitors
263263
)
264264
self.delete_v2 = ActionProcessor(service.delete_v2, action_monitors)
265-
self.purge_v2 = ActionProcessor(service.purge_v2, action_monitors)
265+
self.purge_v2 = SingleEntityActionProcessor(
266+
service.purge_v2, action_monitors, validators=single_entity_rbac_validators
267+
)
266268
self.clone_v2 = ActionProcessor(service.clone_v2, action_monitors)
267269
self.delete_v2_rbac = SingleEntityActionProcessor(
268270
service.delete_v2_rbac, action_monitors, validators=single_entity_rbac_validators

src/ai/backend/manager/services/vfolder/services/vfolder.py

Lines changed: 12 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
from ai.backend.common.bgtask.bgtask import BackgroundTaskManager
1818
from ai.backend.common.clients.valkey_client.valkey_stat.client import ValkeyStatClient
19+
from ai.backend.common.contexts.user import current_user
1920
from ai.backend.common.defs import VFOLDER_GROUP_PERMISSION_MODE
2021
from ai.backend.common.etcd import AsyncEtcd
2122
from ai.backend.common.types import (
@@ -1649,20 +1650,17 @@ async def delete_v2(self, action: DeleteVFolderV2Action) -> DeleteVFolderV2Actio
16491650
return DeleteVFolderV2ActionResult(vfolder_id=action.vfolder_id)
16501651

16511652
async def purge_v2(self, action: PurgeVFolderV2Action) -> PurgeVFolderV2ActionResult:
1652-
"""Purge a vfolder permanently (v2). Resolves policy internally from user_id."""
1653-
user = await self._user_repository.get_user_by_uuid(action.user_id)
1654-
if not user.domain_name:
1655-
raise VFolderInvalidParameter("User has no domain assigned")
1656-
vfolder_data = await self._vfolder_repository.get_by_id_validated(
1657-
action.vfolder_id, user.id, user.domain_name
1658-
)
1659-
1660-
# Host permission check — resolved from user_id
1661-
await self._vfolder_repository.ensure_host_permission_allowed_by_user(
1662-
vfolder_data.host,
1663-
permission=VFolderHostPermission.DELETE,
1664-
user_uuid=action.user_id,
1665-
)
1653+
"""Permanently purge a vfolder by ID. RBAC enforced at processor level."""
1654+
me = current_user()
1655+
vfolder_data = await self._vfolder_repository.get_by_id(action.vfolder_id)
1656+
1657+
# Host permission check — resolved from current user context
1658+
if me is not None:
1659+
await self._vfolder_repository.ensure_host_permission_allowed_by_user(
1660+
vfolder_data.host,
1661+
permission=VFolderHostPermission.DELETE,
1662+
user_uuid=me.user_id,
1663+
)
16661664

16671665
await self._vfolder_repository.delete_vfolders_forever([action.vfolder_id])
16681666
await self._remove_vfolder_from_storage(vfolder_data)

0 commit comments

Comments
 (0)