Skip to content

Commit 71e1b53

Browse files
fregataaclaudelablup-octodog
authored andcommitted
feat(BA-5764): add RBAC-enforced VFolder delete and restore mutations (#11164)
Co-authored-by: Claude Opus 4.6 (1M context) <[email protected]> Co-authored-by: octodog <[email protected]>
1 parent 94a087e commit 71e1b53

20 files changed

Lines changed: 657 additions & 4 deletions

File tree

changes/11164.feature.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Add RBAC-enforced VFolder delete and restore v2 mutations with SingleEntityActionProcessor.

docs/manager/graphql-reference/supergraph.graphql

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10804,6 +10804,11 @@ type Mutation
1080410804
"""Added in 26.4.2. Permanently purge a virtual folder."""
1080510805
purgeVfolderV2(vfolderId: UUID!): PurgeVFolderV2Payload! @join__field(graph: STRAWBERRY)
1080610806

10807+
"""
10808+
Added in UNRELEASED. Restore a trashed virtual folder. RBAC enforced via scope chain.
10809+
"""
10810+
restoreVFolder(vfolderId: UUID!): RestoreVFolderPayload! @join__field(graph: STRAWBERRY)
10811+
1080710812
"""Added in 26.4.2. Deploy a deployment directly from a model VFolder."""
1080810813
deployVfolderV2(vfolderId: UUID!, input: DeployVFolderV2Input!): DeployVFolderV2Payload! @join__field(graph: STRAWBERRY)
1080910814

@@ -14638,6 +14643,16 @@ type RestoreArtifactsPayload
1463814643
artifacts: [Artifact!]!
1463914644
}
1464014645

14646+
"""
14647+
Added in UNRELEASED. Payload returned after restoring a virtual folder from trash.
14648+
"""
14649+
type RestoreVFolderPayload
14650+
@join__type(graph: STRAWBERRY)
14651+
{
14652+
"""ID of the restored virtual folder"""
14653+
id: UUID!
14654+
}
14655+
1464114656
"""
1464214657
Added in 26.4.3. Per-deployment result of an admin bulk revision refresh.
1464314658
"""

docs/manager/graphql-reference/v2-schema.graphql

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6794,6 +6794,11 @@ type Mutation {
67946794
"""Added in 26.4.2. Permanently purge a virtual folder."""
67956795
purgeVfolderV2(vfolderId: UUID!): PurgeVFolderV2Payload!
67966796

6797+
"""
6798+
Added in UNRELEASED. Restore a trashed virtual folder. RBAC enforced via scope chain.
6799+
"""
6800+
restoreVFolder(vfolderId: UUID!): RestoreVFolderPayload!
6801+
67976802
"""Added in 26.4.2. Deploy a deployment directly from a model VFolder."""
67986803
deployVfolderV2(vfolderId: UUID!, input: DeployVFolderV2Input!): DeployVFolderV2Payload!
67996804

@@ -9696,6 +9701,14 @@ type RestoreArtifactsPayload {
96969701
artifacts: [Artifact!]!
96979702
}
96989703

9704+
"""
9705+
Added in UNRELEASED. Payload returned after restoring a virtual folder from trash.
9706+
"""
9707+
type RestoreVFolderPayload {
9708+
"""ID of the restored virtual folder"""
9709+
id: UUID!
9710+
}
9711+
96999712
"""
97009713
Added in 26.4.3. Per-deployment result of an admin bulk revision refresh.
97019714
"""

src/ai/backend/client/cli/v2/vfolder/commands.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,22 @@ async def _run() -> None:
195195
asyncio.run(_run())
196196

197197

198+
@vfolder.command()
199+
@click.argument("vfolder_id", type=click.UUID)
200+
def restore(vfolder_id: UUID) -> None:
201+
"""Restore a trashed vfolder."""
202+
203+
async def _run() -> None:
204+
registry = await create_v2_registry(load_v2_config())
205+
try:
206+
result = await registry.vfolder.restore(vfolder_id)
207+
print_result(result)
208+
finally:
209+
await registry.close()
210+
211+
asyncio.run(_run())
212+
213+
198214
@vfolder.command()
199215
@click.argument("vfolder_id", type=click.UUID)
200216
@click.option(

src/ai/backend/client/v2/domains_v2/vfolder.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
MkdirPayload,
3434
MoveFilePayload,
3535
PurgeVFolderPayload,
36+
RestoreVFolderPayload,
3637
SearchVFoldersPayload,
3738
VFolderNode,
3839
)
@@ -126,6 +127,14 @@ async def purge(self, vfolder_id: UUID) -> PurgeVFolderPayload:
126127
response_model=PurgeVFolderPayload,
127128
)
128129

