Skip to content

Commit 1c326cc

Browse files
committed
refactor: add composite relationship for single-query purger optimization
- Add scope_association_rows relationship to ObjectPermissionRow - Use selectinload chain to load scope associations with object permissions - Remove _get_entity_scopes() and _all_object_permission_entities_in_roles() - Reduce purger queries from 2 to 1
1 parent b7786d6 commit 1c326cc

File tree

2 files changed

+32
-70
lines changed

2 files changed

+32
-70
lines changed

src/ai/backend/manager/models/rbac_models/permission/object_permission.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,9 @@
2020
)
2121

2222
if TYPE_CHECKING:
23+
from ai.backend.manager.models.rbac_models.association_scopes_entities import (
24+
AssociationScopesEntitiesRow,
25+
)
2326
from ai.backend.manager.models.rbac_models.role import RoleRow
2427

2528

@@ -29,6 +32,17 @@ def _get_role_join_condition():
2932
return RoleRow.id == foreign(ObjectPermissionRow.role_id)
3033

3134

35+
def _get_scope_association_join_condition():
36+
from ai.backend.manager.models.rbac_models.association_scopes_entities import (
37+
AssociationScopesEntitiesRow,
38+
)
39+
40+
return sa.and_(
41+
ObjectPermissionRow.entity_type == foreign(AssociationScopesEntitiesRow.entity_type),
42+
ObjectPermissionRow.entity_id == foreign(AssociationScopesEntitiesRow.entity_id),
43+
)
44+
45+
3246
class ObjectPermissionRow(Base):
3347
__tablename__ = "object_permissions"
3448
__table_args__ = (sa.Index("ix_id_role_id_entity_id", "id", "role_id", "entity_id"),)
@@ -51,6 +65,13 @@ class ObjectPermissionRow(Base):
5165
primaryjoin=_get_role_join_condition,
5266
)
5367

68+
scope_association_rows: list[AssociationScopesEntitiesRow] = relationship(
69+
"AssociationScopesEntitiesRow",
70+
primaryjoin=_get_scope_association_join_condition,
71+
viewonly=True,
72+
uselist=True,
73+
)
74+
5475
def object_id(self) -> ObjectId:
5576
return ObjectId(entity_type=self.entity_type, entity_id=self.entity_id)
5677

src/ai/backend/manager/repositories/base/rbac_entity/purger.py

Lines changed: 11 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
from __future__ import annotations
22

