Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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/11846.feature.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Add role preset repository, service, and processor layers covering CRUD, soft-delete with restore, purge, and bulk add/remove of `role_permission_preset` entries.
Empty file.
74 changes: 74 additions & 0 deletions src/ai/backend/manager/data/role_preset/types.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
from __future__ import annotations

from dataclasses import dataclass, field
from datetime import datetime
from typing import TYPE_CHECKING

from ai.backend.common.data.permission.types import (
EntityType,
OperationType,
RBACElementType,
)
from ai.backend.common.identifier.role_permission_preset import RolePermissionPresetID
from ai.backend.common.identifier.role_preset import RolePresetID
from ai.backend.manager.repositories.base.creator import BulkCreatorError
from ai.backend.manager.repositories.base.purger import BulkPurgerError
from ai.backend.manager.repositories.base.updater import BulkUpdaterError

if TYPE_CHECKING:
from ai.backend.manager.models.rbac_models.role_permission_preset.row import (
RolePermissionPresetRow,
)
from ai.backend.manager.models.rbac_models.role_preset.row import RolePresetRow


@dataclass(frozen=True)
class RolePermissionPresetData:
id: RolePermissionPresetID
role_preset_id: RolePresetID
entity_type: EntityType
operation: OperationType
created_at: datetime


@dataclass(frozen=True)
class RolePresetData:
id: RolePresetID
name: str
scope_type: RBACElementType
auto_assign: bool
deleted: bool
created_at: datetime
updated_at: datetime


@dataclass(frozen=True)
class RolePresetSearchResult:
items: list[RolePresetData]
total_count: int
has_next_page: bool
has_previous_page: bool


@dataclass(frozen=True)
class RolePresetBulkPurgeResult:
successes: list[RolePresetData] = field(default_factory=list)
failures: list[BulkPurgerError[RolePresetRow]] = field(default_factory=list)


@dataclass(frozen=True)
class RolePresetBulkUpdateResult:
successes: list[RolePresetData] = field(default_factory=list)
failures: list[BulkUpdaterError[RolePresetRow]] = field(default_factory=list)


@dataclass(frozen=True)
class RolePermissionPresetBulkAddResult:
successes: list[RolePermissionPresetData] = field(default_factory=list)
failures: list[BulkCreatorError[RolePermissionPresetRow]] = field(default_factory=list)


@dataclass(frozen=True)
class RolePermissionPresetBulkRemoveResult:
successes: list[RolePermissionPresetData] = field(default_factory=list)
failures: list[BulkPurgerError[RolePermissionPresetRow]] = field(default_factory=list)
38 changes: 38 additions & 0 deletions src/ai/backend/manager/errors/role_preset.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
"""Role preset domain exceptions."""

from __future__ import annotations

from aiohttp import web

from ai.backend.common.exception import (
BackendAIError,
ErrorCode,
ErrorDetail,
ErrorDomain,
ErrorOperation,
)

from .common import ObjectNotFound


class RolePresetNotFound(ObjectNotFound):
object_name = "role_preset"

def error_code(self) -> ErrorCode:
return ErrorCode(
domain=ErrorDomain.ROLE,
operation=ErrorOperation.READ,
error_detail=ErrorDetail.NOT_FOUND,
)


class RolePermissionPresetConflict(BackendAIError, web.HTTPConflict):
error_type = "https://api.backend.ai/probs/duplicate-role-permission-preset"
error_title = "Duplicate role permission preset entry."