130+
async def restore(self, vfolder_id: UUID) -> RestoreVFolderPayload:
131+
"""Restore a trashed vfolder."""
132+
return await self._client.typed_request(
133+
"POST",
134+
f"{_PATH}/{vfolder_id}/restore",
135+
response_model=RestoreVFolderPayload,
136+
)
137+
129138
async def deploy(
130139
self,
131140
vfolder_id: UUID,

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

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
MkdirPayload,
4141
MoveFilePayload,
4242
PurgeVFolderPayload,
43+
RestoreVFolderPayload,
4344
SearchVFoldersPayload,
4445
VFolderNode,
4546
)
@@ -93,14 +94,19 @@
9394
combine_conditions_or,
9495
negate_conditions,
9596
)
97+
from ai.backend.manager.repositories.base.updater import Updater
9698
from ai.backend.manager.repositories.vfolder.types import (
9799
ProjectVFolderSearchScope,
98100
UserVFolderSearchScope,
99101
)
102+
from ai.backend.manager.repositories.vfolder.updaters import VFolderTrashUpdaterSpec
100103
from ai.backend.manager.services.deployment.actions.create_deployment import CreateDeploymentAction
101104
from ai.backend.manager.services.vfolder.actions.admin_search_vfolders import (
102105
AdminSearchVFoldersAction,
103106
)
107+
from ai.backend.manager.services.vfolder.actions.base import (
108+
RestoreVFolderFromTrashAction,
109+
)
104110
from ai.backend.manager.services.vfolder.actions.batch_load_by_ids import (
105111
BatchLoadVFoldersByIdsAction,
106112
)
@@ -127,6 +133,9 @@
127133
DeleteVFolderV2Action,
128134
PurgeVFolderV2Action,
129135
)
136+
from ai.backend.manager.services.vfolder.actions.vfolder_v2_rbac import (
137+
DeleteVFolderV2RBACAction,
138+
)
130139

131140
from .base import BaseAdapter
132141

@@ -383,13 +392,23 @@ async def get(self, vfolder_id: UUID) -> VFolderNode:
383392
return self._vfolder_data_to_node(result.vfolder)
384393

