Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
fa5095f
feat(BA-3696): Add RBAC base classes for User actions
fregataa Mar 13, 2026
dbd791f
feat(BA-3696): Refactor user actions to use RBAC base classes
fregataa Mar 13, 2026
9993778
feat(BA-3696): Wire RBAC processors to user service
fregataa Mar 13, 2026
2392663
fix(BA-3696): Fix type errors - add _domain_name field and inherit fr…
fregataa Mar 13, 2026
246e5c7
changelog: add news fragment for PR #10055
fregataa Mar 13, 2026
1e027c8
fix(BA-3696): Derive scope info from creator.spec instead of requirin…
fregataa Mar 13, 2026
9e659ae
fix(BA-3696): Remove stale _domain_name arg and fix RBAC mock validators
fregataa Mar 13, 2026
8483c26
refactor(BA-3696): Remove TYPE_CHECKING branch in CreateUserAction
fregataa Mar 13, 2026
d181425
fix(BA-3696): Fix circular import and email-as-entity-ID in User RBAC
fregataa Mar 13, 2026
36413e7
fix(BA-3696): Pass user_uuid to ModifyUserAction and PurgeUserAction …
fregataa Mar 13, 2026
97f7c15
fix(BA-3696): Fix UserCreateSpec/UserUpdateSpec imports and add missi…
fregataa Mar 13, 2026
f569edd
chore(BA-3696): Remove stale fix-report.md
fregataa Mar 13, 2026
49c05bd
fix: sort imports to satisfy ruff I001
fregataa Mar 13, 2026
d5066bc
refactor: exclude search actions from RBAC validator application
fregataa Mar 13, 2026
0ec2791
revert(BA-3696): Remove user UUID query from legacy GQL handlers
fregataa Mar 13, 2026
37a8e71
revert(BA-3696): Restore DeleteUserAction to main state
fregataa Mar 13, 2026
c87aeb9
fix(BA-3696): Align DeleteUserAction references with reverted signature
fregataa Mar 15, 2026
506971c
fix: remove unused imports and format after rebase
fregataa Mar 16, 2026
61ad1ca
revert: restore original import paths in user repository layer
fregataa Mar 16, 2026
aaa5bed
fix(BA-3696): Restore UserCreateSpec/UserUpdateSpec to action files a…
fregataa Mar 16, 2026
61bc7b4
fix(BA-3696): Fix circular import by keeping UserCreateSpec/UserUpdat…
fregataa Mar 16, 2026
fd2c026
fix(BA-3696): Import UserCreateSpec/UserUpdateSpec from types.py in r…
fregataa Mar 16, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changes/10055.feature.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Apply RBAC validator for User actions following the established pattern from Group, VFolder, and Session services
8 changes: 6 additions & 2 deletions src/ai/backend/manager/api/rest/user/handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,10 @@ async def create_user(
)

action_result = await self._user.create_user.wait_for_complete(
CreateUserAction(creator=creator, group_ids=body.parsed.group_ids)
CreateUserAction(
creator=creator,
group_ids=body.parsed.group_ids,
)
)

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

action_result = await self._user.modify_user.wait_for_complete(
ModifyUserAction(email=email, updater=updater)
ModifyUserAction(user_uuid=path.parsed.user_id, email=email, updater=updater)
)

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

await self._user.purge_user.wait_for_complete(
PurgeUserAction(
user_uuid=body.parsed.user_id,
user_info_ctx=user_info_ctx,
email=get_result.user.email,
purge_shared_vfolders=purge_shared,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -107,8 +107,7 @@
RoleUserSearchScope,
)
from ai.backend.manager.repositories.user.updaters import UserUpdaterSpec
from ai.backend.manager.services.user.actions.create_user import UserCreateSpec
from ai.backend.manager.services.user.actions.modify_user import UserUpdateSpec
from ai.backend.manager.services.user.types import UserCreateSpec, UserUpdateSpec

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

Expand Down
3 changes: 1 addition & 2 deletions src/ai/backend/manager/repositories/user/repository.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,7 @@
ProjectUserSearchScope,
RoleUserSearchScope,
)
from ai.backend.manager.services.user.actions.create_user import UserCreateSpec
from ai.backend.manager.services.user.actions.modify_user import UserUpdateSpec
from ai.backend.manager.services.user.types import UserCreateSpec, UserUpdateSpec

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

