diff --git a/changes/9628.feature.md b/changes/9628.feature.md new file mode 100644 index 00000000000..386b5c0f3bd --- /dev/null +++ b/changes/9628.feature.md @@ -0,0 +1 @@ +Apply RBAC permission validation to VFolder operations (create, get, list, update, delete, clone) diff --git a/src/ai/backend/manager/actions/validators/rbac/__init__.py b/src/ai/backend/manager/actions/validators/rbac/__init__.py index e69de29bb2d..a60ea87d9a8 100644 --- a/src/ai/backend/manager/actions/validators/rbac/__init__.py +++ b/src/ai/backend/manager/actions/validators/rbac/__init__.py @@ -0,0 +1,10 @@ +from dataclasses import dataclass + +from ai.backend.manager.actions.validators.rbac.scope import ScopeActionRBACValidator +from ai.backend.manager.actions.validators.rbac.single_entity import SingleEntityActionRBACValidator + + +@dataclass +class RBACValidators: + scope: ScopeActionRBACValidator + single_entity: SingleEntityActionRBACValidator diff --git a/src/ai/backend/manager/dependencies/processing/composer.py b/src/ai/backend/manager/dependencies/processing/composer.py index 9b88a3d5967..4e7c3b0c6f0 100644 --- a/src/ai/backend/manager/dependencies/processing/composer.py +++ b/src/ai/backend/manager/dependencies/processing/composer.py @@ -27,6 +27,9 @@ from ai.backend.manager.actions.monitors.audit_log import AuditLogMonitor from ai.backend.manager.actions.monitors.prometheus import PrometheusMonitor from ai.backend.manager.actions.monitors.reporter import ReporterMonitor +from ai.backend.manager.actions.validators.rbac import RBACValidators +from ai.backend.manager.actions.validators.rbac.scope import ScopeActionRBACValidator +from ai.backend.manager.actions.validators.rbac.single_entity import SingleEntityActionRBACValidator from ai.backend.manager.agent_cache import AgentRPCCache from ai.backend.manager.clients.agent.pool import AgentClientPool from ai.backend.manager.clients.appproxy.client import AppProxyClientPool @@ -203,6 +206,15 @@ async def compose( prometheus_monitor = PrometheusMonitor() audit_log_monitor = AuditLogMonitor(setup_input.repositories.audit_log.repository) + rbac_validators = RBACValidators( + scope=ScopeActionRBACValidator( + setup_input.repositories.permission_controller.repository, + ), + single_entity=SingleEntityActionRBACValidator( + setup_input.repositories.permission_controller.repository, + ), + ) + service_args = ServiceArgs( db=setup_input.db, repositories=setup_input.repositories, @@ -236,6 +248,7 @@ async def compose( ProcessorsProviderInput( service_args=service_args, action_monitors=[reporter_monitor, prometheus_monitor, audit_log_monitor], + rbac_validators=rbac_validators, ), ) diff --git a/src/ai/backend/manager/dependencies/processing/processors.py b/src/ai/backend/manager/dependencies/processing/processors.py index b61950e25dc..3985403d5d5 100644 --- a/src/ai/backend/manager/dependencies/processing/processors.py +++ b/src/ai/backend/manager/dependencies/processing/processors.py @@ -6,6 +6,7 @@ from ai.backend.common.dependencies import NonMonitorableDependencyProvider from ai.backend.manager.actions.monitors.monitor import ActionMonitor +from ai.backend.manager.actions.validators.rbac import RBACValidators from ai.backend.manager.services.processors import ProcessorArgs, Processors, ServiceArgs @@ -15,6 +16,7 @@ class ProcessorsProviderInput: service_args: ServiceArgs action_monitors: list[ActionMonitor] + rbac_validators: RBACValidators class ProcessorsDependency(NonMonitorableDependencyProvider[ProcessorsProviderInput, Processors]): @@ -31,7 +33,10 @@ def stage_name(self) -> str: @asynccontextmanager async def provide(self, setup_input: ProcessorsProviderInput) -> AsyncIterator[Processors]: processors = Processors.create( - ProcessorArgs(service_args=setup_input.service_args), + ProcessorArgs( + service_args=setup_input.service_args, + rbac_validators=setup_input.rbac_validators, + ), setup_input.action_monitors, ) yield processors diff --git a/src/ai/backend/manager/server.py b/src/ai/backend/manager/server.py index 91278cb3884..3681858672d 100644 --- a/src/ai/backend/manager/server.py +++ b/src/ai/backend/manager/server.py @@ -145,6 +145,9 @@ from .actions.monitors.audit_log import AuditLogMonitor from .actions.monitors.prometheus import PrometheusMonitor from .actions.monitors.reporter import ReporterMonitor +from .actions.validators.rbac import RBACValidators +from .actions.validators.rbac.scope import ScopeActionRBACValidator +from .actions.validators.rbac.single_entity import SingleEntityActionRBACValidator from .agent_cache import AgentRPCCache from .api import ManagerStatus from .api.context import RootContext @@ -644,6 +647,15 @@ async def processors_ctx(root_ctx: RootContext) -> AsyncIterator[None]: prometheus_monitor = PrometheusMonitor() audit_log_monitor = AuditLogMonitor(root_ctx.repositories.audit_log.repository) + rbac_validators = RBACValidators( + scope=ScopeActionRBACValidator( + root_ctx.repositories.permission_controller.repository, + ), + single_entity=SingleEntityActionRBACValidator( + root_ctx.repositories.permission_controller.repository, + ), + ) + root_ctx.processors = Processors.create( ProcessorArgs( service_args=ServiceArgs( @@ -673,6 +685,7 @@ async def processors_ctx(root_ctx: RootContext) -> AsyncIterator[None]: prometheus_client=root_ctx.prometheus_client, registry_quota_service=root_ctx.services_ctx.per_project_container_registries_quota, ), + rbac_validators=rbac_validators, ), [reporter_monitor, prometheus_monitor, audit_log_monitor], ) diff --git a/src/ai/backend/manager/services/processors.py b/src/ai/backend/manager/services/processors.py index bd8cc379adb..bfebcdf9d6e 100644 --- a/src/ai/backend/manager/services/processors.py +++ b/src/ai/backend/manager/services/processors.py @@ -16,6 +16,7 @@ from ai.backend.common.plugin.monitor import ErrorPluginContext from ai.backend.manager.actions.monitors.monitor import ActionMonitor from ai.backend.manager.actions.types import AbstractProcessorPackage, ActionSpec +from ai.backend.manager.actions.validators.rbac import RBACValidators from ai.backend.manager.agent_cache import AgentRPCCache from ai.backend.manager.clients.appproxy.client import AppProxyClientPool from ai.backend.manager.config.provider import ManagerConfigProvider @@ -484,6 +485,7 @@ def create(cls, args: ServiceArgs) -> Self: @dataclass class ProcessorArgs: service_args: ServiceArgs + rbac_validators: RBACValidators @dataclass @@ -549,7 +551,11 @@ def create(cls, args: ProcessorArgs, action_monitors: list[ActionMonitor]) -> Se container_registry_processors = ContainerRegistryProcessors( services.container_registry, action_monitors ) - vfolder_processors = VFolderProcessors(services.vfolder, action_monitors) + vfolder_processors = VFolderProcessors( + services.vfolder, + action_monitors, + args.rbac_validators, + ) vfolder_file_processors = VFolderFileProcessors(services.vfolder_file, action_monitors) vfolder_invite_processors = VFolderInviteProcessors( services.vfolder_invite, action_monitors diff --git a/src/ai/backend/manager/services/vfolder/processors/vfolder.py b/src/ai/backend/manager/services/vfolder/processors/vfolder.py index ac672725df9..d391bd7b11b 100644 --- a/src/ai/backend/manager/services/vfolder/processors/vfolder.py +++ b/src/ai/backend/manager/services/vfolder/processors/vfolder.py @@ -1,10 +1,10 @@ -from typing import override +from typing import cast, override from ai.backend.manager.actions.monitors.monitor import ActionMonitor from ai.backend.manager.actions.processor import ActionProcessor -from ai.backend.manager.actions.processor.scope import ScopeActionProcessor -from ai.backend.manager.actions.processor.single_entity import SingleEntityActionProcessor from ai.backend.manager.actions.types import AbstractProcessorPackage, ActionSpec +from ai.backend.manager.actions.validator.base import ActionValidator +from ai.backend.manager.actions.validators.rbac import RBACValidators from ai.backend.manager.services.vfolder.actions.base import ( CloneVFolderAction, CloneVFolderActionResult, @@ -61,27 +61,23 @@ class VFolderProcessors(AbstractProcessorPackage): - create_vfolder: ScopeActionProcessor[CreateVFolderAction, CreateVFolderActionResult] - get_vfolder: SingleEntityActionProcessor[GetVFolderAction, GetVFolderActionResult] - list_vfolder: ScopeActionProcessor[ListVFolderAction, ListVFolderActionResult] - update_vfolder_attribute: SingleEntityActionProcessor[ + create_vfolder: ActionProcessor[CreateVFolderAction, CreateVFolderActionResult] + get_vfolder: ActionProcessor[GetVFolderAction, GetVFolderActionResult] + list_vfolder: ActionProcessor[ListVFolderAction, ListVFolderActionResult] + update_vfolder_attribute: ActionProcessor[ UpdateVFolderAttributeAction, UpdateVFolderAttributeActionResult ] - move_to_trash_vfolder: SingleEntityActionProcessor[ - MoveToTrashVFolderAction, MoveToTrashVFolderActionResult - ] - restore_vfolder_from_trash: SingleEntityActionProcessor[ + move_to_trash_vfolder: ActionProcessor[MoveToTrashVFolderAction, MoveToTrashVFolderActionResult] + restore_vfolder_from_trash: ActionProcessor[ RestoreVFolderFromTrashAction, RestoreVFolderFromTrashActionResult ] - delete_forever_vfolder: SingleEntityActionProcessor[ + delete_forever_vfolder: ActionProcessor[ DeleteForeverVFolderAction, DeleteForeverVFolderActionResult ] - purge_vfolder: SingleEntityActionProcessor[PurgeVFolderAction, PurgeVFolderActionResult] - force_delete_vfolder: SingleEntityActionProcessor[ - ForceDeleteVFolderAction, ForceDeleteVFolderActionResult - ] - clone_vfolder: SingleEntityActionProcessor[CloneVFolderAction, CloneVFolderActionResult] - get_task_logs: SingleEntityActionProcessor[GetTaskLogsAction, GetTaskLogsActionResult] + purge_vfolder: ActionProcessor[PurgeVFolderAction, PurgeVFolderActionResult] + force_delete_vfolder: ActionProcessor[ForceDeleteVFolderAction, ForceDeleteVFolderActionResult] + clone_vfolder: ActionProcessor[CloneVFolderAction, CloneVFolderActionResult] + get_task_logs: ActionProcessor[GetTaskLogsAction, GetTaskLogsActionResult] list_allowed_types: ActionProcessor[ListAllowedTypesAction, ListAllowedTypesActionResult] list_all_hosts: ActionProcessor[ListAllHostsAction, ListAllHostsActionResult] get_volume_perf_metric: ActionProcessor[ @@ -100,28 +96,72 @@ class VFolderProcessors(AbstractProcessorPackage): umount_host: ActionProcessor[UmountHostAction, UmountHostActionResult] get_fstab_contents: ActionProcessor[GetFstabContentsAction, GetFstabContentsActionResult] - def __init__(self, service: VFolderService, action_monitors: list[ActionMonitor]) -> None: - self.create_vfolder = ScopeActionProcessor(service.create, action_monitors) - self.get_vfolder = SingleEntityActionProcessor(service.get, action_monitors) - self.list_vfolder = ScopeActionProcessor(service.list, action_monitors) - self.update_vfolder_attribute = SingleEntityActionProcessor( - service.update_attribute, action_monitors + def __init__( + self, + service: VFolderService, + action_monitors: list[ActionMonitor], + rbac_validators: RBACValidators, + ) -> None: + # Extract RBAC validators + scope_validator = rbac_validators.scope + single_entity_validator = rbac_validators.single_entity + + # Scope actions with RBAC validation + self.create_vfolder = ActionProcessor( + service.create, + action_monitors, + validators=[cast(ActionValidator, scope_validator)], + ) + self.list_vfolder = ActionProcessor( + service.list, + action_monitors, + validators=[cast(ActionValidator, scope_validator)], + ) + + # Single entity actions with RBAC validation + self.get_vfolder = ActionProcessor( + service.get, + action_monitors, + validators=[cast(ActionValidator, single_entity_validator)], + ) + self.update_vfolder_attribute = ActionProcessor( + service.update_attribute, + action_monitors, + validators=[cast(ActionValidator, single_entity_validator)], + ) + self.move_to_trash_vfolder = ActionProcessor( + service.move_to_trash, + action_monitors, + validators=[cast(ActionValidator, single_entity_validator)], ) - self.move_to_trash_vfolder = SingleEntityActionProcessor( - service.move_to_trash, action_monitors + self.restore_vfolder_from_trash = ActionProcessor( + service.restore, + action_monitors, + validators=[cast(ActionValidator, single_entity_validator)], ) - self.restore_vfolder_from_trash = SingleEntityActionProcessor( - service.restore, action_monitors + self.delete_forever_vfolder = ActionProcessor( + service.delete_forever, + action_monitors, + validators=[cast(ActionValidator, single_entity_validator)], ) - self.delete_forever_vfolder = SingleEntityActionProcessor( - service.delete_forever, action_monitors + self.purge_vfolder = ActionProcessor( + service.purge, + action_monitors, + validators=[cast(ActionValidator, single_entity_validator)], ) - self.purge_vfolder = SingleEntityActionProcessor(service.purge, action_monitors) - self.force_delete_vfolder = SingleEntityActionProcessor( - service.force_delete, action_monitors + self.force_delete_vfolder = ActionProcessor( + service.force_delete, + action_monitors, + validators=[cast(ActionValidator, single_entity_validator)], ) - self.clone_vfolder = SingleEntityActionProcessor(service.clone, action_monitors) - self.get_task_logs = SingleEntityActionProcessor(service.get_task_logs, action_monitors) + self.clone_vfolder = ActionProcessor( + service.clone, + action_monitors, + validators=[cast(ActionValidator, single_entity_validator)], + ) + + # Actions without RBAC validation (internal/legacy/storage ops) + self.get_task_logs = ActionProcessor(service.get_task_logs, action_monitors) self.list_allowed_types = ActionProcessor(service.list_allowed_types, action_monitors) self.list_all_hosts = ActionProcessor(service.list_all_hosts, action_monitors) self.get_volume_perf_metric = ActionProcessor(