385394
async def delete(self, vfolder_id: UUID) -> DeleteVFolderPayload:
386-
"""Soft-delete a vfolder (move to trash)."""
395+
"""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)
399+
return DeleteVFolderPayload(id=vfolder_id)
400+
401+
async def restore(self, vfolder_id: UUID) -> RestoreVFolderPayload:
402+
"""Restore a trashed vfolder. RBAC enforced."""
387403
me = current_user()
388404
if me is None:
389405
raise UnreachableError("User context is not available")
390-
action = DeleteVFolderV2Action(user_id=me.user_id, vfolder_id=vfolder_id)
391-
await self._processors.vfolder.delete_v2.wait_for_complete(action)
392-
return DeleteVFolderPayload(id=vfolder_id)
406+
action = RestoreVFolderFromTrashAction(
407+
user_uuid=me.user_id,
408+
vfolder_uuid=vfolder_id,
409+
)
410+
await self._processors.vfolder.restore_vfolder_from_trash.wait_for_complete(action)
411+
return RestoreVFolderPayload(id=vfolder_id)
393412

394413
async def purge(self, vfolder_id: UUID) -> PurgeVFolderPayload:
395414
"""Permanently delete a vfolder."""

src/ai/backend/manager/api/gql/schema.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -421,6 +421,7 @@
421421
my_vfolders,
422422
project_vfolders,
423423
purge_vfolder_v2,
424+
restore_vfolder_v2,
424425
vfolder_create_download_session_v2,
425426
vfolder_create_upload_session_v2,
426427
vfolder_delete_files_v2,
@@ -842,6 +843,7 @@ class Mutation:
842843
create_vfolder_v2 = create_vfolder_v2
843844
delete_vfolder_v2 = delete_vfolder_v2
844845
purge_vfolder_v2 = purge_vfolder_v2
846+
restore_vfolder_v2 = restore_vfolder_v2
845847
deploy_vfolder_v2 = deploy_vfolder_v2
846848
bulk_delete_vfolders_v2 = bulk_delete_vfolders_v2
847849
bulk_purge_vfolders_v2 = bulk_purge_vfolders_v2

src/ai/backend/manager/api/gql/vfolder_v2/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
my_vfolders,
1616
project_vfolders,
1717
purge_vfolder_v2,
18+
restore_vfolder_v2,
1819
vfolder_create_download_session_v2,
1920
vfolder_create_upload_session_v2,
2021
vfolder_delete_files_v2,
@@ -51,6 +52,7 @@
5152
"delete_vfolder_v2",
5253
"deploy_vfolder_v2",
5354
"purge_vfolder_v2",
55+
"restore_vfolder_v2",
5456
"clone_vfolder_v2",
5557
"vfolder_list_files_v2",
5658
"vfolder_mkdir_v2",

src/ai/backend/manager/api/gql/vfolder_v2/resolver/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
delete_vfolder_v2,
99
deploy_vfolder_v2,
1010
purge_vfolder_v2,
11+
restore_vfolder_v2,
1112
vfolder_create_download_session_v2,
1213
vfolder_create_upload_session_v2,
1314
vfolder_delete_files_v2,
@@ -30,6 +31,7 @@
3031
"delete_vfolder_v2",
3132
"deploy_vfolder_v2",
3233
"purge_vfolder_v2",
34+
"restore_vfolder_v2",
3335
"clone_vfolder_v2",
3436
"vfolder_list_files_v2",
3537
"vfolder_mkdir_v2",

src/ai/backend/manager/api/gql/vfolder_v2/resolver/mutation.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
from strawberry import Info
88

9+
from ai.backend.common.meta.meta import NEXT_RELEASE_VERSION
910
from ai.backend.manager.api.gql.decorators import BackendAIGQLMeta, gql_mutation
1011
from ai.backend.manager.api.gql.types import StrawberryGQLContext
1112
from ai.backend.manager.api.gql.vfolder_v2.types.mutations import (
@@ -31,6 +32,7 @@
3132
MoveFileInputGQL,
3233
MoveFilePayloadGQL,
3334
PurgeVFolderPayloadGQL,
35+
RestoreVFolderPayloadGQL,
3436
UploadSessionInputGQL,
3537
UploadSessionPayloadGQL,
3638
)
@@ -78,6 +80,22 @@ async def purge_vfolder_v2(
7880
return PurgeVFolderPayloadGQL.from_pydantic(payload)
7981

8082

83+
@gql_mutation(
84+
BackendAIGQLMeta(
85+
added_version=NEXT_RELEASE_VERSION,
86+
description="Restore a trashed virtual folder. RBAC enforced via scope chain.",
87+
),
88+
name="restoreVFolder",
89+
) # type: ignore[misc]
90+
async def restore_vfolder_v2(
91+
info: Info[StrawberryGQLContext],
92+
vfolder_id: UUID,
93+
) -> RestoreVFolderPayloadGQL:
94+
"""Restore a virtual folder from trash."""
95+
payload = await info.context.adapters.vfolder.restore(vfolder_id)
96+
return RestoreVFolderPayloadGQL.from_pydantic(payload)
97+
98+
8199
@gql_mutation(
82100
BackendAIGQLMeta(
83101
added_version="26.4.2",

0 commit comments

Comments
 (0)