def error_code(self) -> ErrorCode:
return ErrorCode(
domain=ErrorDomain.ROLE,
operation=ErrorOperation.CREATE,
error_detail=ErrorDetail.CONFLICT,
)
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
EntityType,
OperationType,
)
from ai.backend.manager.data.role_preset.types import RolePermissionPresetData
from ai.backend.manager.models.base import (
GUID,
Base,
Expand Down Expand Up @@ -50,3 +51,12 @@ class RolePermissionPresetRow(Base): # type: ignore[misc]
created_at: Mapped[datetime] = mapped_column(
"created_at", sa.DateTime(timezone=True), nullable=False, server_default=sa.func.now()
)

def to_data(self) -> RolePermissionPresetData:
Comment thread
fregataa marked this conversation as resolved.
return RolePermissionPresetData(
id=self.id,
role_preset_id=self.role_preset_id,
entity_type=self.entity_type,
operation=self.operation,
created_at=self.created_at,
)
12 changes: 12 additions & 0 deletions src/ai/backend/manager/models/rbac_models/role_preset/row.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@

from ai.backend.common.identifier.role_preset import RolePresetID
from ai.backend.manager.data.permission.types import ScopeType
from ai.backend.manager.data.role_preset.types import RolePresetData
from ai.backend.manager.models.base import (
GUID,
Base,
Expand Down Expand Up @@ -53,3 +54,14 @@ class RolePresetRow(Base): # type: ignore[misc]
onupdate=sa.func.now(),
nullable=False,
)

def to_data(self) -> RolePresetData:
return RolePresetData(
id=self.id,
name=self.name,
scope_type=self.scope_type.to_element(),
auto_assign=self.auto_assign,
deleted=self.deleted,
created_at=self.created_at,
updated_at=self.updated_at,
)
18 changes: 18 additions & 0 deletions src/ai/backend/manager/repositories/ops/provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@
BulkCreator,
BulkCreatorResult,
BulkCreatorResultWithFailures,
BulkPurgerResultWithFailures,
BulkUpdaterResult,
Creator,
CreatorResult,
DependentCreatorSpec,
Expand All @@ -46,6 +48,8 @@
execute_bulk_creator,
execute_bulk_creator_partial,
execute_bulk_dependent_creator,
execute_bulk_purger_partial,
execute_bulk_updater_partial,
execute_creator,
execute_dependent_creator,
execute_next_value_creator,
Expand Down Expand Up @@ -175,6 +179,13 @@ async def batch_update[TRow: Base](self, updater: BatchUpdater[TRow]) -> BatchUp
"""Update all rows matching the updater conditions."""
return await execute_batch_updater(self._sess, updater)

async def bulk_update_partial[TRow: Base](
self,
updaters: list[Updater[TRow]],
) -> BulkUpdaterResult[TRow]:
"""Update multiple rows individually, isolating each via a savepoint for partial success."""
return await execute_bulk_updater_partial(self._sess, updaters)

async def upsert[TRow: Base](
self,
upserter: Upserter[TRow],
Expand All @@ -191,6 +202,13 @@ async def batch_purge[TRow: Base](self, purger: BatchPurger[TRow]) -> BatchPurge
"""Delete rows in batches matching the purger subquery."""
return await execute_batch_purger(self._sess, purger)

async def bulk_purge_partial[TRow: Base](
self,
purgers: list[Purger[TRow]],
) -> BulkPurgerResultWithFailures[TRow]:
"""Delete multiple rows individually, isolating each via a savepoint for partial success."""
return await execute_bulk_purger_partial(self._sess, purgers)

@asynccontextmanager
async def savepoint(self) -> AsyncIterator[WriteOps]:
"""Open a nested transaction (savepoint) bound to the same session.
Expand Down
4 changes: 4 additions & 0 deletions src/ai/backend/manager/repositories/repositories.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@
from ai.backend.manager.repositories.resource_usage_history.repositories import (
ResourceUsageHistoryRepositories,
)
from ai.backend.manager.repositories.role_preset.repositories import RolePresetRepositories
from ai.backend.manager.repositories.runtime_variant.repositories import RuntimeVariantRepositories
from ai.backend.manager.repositories.runtime_variant_preset.repositories import (
RuntimeVariantPresetRepositories,
Expand Down Expand Up @@ -108,6 +109,7 @@ class Repositories:
reservoir_registry: ReservoirRegistryRepositories
resource_preset: ResourcePresetRepositories
resource_slot: ResourceSlotRepositories
role_preset: RolePresetRepositories
runtime_variant: RuntimeVariantRepositories
runtime_variant_preset: RuntimeVariantPresetRepositories
deployment_revision_preset: DeploymentRevisionPresetRepositories
Expand Down Expand Up @@ -160,6 +162,7 @@ def create(cls, args: RepositoryArgs) -> Self:
reservoir_registry_repositories = ReservoirRegistryRepositories.create(args)
resource_preset_repositories = ResourcePresetRepositories.create(args)
resource_slot_repositories = ResourceSlotRepositories.create(args)
role_preset_repositories = RolePresetRepositories.create(args)
runtime_variant_repositories = RuntimeVariantRepositories.create(args)
runtime_variant_preset_repositories = RuntimeVariantPresetRepositories.create(args)
deployment_revision_preset_repositories = DeploymentRevisionPresetRepositories.create(args)
Expand Down Expand Up @@ -209,6 +212,7 @@ def create(cls, args: RepositoryArgs) -> Self:
reservoir_registry=reservoir_registry_repositories,
resource_preset=resource_preset_repositories,
resource_slot=resource_slot_repositories,
role_preset=role_preset_repositories,
runtime_variant=runtime_variant_repositories,
runtime_variant_preset=runtime_variant_preset_repositories,
deployment_revision_preset=deployment_revision_preset_repositories,
Expand Down
Empty file.
93 changes: 93 additions & 0 deletions src/ai/backend/manager/repositories/role_preset/creators.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
from __future__ import annotations

from collections.abc import Sequence
from dataclasses import dataclass
from typing import override

from ai.backend.common.data.permission.types import (
EntityType,
OperationType,
ScopeType,
)
from ai.backend.common.identifier.role_preset import RolePresetID
from ai.backend.manager.errors.repository import UniqueConstraintViolationError
from ai.backend.manager.errors.role_preset import RolePermissionPresetConflict
from ai.backend.manager.models.rbac_models.role_permission_preset.row import (
RolePermissionPresetRow,
)
from ai.backend.manager.models.rbac_models.role_preset.row import RolePresetRow
from ai.backend.manager.repositories.base.creator import (
CreatorSpec,
DependentCreatorSpec,
)
from ai.backend.manager.repositories.base.types import IntegrityErrorCheck


@dataclass
class RolePresetCreatorSpec(CreatorSpec[RolePresetRow]):
name: str
scope_type: ScopeType
auto_assign: bool = False

@override
def build_row(self) -> RolePresetRow:
return RolePresetRow(
name=self.name,
scope_type=self.scope_type,
auto_assign=self.auto_assign,
)


@dataclass
class RolePermissionPresetDependentCreatorSpec(
Comment thread
fregataa marked this conversation as resolved.
DependentCreatorSpec[RolePresetID, RolePermissionPresetRow]
):
entity_type: EntityType
operation: OperationType

@override
def build_row(self, dependency: RolePresetID) -> RolePermissionPresetRow:
return RolePermissionPresetRow(
role_preset_id=dependency,
entity_type=self.entity_type,
operation=self.operation,
)

@property
@override
def integrity_error_checks(self) -> Sequence[IntegrityErrorCheck]:
return (
IntegrityErrorCheck(
violation_type=UniqueConstraintViolationError,
error=RolePermissionPresetConflict(
f"Duplicate permission entry ({self.entity_type}, {self.operation})."
),
),
)


@dataclass
class RolePermissionPresetCreatorSpec(CreatorSpec[RolePermissionPresetRow]):
role_preset_id: RolePresetID
entity_type: EntityType
operation: OperationType

@override
def build_row(self) -> RolePermissionPresetRow:
return RolePermissionPresetRow(
role_preset_id=self.role_preset_id,
entity_type=self.entity_type,
operation=self.operation,
)

@property
@override
def integrity_error_checks(self) -> Sequence[IntegrityErrorCheck]:
return (
IntegrityErrorCheck(
violation_type=UniqueConstraintViolationError,
error=RolePermissionPresetConflict(
f"Duplicate permission entry ({self.entity_type}, {self.operation})."
),
),
)
Empty file.
Loading
Loading