3-
from collections import defaultdict
43
from collections.abc import Collection
54
from dataclasses import dataclass
65
from uuid import UUID
@@ -49,7 +48,7 @@ async def _get_related_roles(
4948
) -> list[RoleRow]:
5049
"""
5150
Get all roles related to the given entity as object permissions.
52-
Loads all object_permissions, permission_groups, and permission_rows for each role.
51+
Loads object_permissions with their scope_associations, and permission_groups with permissions.
5352
"""
5453
role_scalars = await db_sess.scalars(
5554
sa.select(RoleRow)
@@ -61,7 +60,9 @@ async def _get_related_roles(
6160
)
6261
)
6362
.options(
64-
selectinload(RoleRow.object_permission_rows),
63+
selectinload(RoleRow.object_permission_rows).selectinload(
64+
ObjectPermissionRow.scope_association_rows
65+
),
6566
selectinload(RoleRow.permission_group_rows).selectinload(
6667
PermissionGroupRow.permission_rows
6768
),
@@ -70,41 +71,8 @@ async def _get_related_roles(
7071
return role_scalars.unique().all()
7172

7273

73-
async def _get_entity_scopes(
74-
db_sess: SASession,
75-
entities: Collection[ObjectId],
76-
) -> dict[ObjectId, set[ScopeId]]:
77-
"""
78-
Get all scopes for a list of entities in a single query.
79-
80-
Returns:
81-
Mapping of ObjectId -> set of ScopeId
82-
"""
83-
if not entities:
84-
return {}
85-
86-
entity_tuples = [(e.entity_type, e.entity_id) for e in entities]
87-
assoc_scalars = await db_sess.scalars(
88-
sa.select(AssociationScopesEntitiesRow).where(
89-
sa.tuple_(
90-
AssociationScopesEntitiesRow.entity_type,
91-
AssociationScopesEntitiesRow.entity_id,
92-
).in_(entity_tuples)
93-
)
94-
)
95-
96-
result: defaultdict[ObjectId, set[ScopeId]] = defaultdict(set)
97-
assoc_rows: list[AssociationScopesEntitiesRow] = assoc_scalars.all()
98-
for assoc in assoc_rows:
99-
key = assoc.object_id()
100-
scope = assoc.parsed_scope_id()
101-
result[key].add(scope)
102-
return result
103-
104-
10574
def _perm_group_ids_to_delete_in_role(
10675
role_row: RoleRow,
107-
entity_scopes: dict[ObjectId, set[ScopeId]],
10876
entity_to_delete: ObjectId,
10977
) -> list[UUID]:
11078
"""
@@ -118,13 +86,13 @@ def _perm_group_ids_to_delete_in_role(
11886
if not role_row.permission_group_rows:
11987
return perm_group_ids
12088

89+
# Collect scopes from remaining entities (via eagerly loaded scope_association_rows)
12190
remaining_scopes: set[ScopeId] = set()
12291
for object_permission_row in role_row.object_permission_rows:
123-
object_id = object_permission_row.object_id()
124-
if object_id == entity_to_delete:
92+
if object_permission_row.object_id() == entity_to_delete:
12593
continue
126-
scopes = entity_scopes.get(object_id, set())
127-
remaining_scopes.update(scopes)
94+
for assoc in object_permission_row.scope_association_rows:
95+
remaining_scopes.add(assoc.parsed_scope_id())
12896

12997
for perm_group_row in role_row.permission_group_rows:
13098
# Skip permission groups that have remaining permissions
@@ -138,23 +106,13 @@ def _perm_group_ids_to_delete_in_role(
138106

139107
def _perm_group_ids_to_delete(
140108
role_rows: Collection[RoleRow],
141-
entity_scopes: dict[ObjectId, set[ScopeId]],
142109
entity_to_delete: ObjectId,
143110
) -> list[UUID]:
144-
# all_entites = _all_object_permission_entities_in_roles(role_rows)
145-
# if not all_entites:
146-
# all_entites.add(entity_to_delete)
147-
# entity_scopes = await _get_entity_scopes(db_sess, all_entites)
148-
149111
if not role_rows:
150112
return []
151113
permission_group_ids: list[UUID] = []
152114
for role_row in role_rows:
153-
perm_group_ids = _perm_group_ids_to_delete_in_role(
154-
role_row,
155-
entity_scopes,
156-
entity_to_delete,
157-
)
115+
perm_group_ids = _perm_group_ids_to_delete_in_role(role_row, entity_to_delete)
158116
permission_group_ids.extend(perm_group_ids)
159117
return permission_group_ids
160118

@@ -174,17 +132,6 @@ def _object_permission_ids_to_delete(
174132
return object_permission_ids
175133

176134

177-
def _all_object_permission_entities_in_roles(
178-
role_rows: Collection[RoleRow],
179-
) -> set[ObjectId]:
180-
object_ids: set[ObjectId] = set()
181-
for role_row in role_rows:
182-
for object_permission_row in role_row.object_permission_rows:
183-
object_id = object_permission_row.object_id()
184-
object_ids.add(object_id)
185-
return object_ids
186-
187-
188135
async def _delete_main_object_row(
189136
db_sess: SASession,
190137
purger: Purger[TRow],
@@ -226,16 +173,10 @@ async def _delete_rbac_entity(
226173
db_sess: SASession,
227174
entity_id: ObjectId,
228175
) -> None:
229-
# Get all roles associated with the entity as object permission
176+
# Get all roles with object_permissions and their scope_associations eagerly loaded
230177
role_rows = await _get_related_roles(db_sess, entity_id)
231178
object_permission_ids = _object_permission_ids_to_delete(role_rows, entity_id)
232-
all_entites = _all_object_permission_entities_in_roles(role_rows)
233-
if not all_entites:
234-
all_entites.add(entity_id)
235-
entity_scopes = await _get_entity_scopes(db_sess, all_entites)
236-
237-
# Determine which object_permissions and permission_groups to delete
238-
permission_group_ids = _perm_group_ids_to_delete(role_rows, entity_scopes, entity_id)
179+
permission_group_ids = _perm_group_ids_to_delete(role_rows, entity_id)
239180

240181
# Execute deletions
241182
if object_permission_ids:

0 commit comments

Comments
 (0)