Expand Down
34 changes: 32 additions & 2 deletions src/ai/backend/manager/services/user/actions/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,19 @@
AdminMonthStatsAction,
AdminMonthStatsActionResult,
)
from .base import (
UserAction,
UserScopeAction,
UserScopeActionResult,
UserSingleEntityAction,
UserSingleEntityActionResult,
)
from .create_user import (
BulkCreateUserAction,
BulkCreateUserActionResult,
CreateUserAction,
CreateUserActionResult,
UserCreateSpec,
)
from .delete_user import (
DeleteUserAction,
Expand All @@ -20,8 +30,11 @@
GetUserActionResult,
)
from .modify_user import (
BulkModifyUserAction,
BulkModifyUserActionResult,
ModifyUserAction,
ModifyUserActionResult,
UserUpdateSpec,
)
from .purge_user import (
BulkPurgeUserAction,
Expand All @@ -41,6 +54,10 @@
SearchUsersByProjectAction,
SearchUsersByProjectActionResult,
)
from .search_users_by_role import (
SearchUsersByRoleAction,
SearchUsersByRoleActionResult,
)
from .user_month_stats import (
UserMonthStatsAction,
UserMonthStatsActionResult,
Expand All @@ -49,6 +66,12 @@
__all__ = (
"AdminMonthStatsAction",
"AdminMonthStatsActionResult",
"BulkCreateUserAction",
"BulkCreateUserActionResult",
"BulkModifyUserAction",
"BulkModifyUserActionResult",
"BulkPurgeUserAction",
"BulkPurgeUserActionResult",
"CreateUserAction",
"CreateUserActionResult",
"DeleteUserAction",
Expand All @@ -57,8 +80,6 @@
"GetUserActionResult",
"ModifyUserAction",
"ModifyUserActionResult",
"BulkPurgeUserAction",
"BulkPurgeUserActionResult",
"PurgeUserAction",
"PurgeUserActionResult",
"SearchUsersAction",
Expand All @@ -67,6 +88,15 @@
"SearchUsersByDomainActionResult",
"SearchUsersByProjectAction",
"SearchUsersByProjectActionResult",
"SearchUsersByRoleAction",
"SearchUsersByRoleActionResult",
"UserAction",
"UserCreateSpec",
"UserMonthStatsAction",
"UserMonthStatsActionResult",
"UserScopeAction",
"UserScopeActionResult",
"UserSingleEntityAction",
"UserSingleEntityActionResult",
"UserUpdateSpec",
)
32 changes: 32 additions & 0 deletions src/ai/backend/manager/services/user/actions/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,42 @@

from ai.backend.common.data.permission.types import EntityType
from ai.backend.manager.actions.action import BaseAction
from ai.backend.manager.actions.action.scope import BaseScopeAction, BaseScopeActionResult
from ai.backend.manager.actions.action.single_entity import (
BaseSingleEntityAction,
BaseSingleEntityActionResult,
)
from ai.backend.manager.actions.action.types import FieldData


class UserAction(BaseAction):
@override
@classmethod
def entity_type(cls) -> EntityType:
return EntityType.USER


class UserScopeAction(BaseScopeAction):
@override
@classmethod
def entity_type(cls) -> EntityType:
return EntityType.USER


class UserScopeActionResult(BaseScopeActionResult):
pass


class UserSingleEntityAction(BaseSingleEntityAction):
@override
@classmethod
def entity_type(cls) -> EntityType:
return EntityType.USER

@override
def field_data(self) -> FieldData | None:
return None


class UserSingleEntityActionResult(BaseSingleEntityActionResult):
pass
59 changes: 44 additions & 15 deletions src/ai/backend/manager/services/user/actions/create_user.py
Original file line number Diff line number Diff line change
@@ -1,44 +1,73 @@
from __future__ import annotations

from dataclasses import dataclass
from typing import override
from typing import cast, override

from ai.backend.common.data.permission.types import RBACElementType, ScopeType
from ai.backend.manager.actions.action import BaseActionResult
from ai.backend.manager.actions.types import ActionOperationType
from ai.backend.manager.data.permission.types import RBACElementRef
from ai.backend.manager.data.user.types import BulkUserCreateResultData, UserCreateResultData
from ai.backend.manager.models.user import UserRow
from ai.backend.manager.repositories.base.creator import Creator
from ai.backend.manager.services.user.actions.base import UserAction
from ai.backend.manager.repositories.user.creators import UserCreatorSpec
from ai.backend.manager.services.user.actions.base import (
UserAction,
UserScopeAction,
UserScopeActionResult,
)
from ai.backend.manager.services.user.types import UserCreateSpec

__all__ = (
"CreateUserAction",
"CreateUserActionResult",
"UserCreateSpec",
"BulkCreateUserAction",
"BulkCreateUserActionResult",
)


@dataclass
class CreateUserAction(UserAction):
creator: Creator[UserRow]
class CreateUserAction(UserScopeAction):
creator: Creator[UserRow] # spec: UserCreatorSpec
group_ids: list[str] | None = None
Comment thread
fregataa marked this conversation as resolved.

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

@override
@classmethod
def operation_type(cls) -> ActionOperationType:
return ActionOperationType.CREATE

@override
def scope_type(self) -> ScopeType:
return ScopeType.DOMAIN

@override
def scope_id(self) -> str:
spec = cast(UserCreatorSpec, self.creator.spec)
return spec.domain_name

@override
def target_element(self) -> RBACElementRef:
spec = cast(UserCreatorSpec, self.creator.spec)
return RBACElementRef(RBACElementType.DOMAIN, spec.domain_name)


@dataclass
class CreateUserActionResult(BaseActionResult):
class CreateUserActionResult(UserScopeActionResult):
data: UserCreateResultData

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

@override
def scope_type(self) -> ScopeType:
return ScopeType.DOMAIN

@dataclass
class UserCreateSpec:
"""Specification for creating a single user, including group assignments."""

creator: Creator[UserRow]
group_ids: list[str] | None = None
@override
def scope_id(self) -> str:
# UserCreateResultData always has domain_name set (from creator.spec.domain_name)
return self.data.user.domain_name or ""


@dataclass
Expand Down
28 changes: 20 additions & 8 deletions src/ai/backend/manager/services/user/actions/get_user.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,34 +2,46 @@
from typing import override
from uuid import UUID

from ai.backend.manager.actions.action import BaseActionResult
from ai.backend.common.data.permission.types import RBACElementType
from ai.backend.manager.actions.types import ActionOperationType
from ai.backend.manager.data.permission.types import RBACElementRef
from ai.backend.manager.data.user.types import UserData
from ai.backend.manager.services.user.actions.base import UserAction
from ai.backend.manager.services.user.actions.base import (
UserSingleEntityAction,
UserSingleEntityActionResult,
)


@dataclass
class GetUserAction(UserAction):
class GetUserAction(UserSingleEntityAction):
"""Action to retrieve a single user by UUID."""

user_uuid: UUID

@override
def entity_id(self) -> str | None:
return str(self.user_uuid)

@override
@classmethod
def operation_type(cls) -> ActionOperationType:
return ActionOperationType.GET

@override
def target_entity_id(self) -> str:
return str(self.user_uuid)

@override
def target_element(self) -> RBACElementRef:
return RBACElementRef(RBACElementType.USER, str(self.user_uuid))


@dataclass
class GetUserActionResult(BaseActionResult):
class GetUserActionResult(UserSingleEntityActionResult):
"""Result of GetUserAction containing user data."""

user: UserData

@override
def entity_id(self) -> str | None:
return str(self.user.uuid)

@override
def target_entity_id(self) -> str:
return str(self.user.uuid)
Loading
Loading