Skip to content

Commit 1d1121c

Browse files
fregataaclaude
andauthored
feat(BA-4525): Unify ScopeType/EntityType to RBACElementType in permission controller (#10335)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent a530332 commit 1d1121c

25 files changed

Lines changed: 224 additions & 257 deletions

File tree

changes/10335.enhance.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Unify ScopeType/EntityType to RBACElementType in permission controller repository, service, and their direct GQL/REST callers

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

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -690,9 +690,9 @@ async def create_permission(self, input: CreatePermissionInputDTO) -> Permission
690690
creator: Creator[PermissionRow] = Creator(
691691
spec=PermissionCreatorSpec(
692692
role_id=input.role_id,
693-
scope_type=RBACElementType(input.scope_type).to_scope_type(),
693+
scope_type=RBACElementType(input.scope_type),
694694
scope_id=input.scope_id,
695-
entity_type=RBACElementType(input.entity_type).to_entity_type(),
695+
entity_type=RBACElementType(input.entity_type),
696696
operation=InternalOperationType(input.operation),
697697
)
698698
)
@@ -707,7 +707,7 @@ async def update_permission(self, input: UpdatePermissionInputDTO) -> Permission
707707
"""Update an existing scoped permission."""
708708
spec = PermissionUpdaterSpec(
709709
scope_type=(
710-
OptionalState.update(RBACElementType(input.scope_type).to_scope_type())
710+
OptionalState.update(RBACElementType(input.scope_type))
711711
if input.scope_type is not None
712712
else OptionalState.nop()
713713
),
@@ -717,7 +717,7 @@ async def update_permission(self, input: UpdatePermissionInputDTO) -> Permission
717717
else OptionalState.nop()
718718
),
719719
entity_type=(
720-
OptionalState.update(RBACElementType(input.entity_type).to_entity_type())
720+
OptionalState.update(RBACElementType(input.entity_type))
721721
if input.entity_type is not None
722722
else OptionalState.nop()
723723
),
@@ -878,11 +878,13 @@ def _convert_permission_filter(self, f: PermissionFilterDTO) -> list[QueryCondit
878878
if f.role_id is not None:
879879
conditions.append(ScopedPermissionConditions.by_role_id(f.role_id))
880880
if f.scope_type is not None:
881-
scope_type = RBACElementType(f.scope_type).to_scope_type()
882-
conditions.append(ScopedPermissionConditions.by_scope_type(scope_type))
881+
conditions.append(
882+
ScopedPermissionConditions.by_scope_type(RBACElementType(f.scope_type))
883+
)
883884
if f.entity_type is not None:
884-
entity_type = RBACElementType(f.entity_type).to_entity_type()
885-
conditions.append(ScopedPermissionConditions.by_entity_type(entity_type))
885+
conditions.append(
886+
ScopedPermissionConditions.by_entity_type(RBACElementType(f.entity_type))
887+
)
886888
if f.AND:
887889
for sub in f.AND:
888890
conditions.extend(self._convert_permission_filter(sub))
@@ -1112,8 +1114,7 @@ def _convert_assignment_orders(orders: list[RoleAssignmentOrderByDTO]) -> list[Q
11121114
def _convert_entity_filter(self, f: EntityFilterDTO) -> list[QueryCondition]:
11131115
conditions: list[QueryCondition] = []
11141116
if f.entity_type is not None:
1115-
entity_type = RBACElementType(f.entity_type).to_entity_type()
1116-
conditions.append(EntityScopeConditions.by_entity_type(entity_type))
1117+
conditions.append(EntityScopeConditions.by_entity_type(RBACElementType(f.entity_type)))
11171118
if f.entity_id is not None:
11181119
condition = self.convert_string_filter(
11191120
f.entity_id,

src/ai/backend/manager/api/rest/rbac/entity_adapter.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
from __future__ import annotations
77

8-
from ai.backend.common.data.permission.types import EntityType, ScopeType
8+
from ai.backend.common.data.permission.types import RBACElementType
99
from ai.backend.common.dto.manager.rbac.request import (
1010
SearchEntitiesRequest,
1111
)
@@ -26,17 +26,17 @@ class EntityAdapter(BaseFilterAdapter):
2626

2727
def build_querier(
2828
self,
29-
scope_type: ScopeType,
29+
scope_type: RBACElementType,
3030
scope_id: str,
31-
entity_type: EntityType,
31+
entity_type: RBACElementType,
3232
request: SearchEntitiesRequest,
3333
) -> BatchQuerier:
3434
"""Build a BatchQuerier for entity search.
3535
3636
Args:
37-
scope_type: The scope type to search within
37+
scope_type: The RBAC element type of scope to search within
3838
scope_id: The scope ID to search within
39-
entity_type: The type of entity to search
39+
entity_type: The RBAC element type of entity to search
4040
request: The search request containing pagination info
4141
4242
Returns:

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

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,10 @@
1111
from http import HTTPStatus
1212

1313
from ai.backend.common.api_handlers import APIResponse, BodyParam, PathParam
14+
from ai.backend.common.data.permission.types import (
15+
EntityType,
16+
ScopeType,
17+
)
1418
from ai.backend.common.dto.manager.rbac import (
1519
AssignRoleRequest,
1620
AssignRoleResponse,
@@ -44,6 +48,7 @@
4448
SearchEntitiesResponse,
4549
SearchScopesResponse,
4650
)
51+
from ai.backend.common.exception import RBACTypeConversionError
4752
from ai.backend.manager.data.permission.role import UserRoleAssignmentInput, UserRoleRevocationInput
4853
from ai.backend.manager.dto.context import UserContext
4954
from ai.backend.manager.errors.permission import NotEnoughPermission
@@ -298,7 +303,13 @@ async def get_scope_types(
298303
action_result = await self._permission_controller.get_scope_types.wait_for_complete(
299304
GetScopeTypesAction()
300305
)
301-
resp = GetScopeTypesResponse(items=action_result.scope_types)
306+
scope_types: list[ScopeType] = []
307+
for et in action_result.element_types:
308+
try:
309+
scope_types.append(et.to_scope_type())
310+
except RBACTypeConversionError:
311+
pass
312+
resp = GetScopeTypesResponse(items=scope_types)
302313
return APIResponse.build(status_code=HTTPStatus.OK, response_model=resp)
303314

304315
async def search_scopes(
@@ -312,8 +323,9 @@ async def search_scopes(
312323
raise NotEnoughPermission("Only superadmin can search scopes.")
313324

314325
scope_type = path.parsed.scope_type
326+
element_type = scope_type.to_element()
315327
querier = self._scope_adapter.build_querier(scope_type, body.parsed)
316-
action = SearchScopesAction(scope_type=scope_type, querier=querier)
328+
action = SearchScopesAction(element_type=element_type, querier=querier)
317329
action_result = await self._permission_controller.search_scopes.wait_for_complete(action)
318330
resp = SearchScopesResponse(
319331
items=[self._scope_adapter.convert_to_dto(item) for item in action_result.result.items],
@@ -338,7 +350,13 @@ async def get_entity_types(
338350
action_result = await self._permission_controller.get_entity_types.wait_for_complete(
339351
GetEntityTypesAction()
340352
)
341-
resp = GetEntityTypesResponse(items=action_result.entity_types)
353+
entity_types: list[EntityType] = []
354+
for et in action_result.element_types:
355+
try:
356+
entity_types.append(et.to_entity_type())
357+
except RBACTypeConversionError:
358+
pass
359+
resp = GetEntityTypesResponse(items=entity_types)
342360
return APIResponse.build(status_code=HTTPStatus.OK, response_model=resp)
343361

344362
async def search_entities(
@@ -352,9 +370,9 @@ async def search_entities(
352370
raise NotEnoughPermission("Only superadmin can search entities.")
353371

354372
querier = self._entity_adapter.build_querier(
355-
scope_type=path.parsed.scope_type,
373+
scope_type=path.parsed.scope_type.to_element(),
356374
scope_id=path.parsed.scope_id,
357-
entity_type=path.parsed.entity_type,
375+
entity_type=path.parsed.entity_type.to_element(),
358376
request=body.parsed,
359377
)
360378
action = SearchEntitiesAction(querier=querier)

src/ai/backend/manager/api/rest/rbac/object_permission_adapter.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ def to_create_object_permission_action(
4646
creator = Creator(
4747
spec=ObjectPermissionCreatorSpec(
4848
role_id=request.role_id,
49-
entity_type=request.entity_type,
49+
entity_type=request.entity_type.to_element(),
5050
entity_id=request.entity_id,
5151
operation=request.operation,
5252
status=request.status,

src/ai/backend/manager/api/rest/rbac/permission_adapter.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,9 +43,9 @@ def to_create_permission_action(request: CreatePermissionRequest) -> CreatePermi
4343
creator = Creator(
4444
spec=PermissionCreatorSpec(
4545
role_id=request.role_id,
46-
scope_type=request.scope_type,
46+
scope_type=request.scope_type.to_element(),
4747
scope_id=request.scope_id,
48-
entity_type=request.entity_type,
48+
entity_type=request.entity_type.to_element(),
4949
operation=request.operation,
5050
)
5151
)

src/ai/backend/manager/api/rest/rbac/scope_adapter.py

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -41,8 +41,6 @@ class ScopeAdapter(BaseFilterAdapter):
4141
def build_querier(self, scope_type: ScopeType, request: SearchScopesRequest) -> BatchQuerier:
4242
"""Build a BatchQuerier based on scope type."""
4343
match scope_type:
44-
case ScopeType.GLOBAL:
45-
return self._build_global_scope_querier(request)
4644
case ScopeType.DOMAIN:
4745
return self._build_domain_scope_querier(request)
4846
case ScopeType.PROJECT:
@@ -78,11 +76,6 @@ def _build_user_scope_querier(self, request: SearchScopesRequest) -> BatchQuerie
7876

7977
return BatchQuerier(conditions=conditions, orders=orders, pagination=pagination)
8078

81-
def _build_global_scope_querier(self, request: SearchScopesRequest) -> BatchQuerier:
82-
"""Build a BatchQuerier for global scope (no filtering needed)."""
83-
pagination = OffsetPagination(limit=request.limit, offset=request.offset)
84-
return BatchQuerier(conditions=[], orders=[], pagination=pagination)
85-
8679
def _convert_domain_filter(self, filter: ScopeFilter) -> list[QueryCondition]:
8780
"""Convert scope filter to domain query conditions."""
8881
conditions: list[QueryCondition] = []

src/ai/backend/manager/models/rbac_models/conditions.py

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import sqlalchemy as sa
99

1010
from ai.backend.common.data.filter_specs import StringMatchSpec
11+
from ai.backend.common.data.permission.types import RBACElementType
1112
from ai.backend.manager.data.permission.id import ObjectId
1213
from ai.backend.manager.data.permission.status import RoleStatus
1314
from ai.backend.manager.data.permission.types import (
@@ -175,14 +176,14 @@ def inner() -> sa.sql.expression.ColumnElement[bool]:
175176
return inner
176177

177178
@staticmethod
178-
def by_has_permission_for(entity_type: EntityType) -> QueryCondition:
179+
def by_has_permission_for(element_type: RBACElementType) -> QueryCondition:
179180
"""Filter roles having permission for entity type.
180181
181182
Requires JOIN with ObjectPermissionRow.
182183
"""
183184

184185
def inner() -> sa.sql.expression.ColumnElement[bool]:
185-
return ObjectPermissionRow.entity_type == entity_type
186+
return ObjectPermissionRow.entity_type == element_type.to_entity_type()
186187

187188
return inner
188189

@@ -641,9 +642,9 @@ class EntityScopeConditions:
641642
"""Query conditions for entity scope search."""
642643

643644
@staticmethod
644-
def by_scope_type(scope_type: ScopeType) -> QueryCondition:
645+
def by_scope_type(element_type: RBACElementType) -> QueryCondition:
645646
def inner() -> sa.sql.expression.ColumnElement[bool]:
646-
return AssociationScopesEntitiesRow.scope_type == scope_type
647+
return AssociationScopesEntitiesRow.scope_type == element_type.to_scope_type()
647648

648649
return inner
649650

@@ -655,9 +656,9 @@ def inner() -> sa.sql.expression.ColumnElement[bool]:
655656
return inner
656657

657658
@staticmethod
658-
def by_entity_type(entity_type: EntityType) -> QueryCondition:
659+
def by_entity_type(element_type: RBACElementType) -> QueryCondition:
659660
def inner() -> sa.sql.expression.ColumnElement[bool]:
660-
return AssociationScopesEntitiesRow.entity_type == entity_type
661+
return AssociationScopesEntitiesRow.entity_type == element_type.to_entity_type()
661662

662663
return inner
663664

@@ -791,9 +792,9 @@ class ScopedPermissionConditions:
791792
"""Query conditions for scoped permissions."""
792793

793794
@staticmethod
794-
def by_entity_type(entity_type: EntityType) -> QueryCondition:
795+
def by_entity_type(element_type: RBACElementType) -> QueryCondition:
795796
def inner() -> sa.sql.expression.ColumnElement[bool]:
796-
return PermissionRow.entity_type == entity_type
797+
return PermissionRow.entity_type == element_type.to_entity_type()
797798

798799
return inner
799800

@@ -832,9 +833,9 @@ def inner() -> sa.sql.expression.ColumnElement[bool]:
832833
return inner
833834

834835
@staticmethod
835-
def by_scope_type(scope_type: ScopeType) -> QueryCondition:
836+
def by_scope_type(element_type: RBACElementType) -> QueryCondition:
836837
def inner() -> sa.sql.expression.ColumnElement[bool]:
837-
return PermissionRow.scope_type == scope_type
838+
return PermissionRow.scope_type == element_type.to_scope_type()
838839

839840
return inner
840841

@@ -864,9 +865,9 @@ def inner() -> sa.sql.expression.ColumnElement[bool]:
864865
return inner
865866

866867
@staticmethod
867-
def by_entity_type(entity_type: EntityType) -> QueryCondition:
868+
def by_entity_type(element_type: RBACElementType) -> QueryCondition:
868869
def inner() -> sa.sql.expression.ColumnElement[bool]:
869-
return ObjectPermissionRow.entity_type == entity_type
870+
return ObjectPermissionRow.entity_type == element_type.to_entity_type()
870871

871872
return inner
872873

src/ai/backend/manager/repositories/permission_controller/creators.py

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,12 @@
77
from dataclasses import dataclass
88
from typing import override
99

10+
from ai.backend.common.data.permission.types import RBACElementType
1011
from ai.backend.manager.data.permission.id import ObjectId, ScopeId
1112
from ai.backend.manager.data.permission.status import PermissionStatus, RoleStatus
1213
from ai.backend.manager.data.permission.types import (
13-
EntityType,
1414
OperationType,
1515
RoleSource,
16-
ScopeType,
1716
)
1817
from ai.backend.manager.errors.permission import RoleAlreadyAssigned
1918
from ai.backend.manager.errors.repository import UniqueConstraintViolationError
@@ -56,18 +55,18 @@ class PermissionCreatorSpec(CreatorSpec[PermissionRow]):
5655
"""CreatorSpec for permissions."""
5756

5857
role_id: uuid.UUID
59-
scope_type: ScopeType
58+
scope_type: RBACElementType
6059
scope_id: str
61-
entity_type: EntityType
60+
entity_type: RBACElementType
6261
operation: OperationType
6362

6463
@override
6564
def build_row(self) -> PermissionRow:
6665
return PermissionRow(
6766
role_id=self.role_id,
68-
scope_type=self.scope_type,
67+
scope_type=self.scope_type.to_scope_type(),
6968
scope_id=self.scope_id,
70-
entity_type=self.entity_type,
69+
entity_type=self.entity_type.to_entity_type(),
7170
operation=self.operation,
7271
)
7372

@@ -77,7 +76,7 @@ class ObjectPermissionCreatorSpec(CreatorSpec[ObjectPermissionRow]):
7776
"""CreatorSpec for object permissions."""
7877

7978
role_id: uuid.UUID
80-
entity_type: EntityType
79+
entity_type: RBACElementType
8180
entity_id: str
8281
operation: OperationType
8382
status: PermissionStatus = PermissionStatus.ACTIVE
@@ -86,7 +85,7 @@ class ObjectPermissionCreatorSpec(CreatorSpec[ObjectPermissionRow]):
8685
def build_row(self) -> ObjectPermissionRow:
8786
return ObjectPermissionRow(
8887
role_id=self.role_id,
89-
entity_type=self.entity_type,
88+
entity_type=self.entity_type.to_entity_type(),
9089
entity_id=self.entity_id,
9190
operation=self.operation,
9291
)

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

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
from sqlalchemy.orm import contains_eager, selectinload
1010

1111
from ai.backend.common.data.permission.types import (
12+
RBACElementType,
1213
RelationType,
1314
)
1415
from ai.backend.logging.utils import BraceStyleAdapter
@@ -338,9 +339,9 @@ async def update_role_permissions(
338339
perm_creator = Creator(
339340
spec=PermissionCreatorSpec(
340341
role_id=input_data.role_id,
341-
scope_type=scoped_perm_input.scope_type,
342+
scope_type=RBACElementType(scoped_perm_input.scope_type.value),
342343
scope_id=scoped_perm_input.scope_id,
343-
entity_type=scoped_perm_input.entity_type,
344+
entity_type=RBACElementType(scoped_perm_input.entity_type.value),
344345
operation=scoped_perm_input.operation,
345346
)
346347
)
@@ -356,7 +357,7 @@ async def update_role_permissions(
356357
obj_perm_creator = Creator(
357358
spec=ObjectPermissionCreatorSpec(
358359
role_id=input_data.role_id,
359-
entity_type=obj_perm_input.entity_type,
360+
entity_type=RBACElementType(obj_perm_input.entity_type.value),
360361
entity_id=obj_perm_input.entity_id,
361362
operation=obj_perm_input.operation,
362363
status=obj_perm_input.status,

0 commit comments

Comments
 (0)