Skip to content
1 change: 1 addition & 0 deletions changes/9628.feature.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Apply RBAC permission validation to VFolder operations (create, get, list, update, delete, clone)
10 changes: 10 additions & 0 deletions src/ai/backend/manager/actions/validators/rbac/__init__.py
Original file line number Diff line number Diff line change
@@ -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
13 changes: 13 additions & 0 deletions src/ai/backend/manager/dependencies/processing/composer.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
),
)

Expand Down
7 changes: 6 additions & 1 deletion src/ai/backend/manager/dependencies/processing/processors.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand All @@ -15,6 +16,7 @@ class ProcessorsProviderInput:

service_args: ServiceArgs
action_monitors: list[ActionMonitor]
rbac_validators: RBACValidators


class ProcessorsDependency(NonMonitorableDependencyProvider[ProcessorsProviderInput, Processors]):
Expand All @@ -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
13 changes: 13 additions & 0 deletions src/ai/backend/manager/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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],
)
Expand Down
8 changes: 7 additions & 1 deletion src/ai/backend/manager/services/processors.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -484,6 +485,7 @@ def create(cls, args: ServiceArgs) -> Self:
@dataclass
class ProcessorArgs:
service_args: ServiceArgs
rbac_validators: RBACValidators


@dataclass
Expand Down Expand Up @@ -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
Expand Down
110 changes: 75 additions & 35 deletions src/ai/backend/manager/services/vfolder/processors/vfolder.py
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -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[
Expand All @@ -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(
Expand Down
Loading