Skip to content

Commit 9e5097e

Browse files
committed
feat: Add RBAC Entity Creator and Deletor
1 parent 153579b commit 9e5097e

6 files changed

Lines changed: 165 additions & 0 deletions

File tree

changes/6413.feature.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Implement RBAC Creator Pattern to ensure that RBAC records are created whenever any RBAC-related entities are created
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import logging
2+
from abc import ABC, abstractmethod
3+
from dataclasses import dataclass
4+
from typing import Generic, TypeVar
5+
6+
import sqlalchemy as sa
7+
from sqlalchemy.exc import IntegrityError
8+
from sqlalchemy.ext.asyncio import AsyncSession as SASession
9+
10+
from ai.backend.logging import BraceStyleAdapter
11+
from ai.backend.manager.data.permission.association_scopes_entities import (
12+
AssociationScopesEntitiesCreateInput,
13+
)
14+
from ai.backend.manager.data.permission.id import ObjectId, ScopeId
15+
from ai.backend.manager.models.rbac_models.association_scopes_entities import (
16+
AssociationScopesEntitiesRow,
17+
)
18+
19+
log = BraceStyleAdapter(logging.getLogger(__name__))
20+
21+
22+
@dataclass
23+
class RBACEntityCreateInput:
24+
scope_id: ScopeId
25+
object_id: ObjectId
26+
27+
28+
TEntityCreateInput = TypeVar("TEntityCreateInput")
29+
TCreatedEntity = TypeVar("TCreatedEntity")
30+
31+
32+
class RBACEntityCreator(Generic[TEntityCreateInput, TCreatedEntity], ABC):
33+
async def create_entity(
34+
self,
35+
db_session: SASession,
36+
input: TEntityCreateInput,
37+
rbac_input: RBACEntityCreateInput,
38+
) -> TCreatedEntity:
39+
result = await self._create_entity(db_session, input)
40+
await self._create_rbac_entity(db_session, rbac_input)
41+
return result
42+
43+
@abstractmethod
44+
async def _create_entity(
45+
self,
46+
db_session: SASession,
47+
input: TEntityCreateInput,
48+
) -> TCreatedEntity:
49+
raise NotImplementedError
50+
51+
async def _create_rbac_entity(
52+
self,
53+
db_session: SASession,
54+
rbac_input: RBACEntityCreateInput,
55+
) -> None:
56+
scope_id = rbac_input.scope_id
57+
entity_id = rbac_input.object_id
58+
creator = AssociationScopesEntitiesCreateInput(
59+
scope_id=scope_id,
60+
object_id=entity_id,
61+
)
62+
try:
63+
await db_session.execute(
64+
sa.insert(AssociationScopesEntitiesRow).values(creator.fields_to_store())
65+
)
66+
except IntegrityError:
67+
log.exception(
68+
"entity and scope mapping already exists: {}, {}. Skipping.",
69+
entity_id.to_str(),
70+
scope_id.to_str(),
71+
)
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import logging
2+
from abc import ABC, abstractmethod
3+
from typing import Generic, TypeVar, final
4+
5+
import sqlalchemy as sa
6+
from sqlalchemy.exc import IntegrityError
7+
from sqlalchemy.ext.asyncio import AsyncSession as SASession
8+
9+
from ai.backend.logging import BraceStyleAdapter
10+
from ai.backend.manager.data.permission.id import ObjectId, ScopeId
11+
from ai.backend.manager.models.rbac_models.association_scopes_entities import (
12+
AssociationScopesEntitiesRow,
13+
)
14+
15+
log = BraceStyleAdapter(logging.getLogger(__name__))
16+
17+
18+
class RBACEntityDeletor(ABC):
19+
async def delete_entity(self, db_session: SASession) -> None:
20+
scope_id = self.scope_id()
21+
entity_id = self.object_id()
22+
try:
23+
await db_session.execute(
24+
sa.delete(AssociationScopesEntitiesRow).where(
25+
sa.and_(
26+
AssociationScopesEntitiesRow.scope_id == scope_id.scope_id,
27+
AssociationScopesEntitiesRow.scope_type == scope_id.scope_type,
28+
AssociationScopesEntitiesRow.entity_id == entity_id,
29+
AssociationScopesEntitiesRow.entity_type == entity_id.entity_type,
30+
)
31+
)
32+
)
33+
except IntegrityError:
34+
log.exception(
35+
"failed to delete entity and scope mapping: {}, {}.",
36+
entity_id.to_str(),
37+
scope_id.to_str(),
38+
)
39+
40+
@abstractmethod
41+
def scope_id(self) -> ScopeId:
42+
raise NotImplementedError
43+
44+
@abstractmethod
45+
def object_id(self) -> ObjectId:
46+
raise NotImplementedError
47+
48+
49+
TDeletedEntity = TypeVar("TDeletedEntity")
50+
51+
52+
class RBACDeletor(Generic[TDeletedEntity], ABC):
53+
def __init__(self, rbac_entity_deletor: RBACEntityDeletor) -> None:
54+
self._rbac_entity_deletor = rbac_entity_deletor
55+
56+
@final
57+
async def delete(self, db_session: SASession) -> TDeletedEntity:
58+
entity = await self._delete(db_session)
59+
await self._rbac_entity_deletor.delete_entity(db_session)
60+
return entity
61+
62+
@abstractmethod
63+
async def _delete(self, db_session: SASession) -> TDeletedEntity:
64+
raise NotImplementedError
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
python_tests(
2+
name="tests",
3+
dependencies=[
4+
"src/ai/backend/manager:src",
5+
],
6+
)

tests/manager/repositories/permission_controller/__init__.py

Whitespace-only changes.
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
"""
2+
Tests for RBACEntityCreator and RBACCreator functionality.
3+
Tests the creator classes with real database operations.
4+
"""
5+
6+
# from __future__ import annotations
7+
8+
# import uuid
9+
10+
# import pytest
11+
# import sqlalchemy as sa
12+
# from sqlalchemy.ext.asyncio import AsyncSession as SASession
13+
14+
# from ai.backend.manager.data.permission.id import ObjectId, ScopeId
15+
# from ai.backend.manager.data.permission.types import EntityType, ScopeType
16+
# from ai.backend.manager.models.rbac_models.association_scopes_entities import (
17+
# AssociationScopesEntitiesRow,
18+
# )
19+
# from ai.backend.manager.models.utils import ExtendedAsyncSAEngine
20+
# from ai.backend.manager.repositories.permission_controller.creator import (
21+
# RBACEntityCreateInput,
22+
# RBACEntityCreator,
23+
# )

0 commit comments

Comments
 (0)