Skip to content

Commit be17c11

Browse files
fregataaclaude
andcommitted
fix(BA-5737): implement create_in_project without legacy role check
Replace the create_v2() delegation with an independent implementation that skips the UserRole.ADMIN/SUPERADMIN gate. Project CREATE permission is validated by the ScopeActionProcessor RBAC — the service no longer blocks regular users who hold project-level grants. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent c3ea9c2 commit be17c11

3 files changed

Lines changed: 101 additions & 21 deletions

File tree

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

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,10 @@
102102
ProjectVFolderSearchScope,
103103
UserVFolderSearchScope,
104104
)
105-
from ai.backend.manager.repositories.vfolder.updaters import VFolderAttributeUpdaterSpec
105+
from ai.backend.manager.repositories.vfolder.updaters import (
106+
VFolderAttributeUpdaterSpec,
107+
VFolderTrashUpdaterSpec,
108+
)
106109
from ai.backend.manager.services.deployment.actions.create_deployment import CreateDeploymentAction
107110
from ai.backend.manager.services.vfolder.actions.admin_search_vfolders import (
108111
AdminSearchVFoldersAction,
@@ -445,7 +448,8 @@ async def update(self, vfolder_id: UUID, input: UpdateVFolderInput) -> UpdateVFo
445448

446449
async def delete(self, vfolder_id: UUID) -> DeleteVFolderPayload:
447450
"""Soft-delete a vfolder (move to trash). RBAC enforced."""
448-
action = DeleteVFolderV2Action(vfolder_id=vfolder_id)
451+
updater = Updater(spec=VFolderTrashUpdaterSpec(), pk_value=vfolder_id)
452+
action = DeleteVFolderV2Action(vfolder_id=vfolder_id, updater=updater)
449453
await self._processors.vfolder.delete_v2.wait_for_complete(action)
450454
return DeleteVFolderPayload(id=vfolder_id)
451455

@@ -537,7 +541,8 @@ async def deploy(
537541
async def bulk_delete(self, input: BulkDeleteVFoldersInput) -> BulkDeleteVFoldersPayload:
538542
"""Soft-delete multiple vfolders. RBAC enforced per item."""
539543
for vfolder_id in input.ids:
540-
action = DeleteVFolderV2Action(vfolder_id=vfolder_id)
544+
updater = Updater(spec=VFolderTrashUpdaterSpec(), pk_value=vfolder_id)
545+
action = DeleteVFolderV2Action(vfolder_id=vfolder_id, updater=updater)
541546
await self._processors.vfolder.delete_v2.wait_for_complete(action)
542547
return BulkDeleteVFoldersPayload(deleted_count=len(input.ids))
543548

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212
from ai.backend.common.data.permission.types import RBACElementType
1313
from ai.backend.manager.actions.types import ActionOperationType
1414
from ai.backend.manager.data.permission.types import RBACElementRef
15+
from ai.backend.manager.models.vfolder import VFolderRow
16+
from ai.backend.manager.repositories.base.updater import Updater
1517
from ai.backend.manager.services.vfolder.actions.base import (
1618
VFolderSingleEntityAction,
1719
VFolderSingleEntityActionResult,
@@ -23,6 +25,7 @@ class DeleteVFolderV2Action(VFolderSingleEntityAction):
2325
"""Soft-delete a vfolder by ID with RBAC enforcement."""
2426

2527
vfolder_id: uuid.UUID
28+
updater: Updater[VFolderRow]
2629

2730
@override
2831
def entity_id(self) -> str | None:

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

Lines changed: 90 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -65,13 +65,9 @@
6565
verify_vfolder_name,
6666
vfolder_status_map,
6767
)
68-
from ai.backend.manager.repositories.base.updater import Updater
6968
from ai.backend.manager.repositories.user.repository import UserRepository
7069
from ai.backend.manager.repositories.vfolder.repository import VfolderRepository
71-
from ai.backend.manager.repositories.vfolder.updaters import (
72-
VFolderAttributeUpdaterSpec,
73-
VFolderTrashUpdaterSpec,
74-
)
70+
from ai.backend.manager.repositories.vfolder.updaters import VFolderAttributeUpdaterSpec
7571
from ai.backend.manager.services.vfolder.actions.base import (
7672
CloneVFolderAction,
7773
CloneVFolderActionResult,
@@ -1636,25 +1632,102 @@ async def get_v2(self, action: GetVFolderV2Action) -> GetVFolderV2ActionResult:
16361632
async def create_in_project(
16371633
self, action: CreateVFolderInProjectAction
16381634
) -> CreateVFolderInProjectActionResult:
1639-
"""Create a vfolder scoped to a project.
1635+
"""Create a vfolder owned by a project.
16401636
1641-
RBAC is enforced by the ScopeActionProcessor wiring; this method
1642-
delegates the heavy lifting to ``create_v2`` with ``project_id`` set.
1637+
RBAC is enforced by the ScopeActionProcessor at the processor level.
1638+
Unlike ``create_v2`` this method does NOT check ``UserRole`` — project
1639+
CREATE permission is validated by the scope RBAC validator.
16431640
"""
1644-
inner_action = CreateVFolderV2Action(
1641+
user_uuid = action.user_id
1642+
domain_name = action.domain_name
1643+
project_id = action.project_id
1644+
1645+
# Resolve host
1646+
folder_host = action.host
1647+
if not folder_host:
1648+
folder_host = self._config_provider.config.volumes.default_host
1649+
if not folder_host:
1650+
raise VFolderInvalidParameter(
1651+
"You must specify the vfolder host because the default host is not configured."
1652+
)
1653+
1654+
if action.name.startswith(".") and action.name != ".local":
1655+
raise VFolderInvalidParameter("dot-prefixed vfolders cannot be a group folder.")
1656+
1657+
# Resolve project info
1658+
group_info = await self._vfolder_repository.get_group_resource_info(project_id, domain_name)
1659+
if not group_info:
1660+
raise ProjectNotFound(f"Project with {project_id} not found.")
1661+
group_uuid, max_vfolder_count, max_quota_scope_size, group_type = group_info
1662+
1663+
quota_scope_id = QuotaScopeID(QuotaScopeType.PROJECT, group_uuid)
1664+
1665+
if group_type == ProjectType.MODEL_STORE:
1666+
if action.usage_mode != VFolderUsageMode.MODEL:
1667+
raise VFolderInvalidParameter(
1668+
"Only Model VFolder can be created under the model store project"
1669+
)
1670+
1671+
# Host permission check
1672+
await self._vfolder_repository.ensure_host_permission_allowed_by_user(
1673+
folder_host,
1674+
permission=VFolderHostPermission.CREATE,
1675+
user_uuid=user_uuid,
1676+
group_id=group_uuid,
1677+
)
1678+
1679+
# Quota check
1680+
if max_vfolder_count > 0:
1681+
current_count = await self._vfolder_repository.count_vfolders_by_group(group_uuid)
1682+
if current_count >= max_vfolder_count:
1683+
raise VFolderInvalidParameter("You cannot create more vfolders.")
1684+
1685+
# Create in storage
1686+
folder_id = uuid.uuid4()
1687+
mount_permission = action.permission
1688+
if group_type == ProjectType.MODEL_STORE:
1689+
mount_permission = VFolderPermission.READ_ONLY
1690+
1691+
try:
1692+
vfid = VFolderID(quota_scope_id, folder_id)
1693+
proxy_name, volume_name = self._storage_manager.get_proxy_and_volume(folder_host, False)
1694+
manager_client = self._storage_manager.get_manager_facing_client(proxy_name)
1695+
await manager_client.create_folder(volume_name, str(vfid), max_quota_scope_size, None)
1696+
except aiohttp.ClientResponseError as e:
1697+
raise VFolderCreationFailure from e
1698+
1699+
# Create in DB
1700+
params = VFolderCreateParams(
1701+
id=folder_id,
16451702
name=action.name,
1646-
user_id=action.user_id,
1647-
domain_name=action.domain_name,
1648-
project_id=action.project_id,
1649-
host=action.host,
1703+
domain_name=domain_name,
1704+
quota_scope_id=str(quota_scope_id),
16501705
usage_mode=action.usage_mode,
1651-
permission=action.permission,
1706+
permission=mount_permission,
1707+
host=folder_host,
1708+
creator=str(user_uuid),
1709+
creator_id=user_uuid,
1710+
ownership_type=VFolderOwnershipType.GROUP,
1711+
user=None,
1712+
group=group_uuid,
1713+
unmanaged_path=None,
16521714
cloneable=action.cloneable,
1715+
status=VFolderOperationStatus.READY,
16531716
)
1654-
inner_result = await self.create_v2(inner_action)
1717+
1718+
try:
1719+
create_owner_permission = group_type == ProjectType.MODEL_STORE
1720+
await self._vfolder_repository.create_vfolder_with_permission(
1721+
params, create_owner_permission=create_owner_permission
1722+
)
1723+
except sa_exc.DataError as e:
1724+
raise VFolderInvalidParameter from e
1725+
1726+
# Fetch created vfolder data for response
1727+
vfolder_data = await self._vfolder_repository.get_by_id(folder_id)
16551728
return CreateVFolderInProjectActionResult(
16561729
project_id=action.project_id,
1657-
vfolder=inner_result.vfolder,
1730+
vfolder=vfolder_data,
16581731
)
16591732

16601733
async def delete_v2(self, action: DeleteVFolderV2Action) -> DeleteVFolderV2ActionResult:
@@ -1666,8 +1739,7 @@ async def delete_v2(self, action: DeleteVFolderV2Action) -> DeleteVFolderV2Actio
16661739
matching row exists, which the repository translates to
16671740
``VFolderNotFound``.
16681741
"""
1669-
updater = Updater(spec=VFolderTrashUpdaterSpec(), pk_value=action.vfolder_id)
1670-
await self._vfolder_repository.trash_vfolder(updater)
1742+
await self._vfolder_repository.trash_vfolder(action.updater)
16711743
return DeleteVFolderV2ActionResult(vfolder_id=action.vfolder_id)
16721744

16731745
async def purge_v2(self, action: PurgeVFolderV2Action) -> PurgeVFolderV2ActionResult:

0 commit comments

Comments
 (0)