Skip to content

Commit 0c686f4

Browse files
fregataaclaude
andcommitted
refactor(BA-5795): unify delete actions into RBAC-enforced DeleteVFolderV2Action
Remove the separate delete_v2_rbac path and convert the existing delete_v2 action to use SingleEntityActionProcessor with RBAC validators, following the same pattern applied to purge_v2 in BA-5765. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 6ab45a2 commit 0c686f4

5 files changed

Lines changed: 32 additions & 127 deletions

File tree

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

Lines changed: 3 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -94,12 +94,10 @@
9494
combine_conditions_or,
9595
negate_conditions,
9696
)
97-
from ai.backend.manager.repositories.base.updater import Updater
9897
from ai.backend.manager.repositories.vfolder.types import (
9998
ProjectVFolderSearchScope,
10099
UserVFolderSearchScope,
101100
)
102-
from ai.backend.manager.repositories.vfolder.updaters import VFolderTrashUpdaterSpec
103101
from ai.backend.manager.services.deployment.actions.create_deployment import CreateDeploymentAction
104102
from ai.backend.manager.services.vfolder.actions.admin_search_vfolders import (
105103
AdminSearchVFoldersAction,
@@ -133,9 +131,6 @@
133131
DeleteVFolderV2Action,
134132
PurgeVFolderV2Action,
135133
)
136-
from ai.backend.manager.services.vfolder.actions.vfolder_v2_rbac import (
137-
DeleteVFolderV2RBACAction,
138-
)
139134

140135
from .base import BaseAdapter
141136

@@ -393,9 +388,8 @@ async def get(self, vfolder_id: UUID) -> VFolderNode:
393388

394389
async def delete(self, vfolder_id: UUID) -> DeleteVFolderPayload:
395390
"""Soft-delete a vfolder (move to trash). RBAC enforced."""
396-
updater = Updater(spec=VFolderTrashUpdaterSpec(), pk_value=vfolder_id)
397-
action = DeleteVFolderV2RBACAction(vfolder_id=vfolder_id, updater=updater)
398-
await self._processors.vfolder.delete_v2_rbac.wait_for_complete(action)
391+
action = DeleteVFolderV2Action(vfolder_id=vfolder_id)
392+
await self._processors.vfolder.delete_v2.wait_for_complete(action)
399393
return DeleteVFolderPayload(id=vfolder_id)
400394

