Skip to content

Commit 5821d9e

Browse files
fregataaclaude
andauthored
feat(BA-3696): Apply RBAC validator for User actions (#10055)
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
1 parent 313121a commit 5821d9e

17 files changed

Lines changed: 317 additions & 97 deletions

File tree

changes/10055.feature.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Apply RBAC validator for User actions following the established pattern from Group, VFolder, and Session services

src/ai/backend/manager/api/rest/user/handler.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,10 @@ async def create_user(
102102
)
103103

104104
action_result = await self._user.create_user.wait_for_complete(
105-
CreateUserAction(creator=creator, group_ids=body.parsed.group_ids)
105+
CreateUserAction(
106+
creator=creator,
107+
group_ids=body.parsed.group_ids,
108+
)
106109
)
107110

108111
resp = CreateUserResponse(user=self._adapter.convert_to_dto(action_result.data.user))
@@ -182,7 +185,7 @@ async def update_user(
182185
updater = self._adapter.build_updater(body.parsed, email, password_info)
183186

184187
action_result = await self._user.modify_user.wait_for_complete(
185-
ModifyUserAction(email=email, updater=updater)
188+
ModifyUserAction(user_uuid=path.parsed.user_id, email=email, updater=updater)
186189
)
187190

188191
resp = UpdateUserResponse(user=self._adapter.convert_to_dto(action_result.data))
@@ -247,6 +250,7 @@ async def purge_user(
247250

248251
await self._user.purge_user.wait_for_complete(
249252
PurgeUserAction(
253+
user_uuid=body.parsed.user_id,
250254
user_info_ctx=user_info_ctx,
251255
email=get_result.user.email,
252256
purge_shared_vfolders=purge_shared,

src/ai/backend/manager/repositories/user/db_source/db_source.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -107,8 +107,7 @@
107107
RoleUserSearchScope,
108108
)
109109
from ai.backend.manager.repositories.user.updaters import UserUpdaterSpec
110-
from ai.backend.manager.services.user.actions.create_user import UserCreateSpec
111-
from ai.backend.manager.services.user.actions.modify_user import UserUpdateSpec
110+
from ai.backend.manager.services.user.types import UserCreateSpec, UserUpdateSpec
112111

113112
log = BraceStyleAdapter(logging.getLogger(__spec__.name))
114113

src/ai/backend/manager/repositories/user/repository.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,8 +40,7 @@
4040
ProjectUserSearchScope,
4141
RoleUserSearchScope,
4242
)
43-
from ai.backend.manager.services.user.actions.create_user import UserCreateSpec
44-
from ai.backend.manager.services.user.actions.modify_user import UserUpdateSpec
43+
from ai.backend.manager.services.user.types import UserCreateSpec, UserUpdateSpec
4544

4645
log = BraceStyleAdapter(logging.getLogger(__spec__.name))
4746

src/ai/backend/manager/services/user/actions/__init__.py

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,19 @@
77
AdminMonthStatsAction,
88
AdminMonthStatsActionResult,
99
)
10+
from .base import (
11+
UserAction,
12+
UserScopeAction,
13+
UserScopeActionResult,
14+
UserSingleEntityAction,
15+
UserSingleEntityActionResult,
16+
)
1017
from .create_user import (
18+
BulkCreateUserAction,
19+
BulkCreateUserActionResult,
1120
CreateUserAction,
1221
CreateUserActionResult,
22+
UserCreateSpec,
1323
)
1424
from .delete_user import (
1525
DeleteUserAction,
@@ -20,8 +30,11 @@
2030
GetUserActionResult,
2131
)
2232
from .modify_user import (
33+
BulkModifyUserAction,
34+
BulkModifyUserActionResult,
2335
ModifyUserAction,
2436
ModifyUserActionResult,
37+
UserUpdateSpec,
2538
)
2639
from .purge_user import (
2740
BulkPurgeUserAction,
@@ -41,6 +54,10 @@
4154
SearchUsersByProjectAction,
4255
SearchUsersByProjectActionResult,
4356
)
57+
from .search_users_by_role import (
58+
SearchUsersByRoleAction,
59+
SearchUsersByRoleActionResult,
60+
)
4461
from .user_month_stats import (
4562
UserMonthStatsAction,
4663
UserMonthStatsActionResult,
@@ -49,6 +66,12 @@
4966
__all__ = (
5067
"AdminMonthStatsAction",
5168
"AdminMonthStatsActionResult",
69+
"BulkCreateUserAction",
70+
"BulkCreateUserActionResult",
71+
"BulkModifyUserAction",
72+
"BulkModifyUserActionResult",
73+
"BulkPurgeUserAction",
74+
"BulkPurgeUserActionResult",
5275
"CreateUserAction",
5376
"CreateUserActionResult",
5477
"DeleteUserAction",
@@ -57,8 +80,6 @@
5780
"GetUserActionResult",
5881
"ModifyUserAction",
5982
"ModifyUserActionResult",
60-
"BulkPurgeUserAction",
61-
"BulkPurgeUserActionResult",
6283
"PurgeUserAction",
6384
"PurgeUserActionResult",
6485
"SearchUsersAction",
@@ -67,6 +88,15 @@
6788
"SearchUsersByDomainActionResult",
6889
"SearchUsersByProjectAction",
6990
"SearchUsersByProjectActionResult",
91+
"SearchUsersByRoleAction",
92+
"SearchUsersByRoleActionResult",
93+
"UserAction",
94+
"UserCreateSpec",
7095
"UserMonthStatsAction",
7196
"UserMonthStatsActionResult",
97+
"UserScopeAction",
98+
"UserScopeActionResult",
99+
"UserSingleEntityAction",
100+
"UserSingleEntityActionResult",
101+
"UserUpdateSpec",
72102
)

src/ai/backend/manager/services/user/actions/base.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,42 @@
22

33
from ai.backend.common.data.permission.types import EntityType
44
from ai.backend.manager.actions.action import BaseAction
5+
from ai.backend.manager.actions.action.scope import BaseScopeAction, BaseScopeActionResult
6+
from ai.backend.manager.actions.action.single_entity import (
7+
BaseSingleEntityAction,
8+
BaseSingleEntityActionResult,
9+
)
10+
from ai.backend.manager.actions.action.types import FieldData
511

612

713
class UserAction(BaseAction):
814
@override
915
@classmethod
1016
def entity_type(cls) -> EntityType:
1117
return EntityType.USER
18+
19+
20+
class UserScopeAction(BaseScopeAction):
21+
@override
22+
@classmethod
23+
def entity_type(cls) -> EntityType:
24+
return EntityType.USER
25+
26+
27+
class UserScopeActionResult(BaseScopeActionResult):
28+
pass
29+
30+
31+
class UserSingleEntityAction(BaseSingleEntityAction):
32+
@override
33+
@classmethod
34+
def entity_type(cls) -> EntityType:
35+
return EntityType.USER
36+
37+
@override
38+
def field_data(self) -> FieldData | None:
39+
return None
40+
41+
42+
class UserSingleEntityActionResult(BaseSingleEntityActionResult):
43+
pass

src/ai/backend/manager/services/user/actions/create_user.py

Lines changed: 44 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,44 +1,73 @@
1+
from __future__ import annotations
2+
13
from dataclasses import dataclass
2-
from typing import override
4+
from typing import cast, override
35

6+
from ai.backend.common.data.permission.types import RBACElementType, ScopeType
47
from ai.backend.manager.actions.action import BaseActionResult
58
from ai.backend.manager.actions.types import ActionOperationType
9+
from ai.backend.manager.data.permission.types import RBACElementRef
610
from ai.backend.manager.data.user.types import BulkUserCreateResultData, UserCreateResultData
711
from ai.backend.manager.models.user import UserRow
812
from ai.backend.manager.repositories.base.creator import Creator
9-
from ai.backend.manager.services.user.actions.base import UserAction
13+
from ai.backend.manager.repositories.user.creators import UserCreatorSpec
14+
from ai.backend.manager.services.user.actions.base import (
15+
UserAction,
16+
UserScopeAction,
17+
UserScopeActionResult,
18+
)
19+
from ai.backend.manager.services.user.types import UserCreateSpec
20+
21+
__all__ = (
22+
"CreateUserAction",
23+
"CreateUserActionResult",
24+
"UserCreateSpec",
25+
"BulkCreateUserAction",
26+
"BulkCreateUserActionResult",
27+
)
1028

1129

1230
@dataclass
13-
class CreateUserAction(UserAction):
14-
creator: Creator[UserRow]
31+
class CreateUserAction(UserScopeAction):
32+
creator: Creator[UserRow] # spec: UserCreatorSpec
1533
group_ids: list[str] | None = None
1634

17-
@override
18-
def entity_id(self) -> str | None:
19-
return None
20-
2135
@override
2236
@classmethod
2337
def operation_type(cls) -> ActionOperationType:
2438
return ActionOperationType.CREATE
2539

40+
@override
41+
def scope_type(self) -> ScopeType:
42+
return ScopeType.DOMAIN
43+
44+
@override
45+
def scope_id(self) -> str:
46+
spec = cast(UserCreatorSpec, self.creator.spec)
47+
return spec.domain_name
48+
49+
@override
50+
def target_element(self) -> RBACElementRef:
51+
spec = cast(UserCreatorSpec, self.creator.spec)
52+
return RBACElementRef(RBACElementType.DOMAIN, spec.domain_name)
53+
2654

2755
@dataclass
28-
class CreateUserActionResult(BaseActionResult):
56+
class CreateUserActionResult(UserScopeActionResult):
2957
data: UserCreateResultData
3058

3159
@override
3260
def entity_id(self) -> str | None:
3361
return str(self.data.user.id)
3462

63+
@override
64+
def scope_type(self) -> ScopeType:
65+
return ScopeType.DOMAIN
3566

36-
@dataclass
37-
class UserCreateSpec:
38-
"""Specification for creating a single user, including group assignments."""
39-
40-
creator: Creator[UserRow]
41-
group_ids: list[str] | None = None
67+
@override
68+
def scope_id(self) -> str:
69+
# UserCreateResultData always has domain_name set (from creator.spec.domain_name)
70+
return self.data.user.domain_name or ""
4271

4372

4473
@dataclass

src/ai/backend/manager/services/user/actions/get_user.py

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,34 +2,46 @@
22
from typing import override
33
from uuid import UUID
44

5-
from ai.backend.manager.actions.action import BaseActionResult
5+
from ai.backend.common.data.permission.types import RBACElementType
66
from ai.backend.manager.actions.types import ActionOperationType
7+
from ai.backend.manager.data.permission.types import RBACElementRef
78
from ai.backend.manager.data.user.types import UserData
8-
from ai.backend.manager.services.user.actions.base import UserAction
9+
from ai.backend.manager.services.user.actions.base import (
10+
UserSingleEntityAction,
11+
UserSingleEntityActionResult,
12+
)
913

1014

1115
@dataclass
12-
class GetUserAction(UserAction):
16+
class GetUserAction(UserSingleEntityAction):
1317
"""Action to retrieve a single user by UUID."""
1418

1519
user_uuid: UUID
1620

17-
@override
18-
def entity_id(self) -> str | None:
19-
return str(self.user_uuid)
20-
2121
@override
2222
@classmethod
2323
def operation_type(cls) -> ActionOperationType:
2424
return ActionOperationType.GET
2525

26+
@override
27+
def target_entity_id(self) -> str:
28+
return str(self.user_uuid)
29+
30+
@override
31+
def target_element(self) -> RBACElementRef:
32+
return RBACElementRef(RBACElementType.USER, str(self.user_uuid))
33+
2634

2735
@dataclass
28-
class GetUserActionResult(BaseActionResult):
36+
class GetUserActionResult(UserSingleEntityActionResult):
2937
"""Result of GetUserAction containing user data."""
3038

3139
user: UserData
3240

3341
@override
3442
def entity_id(self) -> str | None:
3543
return str(self.user.uuid)
44+
45+
@override
46+
def target_entity_id(self) -> str:
47+
return str(self.user.uuid)

0 commit comments

Comments
 (0)