From 3dba72ed8addfaa78f722a1f33d544bbe35d74c6 Mon Sep 17 00:00:00 2001 From: Sanghun Lee Date: Fri, 13 Mar 2026 00:50:17 +0900 Subject: [PATCH 1/5] refactor(BA-5071): Apply RBAC Creator pattern to ImageAlias MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add IMAGE_ALIAS to RBACElementType enum - Replace Creator/execute_creator with RBACEntityCreator/execute_rbac_entity_creator for ImageAlias creation in image domain - Scope association: ImageAlias → IMAGE (parent scope) Co-Authored-By: Claude Opus 4.6 --- .../backend/common/data/permission/types.py | 1 + .../repositories/image/db_source/db_source.py | 35 ++++++++++++++----- .../manager/repositories/image/repository.py | 9 +++-- .../backend/manager/services/image/service.py | 15 +++++--- .../services/image/test_image_service.py | 12 +++++-- 5 files changed, 54 insertions(+), 18 deletions(-) diff --git a/src/ai/backend/common/data/permission/types.py b/src/ai/backend/common/data/permission/types.py index c0817a2c537..66db22a58a1 100644 --- a/src/ai/backend/common/data/permission/types.py +++ b/src/ai/backend/common/data/permission/types.py @@ -392,6 +392,7 @@ class RBACElementType(enum.StrEnum): DEPLOYMENT_TOKEN = "deployment:token" DEPLOYMENT_POLICY = "deployment:policy" DEPLOYMENT_REVISION = "deployment:revision" + IMAGE_ALIAS = "image:alias" # === Entity-level scopes (for entity-scope permissions) === ARTIFACT_REVISION = "artifact_revision" diff --git a/src/ai/backend/manager/repositories/image/db_source/db_source.py b/src/ai/backend/manager/repositories/image/db_source/db_source.py index 60148edafe8..2b6824283c1 100644 --- a/src/ai/backend/manager/repositories/image/db_source/db_source.py +++ b/src/ai/backend/manager/repositories/image/db_source/db_source.py @@ -12,6 +12,7 @@ from sqlalchemy.orm import selectinload from ai.backend.common.bgtask.reporter import ProgressReporter +from ai.backend.common.data.permission.types import RBACElementType from ai.backend.common.docker import ImageRef from ai.backend.common.exception import UnknownImageReference from ai.backend.common.types import ImageAlias, ImageID @@ -26,6 +27,7 @@ RescanImagesResult, ResourceLimitInput, ) +from ai.backend.manager.data.permission.types import RBACElementRef from ai.backend.manager.errors.image import ( AliasImageActionDBError, AliasImageActionValueError, @@ -45,8 +47,11 @@ ) from ai.backend.manager.models.kernel.row import KernelRow from ai.backend.manager.models.utils import ExtendedAsyncSAEngine -from ai.backend.manager.repositories.base import BatchQuerier, Creator, execute_batch_querier -from ai.backend.manager.repositories.base.creator import execute_creator +from ai.backend.manager.repositories.base import BatchQuerier, execute_batch_querier +from ai.backend.manager.repositories.base.rbac.entity_creator import ( + RBACEntityCreator, + execute_rbac_entity_creator, +) from ai.backend.manager.repositories.base.updater import Updater, execute_updater from ai.backend.manager.repositories.image.creators import ImageAliasCreatorSpec @@ -244,10 +249,20 @@ async def insert_image_alias( image_row = await ImageRow.resolve( session, [ImageIdentifier(image_canonical, architecture)] ) - image_alias = ImageAliasRow(alias=alias, image_id=image_row.id) - image_row.aliases.append(image_alias) + rbac_creator = RBACEntityCreator( + spec=ImageAliasCreatorSpec( + alias=alias, + image_id=image_row.id, + ), + element_type=RBACElementType.IMAGE_ALIAS, + scope_ref=RBACElementRef( + element_type=RBACElementType.IMAGE, + element_id=str(image_row.id), + ), + ) + result = await execute_rbac_entity_creator(session, rbac_creator) row_id = image_row.id - alias_data = ImageAliasData(id=image_alias.id, alias=image_alias.alias or "") + alias_data = ImageAliasData(id=result.row.id, alias=result.row.alias or "") return row_id, alias_data except ValueError as e: raise AliasImageActionValueError from e @@ -327,16 +342,18 @@ async def clear_image_resource_limits( image_row._resources = {} return image_row.to_dataclass() - async def insert_image_alias_by_id(self, creator: Creator[ImageAliasRow]) -> ImageAliasData: + async def insert_image_alias_by_id( + self, creator: RBACEntityCreator[ImageAliasRow] + ) -> ImageAliasData: """ - Creates an image alias using the Creator pattern. + Creates an image alias using the RBACEntityCreator pattern. """ - spec = cast(ImageAliasCreatorSpec, creator.spec) try: async with self._db.begin_session() as session: + spec = cast(ImageAliasCreatorSpec, creator.spec) # Validate that the image exists await self._get_image_by_id(session, spec.image_id) - result = await execute_creator(session, creator) + result = await execute_rbac_entity_creator(session, creator) return ImageAliasData(id=result.row.id, alias=result.row.alias or "") except ValueError as e: raise AliasImageActionValueError from e diff --git a/src/ai/backend/manager/repositories/image/repository.py b/src/ai/backend/manager/repositories/image/repository.py index 22e480a534f..b6d085121b8 100644 --- a/src/ai/backend/manager/repositories/image/repository.py +++ b/src/ai/backend/manager/repositories/image/repository.py @@ -32,7 +32,8 @@ ) from ai.backend.manager.models.image.row import ImageAliasRow from ai.backend.manager.models.utils import ExtendedAsyncSAEngine -from ai.backend.manager.repositories.base import BatchQuerier, Creator +from ai.backend.manager.repositories.base import BatchQuerier +from ai.backend.manager.repositories.base.rbac.entity_creator import RBACEntityCreator from ai.backend.manager.repositories.base.updater import Updater from ai.backend.manager.repositories.image.db_source.db_source import ImageDBSource from ai.backend.manager.repositories.image.stateful_source.stateful_source import ( @@ -281,9 +282,11 @@ async def clear_image_custom_resource_limit( return await self._db_source.clear_image_resource_limits(image_canonical, architecture) @image_repository_resilience.apply() - async def add_image_alias_by_id(self, creator: Creator[ImageAliasRow]) -> ImageAliasData: + async def add_image_alias_by_id( + self, creator: RBACEntityCreator[ImageAliasRow] + ) -> ImageAliasData: """ - Creates an image alias using the Creator pattern. + Creates an image alias using the RBACEntityCreator pattern. """ return await self._db_source.insert_image_alias_by_id(creator) diff --git a/src/ai/backend/manager/services/image/service.py b/src/ai/backend/manager/services/image/service.py index caa80228dba..1525fbfc837 100644 --- a/src/ai/backend/manager/services/image/service.py +++ b/src/ai/backend/manager/services/image/service.py @@ -2,6 +2,7 @@ from uuid import UUID from ai.backend.common.contexts.user import current_user +from ai.backend.common.data.permission.types import RBACElementType from ai.backend.common.docker import ImageRef from ai.backend.common.dto.manager.rpc_request import PurgeImagesReq from ai.backend.common.exception import UnknownImageReference @@ -9,6 +10,7 @@ from ai.backend.logging.utils import BraceStyleAdapter from ai.backend.manager.config.provider import ManagerConfigProvider from ai.backend.manager.data.image.types import ImageWithAgentInstallStatus +from ai.backend.manager.data.permission.types import RBACElementRef from ai.backend.manager.errors.image import ImageAccessForbiddenError, ImageNotFound from ai.backend.manager.models.image import ( ImageIdentifier, @@ -16,7 +18,7 @@ ) from ai.backend.manager.models.user import UserRole from ai.backend.manager.registry import AgentRegistry -from ai.backend.manager.repositories.base import Creator +from ai.backend.manager.repositories.base.rbac.entity_creator import RBACEntityCreator from ai.backend.manager.repositories.base.updater import Updater from ai.backend.manager.repositories.image.creators import ImageAliasCreatorSpec from ai.backend.manager.repositories.image.repository import ImageRepository @@ -405,13 +407,18 @@ async def alias_image_by_id(self, action: AliasImageByIdAction) -> AliasImageByI """ Creates an alias for an image by its ID. """ - creator = Creator( + rbac_creator = RBACEntityCreator( spec=ImageAliasCreatorSpec( alias=action.alias, image_id=action.image_id, - ) + ), + element_type=RBACElementType.IMAGE_ALIAS, + scope_ref=RBACElementRef( + element_type=RBACElementType.IMAGE, + element_id=str(action.image_id), + ), ) - image_alias = await self._image_repository.add_image_alias_by_id(creator) + image_alias = await self._image_repository.add_image_alias_by_id(rbac_creator) return AliasImageByIdActionResult( image_id=action.image_id, image_alias=image_alias, diff --git a/tests/unit/manager/services/image/test_image_service.py b/tests/unit/manager/services/image/test_image_service.py index b754d6600c3..16453b242b1 100644 --- a/tests/unit/manager/services/image/test_image_service.py +++ b/tests/unit/manager/services/image/test_image_service.py @@ -16,6 +16,7 @@ from ai.backend.common.container_registry import ContainerRegistryType from ai.backend.common.contexts.user import with_user +from ai.backend.common.data.permission.types import RBACElementType from ai.backend.common.data.user.types import UserData from ai.backend.common.dto.agent.response import PurgeImageResp, PurgeImagesResp from ai.backend.common.exception import UnknownImageReference @@ -36,6 +37,7 @@ RescanImagesResult, ResourceLimitInput, ) +from ai.backend.manager.data.permission.types import RBACElementRef from ai.backend.manager.errors.image import ( ImageAccessForbiddenError, ImageAliasNotFound, @@ -43,7 +45,8 @@ ) from ai.backend.manager.models.image import ImageStatus, ImageType from ai.backend.manager.models.user import UserRole -from ai.backend.manager.repositories.base import BatchQuerier, Creator, OffsetPagination +from ai.backend.manager.repositories.base import BatchQuerier, OffsetPagination +from ai.backend.manager.repositories.base.rbac.entity_creator import RBACEntityCreator from ai.backend.manager.repositories.image.creators import ImageAliasCreatorSpec from ai.backend.manager.repositories.image.repository import ImageRepository from ai.backend.manager.repositories.image.updaters import ImageUpdaterSpec @@ -1004,10 +1007,15 @@ async def test_alias_image_by_id_success( assert result.image_alias == image_alias_data mock_image_repository.add_image_alias_by_id.assert_called_once() creator_arg = mock_image_repository.add_image_alias_by_id.call_args[0][0] - assert isinstance(creator_arg, Creator) + assert isinstance(creator_arg, RBACEntityCreator) assert isinstance(creator_arg.spec, ImageAliasCreatorSpec) assert creator_arg.spec.alias == "python" assert creator_arg.spec.image_id == image_id + assert creator_arg.element_type == RBACElementType.IMAGE_ALIAS + assert creator_arg.scope_ref == RBACElementRef( + element_type=RBACElementType.IMAGE, + element_id=str(image_id), + ) class TestClearImageCustomResourceLimitById(ImageServiceBaseFixtures): From 2de6fb4bc5bf2ebe0eefb1a17c3442e4c504c7d5 Mon Sep 17 00:00:00 2001 From: Sanghun Lee Date: Fri, 13 Mar 2026 00:59:12 +0900 Subject: [PATCH 2/5] changelog: add news fragment for PR #10014 Co-Authored-By: Claude Opus 4.6 --- changes/10014.enhance.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 changes/10014.enhance.md diff --git a/changes/10014.enhance.md b/changes/10014.enhance.md new file mode 100644 index 00000000000..f34f5f8b95e --- /dev/null +++ b/changes/10014.enhance.md @@ -0,0 +1 @@ +Apply RBAC Creator pattern to auto sub-entity (BA-5071) From 44e59a59325bab1b6041b1ff7a3f1c84e9f39106 Mon Sep 17 00:00:00 2001 From: Sanghun Lee Date: Fri, 13 Mar 2026 01:37:46 +0900 Subject: [PATCH 3/5] fix(BA-5071): Add IMAGE_ALIAS to exhaustive match patterns in GQL types Co-Authored-By: Claude Opus 4.6 --- src/ai/backend/manager/api/gql/rbac/types/entity.py | 1 + src/ai/backend/manager/api/gql/rbac/types/permission.py | 1 + 2 files changed, 2 insertions(+) diff --git a/src/ai/backend/manager/api/gql/rbac/types/entity.py b/src/ai/backend/manager/api/gql/rbac/types/entity.py index b8395eab527..a5f338650b4 100644 --- a/src/ai/backend/manager/api/gql/rbac/types/entity.py +++ b/src/ai/backend/manager/api/gql/rbac/types/entity.py @@ -175,6 +175,7 @@ async def entity( | RBACElementType.DEPLOYMENT_TOKEN | RBACElementType.DEPLOYMENT_POLICY | RBACElementType.DEPLOYMENT_REVISION + | RBACElementType.IMAGE_ALIAS ): return None diff --git a/src/ai/backend/manager/api/gql/rbac/types/permission.py b/src/ai/backend/manager/api/gql/rbac/types/permission.py index 526593919de..5a51e67b9ed 100644 --- a/src/ai/backend/manager/api/gql/rbac/types/permission.py +++ b/src/ai/backend/manager/api/gql/rbac/types/permission.py @@ -276,6 +276,7 @@ async def scope( | RBACElementType.DEPLOYMENT_TOKEN | RBACElementType.DEPLOYMENT_POLICY | RBACElementType.DEPLOYMENT_REVISION + | RBACElementType.IMAGE_ALIAS ): return None From 41cb03d6d66b1171afac99c9aa71140f5ab1db94 Mon Sep 17 00:00:00 2001 From: Sanghun Lee Date: Fri, 13 Mar 2026 02:12:58 +0900 Subject: [PATCH 4/5] fix(BA-5071): add IMAGE_ALIAS to RBACElementTypeGQL enum Co-Authored-By: Claude Opus 4.6 --- src/ai/backend/manager/api/gql/rbac/types/permission.py | 1 + .../manager/repositories/deployment/db_source/db_source.py | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ai/backend/manager/api/gql/rbac/types/permission.py b/src/ai/backend/manager/api/gql/rbac/types/permission.py index 5a51e67b9ed..8b32e43db3c 100644 --- a/src/ai/backend/manager/api/gql/rbac/types/permission.py +++ b/src/ai/backend/manager/api/gql/rbac/types/permission.py @@ -89,6 +89,7 @@ class RBACElementTypeGQL(StrEnum): DEPLOYMENT_TOKEN = "deployment:token" DEPLOYMENT_POLICY = "deployment:policy" DEPLOYMENT_REVISION = "deployment:revision" + IMAGE_ALIAS = "image:alias" # Entity-level scopes ARTIFACT_REVISION = "artifact_revision" diff --git a/src/ai/backend/manager/repositories/deployment/db_source/db_source.py b/src/ai/backend/manager/repositories/deployment/db_source/db_source.py index 17d7582410f..aab1ec14634 100644 --- a/src/ai/backend/manager/repositories/deployment/db_source/db_source.py +++ b/src/ai/backend/manager/repositories/deployment/db_source/db_source.py @@ -135,7 +135,6 @@ from ai.backend.manager.repositories.deployment.creators import ( DeploymentCreatorSpec, DeploymentPolicyCreatorSpec, - DeploymentRevisionCreatorSpec, ) from ai.backend.manager.repositories.deployment.creators.endpoint import LegacyEndpointCreatorSpec from ai.backend.manager.repositories.deployment.types import ( From 6ee05cb99c145f249724146aeac10651f4752281 Mon Sep 17 00:00:00 2001 From: Sanghun Lee Date: Thu, 12 Mar 2026 17:21:08 +0000 Subject: [PATCH 5/5] chore: update api schema dump Co-authored-by: octodog --- docs/manager/graphql-reference/supergraph.graphql | 1 + docs/manager/graphql-reference/v2-schema.graphql | 1 + .../manager/repositories/deployment/db_source/db_source.py | 1 + 3 files changed, 3 insertions(+) diff --git a/docs/manager/graphql-reference/supergraph.graphql b/docs/manager/graphql-reference/supergraph.graphql index b6b636086e7..193e5ccff38 100644 --- a/docs/manager/graphql-reference/supergraph.graphql +++ b/docs/manager/graphql-reference/supergraph.graphql @@ -10501,6 +10501,7 @@ enum RBACElementType DEPLOYMENT_TOKEN @join__enumValue(graph: STRAWBERRY) DEPLOYMENT_POLICY @join__enumValue(graph: STRAWBERRY) DEPLOYMENT_REVISION @join__enumValue(graph: STRAWBERRY) + IMAGE_ALIAS @join__enumValue(graph: STRAWBERRY) ARTIFACT_REVISION @join__enumValue(graph: STRAWBERRY) } diff --git a/docs/manager/graphql-reference/v2-schema.graphql b/docs/manager/graphql-reference/v2-schema.graphql index 2dee3e5a954..56b7f9ae9e1 100644 --- a/docs/manager/graphql-reference/v2-schema.graphql +++ b/docs/manager/graphql-reference/v2-schema.graphql @@ -6087,6 +6087,7 @@ enum RBACElementType { DEPLOYMENT_TOKEN DEPLOYMENT_POLICY DEPLOYMENT_REVISION + IMAGE_ALIAS ARTIFACT_REVISION } diff --git a/src/ai/backend/manager/repositories/deployment/db_source/db_source.py b/src/ai/backend/manager/repositories/deployment/db_source/db_source.py index aab1ec14634..17d7582410f 100644 --- a/src/ai/backend/manager/repositories/deployment/db_source/db_source.py +++ b/src/ai/backend/manager/repositories/deployment/db_source/db_source.py @@ -135,6 +135,7 @@ from ai.backend.manager.repositories.deployment.creators import ( DeploymentCreatorSpec, DeploymentPolicyCreatorSpec, + DeploymentRevisionCreatorSpec, ) from ai.backend.manager.repositories.deployment.creators.endpoint import LegacyEndpointCreatorSpec from ai.backend.manager.repositories.deployment.types import (