401395
async def restore(self, vfolder_id: UUID) -> RestoreVFolderPayload:
@@ -497,11 +491,8 @@ async def deploy(
497491

498492
async def bulk_delete(self, input: BulkDeleteVFoldersInput) -> BulkDeleteVFoldersPayload:
499493
"""Soft-delete multiple vfolders."""
500-
me = current_user()
501-
if me is None:
502-
raise UnreachableError("User context is not available")
503494
for vfolder_id in input.ids:
504-
action = DeleteVFolderV2Action(user_id=me.user_id, vfolder_id=vfolder_id)
495+
action = DeleteVFolderV2Action(vfolder_id=vfolder_id)
505496
await self._processors.vfolder.delete_v2.wait_for_complete(action)
506497
return BulkDeleteVFoldersPayload(deleted_count=len(input.ids))
507498

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,35 +13,31 @@
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
19-
class DeleteVFolderV2Action(BaseScopeAction):
20-
user_id: uuid.UUID
23+
class DeleteVFolderV2Action(VFolderSingleEntityAction):
24+
"""Soft-delete a vfolder by ID with RBAC enforcement."""
25+
2126
vfolder_id: uuid.UUID
2227

2328
@override
24-
@classmethod
25-
def entity_type(cls) -> EntityType:
26-
return EntityType.VFOLDER
29+
def entity_id(self) -> str | None:
30+
return str(self.vfolder_id)
2731

2832
@override
2933
@classmethod
3034
def operation_type(cls) -> ActionOperationType:
3135
return ActionOperationType.DELETE
3236

3337
@override
34-
def entity_id(self) -> str | None:
38+
def target_entity_id(self) -> str:
3539
return str(self.vfolder_id)
3640

37-
@override
38-
def scope_type(self) -> ScopeType:
39-
return ScopeType.USER
40-
41-
@override
42-
def scope_id(self) -> str:
43-
return str(self.user_id)
44-
4541
@override
4642
def target_element(self) -> RBACElementRef:
4743
return RBACElementRef(
@@ -51,13 +47,17 @@ def target_element(self) -> RBACElementRef:
5147

5248

5349
@dataclass
54-
class DeleteVFolderV2ActionResult(BaseActionResult):
50+
class DeleteVFolderV2ActionResult(VFolderSingleEntityActionResult):
5551
vfolder_id: uuid.UUID
5652

5753
@override
5854
def entity_id(self) -> str | None:
5955
return str(self.vfolder_id)
6056

57+
@override
58+
def target_entity_id(self) -> str:
59+
return str(self.vfolder_id)
60+
6161

6262
@dataclass
6363
class PurgeVFolderV2Action(BaseScopeAction):

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

Lines changed: 0 additions & 66 deletions
This file was deleted.

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

Lines changed: 4 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -98,10 +98,6 @@
9898
PurgeVFolderV2Action,
9999
PurgeVFolderV2ActionResult,
100100
)
101-
from ai.backend.manager.services.vfolder.actions.vfolder_v2_rbac import (
102-
DeleteVFolderV2RBACAction,
103-
DeleteVFolderV2RBACActionResult,
104-
)
105101
from ai.backend.manager.services.vfolder.services.vfolder import VFolderService
106102

107103

@@ -164,12 +160,9 @@ class VFolderProcessors(AbstractProcessorPackage):
164160
create_upload_session_v2: ActionProcessor[
165161
CreateUploadSessionV2Action, CreateUploadSessionV2ActionResult
166162
]
167-
delete_v2: ActionProcessor[DeleteVFolderV2Action, DeleteVFolderV2ActionResult]
163+
delete_v2: SingleEntityActionProcessor[DeleteVFolderV2Action, DeleteVFolderV2ActionResult]
168164
purge_v2: ActionProcessor[PurgeVFolderV2Action, PurgeVFolderV2ActionResult]
169165
clone_v2: ActionProcessor[CloneVFolderV2Action, CloneVFolderV2ActionResult]
170-
delete_v2_rbac: SingleEntityActionProcessor[
171-
DeleteVFolderV2RBACAction, DeleteVFolderV2RBACActionResult
172-
]
173166

174167
def __init__(
175168
self,
@@ -261,12 +254,11 @@ def __init__(
261254
self.create_upload_session_v2 = ActionProcessor(
262255
service.create_upload_session_v2, action_monitors
263256
)
264-
self.delete_v2 = ActionProcessor(service.delete_v2, action_monitors)
257+
self.delete_v2 = SingleEntityActionProcessor(
258+
service.delete_v2, action_monitors, validators=single_entity_rbac_validators
259+
)
265260
self.purge_v2 = ActionProcessor(service.purge_v2, action_monitors)
266261
self.clone_v2 = ActionProcessor(service.clone_v2, action_monitors)
267-
self.delete_v2_rbac = SingleEntityActionProcessor(
268-
service.delete_v2_rbac, action_monitors, validators=single_entity_rbac_validators
269-
)
270262

271263
@override
272264
def supported_actions(self) -> list[ActionSpec]:
@@ -306,5 +298,4 @@ def supported_actions(self) -> list[ActionSpec]:
306298
DeleteVFolderV2Action.spec(),
307299
PurgeVFolderV2Action.spec(),
308300
CloneVFolderV2Action.spec(),
309-
DeleteVFolderV2RBACAction.spec(),
310301
]

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

Lines changed: 9 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,10 @@
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
22+
from ai.backend.common.exception import UnreachableError
2123
from ai.backend.common.types import (
2224
QuotaScopeID,
2325
QuotaScopeType,
@@ -162,10 +164,6 @@
162164
PurgeVFolderV2Action,
163165
PurgeVFolderV2ActionResult,
164166
)
165-
from ai.backend.manager.services.vfolder.actions.vfolder_v2_rbac import (
166-
DeleteVFolderV2RBACAction,
167-
DeleteVFolderV2RBACActionResult,
168-
)
169167
from ai.backend.manager.services.vfolder.types import (
170168
VFolderBaseInfo,
171169
VFolderOwnershipInfo,
@@ -1630,19 +1628,17 @@ async def get_v2(self, action: GetVFolderV2Action) -> GetVFolderV2ActionResult:
16301628
return GetVFolderV2ActionResult(vfolder=vfolder_data)
16311629

16321630
async def delete_v2(self, action: DeleteVFolderV2Action) -> DeleteVFolderV2ActionResult:
1633-
"""Delete (trash) a vfolder (v2). Resolves policy internally from user_id."""
1634-
user = await self._user_repository.get_user_by_uuid(action.user_id)
1635-
if not user.domain_name:
1636-
raise VFolderInvalidParameter("User has no domain assigned")
1637-
vfolder_data = await self._vfolder_repository.get_by_id_validated(
1638-
action.vfolder_id, user.id, user.domain_name
1639-
)
1631+
"""Soft-delete a vfolder by ID. RBAC enforced at processor level."""
1632+
me = current_user()
1633+
if me is None:
1634+
raise UnreachableError("User context is not available")
1635+
vfolder_data = await self._vfolder_repository.get_by_id(action.vfolder_id)
16401636

1641-
# Host permission check — resolved from user_id
1637+
# Host permission check — resolved from current user context
16421638
await self._vfolder_repository.ensure_host_permission_allowed_by_user(
16431639
vfolder_data.host,
16441640
permission=VFolderHostPermission.DELETE,
1645-
user_uuid=action.user_id,
1641+
user_uuid=me.user_id,
16461642
)
16471643

16481644
await self._vfolder_repository.move_vfolders_to_trash([vfolder_data.id])
@@ -1668,13 +1664,6 @@ async def purge_v2(self, action: PurgeVFolderV2Action) -> PurgeVFolderV2ActionRe
16681664
await self._remove_vfolder_from_storage(vfolder_data)
16691665
return PurgeVFolderV2ActionResult(vfolder_id=action.vfolder_id)
16701666

1671-
async def delete_v2_rbac(
1672-
self, action: DeleteVFolderV2RBACAction
1673-
) -> DeleteVFolderV2RBACActionResult:
1674-
"""Soft-delete a vfolder by ID. RBAC enforced at processor level."""
1675-
await self._vfolder_repository.trash_vfolder(action.updater)
1676-
return DeleteVFolderV2RBACActionResult(vfolder_id=action.vfolder_id)
1677-
16781667
async def clone_v2(self, action: CloneVFolderV2Action) -> CloneVFolderV2ActionResult:
16791668
"""Clone a vfolder (v2). Resolves policy internally from user_id."""
16801669
allowed_vfolder_types = (

0 commit comments

Comments
 (0)