From 0f718d3a7f083f8c1cf56badd28b993b77b810d0 Mon Sep 17 00:00:00 2001 From: Sanghun Lee Date: Fri, 13 Mar 2026 10:50:59 +0900 Subject: [PATCH 1/6] feat(BA-2476): add ProjectScopeAction and ProjectSingleEntityAction base classes Add project-specific RBAC action base classes that return EntityType.PROJECT: - ProjectScopeAction / ProjectScopeActionResult - ProjectSingleEntityAction / ProjectSingleEntityActionResult These will be used by project-related actions to properly implement RBAC validation. Co-Authored-By: Claude Sonnet 4.5 --- .../manager/services/group/actions/base.py | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/src/ai/backend/manager/services/group/actions/base.py b/src/ai/backend/manager/services/group/actions/base.py index 2ab78041d05..595bd815096 100644 --- a/src/ai/backend/manager/services/group/actions/base.py +++ b/src/ai/backend/manager/services/group/actions/base.py @@ -41,3 +41,29 @@ def field_data(self) -> FieldData | None: class GroupSingleEntityActionResult(BaseSingleEntityActionResult): pass + + +class ProjectScopeAction(BaseScopeAction): + @override + @classmethod + def entity_type(cls) -> EntityType: + return EntityType.PROJECT + + +class ProjectScopeActionResult(BaseScopeActionResult): + pass + + +class ProjectSingleEntityAction(BaseSingleEntityAction): + @override + @classmethod + def entity_type(cls) -> EntityType: + return EntityType.PROJECT + + @override + def field_data(self) -> FieldData | None: + return None + + +class ProjectSingleEntityActionResult(BaseSingleEntityActionResult): + pass From ba0b47ab4f688fd5fd24f524b01d24ef74fe1277 Mon Sep 17 00:00:00 2001 From: Sanghun Lee Date: Fri, 13 Mar 2026 10:54:23 +0900 Subject: [PATCH 2/6] feat(BA-2476): refactor project actions to use ProjectScopeAction and ProjectSingleEntityAction bases MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Classify and refactor project-related actions in search_projects.py: - SearchProjectsByDomainAction → ProjectScopeAction - SearchProjectsByUserAction → ProjectScopeAction - GetProjectAction → ProjectSingleEntityAction - SearchProjectsAction remains as GroupAction (admin/internal) All RBAC methods (scope_type, scope_id, target_entity_id, target_element) already implemented correctly. Co-Authored-By: Claude Sonnet 4.5 --- .../services/group/actions/search_projects.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/ai/backend/manager/services/group/actions/search_projects.py b/src/ai/backend/manager/services/group/actions/search_projects.py index 4bc438ffd53..ced71d19ca3 100644 --- a/src/ai/backend/manager/services/group/actions/search_projects.py +++ b/src/ai/backend/manager/services/group/actions/search_projects.py @@ -18,10 +18,10 @@ ) from ai.backend.manager.services.group.actions.base import ( GroupAction, - GroupScopeAction, - GroupScopeActionResult, - GroupSingleEntityAction, - GroupSingleEntityActionResult, + ProjectScopeAction, + ProjectScopeActionResult, + ProjectSingleEntityAction, + ProjectSingleEntityActionResult, ) @@ -42,7 +42,7 @@ def operation_type(cls) -> ActionOperationType: @dataclass -class SearchProjectsByDomainAction(GroupScopeAction): +class SearchProjectsByDomainAction(ProjectScopeAction): """Search projects within a domain.""" scope: DomainProjectSearchScope @@ -67,7 +67,7 @@ def target_element(self) -> RBACElementRef: @dataclass -class SearchProjectsByUserAction(GroupScopeAction): +class SearchProjectsByUserAction(ProjectScopeAction): """Search projects a user is member of.""" scope: UserProjectSearchScope @@ -92,7 +92,7 @@ def target_element(self) -> RBACElementRef: @dataclass -class GetProjectAction(GroupSingleEntityAction): +class GetProjectAction(ProjectSingleEntityAction): """Get a single project by UUID.""" project_id: UUID @@ -129,7 +129,7 @@ def entity_id(self) -> str | None: @dataclass -class ScopedSearchProjectsActionResult(GroupScopeActionResult): +class ScopedSearchProjectsActionResult(ProjectScopeActionResult): """Result from searching projects within a scope.""" items: list[GroupData] @@ -149,7 +149,7 @@ def scope_id(self) -> str: @dataclass -class GetProjectActionResult(GroupSingleEntityActionResult): +class GetProjectActionResult(ProjectSingleEntityActionResult): """Result from getting a single project.""" data: GroupData From 9ae9b4b644b3f71b1071a64a131e1189c475d0a2 Mon Sep 17 00:00:00 2001 From: Sanghun Lee Date: Fri, 13 Mar 2026 10:57:38 +0900 Subject: [PATCH 3/6] feat(BA-2476): add RBAC validators to project action processors Add RBAC validators to ScopeActionProcessor and SingleEntityActionProcessor for project-related actions in the group service. Changes: - Pass validators.rbac.scope to ScopeActionProcessor for: - create_group, search_projects_by_domain, search_projects_by_user - Pass validators.rbac.single_entity to SingleEntityActionProcessor for: - modify_group, delete_group, purge_group, get_project - Internal/admin actions (usage_per_month, usage_per_period, search_projects) remain on plain ActionProcessor without RBAC validators Co-Authored-By: Claude Sonnet 4.5 --- .../manager/services/group/processors.py | 28 ++++++++++++++----- 1 file changed, 21 insertions(+), 7 deletions(-) diff --git a/src/ai/backend/manager/services/group/processors.py b/src/ai/backend/manager/services/group/processors.py index 51c0e6c1719..f795cfed456 100644 --- a/src/ai/backend/manager/services/group/processors.py +++ b/src/ai/backend/manager/services/group/processors.py @@ -64,20 +64,34 @@ def __init__( action_monitors: list[ActionMonitor], validators: ActionValidators, ) -> None: - self.create_group = ScopeActionProcessor(group_service.create_group, action_monitors) - self.modify_group = SingleEntityActionProcessor(group_service.modify_group, action_monitors) - self.delete_group = SingleEntityActionProcessor(group_service.delete_group, action_monitors) - self.purge_group = SingleEntityActionProcessor(group_service.purge_group, action_monitors) + self.create_group = ScopeActionProcessor( + group_service.create_group, action_monitors, validators=[validators.rbac.scope] + ) + self.modify_group = SingleEntityActionProcessor( + group_service.modify_group, action_monitors, validators=[validators.rbac.single_entity] + ) + self.delete_group = SingleEntityActionProcessor( + group_service.delete_group, action_monitors, validators=[validators.rbac.single_entity] + ) + self.purge_group = SingleEntityActionProcessor( + group_service.purge_group, action_monitors, validators=[validators.rbac.single_entity] + ) self.usage_per_month = ActionProcessor(group_service.usage_per_month, action_monitors) self.usage_per_period = ActionProcessor(group_service.usage_per_period, action_monitors) self.search_projects = ActionProcessor(group_service.search_projects, action_monitors) self.search_projects_by_domain = ScopeActionProcessor( - group_service.search_projects_by_domain, action_monitors + group_service.search_projects_by_domain, + action_monitors, + validators=[validators.rbac.scope], ) self.search_projects_by_user = ScopeActionProcessor( - group_service.search_projects_by_user, action_monitors + group_service.search_projects_by_user, + action_monitors, + validators=[validators.rbac.scope], + ) + self.get_project = SingleEntityActionProcessor( + group_service.get_project, action_monitors, validators=[validators.rbac.single_entity] ) - self.get_project = SingleEntityActionProcessor(group_service.get_project, action_monitors) @override def supported_actions(self) -> list[ActionSpec]: From 15a3684c4b1d4cfc9a236bb92fdce20d3688ed02 Mon Sep 17 00:00:00 2001 From: Sanghun Lee Date: Fri, 13 Mar 2026 10:59:31 +0900 Subject: [PATCH 4/6] changelog: add news fragment for PR #10029 --- changes/10029.feature.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 changes/10029.feature.md diff --git a/changes/10029.feature.md b/changes/10029.feature.md new file mode 100644 index 00000000000..b857589fa0e --- /dev/null +++ b/changes/10029.feature.md @@ -0,0 +1 @@ +Apply RBAC validators to project (group) action processors for proper permission enforcement \ No newline at end of file From bbe6a9e6aa617a3e3edcdc5e44a9dd81509df803 Mon Sep 17 00:00:00 2001 From: Sanghun Lee Date: Fri, 13 Mar 2026 21:01:07 +0900 Subject: [PATCH 5/6] refactor: exclude search actions from RBAC validator application Search actions are excluded from RBAC validator scope per BA-2946. Search results are already filtered by scope through the existing SearchScope mechanism. Co-Authored-By: Claude Opus 4.6 --- src/ai/backend/manager/services/group/processors.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/ai/backend/manager/services/group/processors.py b/src/ai/backend/manager/services/group/processors.py index f795cfed456..fd84eda9951 100644 --- a/src/ai/backend/manager/services/group/processors.py +++ b/src/ai/backend/manager/services/group/processors.py @@ -80,14 +80,10 @@ def __init__( self.usage_per_period = ActionProcessor(group_service.usage_per_period, action_monitors) self.search_projects = ActionProcessor(group_service.search_projects, action_monitors) self.search_projects_by_domain = ScopeActionProcessor( - group_service.search_projects_by_domain, - action_monitors, - validators=[validators.rbac.scope], + group_service.search_projects_by_domain, action_monitors ) self.search_projects_by_user = ScopeActionProcessor( - group_service.search_projects_by_user, - action_monitors, - validators=[validators.rbac.scope], + group_service.search_projects_by_user, action_monitors ) self.get_project = SingleEntityActionProcessor( group_service.get_project, action_monitors, validators=[validators.rbac.single_entity] From 1d9b6876271936576238a4b66e0004efb618468b Mon Sep 17 00:00:00 2001 From: Sanghun Lee Date: Tue, 17 Mar 2026 17:22:58 +0900 Subject: [PATCH 6/6] set variable --- src/ai/backend/manager/services/group/processors.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/ai/backend/manager/services/group/processors.py b/src/ai/backend/manager/services/group/processors.py index fd84eda9951..73a02dddd4e 100644 --- a/src/ai/backend/manager/services/group/processors.py +++ b/src/ai/backend/manager/services/group/processors.py @@ -64,17 +64,19 @@ def __init__( action_monitors: list[ActionMonitor], validators: ActionValidators, ) -> None: + rbac_scope_validators = [validators.rbac.scope] + rbac_single_entity_validators = [validators.rbac.single_entity] self.create_group = ScopeActionProcessor( - group_service.create_group, action_monitors, validators=[validators.rbac.scope] + group_service.create_group, action_monitors, validators=rbac_scope_validators ) self.modify_group = SingleEntityActionProcessor( - group_service.modify_group, action_monitors, validators=[validators.rbac.single_entity] + group_service.modify_group, action_monitors, validators=rbac_single_entity_validators ) self.delete_group = SingleEntityActionProcessor( - group_service.delete_group, action_monitors, validators=[validators.rbac.single_entity] + group_service.delete_group, action_monitors, validators=rbac_single_entity_validators ) self.purge_group = SingleEntityActionProcessor( - group_service.purge_group, action_monitors, validators=[validators.rbac.single_entity] + group_service.purge_group, action_monitors, validators=rbac_single_entity_validators ) self.usage_per_month = ActionProcessor(group_service.usage_per_month, action_monitors) self.usage_per_period = ActionProcessor(group_service.usage_per_period, action_monitors) @@ -86,7 +88,7 @@ def __init__( group_service.search_projects_by_user, action_monitors ) self.get_project = SingleEntityActionProcessor( - group_service.get_project, action_monitors, validators=[validators.rbac.single_entity] + group_service.get_project, action_monitors, validators=rbac_single_entity_validators ) @override