diff --git a/api/commands/__init__.py b/api/commands/__init__.py index 73009124062..14bffa072e3 100644 --- a/api/commands/__init__.py +++ b/api/commands/__init__.py @@ -23,6 +23,7 @@ from .plugin import ( transform_datasource_credentials, ) from .rbac import ( + migrate_agent_permissions_to_rbac, migrate_dataset_permissions_to_rbac, migrate_member_roles_to_rbac, migrate_only_me_resource_whitelist_scopes_to_automatic_include, @@ -81,6 +82,7 @@ __all__ = [ "install_plugins", "install_rag_pipeline_plugins", "legacy_model_types", + "migrate_agent_permissions_to_rbac", "migrate_annotation_vector_database", "migrate_data_for_plugin", "migrate_dataset_permissions_to_rbac", diff --git a/api/commands/rbac.py b/api/commands/rbac.py index 5c10c77a1dd..ada2f9606d9 100644 --- a/api/commands/rbac.py +++ b/api/commands/rbac.py @@ -3,6 +3,8 @@ from __future__ import annotations import json from collections.abc import Iterator from concurrent.futures import ThreadPoolExecutor, as_completed +from dataclasses import dataclass +from enum import StrEnum import click from sqlalchemy import select @@ -11,12 +13,34 @@ from sqlalchemy.orm import Session from configs import dify_config from core.db.session_factory import session_factory from core.rbac import RBACResourceWhitelistScope -from models import App, Dataset, DatasetPermission, DatasetPermissionEnum, TenantAccountJoin, TenantAccountRole -from services.enterprise.rbac_service import ListOption, RBACService, ReplaceMemberBindings, ReplaceUserAccessPolicies +from models import ( + Agent, + AgentStatus, + App, + Dataset, + DatasetPermission, + DatasetPermissionEnum, + Tenant, + TenantAccountJoin, + TenantAccountRole, +) +from services.enterprise.rbac_service import ( + LegacyAgentRoleMigration, + ListOption, + RBACResourceType, + RBACService, + ReplaceMemberBindings, + ReplaceUserAccessPolicies, +) _RBAC_DEFAULT_ACCESS_POLICY_ID = "default" _RBAC_RESOURCE_ACCESS_POLICY_BATCH_SIZE = 500 +_AGENT_MIGRATION_STATE_LABEL = { + False: "would change", + True: "changed", +} + _LEGACY_ROLE_TO_BUILTIN_TAG = { TenantAccountRole.OWNER.value: "owner", TenantAccountRole.ADMIN.value: "admin", @@ -67,6 +91,88 @@ def _resolve_builtin_role_id(tenant_id: str, operator_account_id: str, legacy_ro return _resolve_builtin_role_ids(tenant_id, operator_account_id)[legacy_role] +def _iter_tenant_ids(tenant_id: str | None, *, batch_size: int) -> Iterator[str]: + if tenant_id: + yield tenant_id + return + last_id: str | None = None + while True: + with session_factory.create_session() as session: + stmt = select(Tenant.id).order_by(Tenant.id.asc()).limit(batch_size) + if last_id is not None: + stmt = stmt.where(Tenant.id > last_id) + rows = session.execute(stmt).scalars().all() + if not rows: + return + for row in rows: + yield str(row) + last_id = str(rows[-1]) + + +def _emit_agent_migration_event(payload: dict[str, object]) -> None: + click.echo(json.dumps(payload, sort_keys=True)) + + +@dataclass(frozen=True) +class _AgentMigrationEventKind: + event_stem: str + include_tenant_id: bool = True + + @property + def skipped(self) -> str: + return f"{self.event_stem}_skipped" + + @property + def failed(self) -> str: + return f"{self.event_stem}_failed" + + def outcome(self, *, apply: bool) -> str: + return f"{self.event_stem}_applied" if apply else f"{self.event_stem}_proposed_change" + + +_AGENT_ROLE_MIGRATION_EVENT_KIND = _AgentMigrationEventKind(event_stem="agent_manage_role_migration") +_AGENT_ROLE_TEMPLATE_MIGRATION_EVENT_KIND = _AgentMigrationEventKind( + event_stem="agent_manage_role_template_migration", include_tenant_id=False +) +_AGENT_ACCESS_BOOTSTRAP_EVENT_KIND = _AgentMigrationEventKind(event_stem="agent_access_bootstrap") + +_AGENT_BACKING_APP_SPECIFIC_WHITELIST_EVENT = "agent_backing_app_has_specific_whitelist" +_AGENT_ACCESS_BOOTSTRAP_MEMBER_SOURCE = "workspace_members" + + +class _AgentAccessBootstrapReason(StrEnum): + ALREADY_INITIALIZED = "already_initialized" + MISSING_WHITELIST_SCOPE = "missing_whitelist_scope" + NO_CREATOR = "no_creator" + + +def _agent_manage_role_event( + tenant_id: str | None, + entry: LegacyAgentRoleMigration, + *, + apply: bool, + kind: _AgentMigrationEventKind, +) -> dict[str, object]: + base: dict[str, object] = { + "dry_run": not apply, + "role_id": entry.role_id, + "role_name": entry.role_name, + } + if kind.include_tenant_id: + base["tenant_id"] = tenant_id + if entry.skipped: + return {**base, "event": kind.skipped, "reason": entry.skipped} + return { + **base, + "event": kind.outcome(apply=apply), + "after": { + "added_keys": entry.added_keys, + "removed_keys": entry.removed_keys, + "bound_policies": entry.bound_policies, + }, + } + + def _iter_tenant_member_batches( tenant_id: str | None, *, @@ -1005,3 +1111,302 @@ def migrate_dataset_permissions_to_rbac( fg="green", ) ) + + +_AgentRow = tuple[str, str | None, str | None] + + +def _iter_agent_row_batches(tenant_id: str, batch_size: int) -> Iterator[list[_AgentRow]]: + last_agent_id: str | None = None + while True: + with session_factory.create_session() as session: + stmt = ( + select(Agent.id, Agent.created_by, Agent.backing_app_id) + .where(Agent.tenant_id == tenant_id, Agent.status != AgentStatus.ARCHIVED) + .order_by(Agent.id.asc()) + .limit(batch_size) + ) + if last_agent_id: + stmt = stmt.where(Agent.id > last_agent_id) + + rows = list(session.execute(stmt).all()) + if not rows: + return + + yield [ + ( + str(row.id), + str(row.created_by) if row.created_by else None, + str(row.backing_app_id) if row.backing_app_id else None, + ) + for row in rows + ] + + last_agent_id = str(rows[-1].id) + + +@dataclass +class _AgentAccessBootstrapCounts: + changed: int = 0 + already_initialized: int = 0 + + +@dataclass(frozen=True) +class _AgentAccessBootstrapOptions: + tenant_id: str + agent_id: str + creator_account_id: str | None + backing_app_id: str | None + operator_account_id: str + member_batch_size: int + apply: bool + initialized: bool + + +def _report_backing_app_specific_whitelist(options: _AgentAccessBootstrapOptions) -> None: + if not options.backing_app_id: + return + app_config = _resource_legacy_whitelist_config( + "app", + tenant_id=options.tenant_id, + operator_account_id=options.operator_account_id, + resource_id=options.backing_app_id, + ) + if _normalize_rbac_whitelist_scope(app_config.rbac_whitelist_scope) is not RBACResourceWhitelistScope.SPECIFIC: + return + _emit_agent_migration_event( + { + "event": _AGENT_BACKING_APP_SPECIFIC_WHITELIST_EVENT, + "dry_run": not options.apply, + "tenant_id": options.tenant_id, + "agent_id": options.agent_id, + "app_id": options.backing_app_id, + "backing_app_account_ids": sorted(set(app_config.account_ids)), + } + ) + + +def _write_agent_access_rows(options: _AgentAccessBootstrapOptions) -> None: + for batch in _workspace_member_account_id_batches(options.tenant_id, options.member_batch_size): + RBACService.AgentAccess.replace_user_access_policies( + tenant_id=options.tenant_id, + account_id=options.operator_account_id, + agent_id=options.agent_id, + target_account_id=None, + payload=ReplaceUserAccessPolicies( + access_policy_ids=[_RBAC_DEFAULT_ACCESS_POLICY_ID], + account_ids=batch, + ), + ) + if options.creator_account_id: + RBACService.AccessPolicies.sync_creator_access_policy_member_bindings( + tenant_id=options.tenant_id, + account_id=options.creator_account_id, + resource_type=RBACResourceType.AGENT, + resource_id=options.agent_id, + ) + RBACService.AgentAccess.replace_whitelist( + tenant_id=options.tenant_id, + account_id=options.operator_account_id, + agent_id=options.agent_id, + payload=ReplaceMemberBindings(automatic_include_workspace_members=True), + ) + + +def _emit_agent_access_bootstrap_skipped( + options: _AgentAccessBootstrapOptions, + reason: _AgentAccessBootstrapReason, +) -> None: + _emit_agent_migration_event( + { + "event": _AGENT_ACCESS_BOOTSTRAP_EVENT_KIND.skipped, + "reason": reason.value, + "dry_run": not options.apply, + "tenant_id": options.tenant_id, + "agent_id": options.agent_id, + "operator_account_id": options.operator_account_id, + } + ) + + +def _agent_access_bootstrap_failure( + options: _AgentAccessBootstrapOptions, + exc: Exception, +) -> click.ClickException: + _emit_agent_migration_event( + { + "event": _AGENT_ACCESS_BOOTSTRAP_EVENT_KIND.failed, + "tenant_id": options.tenant_id, + "agent_id": options.agent_id, + "error": str(exc), + } + ) + return click.ClickException(f"tenant {options.tenant_id} agent {options.agent_id}: {exc}") + + +def _bootstrap_agent_access(options: _AgentAccessBootstrapOptions, counts: _AgentAccessBootstrapCounts) -> None: + if options.initialized: + counts.already_initialized += 1 + _emit_agent_access_bootstrap_skipped(options, _AgentAccessBootstrapReason.ALREADY_INITIALIZED) + return + + config = RBACService.AgentAccess.legacy_whitelist_config( + tenant_id=options.tenant_id, + account_id=options.operator_account_id, + agent_id=options.agent_id, + ) + try: + scope = _normalize_rbac_whitelist_scope(config.rbac_whitelist_scope) + except ValueError as exc: + raise _agent_access_bootstrap_failure(options, exc) from exc + if scope is None: + _emit_agent_access_bootstrap_skipped(options, _AgentAccessBootstrapReason.MISSING_WHITELIST_SCOPE) + return + + account_ids = sorted(set(config.account_ids)) + _report_backing_app_specific_whitelist(options) + + if options.apply: + try: + _write_agent_access_rows(options) + except Exception as exc: + raise _agent_access_bootstrap_failure(options, exc) from exc + counts.changed += 1 + + event: dict[str, object] = { + "event": _AGENT_ACCESS_BOOTSTRAP_EVENT_KIND.outcome(apply=options.apply), + "dry_run": not options.apply, + "tenant_id": options.tenant_id, + "agent_id": options.agent_id, + "operator_account_id": options.operator_account_id, + "before": {"rbac_whitelist_scope": scope.value, "whitelist_account_ids": account_ids}, + "after": { + "automatic_include_workspace_members": True, + "default_policy_member_source": _AGENT_ACCESS_BOOTSTRAP_MEMBER_SOURCE, + "creator_access_policy_synced": bool(options.creator_account_id), + }, + } + if not options.creator_account_id: + event["reason"] = _AgentAccessBootstrapReason.NO_CREATOR.value + _emit_agent_migration_event(event) + + +def _bootstrap_tenant_agent_access( + tenant_id: str, + *, + agent_batch_size: int, + member_batch_size: int, + apply: bool, + counts: _AgentAccessBootstrapCounts, +) -> None: + owner_account_id: str | None = None + for rows in _iter_agent_row_batches(tenant_id, agent_batch_size): + initialized_agent_ids = set( + RBACService.Migrations.list_configured_agent_ids(tenant_id, [agent_id for agent_id, _, _ in rows]) + ) + for agent_id, creator_account_id, backing_app_id in rows: + if creator_account_id: + operator_account_id = creator_account_id + else: + if owner_account_id is None: + with session_factory.create_session() as session: + owner_account_id = _owner_account_id(tenant_id, session=session) + operator_account_id = owner_account_id + _bootstrap_agent_access( + _AgentAccessBootstrapOptions( + tenant_id=tenant_id, + agent_id=agent_id, + creator_account_id=creator_account_id, + backing_app_id=backing_app_id, + operator_account_id=operator_account_id, + member_batch_size=member_batch_size, + apply=apply, + initialized=agent_id in initialized_agent_ids, + ), + counts, + ) + + +@click.command( + "rbac-migrate-agent-permissions", + help=( + "Upgrade step for agent RBAC. Phase 1 asks the RBAC service to replace agent.manage on every " + "custom role with agent.create plus the agent.full_access binding. Phase 2 bootstraps the access " + "rows pre-existing agents never got, so they stay visible to workspace members. Agents that " + "already have a whitelist scope row are skipped. Dry run by default." + ), +) +@click.option("--tenant-id", help="Only migrate a single workspace.") +@click.option( + "--batch-size", + default=500, + show_default=True, + type=click.IntRange(min=1), + help="Tenants fetched per database batch.", +) +@click.option( + "--agent-batch-size", + default=500, + show_default=True, + type=click.IntRange(min=1), + help="Agents fetched per database batch.", +) +@click.option( + "--member-batch-size", + default=_RBAC_RESOURCE_ACCESS_POLICY_BATCH_SIZE, + show_default=True, + type=click.IntRange(min=1), + help="Workspace members written per default-policy call when bootstrapping an agent.", +) +@click.option("--apply", is_flag=True, default=False, help="Write changes. Without it nothing is written.") +def migrate_agent_permissions_to_rbac( + tenant_id: str | None, + batch_size: int, + agent_batch_size: int, + member_batch_size: int, + apply: bool, +) -> None: + click.echo(click.style("Starting agent RBAC migration: custom roles holding agent.manage.", fg="green")) + tenant_count = 0 + role_count = 0 + skipped_count = 0 + seen_template_ids: set[str] = set() + template_count = 0 + agent_counts = _AgentAccessBootstrapCounts() + for workspace_id in _iter_tenant_ids(tenant_id, batch_size=batch_size): + tenant_count += 1 + try: + report = RBACService.Migrations.migrate_agent_manage_roles(workspace_id, apply=apply) + except Exception as exc: + raise click.ClickException(f"tenant {workspace_id}: {exc}") from exc + for entry in report.roles: + role_count += 1 + if entry.skipped: + skipped_count += 1 + _emit_agent_migration_event( + _agent_manage_role_event(workspace_id, entry, apply=apply, kind=_AGENT_ROLE_MIGRATION_EVENT_KIND) + ) + for template in report.role_templates: + if template.role_id in seen_template_ids: + continue + seen_template_ids.add(template.role_id) + template_count += 1 + _emit_agent_migration_event( + _agent_manage_role_event(None, template, apply=apply, kind=_AGENT_ROLE_TEMPLATE_MIGRATION_EVENT_KIND) + ) + _bootstrap_tenant_agent_access( + workspace_id, + agent_batch_size=agent_batch_size, + member_batch_size=member_batch_size, + apply=apply, + counts=agent_counts, + ) + role_state = _AGENT_MIGRATION_STATE_LABEL[apply] + click.echo( + f"{tenant_count} tenant(s), {role_count} role(s) {role_state}, {skipped_count} skipped, " + f"{template_count} template(s) {role_state}, " + f"{agent_counts.changed} agent(s) {role_state}, " + f"{agent_counts.already_initialized} already initialised" + ) + if not apply: + click.echo(click.style("Dry run: no changes written. Re-run with --apply.", fg="yellow")) diff --git a/api/controllers/common/rbac/__init__.py b/api/controllers/common/rbac/__init__.py new file mode 100644 index 00000000000..6f227d5034d --- /dev/null +++ b/api/controllers/common/rbac/__init__.py @@ -0,0 +1,29 @@ +from core.rbac import RBACPermission, RBACResourceScope + +from .checks import RBAC_CHECKS_ATTR, RBACCheck, enforce_rbac_checks +from .locators import ( + AgentBehindApp, + AgentId, + DatasetByPipeline, + DatasetId, + PlainApp, + ResourceIdentity, + ResourceLocator, + Workspace, +) + +__all__ = [ + "RBAC_CHECKS_ATTR", + "AgentBehindApp", + "AgentId", + "DatasetByPipeline", + "DatasetId", + "PlainApp", + "RBACCheck", + "RBACPermission", + "RBACResourceScope", + "ResourceIdentity", + "ResourceLocator", + "Workspace", + "enforce_rbac_checks", +] diff --git a/api/controllers/common/rbac/checks.py b/api/controllers/common/rbac/checks.py new file mode 100644 index 00000000000..5cd1ee74159 --- /dev/null +++ b/api/controllers/common/rbac/checks.py @@ -0,0 +1,65 @@ +from collections.abc import Mapping, Sequence +from dataclasses import dataclass + +from flask import request +from werkzeug.exceptions import Forbidden, NotFound + +from configs import dify_config +from core.rbac import RBACPermission, RBACResourceScope +from services.enterprise.rbac_service import RBACService + +from .locators import ResourceLocator, Workspace + +__all__ = ["RBAC_CHECKS_ATTR", "RBACCheck", "enforce_rbac_checks"] + +RBAC_CHECKS_ATTR = "rbac_checks" + + +@dataclass(frozen=True) +class RBACCheck: + scene: RBACPermission + locator: ResourceLocator + + def __post_init__(self) -> None: + if isinstance(self.locator, Workspace) or self.scene.scope is RBACResourceScope.WORKSPACE: + return + if self.locator.scope is not self.scene.scope: + raise ValueError( + f"{self.scene} is a {self.scene.scope} scene but {self.locator!r} locates {self.locator.scope}" + ) + + +def enforce_rbac_checks( + *, + tenant_id: str, + account_id: str, + checks: Sequence[RBACCheck], + path_args: Mapping[str, object] | None = None, +) -> None: + if not dify_config.RBAC_ENABLED: + return + try: + view_args = request.view_args or {} + except RuntimeError: + view_args = {} + merged: dict[str, object] = {**view_args, **(path_args or {})} + applicable = [ + (check, identity) for check in checks if (identity := check.locator.locate(tenant_id, merged)) is not None + ] + if not applicable: + raise NotFound() + for check, identity in applicable: + is_workspace = identity.scope is RBACResourceScope.WORKSPACE + owner = None if is_workspace else check.locator.owner_id(tenant_id, identity) + if owner is not None and owner == account_id: + return + allowed = RBACService.CheckAccess.check( + tenant_id, + account_id, + scene=check.scene, + resource_type=None if is_workspace else identity.scope, + resource_id=None if is_workspace else identity.id, + ) + if allowed: + return + raise Forbidden() diff --git a/api/controllers/common/rbac/locators.py b/api/controllers/common/rbac/locators.py new file mode 100644 index 00000000000..9fcf88bc3f1 --- /dev/null +++ b/api/controllers/common/rbac/locators.py @@ -0,0 +1,144 @@ +from collections.abc import Mapping +from dataclasses import dataclass +from typing import TYPE_CHECKING, Protocol, override, runtime_checkable + +from flask import g +from werkzeug.exceptions import NotFound + +from core.rbac import RBACResourceScope +from services.rbac_resource_service import RBACResourceService + +if TYPE_CHECKING: + from models.agent import Agent + +__all__ = [ + "AgentBehindApp", + "AgentId", + "DatasetByPipeline", + "DatasetId", + "PlainApp", + "ResourceIdentity", + "ResourceLocator", + "Workspace", + "agent_binding", +] + +_AGENT_BINDING_CACHE_KEY = "_rbac_agent_bindings" + + +@dataclass(frozen=True) +class ResourceIdentity: + scope: RBACResourceScope + id: str + + +@runtime_checkable +class ResourceLocator(Protocol): + scope: RBACResourceScope + + def locate(self, tenant_id: str, path_args: Mapping[str, object]) -> ResourceIdentity | None: ... + + def owner_id(self, tenant_id: str, identity: ResourceIdentity) -> str | None: ... + + +def _required(path_args: Mapping[str, object], param: str) -> str: + value = path_args.get(param) + if not value: + raise ValueError(f"Missing {param} in request path") + return str(value) + + +def agent_binding(tenant_id: str, app_id: str) -> "Agent | None": + cache: dict[tuple[str, str], Agent | None] = g.setdefault(_AGENT_BINDING_CACHE_KEY, {}) + key = (tenant_id, app_id) + if key not in cache: + cache[key] = RBACResourceService.get_app_agent_binding(tenant_id, app_id) + return cache[key] + + +class Workspace: + scope = RBACResourceScope.WORKSPACE + + def locate(self, tenant_id: str, path_args: Mapping[str, object]) -> ResourceIdentity | None: + return ResourceIdentity(self.scope, "") + + def owner_id(self, tenant_id: str, identity: ResourceIdentity) -> str | None: + return None + + def __repr__(self) -> str: + return "Workspace()" + + +class _ParamLocator: + scope: RBACResourceScope + default_param: str + + def __init__(self, param: str | None = None): + self.param = param or self.default_param + + def __repr__(self) -> str: + return f"{type(self).__name__}({self.param!r})" + + +class AgentId(_ParamLocator): + scope = RBACResourceScope.AGENT + default_param = "agent_id" + + def locate(self, tenant_id: str, path_args: Mapping[str, object]) -> ResourceIdentity | None: + return ResourceIdentity(self.scope, _required(path_args, self.param)) + + def owner_id(self, tenant_id: str, identity: ResourceIdentity) -> str | None: + return None + + +class PlainApp(_ParamLocator): + scope = RBACResourceScope.APP + default_param = "app_id" + + def locate(self, tenant_id: str, path_args: Mapping[str, object]) -> ResourceIdentity | None: + app_id = _required(path_args, self.param) + if agent_binding(tenant_id, app_id) is not None: + return None + return ResourceIdentity(self.scope, app_id) + + def owner_id(self, tenant_id: str, identity: ResourceIdentity) -> str | None: + return RBACResourceService.get_app_maintainer(tenant_id, identity.id) + + +class AgentBehindApp(_ParamLocator): + scope = RBACResourceScope.AGENT + default_param = "app_id" + + def locate(self, tenant_id: str, path_args: Mapping[str, object]) -> ResourceIdentity | None: + from models.agent import AgentScope + + binding = agent_binding(tenant_id, _required(path_args, self.param)) + if binding is None or binding.scope == AgentScope.WORKFLOW_ONLY: + return None + return ResourceIdentity(self.scope, str(binding.id)) + + def owner_id(self, tenant_id: str, identity: ResourceIdentity) -> str | None: + return None + + +class DatasetId(_ParamLocator): + scope = RBACResourceScope.DATASET + default_param = "dataset_id" + + def locate(self, tenant_id: str, path_args: Mapping[str, object]) -> ResourceIdentity | None: + return ResourceIdentity(self.scope, _required(path_args, self.param)) + + def owner_id(self, tenant_id: str, identity: ResourceIdentity) -> str | None: + return RBACResourceService.get_dataset_maintainer(tenant_id, identity.id) + + +class DatasetByPipeline(DatasetId): + default_param = "pipeline_id" + + @override + def locate(self, tenant_id: str, path_args: Mapping[str, object]) -> ResourceIdentity | None: + pipeline_id = _required(path_args, self.param) + dataset_id = RBACResourceService.get_dataset_id_by_pipeline(tenant_id, pipeline_id) + if dataset_id is None: + raise NotFound("Dataset not found for pipeline") + return ResourceIdentity(self.scope, dataset_id) diff --git a/api/controllers/common/wraps.py b/api/controllers/common/wraps.py index 236423dd351..688e36959a7 100644 --- a/api/controllers/common/wraps.py +++ b/api/controllers/common/wraps.py @@ -1,181 +1,32 @@ from collections.abc import Callable from functools import wraps -from sqlalchemy import select -from werkzeug.exceptions import Forbidden, NotFound - from configs import dify_config -from core.rbac import RBACPermission, RBACResourceScope -from extensions.ext_database import db +from controllers.common.rbac import RBAC_CHECKS_ATTR, RBACCheck, enforce_rbac_checks from libs.login import current_account_with_tenant -from models.dataset import Dataset -from models.model import App -from services.agent.roster_service import AgentRosterService -from services.enterprise.rbac_service import RBACService -__all__ = ["RBACPermission", "RBACResourceScope", "enforce_rbac_access", "rbac_permission_required"] +__all__ = ["rbac_permission_required"] -def enforce_rbac_access( - *, - tenant_id: str, - account_id: str, - resource_type: RBACResourceScope, - scene: RBACPermission, - resource_required: bool = True, - path_args: dict[str, object] | None = None, -) -> None: - """Enforce enterprise RBAC for an explicit account/tenant pair. - - This is the flask-login-independent core of the RBAC gate so it can run - inside request-handling layers that resolve the caller themselves (e.g. the - openapi auth pipeline, which has the account on ``AuthData`` before - flask-login is mounted). - - No-op when ``RBAC_ENABLED`` is ``False``. For resource-scoped checks the - resource ID is taken from ``path_args`` merged with ``request.view_args``; - resource ownership short-circuits the check. Raises ``Forbidden`` when - access is denied. For workspace-level checks pass ``resource_required=False`` - so the RBAC request omits ``resource_id``. - - Args: - tenant_id: The tenant the access is evaluated against. - account_id: The account requesting access. - resource_type: The :class:`RBACResourceScope` member (app/dataset/workspace). - scene: The :class:`RBACPermission` permission point, e.g. ``RBACPermission.APP_DELETE``. - resource_required: Whether a concrete resource ID is required. - path_args: Extra path arguments to merge with ``request.view_args``. - """ - if not dify_config.RBAC_ENABLED: - return - - check_resource_type = None if resource_type == RBACResourceScope.WORKSPACE else resource_type - resource_id = None - if resource_required and check_resource_type: - resource_id = _extract_resource_id(resource_type, tenant_id, path_args) - if _is_resource_owned_by_current_user(tenant_id, account_id, resource_type, resource_id): - return - allowed = RBACService.CheckAccess.check( - tenant_id, - account_id, - scene=scene, - resource_type=check_resource_type, - resource_id=resource_id, - ) - if not allowed: - raise Forbidden() - - -def rbac_permission_required[**P, R]( - resource_type: RBACResourceScope, - scene: RBACPermission, - *, - resource_required: bool = True, -) -> Callable[[Callable[P, R]], Callable[P, R]]: - """Check enterprise RBAC permissions for the current flask-login user. - - When ``RBAC_ENABLED`` is ``False`` the decorator is a no-op and the - request passes through unchanged. When enabled it resolves the current - account/tenant and delegates to :func:`enforce_rbac_access`, raising - ``Forbidden`` if access is denied. - - Args: - resource_type: The :class:`RBACResourceScope` member (app/dataset/workspace). - scene: The :class:`RBACPermission` permission point, e.g. ``RBACPermission.APP_DELETE``. - resource_required: Whether a concrete resource ID is required. - """ +def rbac_permission_required[**P, R](*checks: RBACCheck) -> Callable[[Callable[P, R]], Callable[P, R]]: + if not checks: + raise ValueError("rbac_permission_required requires at least one RBACCheck") def decorator(view: Callable[P, R]) -> Callable[P, R]: - @wraps(view) + @wraps(view, updated=()) def decorated(*args: P.args, **kwargs: P.kwargs) -> R: if not dify_config.RBAC_ENABLED: return view(*args, **kwargs) - current_user, current_tenant_id = current_account_with_tenant() - enforce_rbac_access( + enforce_rbac_checks( tenant_id=current_tenant_id, account_id=current_user.id, - resource_type=resource_type, - scene=scene, - resource_required=resource_required, + checks=checks, path_args=kwargs, ) return view(*args, **kwargs) + setattr(decorated, RBAC_CHECKS_ATTR, checks) return decorated return decorator - - -def _is_resource_owned_by_current_user( - tenant_id: str, account_id: str, resource_type: RBACResourceScope, resource_id: str -) -> bool: - if resource_type == RBACResourceScope.APP: - maintainer = db.session.scalar( - select(App.maintainer).where( - App.id == resource_id, - App.tenant_id == tenant_id, - App.status == "normal", - ) - ) - return maintainer == account_id - - if resource_type == RBACResourceScope.DATASET: - maintainer = db.session.scalar( - select(Dataset.maintainer).where( - Dataset.id == resource_id, - Dataset.tenant_id == tenant_id, - ) - ) - return maintainer == account_id - - return False - - -def _extract_resource_id( - resource_type: RBACResourceScope, tenant_id: str, path_args: dict[str, object] | None = None -) -> str: - """Extract the resource ID from matched path arguments. - - Some legacy route classes use neutral names such as ``resource_id`` for - app/dataset resources, and Agent routes carry ``agent_id``, which is - resolved to the App backing that Agent. - Dataset endpoints behind a rag-pipeline route contain ``pipeline_id`` - instead of ``dataset_id``. In that case we look up the associated - ``Dataset`` row via ``Dataset.pipeline_id``. - """ - from flask import request - - view_args = request.view_args or {} - matched_args = {**view_args, **(path_args or {})} - - if resource_type == RBACResourceScope.APP: - app_id = matched_args.get("app_id") - if app_id: - return str(app_id) # pyrefly: ignore[unnecessary-type-conversion] - - agent_id = matched_args.get("agent_id") - if agent_id: - authz_app_id = AgentRosterService(db.session).peek_authz_app_id(tenant_id=tenant_id, agent_id=str(agent_id)) - return authz_app_id or str(agent_id) - - resource_id = matched_args.get("resource_id") - if resource_id: - return str(resource_id) # pyrefly: ignore[unnecessary-type-conversion] - raise ValueError("Missing app_id in request path") - - if resource_type == RBACResourceScope.DATASET: - dataset_id = matched_args.get("dataset_id") or matched_args.get("resource_id") - if dataset_id: - return str(dataset_id) - - pipeline_id = matched_args.get("pipeline_id") - if pipeline_id: - dataset = db.session.scalar( - select(Dataset).where(Dataset.pipeline_id == str(pipeline_id), Dataset.tenant_id == tenant_id) - ) - if not dataset: - raise NotFound("Dataset not found for pipeline") - return str(dataset.id) # pyrefly: ignore[unnecessary-type-conversion] - raise ValueError("Missing dataset_id or pipeline_id in request path") - raise ValueError(f"Unknown resource_type: {resource_type}") diff --git a/api/controllers/console/agent/composer.py b/api/controllers/console/agent/composer.py index e5ed3ed31e9..2ee9933309e 100644 --- a/api/controllers/console/agent/composer.py +++ b/api/controllers/console/agent/composer.py @@ -4,13 +4,13 @@ from flask_restx import Resource from sqlalchemy.orm import Session from werkzeug.exceptions import NotFound +from controllers.common.rbac import AgentId, PlainApp, RBACCheck, Workspace from controllers.common.schema import query_params_from_model, register_response_schema_models, register_schema_models from controllers.common.session import with_session from controllers.console import console_ns -from controllers.console.app.wraps import agent_manage_required_for_agent_app, get_app_model +from controllers.console.app.wraps import get_app_model from controllers.console.wraps import ( RBACPermission, - RBACResourceScope, account_initialization_required, edit_permission_required, model_validate, @@ -94,7 +94,7 @@ class WorkflowAgentComposerApi(Resource): @login_required @account_initialization_required @edit_permission_required - @rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_EDIT) + @rbac_permission_required(RBACCheck(RBACPermission.APP_EDIT, PlainApp())) @with_current_user_id @with_current_tenant_id @with_session @@ -134,7 +134,7 @@ class WorkflowAgentComposerCopyFromRosterApi(Resource): @login_required @account_initialization_required @edit_permission_required - @rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_EDIT) + @rbac_permission_required(RBACCheck(RBACPermission.APP_EDIT, PlainApp())) @with_current_user_id @with_current_tenant_id @with_session @@ -246,8 +246,8 @@ class WorkflowAgentComposerSaveToRosterApi(Resource): @login_required @account_initialization_required @edit_permission_required - @rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_EDIT) - @rbac_permission_required(RBACResourceScope.WORKSPACE, RBACPermission.AGENT_MANAGE, resource_required=False) + @rbac_permission_required(RBACCheck(RBACPermission.APP_EDIT, PlainApp())) + @rbac_permission_required(RBACCheck(RBACPermission.AGENT_CREATE, Workspace())) @with_current_user_id @with_current_tenant_id @with_session @@ -323,9 +323,7 @@ class SnippetAgentComposerApi(Resource): @login_required @account_initialization_required @edit_permission_required - @rbac_permission_required( - RBACResourceScope.WORKSPACE, RBACPermission.SNIPPETS_CREATE_AND_MODIFY, resource_required=False - ) + @rbac_permission_required(RBACCheck(RBACPermission.SNIPPETS_CREATE_AND_MODIFY, Workspace())) @with_current_user_id @with_current_tenant_id @with_session @@ -362,9 +360,7 @@ class SnippetAgentComposerCopyFromRosterApi(Resource): @login_required @account_initialization_required @edit_permission_required - @rbac_permission_required( - RBACResourceScope.WORKSPACE, RBACPermission.SNIPPETS_CREATE_AND_MODIFY, resource_required=False - ) + @rbac_permission_required(RBACCheck(RBACPermission.SNIPPETS_CREATE_AND_MODIFY, Workspace())) @with_current_user_id @with_current_tenant_id @with_session @@ -476,10 +472,8 @@ class SnippetAgentComposerSaveToRosterApi(Resource): @login_required @account_initialization_required @edit_permission_required - @rbac_permission_required( - RBACResourceScope.WORKSPACE, RBACPermission.SNIPPETS_CREATE_AND_MODIFY, resource_required=False - ) - @rbac_permission_required(RBACResourceScope.WORKSPACE, RBACPermission.AGENT_MANAGE, resource_required=False) + @rbac_permission_required(RBACCheck(RBACPermission.SNIPPETS_CREATE_AND_MODIFY, Workspace())) + @rbac_permission_required(RBACCheck(RBACPermission.AGENT_CREATE, Workspace())) @with_current_user_id @with_current_tenant_id @with_session @@ -512,7 +506,7 @@ class AgentComposerApi(Resource): @setup_required @login_required @account_initialization_required - @rbac_permission_required(RBACResourceScope.WORKSPACE, RBACPermission.AGENT_MANAGE, resource_required=False) + @rbac_permission_required(RBACCheck(RBACPermission.AGENT_PREVIEW, AgentId())) @with_current_tenant_id @with_session def get(self, session: Session, tenant_id: str, agent_id: UUID): @@ -527,7 +521,7 @@ class AgentComposerApi(Resource): @login_required @account_initialization_required @edit_permission_required - @agent_manage_required_for_agent_app(scene=RBACPermission.APP_EDIT) + @rbac_permission_required(RBACCheck(RBACPermission.AGENT_EDIT, AgentId())) @with_current_user_id @with_current_tenant_id @with_session @@ -554,6 +548,7 @@ class AgentComposerValidateApi(Resource): @setup_required @login_required @account_initialization_required + @rbac_permission_required(RBACCheck(RBACPermission.AGENT_PREVIEW, AgentId())) @with_current_tenant_id @with_session @model_validate(ComposerSavePayload) @@ -575,6 +570,7 @@ class AgentComposerCandidatesApi(Resource): @setup_required @login_required @account_initialization_required + @rbac_permission_required(RBACCheck(RBACPermission.AGENT_PREVIEW, AgentId())) @with_current_user_id @with_current_tenant_id @with_session(write=False) diff --git a/api/controllers/console/agent/roster.py b/api/controllers/console/agent/roster.py index 6c2798a2017..a8ba32477af 100644 --- a/api/controllers/console/agent/roster.py +++ b/api/controllers/console/agent/roster.py @@ -7,6 +7,7 @@ from pydantic import AliasChoices, BaseModel, Field, field_validator from sqlalchemy import func, or_, select from sqlalchemy.orm import Session +from controllers.common.rbac import AgentId, RBACCheck, Workspace from controllers.common.schema import ( query_params_from_model, query_params_from_request, @@ -32,10 +33,8 @@ from controllers.console.app.app import ( from controllers.console.app.app import ( UpdateAppPayload as GenericUpdateAppPayload, ) -from controllers.console.app.wraps import agent_manage_required_for_agent_app from controllers.console.wraps import ( RBACPermission, - RBACResourceScope, account_initialization_required, edit_permission_required, enterprise_license_required, @@ -608,7 +607,7 @@ class AgentAppListApi(Resource): @setup_required @login_required @account_initialization_required - @rbac_permission_required(RBACResourceScope.WORKSPACE, RBACPermission.AGENT_MANAGE, resource_required=False) + @rbac_permission_required(RBACCheck(RBACPermission.AGENT_PREVIEW, Workspace())) @with_current_user @with_current_tenant_id @with_session @@ -668,7 +667,7 @@ class AgentAppListApi(Resource): @login_required @account_initialization_required @edit_permission_required - @rbac_permission_required(RBACResourceScope.WORKSPACE, RBACPermission.AGENT_MANAGE, resource_required=False) + @rbac_permission_required(RBACCheck(RBACPermission.AGENT_CREATE, Workspace())) @with_current_user @with_current_tenant_id @with_session @@ -694,7 +693,7 @@ class AgentAppApi(Resource): @setup_required @login_required @account_initialization_required - @rbac_permission_required(RBACResourceScope.WORKSPACE, RBACPermission.AGENT_MANAGE, resource_required=False) + @rbac_permission_required(RBACCheck(RBACPermission.AGENT_PREVIEW, AgentId())) @enterprise_license_required @with_current_user @with_current_tenant_id @@ -711,7 +710,7 @@ class AgentAppApi(Resource): @login_required @account_initialization_required @edit_permission_required - @rbac_permission_required(RBACResourceScope.WORKSPACE, RBACPermission.AGENT_MANAGE, resource_required=False) + @rbac_permission_required(RBACCheck(RBACPermission.AGENT_EDIT, AgentId())) @with_current_user @with_current_tenant_id @with_session @@ -744,7 +743,7 @@ class AgentAppApi(Resource): @login_required @account_initialization_required @edit_permission_required - @rbac_permission_required(RBACResourceScope.WORKSPACE, RBACPermission.AGENT_MANAGE, resource_required=False) + @rbac_permission_required(RBACCheck(RBACPermission.AGENT_DELETE, AgentId())) @with_current_tenant_id @with_session def delete(self, session: Session, tenant_id: str, agent_id: UUID): @@ -765,6 +764,7 @@ class AgentDebugConversationRefreshApi(Resource): @login_required @account_initialization_required @edit_permission_required + @rbac_permission_required(RBACCheck(RBACPermission.AGENT_TEST_AND_RUN, AgentId())) @with_current_user @with_current_tenant_id @with_session @@ -790,7 +790,7 @@ class AgentPublishApi(Resource): @login_required @account_initialization_required @edit_permission_required - @rbac_permission_required(RBACResourceScope.WORKSPACE, RBACPermission.AGENT_MANAGE, resource_required=False) + @rbac_permission_required(RBACCheck(RBACPermission.AGENT_RELEASE_AND_VERSION, AgentId())) @with_current_user @with_current_tenant_id @with_session @@ -820,7 +820,7 @@ class AgentBuildDraftCheckoutApi(Resource): @login_required @account_initialization_required @edit_permission_required - @rbac_permission_required(RBACResourceScope.WORKSPACE, RBACPermission.AGENT_MANAGE, resource_required=False) + @rbac_permission_required(RBACCheck(RBACPermission.AGENT_EDIT, AgentId())) @with_current_user @with_current_tenant_id @with_session(write=False) @@ -850,6 +850,7 @@ class AgentBuildDraftApi(Resource): @login_required @account_initialization_required @edit_permission_required + @rbac_permission_required(RBACCheck(RBACPermission.AGENT_PREVIEW, AgentId())) @with_current_user @with_current_tenant_id @with_session(write=False) @@ -867,6 +868,7 @@ class AgentBuildDraftApi(Resource): @login_required @account_initialization_required @edit_permission_required + @rbac_permission_required(RBACCheck(RBACPermission.AGENT_EDIT, AgentId())) @with_current_user @with_current_tenant_id @with_session @@ -892,6 +894,7 @@ class AgentBuildDraftApi(Resource): @login_required @account_initialization_required @edit_permission_required + @rbac_permission_required(RBACCheck(RBACPermission.AGENT_EDIT, AgentId())) @with_current_user @with_current_tenant_id @with_session(write=False) @@ -911,7 +914,7 @@ class AgentBuildDraftApplyApi(Resource): @login_required @account_initialization_required @edit_permission_required - @rbac_permission_required(RBACResourceScope.WORKSPACE, RBACPermission.AGENT_MANAGE, resource_required=False) + @rbac_permission_required(RBACCheck(RBACPermission.AGENT_EDIT, AgentId())) @with_current_user @with_current_tenant_id @with_session(write=False) @@ -934,7 +937,8 @@ class AgentAppCopyApi(Resource): @login_required @account_initialization_required @edit_permission_required - @rbac_permission_required(RBACResourceScope.WORKSPACE, RBACPermission.AGENT_MANAGE, resource_required=False) + @rbac_permission_required(RBACCheck(RBACPermission.AGENT_CREATE, Workspace())) + @rbac_permission_required(RBACCheck(RBACPermission.AGENT_PREVIEW, AgentId())) @with_current_user @with_current_tenant_id @with_session @@ -967,7 +971,7 @@ class AgentApiAccessApi(Resource): @setup_required @login_required @account_initialization_required - @rbac_permission_required(RBACResourceScope.WORKSPACE, RBACPermission.AGENT_MANAGE, resource_required=False) + @rbac_permission_required(RBACCheck(RBACPermission.AGENT_ACCESS_POINT_VIEW, AgentId())) @with_current_tenant_id @with_session(write=False) def get(self, session: Session, tenant_id: str, agent_id: UUID): @@ -984,7 +988,7 @@ class AgentApiStatusApi(Resource): @login_required @is_admin_or_owner_required @account_initialization_required - @agent_manage_required_for_agent_app(scene=RBACPermission.APP_RELEASE_AND_VERSION) + @rbac_permission_required(RBACCheck(RBACPermission.AGENT_ACCESS_POINT_MANAGE, AgentId())) @with_current_tenant_id @with_session @model_validate(AgentApiStatusPayload) @@ -1002,7 +1006,7 @@ class AgentApiKeyListApi(BaseApiKeyListResource): token_prefix = "app-" @console_ns.response(200, "Agent service API keys", console_ns.models[ApiKeyList.__name__]) - @agent_manage_required_for_agent_app(scene=RBACPermission.APP_RELEASE_AND_VERSION) + @rbac_permission_required(RBACCheck(RBACPermission.AGENT_ACCESS_POINT_VIEW, AgentId())) @with_current_tenant_id @edit_permission_required @with_session(write=False) @@ -1014,7 +1018,7 @@ class AgentApiKeyListApi(BaseApiKeyListResource): @console_ns.response(400, "Maximum keys exceeded") @with_current_tenant_id @edit_permission_required - @agent_manage_required_for_agent_app(scene=RBACPermission.APP_RELEASE_AND_VERSION) + @rbac_permission_required(RBACCheck(RBACPermission.AGENT_ACCESS_POINT_MANAGE, AgentId())) @with_session def post(self, session: Session, tenant_id: str, agent_id: UUID) -> tuple[dict[str, object], int]: app_model = _resolve_agent_app_model(session, tenant_id=tenant_id, agent_id=agent_id) @@ -1033,7 +1037,7 @@ class AgentApiKeyApi(BaseApiKeyResource): @console_ns.response(204, "Agent service API key deleted") @with_current_user @with_current_tenant_id - @agent_manage_required_for_agent_app(scene=RBACPermission.APP_RELEASE_AND_VERSION) + @rbac_permission_required(RBACCheck(RBACPermission.AGENT_ACCESS_POINT_MANAGE, AgentId())) @with_session def delete( self, @@ -1055,6 +1059,7 @@ class AgentInviteOptionsApi(Resource): @setup_required @login_required @account_initialization_required + @rbac_permission_required(RBACCheck(RBACPermission.AGENT_PREVIEW, Workspace())) @with_current_tenant_id @with_session(write=False) @model_validate(AgentInviteOptionsQuery) @@ -1078,7 +1083,7 @@ class AgentLogsApi(Resource): @setup_required @login_required @account_initialization_required - @rbac_permission_required(RBACResourceScope.WORKSPACE, RBACPermission.AGENT_MANAGE, resource_required=False) + @rbac_permission_required(RBACCheck(RBACPermission.AGENT_LOG_MANAGE, AgentId())) @with_current_user @with_current_tenant_id @with_session(write=False) @@ -1117,7 +1122,7 @@ class AgentLogMessagesApi(Resource): @setup_required @login_required @account_initialization_required - @rbac_permission_required(RBACResourceScope.WORKSPACE, RBACPermission.AGENT_MANAGE, resource_required=False) + @rbac_permission_required(RBACCheck(RBACPermission.AGENT_LOG_MANAGE, AgentId())) @with_current_user @with_current_tenant_id @with_session(write=False) @@ -1156,7 +1161,7 @@ class AgentLogSourcesApi(Resource): @setup_required @login_required @account_initialization_required - @rbac_permission_required(RBACResourceScope.WORKSPACE, RBACPermission.AGENT_MANAGE, resource_required=False) + @rbac_permission_required(RBACCheck(RBACPermission.AGENT_LOG_MANAGE, AgentId())) @with_current_user @with_current_tenant_id @with_session(write=False) @@ -1177,7 +1182,7 @@ class AgentStatisticsSummaryApi(Resource): @setup_required @login_required @account_initialization_required - @rbac_permission_required(RBACResourceScope.WORKSPACE, RBACPermission.AGENT_MANAGE, resource_required=False) + @rbac_permission_required(RBACCheck(RBACPermission.AGENT_MONITOR, AgentId())) @with_current_user @with_current_tenant_id @with_session(write=False) @@ -1210,7 +1215,7 @@ class AgentRosterVersionsApi(Resource): @setup_required @login_required @account_initialization_required - @rbac_permission_required(RBACResourceScope.WORKSPACE, RBACPermission.AGENT_MANAGE, resource_required=False) + @rbac_permission_required(RBACCheck(RBACPermission.AGENT_RELEASE_AND_VERSION, AgentId())) @with_current_tenant_id @with_session(write=False) def get(self, session: Session, tenant_id: str, agent_id: UUID): @@ -1226,7 +1231,7 @@ class AgentRosterVersionDetailApi(Resource): @setup_required @login_required @account_initialization_required - @rbac_permission_required(RBACResourceScope.WORKSPACE, RBACPermission.AGENT_MANAGE, resource_required=False) + @rbac_permission_required(RBACCheck(RBACPermission.AGENT_RELEASE_AND_VERSION, AgentId())) @with_current_tenant_id @with_session(write=False) def get(self, session: Session, tenant_id: str, agent_id: UUID, version_id: UUID): @@ -1247,7 +1252,7 @@ class AgentRosterVersionRestoreApi(Resource): @login_required @account_initialization_required @edit_permission_required - @rbac_permission_required(RBACResourceScope.WORKSPACE, RBACPermission.AGENT_MANAGE, resource_required=False) + @rbac_permission_required(RBACCheck(RBACPermission.AGENT_RELEASE_AND_VERSION, AgentId())) @with_current_user @with_current_tenant_id @with_session diff --git a/api/controllers/console/apikey.py b/api/controllers/console/apikey.py index 98ba57d45c3..7519d0caa8f 100644 --- a/api/controllers/console/apikey.py +++ b/api/controllers/console/apikey.py @@ -11,9 +11,9 @@ from sqlalchemy.orm import Session from werkzeug.exceptions import Forbidden from configs import dify_config +from controllers.common.rbac import AgentBehindApp, DatasetId, PlainApp, RBACCheck from controllers.common.schema import register_response_schema_models from controllers.common.session import with_session -from controllers.console.app.wraps import agent_manage_required_for_agent_app from fields.base import ResponseModel from libs.helper import dump_response, to_timestamp from libs.login import login_required @@ -27,7 +27,6 @@ from services.app_service import AppService from . import console_ns from .wraps import ( RBACPermission, - RBACResourceScope, account_initialization_required, edit_permission_required, rbac_permission_required, @@ -242,8 +241,10 @@ class AppApiKeyListResource(BaseApiKeyListResource): @console_ns.response(200, "API keys retrieved successfully", console_ns.models[ApiKeyList.__name__]) @with_current_tenant_id @edit_permission_required - @rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_RELEASE_AND_VERSION) - @agent_manage_required_for_agent_app + @rbac_permission_required( + RBACCheck(RBACPermission.APP_RELEASE_AND_VERSION, PlainApp("resource_id")), + RBACCheck(RBACPermission.AGENT_ACCESS_POINT_VIEW, AgentBehindApp("resource_id")), + ) @with_session(write=False) def get(self, session: Session, current_tenant_id: str, resource_id: UUID) -> dict[str, object]: """Get all API keys for an app""" @@ -259,8 +260,10 @@ class AppApiKeyListResource(BaseApiKeyListResource): @console_ns.response(400, "Maximum keys exceeded") @with_current_tenant_id @edit_permission_required - @rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_RELEASE_AND_VERSION) - @agent_manage_required_for_agent_app + @rbac_permission_required( + RBACCheck(RBACPermission.APP_RELEASE_AND_VERSION, PlainApp("resource_id")), + RBACCheck(RBACPermission.AGENT_ACCESS_POINT_MANAGE, AgentBehindApp("resource_id")), + ) @with_session def post(self, session: Session, current_tenant_id: str, resource_id: UUID) -> tuple[dict[str, object], int]: """Create a new API key for an app""" @@ -283,8 +286,10 @@ class AppApiKeyResource(BaseApiKeyResource): @console_ns.response(204, "API key deleted successfully") @with_current_user @with_current_tenant_id - @rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_RELEASE_AND_VERSION) - @agent_manage_required_for_agent_app + @rbac_permission_required( + RBACCheck(RBACPermission.APP_RELEASE_AND_VERSION, PlainApp("resource_id")), + RBACCheck(RBACPermission.AGENT_ACCESS_POINT_MANAGE, AgentBehindApp("resource_id")), + ) @with_session def delete( self, @@ -321,7 +326,7 @@ class DatasetApiKeyListResource(BaseApiKeyListResource): @console_ns.response(200, "API keys retrieved successfully", console_ns.models[ApiKeyList.__name__]) @with_current_tenant_id @edit_permission_required - @rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_API_KEY_MANAGE) + @rbac_permission_required(RBACCheck(RBACPermission.DATASET_API_KEY_MANAGE, DatasetId("resource_id"))) @with_session(write=False) def get(self, session: Session, current_tenant_id: str, resource_id: UUID) -> dict[str, object]: """Get all API keys for a dataset""" @@ -337,7 +342,7 @@ class DatasetApiKeyListResource(BaseApiKeyListResource): @console_ns.response(400, "Maximum keys exceeded") @with_current_tenant_id @edit_permission_required - @rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_API_KEY_MANAGE) + @rbac_permission_required(RBACCheck(RBACPermission.DATASET_API_KEY_MANAGE, DatasetId("resource_id"))) @with_session def post(self, session: Session, current_tenant_id: str, resource_id: UUID) -> tuple[dict[str, object], int]: """Create a new API key for a dataset""" @@ -360,7 +365,7 @@ class DatasetApiKeyResource(BaseApiKeyResource): @console_ns.response(204, "API key deleted successfully") @with_current_user @with_current_tenant_id - @rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_API_KEY_MANAGE) + @rbac_permission_required(RBACCheck(RBACPermission.DATASET_API_KEY_MANAGE, DatasetId("resource_id"))) @with_session def delete( self, diff --git a/api/controllers/console/app/agent.py b/api/controllers/console/app/agent.py index 4fc9c7f6a6e..619d7da6371 100644 --- a/api/controllers/console/app/agent.py +++ b/api/controllers/console/app/agent.py @@ -4,13 +4,13 @@ from flask_restx import Resource from pydantic import BaseModel, Field, field_validator from sqlalchemy.orm import Session +from controllers.common.rbac import PlainApp, RBACCheck from controllers.common.schema import query_params_from_model, register_response_schema_models from controllers.common.session import with_session from controllers.console import console_ns from controllers.console.app.wraps import get_app_model from controllers.console.wraps import ( RBACPermission, - RBACResourceScope, account_initialization_required, model_validate, rbac_permission_required, @@ -84,7 +84,7 @@ class AgentLogApi(Resource): @setup_required @login_required @account_initialization_required - @rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_VIEW_LAYOUT) + @rbac_permission_required(RBACCheck(RBACPermission.APP_VIEW_LAYOUT, PlainApp())) @with_session(write=False) @get_app_model(mode=[AppMode.AGENT_CHAT]) @model_validate(AgentLogQuery) diff --git a/api/controllers/console/app/agent_app_access.py b/api/controllers/console/app/agent_app_access.py index 14c049d2f89..b8b81d26bc3 100644 --- a/api/controllers/console/app/agent_app_access.py +++ b/api/controllers/console/app/agent_app_access.py @@ -11,11 +11,18 @@ from flask_restx import Resource from pydantic import Field from sqlalchemy.orm import Session +from controllers.common.rbac import AgentId, RBACCheck from controllers.common.schema import register_response_schema_models from controllers.common.session import with_session from controllers.console import console_ns from controllers.console.agent.app_helpers import resolve_agent_app_model -from controllers.console.wraps import account_initialization_required, setup_required, with_current_tenant_id +from controllers.console.wraps import ( + RBACPermission, + account_initialization_required, + rbac_permission_required, + setup_required, + with_current_tenant_id, +) from fields.base import ResponseModel from libs.login import login_required from services.agent.roster_service import AgentRosterService @@ -55,6 +62,7 @@ class AgentAppReferencingWorkflowsResource(Resource): @setup_required @login_required @account_initialization_required + @rbac_permission_required(RBACCheck(RBACPermission.AGENT_PREVIEW, AgentId())) @with_current_tenant_id @with_session(write=False) def get(self, session: Session, tenant_id: str, agent_id: UUID): diff --git a/api/controllers/console/app/agent_app_feature.py b/api/controllers/console/app/agent_app_feature.py index d88496cbb06..a826177f8d5 100644 --- a/api/controllers/console/app/agent_app_feature.py +++ b/api/controllers/console/app/agent_app_feature.py @@ -16,13 +16,13 @@ from pydantic import BaseModel, Field from sqlalchemy.orm import Session from controllers.common.fields import SimpleResultResponse +from controllers.common.rbac import AgentId, RBACCheck from controllers.common.schema import register_response_schema_models, register_schema_models from controllers.common.session import with_session from controllers.console import console_ns from controllers.console.agent.app_helpers import resolve_agent_runtime_app_model from controllers.console.wraps import ( RBACPermission, - RBACResourceScope, account_initialization_required, edit_permission_required, model_validate, @@ -83,7 +83,7 @@ class AgentAppFeatureConfigResource(Resource): @setup_required @login_required @edit_permission_required - @rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_CREATE_AND_MANAGEMENT) + @rbac_permission_required(RBACCheck(RBACPermission.AGENT_EDIT, AgentId())) @account_initialization_required @with_current_user @with_current_tenant_id diff --git a/api/controllers/console/app/agent_app_sandbox.py b/api/controllers/console/app/agent_app_sandbox.py index b306d570e9e..165994d5a4e 100644 --- a/api/controllers/console/app/agent_app_sandbox.py +++ b/api/controllers/console/app/agent_app_sandbox.py @@ -15,6 +15,7 @@ from flask_restx import Resource from pydantic import BaseModel, Field from clients.agent_backend.errors import backend_error_detail, backend_reported_failure +from controllers.common.rbac import AgentId, PlainApp, RBACCheck from controllers.common.schema import ( query_params_from_model, query_params_from_request, @@ -23,10 +24,9 @@ from controllers.common.schema import ( ) from controllers.console import console_ns from controllers.console.app.error import AppNotFoundError -from controllers.console.app.wraps import agent_manage_required_for_agent_app, get_app_model +from controllers.console.app.wraps import get_app_model from controllers.console.wraps import ( RBACPermission, - RBACResourceScope, account_initialization_required, model_validate, rbac_permission_required, @@ -153,7 +153,7 @@ class AgentAppSandboxInfoResource(Resource): @setup_required @login_required @account_initialization_required - @agent_manage_required_for_agent_app(scene=RBACPermission.APP_VIEW_LAYOUT) + @rbac_permission_required(RBACCheck(RBACPermission.AGENT_PREVIEW, AgentId())) @with_current_tenant_id @with_current_user def get(self, current_user: Account, tenant_id: str, agent_id: UUID): @@ -183,7 +183,7 @@ class AgentAppSandboxListResource(Resource): @setup_required @login_required @account_initialization_required - @agent_manage_required_for_agent_app(scene=RBACPermission.APP_VIEW_LAYOUT) + @rbac_permission_required(RBACCheck(RBACPermission.AGENT_PREVIEW, AgentId())) @with_current_tenant_id @with_current_user def get(self, current_user: Account, tenant_id: str, agent_id: UUID): @@ -214,7 +214,7 @@ class AgentAppSandboxReadResource(Resource): @setup_required @login_required @account_initialization_required - @agent_manage_required_for_agent_app(scene=RBACPermission.APP_VIEW_LAYOUT) + @rbac_permission_required(RBACCheck(RBACPermission.AGENT_PREVIEW, AgentId())) @with_current_tenant_id @with_current_user def get(self, current_user: Account, tenant_id: str, agent_id: UUID): @@ -245,7 +245,7 @@ class AgentAppSandboxDownloadResource(Resource): @setup_required @login_required @account_initialization_required - @agent_manage_required_for_agent_app(scene=RBACPermission.APP_VIEW_LAYOUT) + @rbac_permission_required(RBACCheck(RBACPermission.AGENT_EDIT, AgentId())) @with_current_tenant_id @with_current_user @model_validate(AgentSandboxDownloadPayload) @@ -289,7 +289,7 @@ class WorkflowAgentSandboxListResource(Resource): @setup_required @login_required @account_initialization_required - @rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_VIEW_LAYOUT) + @rbac_permission_required(RBACCheck(RBACPermission.APP_VIEW_LAYOUT, PlainApp())) @get_app_model(mode=[AppMode.ADVANCED_CHAT, AppMode.WORKFLOW]) @with_current_tenant_id def get(self, tenant_id: str, app_model: App, workflow_run_id: UUID, node_id: str): @@ -327,7 +327,7 @@ class WorkflowAgentSandboxReadResource(Resource): @setup_required @login_required @account_initialization_required - @rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_VIEW_LAYOUT) + @rbac_permission_required(RBACCheck(RBACPermission.APP_VIEW_LAYOUT, PlainApp())) @get_app_model(mode=[AppMode.ADVANCED_CHAT, AppMode.WORKFLOW]) @with_current_tenant_id def get(self, tenant_id: str, app_model: App, workflow_run_id: UUID, node_id: str): @@ -358,7 +358,7 @@ class WorkflowAgentSandboxDownloadResource(Resource): @setup_required @login_required @account_initialization_required - @rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_VIEW_LAYOUT) + @rbac_permission_required(RBACCheck(RBACPermission.APP_VIEW_LAYOUT, PlainApp())) @with_current_user @with_current_tenant_id @model_validate(WorkflowAgentSandboxDownloadPayload) diff --git a/api/controllers/console/app/agent_config_inspector.py b/api/controllers/console/app/agent_config_inspector.py index fe211db7218..33503512907 100644 --- a/api/controllers/console/app/agent_config_inspector.py +++ b/api/controllers/console/app/agent_config_inspector.py @@ -15,6 +15,7 @@ from flask_restx import Resource from pydantic import BaseModel, Field from sqlalchemy.orm import Session +from controllers.common.rbac import AgentId, PlainApp, RBACCheck from controllers.common.schema import ( query_params_from_model, query_params_from_request, @@ -27,7 +28,6 @@ from controllers.console.agent.app_helpers import resolve_agent_runtime_app_mode from controllers.console.app.wraps import get_app_model from controllers.console.wraps import ( RBACPermission, - RBACResourceScope, account_initialization_required, edit_permission_required, model_validate, @@ -659,6 +659,7 @@ class AgentConfigManifestByAgentApi(Resource): @setup_required @login_required @account_initialization_required + @rbac_permission_required(RBACCheck(RBACPermission.AGENT_PREVIEW, AgentId())) @with_current_user @with_current_tenant_id @with_session @@ -704,6 +705,7 @@ class AgentConfigSkillUploadByAgentApi(Resource): @setup_required @login_required @account_initialization_required + @rbac_permission_required(RBACCheck(RBACPermission.AGENT_EDIT, AgentId())) @with_current_user @with_current_tenant_id @with_session @@ -733,7 +735,7 @@ class AgentConfigSkillUploadApi(Resource): @login_required @account_initialization_required @edit_permission_required - @rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_EDIT) + @rbac_permission_required(RBACCheck(RBACPermission.APP_EDIT, PlainApp())) @with_current_user @with_session @get_app_model(mode=_WORKFLOW_APP_MODES) @@ -751,6 +753,7 @@ class AgentConfigSkillsByAgentApi(Resource): @setup_required @login_required @account_initialization_required + @rbac_permission_required(RBACCheck(RBACPermission.AGENT_PREVIEW, AgentId())) @with_current_user @with_current_tenant_id @with_session @@ -789,6 +792,7 @@ class AgentConfigFilesByAgentApi(Resource): @setup_required @login_required @account_initialization_required + @rbac_permission_required(RBACCheck(RBACPermission.AGENT_PREVIEW, AgentId())) @with_current_user @with_current_tenant_id @with_session @@ -808,6 +812,7 @@ class AgentConfigFilesByAgentApi(Resource): @setup_required @login_required @account_initialization_required + @rbac_permission_required(RBACCheck(RBACPermission.AGENT_EDIT, AgentId())) @with_current_user @with_current_tenant_id @with_session @@ -853,7 +858,7 @@ class AgentConfigFilesApi(Resource): @login_required @account_initialization_required @edit_permission_required - @rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_EDIT) + @rbac_permission_required(RBACCheck(RBACPermission.APP_EDIT, PlainApp())) @with_current_user @with_session @get_app_model(mode=_WORKFLOW_APP_MODES) @@ -877,6 +882,7 @@ class AgentConfigSkillInspectByAgentApi(Resource): @setup_required @login_required @account_initialization_required + @rbac_permission_required(RBACCheck(RBACPermission.AGENT_PREVIEW, AgentId())) @with_current_user @with_current_tenant_id @with_session @@ -928,6 +934,7 @@ class AgentConfigSkillFilePreviewByAgentApi(Resource): @setup_required @login_required @account_initialization_required + @rbac_permission_required(RBACCheck(RBACPermission.AGENT_PREVIEW, AgentId())) @with_current_user @with_current_tenant_id @with_session @@ -988,6 +995,7 @@ class AgentConfigSkillDownloadByAgentApi(Resource): @setup_required @login_required @account_initialization_required + @rbac_permission_required(RBACCheck(RBACPermission.AGENT_PREVIEW, AgentId())) @with_current_user @with_current_tenant_id @with_session @@ -1037,6 +1045,7 @@ class AgentConfigSkillFileDownloadByAgentApi(Resource): @setup_required @login_required @account_initialization_required + @rbac_permission_required(RBACCheck(RBACPermission.AGENT_PREVIEW, AgentId())) @with_current_user @with_current_tenant_id @with_session @@ -1106,6 +1115,7 @@ class AgentConfigSkillFileDownloadContentByAgentApi(Resource): @setup_required @login_required @account_initialization_required + @rbac_permission_required(RBACCheck(RBACPermission.AGENT_PREVIEW, AgentId())) @with_current_user @with_current_tenant_id @with_session @@ -1158,6 +1168,7 @@ class AgentConfigSkillByAgentApi(Resource): @setup_required @login_required @account_initialization_required + @rbac_permission_required(RBACCheck(RBACPermission.AGENT_EDIT, AgentId())) @with_current_user @with_current_tenant_id @with_session @@ -1182,7 +1193,7 @@ class AgentConfigSkillApi(Resource): @login_required @account_initialization_required @edit_permission_required - @rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_EDIT) + @rbac_permission_required(RBACCheck(RBACPermission.APP_EDIT, PlainApp())) @with_current_user @with_session @get_app_model(mode=_WORKFLOW_APP_MODES) @@ -1205,6 +1216,7 @@ class AgentConfigFilePreviewByAgentApi(Resource): @setup_required @login_required @account_initialization_required + @rbac_permission_required(RBACCheck(RBACPermission.AGENT_PREVIEW, AgentId())) @with_current_user @with_current_tenant_id @with_session @@ -1250,6 +1262,7 @@ class AgentConfigFileDownloadByAgentApi(Resource): @setup_required @login_required @account_initialization_required + @rbac_permission_required(RBACCheck(RBACPermission.AGENT_PREVIEW, AgentId())) @with_current_user @with_current_tenant_id @with_session @@ -1295,6 +1308,7 @@ class AgentConfigFileByAgentApi(Resource): @setup_required @login_required @account_initialization_required + @rbac_permission_required(RBACCheck(RBACPermission.AGENT_EDIT, AgentId())) @with_current_user @with_current_tenant_id @with_session @@ -1319,7 +1333,7 @@ class AgentConfigFileApi(Resource): @login_required @account_initialization_required @edit_permission_required - @rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_EDIT) + @rbac_permission_required(RBACCheck(RBACPermission.APP_EDIT, PlainApp())) @with_current_user @with_session @get_app_model(mode=_WORKFLOW_APP_MODES) diff --git a/api/controllers/console/app/annotation.py b/api/controllers/console/app/annotation.py index dcad58593a0..ad35cd16aa8 100644 --- a/api/controllers/console/app/annotation.py +++ b/api/controllers/console/app/annotation.py @@ -9,12 +9,12 @@ from sqlalchemy.orm import Session from werkzeug.exceptions import NotFound from controllers.common.errors import NoFileUploadedError, TooManyFilesError +from controllers.common.rbac import PlainApp, RBACCheck from controllers.common.schema import query_params_from_model, register_response_schema_models, register_schema_models from controllers.common.session import with_session from controllers.console import console_ns from controllers.console.wraps import ( RBACPermission, - RBACResourceScope, account_initialization_required, annotation_import_concurrency_limit, annotation_import_rate_limit, @@ -180,7 +180,7 @@ class AnnotationReplyActionApi(Resource): @account_initialization_required @cloud_edition_billing_resource_check("annotation") @edit_permission_required - @rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_EDIT) + @rbac_permission_required(RBACCheck(RBACPermission.APP_EDIT, PlainApp())) @model_validate(AnnotationReplyPayload) def post(self, req_data: AnnotationReplyPayload, app_id: UUID, action: Literal["enable", "disable"]): match action: @@ -211,7 +211,7 @@ class AppAnnotationSettingDetailApi(Resource): @login_required @account_initialization_required @edit_permission_required - @rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_VIEW_LAYOUT) + @rbac_permission_required(RBACCheck(RBACPermission.APP_VIEW_LAYOUT, PlainApp())) @with_session(write=False) def get(self, session: Session, app_id: UUID): result = AppAnnotationService.get_app_annotation_setting_by_app_id(str(app_id), session) @@ -230,7 +230,7 @@ class AppAnnotationSettingUpdateApi(Resource): @login_required @account_initialization_required @edit_permission_required - @rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_EDIT) + @rbac_permission_required(RBACCheck(RBACPermission.APP_EDIT, PlainApp())) @with_session @model_validate(AnnotationSettingUpdatePayload) def post( @@ -263,7 +263,7 @@ class AnnotationReplyActionStatusApi(Resource): @account_initialization_required @cloud_edition_billing_resource_check("annotation") @edit_permission_required - @rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_VIEW_LAYOUT) + @rbac_permission_required(RBACCheck(RBACPermission.APP_VIEW_LAYOUT, PlainApp())) def get(self, app_id: UUID, job_id: UUID, action: str): job_id_str = str(job_id) app_annotation_job_key = f"{action}_app_annotation_job_{job_id_str}" @@ -294,7 +294,7 @@ class AnnotationApi(Resource): @login_required @account_initialization_required @edit_permission_required - @rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_VIEW_LAYOUT) + @rbac_permission_required(RBACCheck(RBACPermission.APP_VIEW_LAYOUT, PlainApp())) @with_session(write=False) @model_validate(AnnotationListQuery) def get(self, req_data: AnnotationListQuery, session: Session, app_id: UUID): @@ -321,7 +321,7 @@ class AnnotationApi(Resource): @account_initialization_required @cloud_edition_billing_resource_check("annotation") @edit_permission_required - @rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_EDIT) + @rbac_permission_required(RBACCheck(RBACPermission.APP_EDIT, PlainApp())) @with_session @model_validate(CreateAnnotationPayload) def post(self, req_data: CreateAnnotationPayload, session: Session, app_id: UUID): @@ -341,7 +341,7 @@ class AnnotationApi(Resource): @login_required @account_initialization_required @edit_permission_required - @rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_CREATE_AND_MANAGEMENT) + @rbac_permission_required(RBACCheck(RBACPermission.APP_CREATE_AND_MANAGEMENT, PlainApp())) @console_ns.response(204, "Annotations deleted successfully") @with_session def delete(self, session: Session, app_id: UUID): @@ -382,7 +382,7 @@ class AnnotationExportApi(Resource): @login_required @account_initialization_required @edit_permission_required - @rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_VIEW_LAYOUT) + @rbac_permission_required(RBACCheck(RBACPermission.APP_VIEW_LAYOUT, PlainApp())) @with_session(write=False) def get(self, session: Session, app_id: UUID): annotation_list = AppAnnotationService.export_annotation_list_by_app_id(str(app_id), session) @@ -411,7 +411,7 @@ class AnnotationUpdateDeleteApi(Resource): @account_initialization_required @cloud_edition_billing_resource_check("annotation") @edit_permission_required - @rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_EDIT) + @rbac_permission_required(RBACCheck(RBACPermission.APP_EDIT, PlainApp())) @with_session @model_validate(UpdateAnnotationPayload) def post(self, req_data: UpdateAnnotationPayload, session: Session, app_id: UUID, annotation_id: UUID): @@ -429,7 +429,7 @@ class AnnotationUpdateDeleteApi(Resource): @login_required @account_initialization_required @edit_permission_required - @rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_EDIT) + @rbac_permission_required(RBACCheck(RBACPermission.APP_EDIT, PlainApp())) @console_ns.response(204, "Annotation deleted successfully") @with_session def delete(self, session: Session, app_id: UUID, annotation_id: UUID): @@ -458,7 +458,7 @@ class AnnotationBatchImportApi(Resource): @annotation_import_rate_limit @annotation_import_concurrency_limit @edit_permission_required - @rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_EDIT) + @rbac_permission_required(RBACCheck(RBACPermission.APP_EDIT, PlainApp())) @with_session def post(self, session: Session, app_id: UUID): from configs import dify_config @@ -513,7 +513,7 @@ class AnnotationBatchImportStatusApi(Resource): @account_initialization_required @cloud_edition_billing_resource_check("annotation") @edit_permission_required - @rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_VIEW_LAYOUT) + @rbac_permission_required(RBACCheck(RBACPermission.APP_VIEW_LAYOUT, PlainApp())) def get(self, app_id: UUID, job_id: UUID): indexing_cache_key = f"app_annotation_batch_import_{str(job_id)}" cache_result = redis_client.get(indexing_cache_key) @@ -546,7 +546,7 @@ class AnnotationHitHistoryListApi(Resource): @login_required @account_initialization_required @edit_permission_required - @rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_VIEW_LAYOUT) + @rbac_permission_required(RBACCheck(RBACPermission.APP_VIEW_LAYOUT, PlainApp())) @with_session(write=False) def get(self, session: Session, app_id: UUID, annotation_id: UUID): page = request.args.get("page", default=1, type=int) diff --git a/api/controllers/console/app/app.py b/api/controllers/console/app/app.py index 87ff0317971..a750dc4bd96 100644 --- a/api/controllers/console/app/app.py +++ b/api/controllers/console/app/app.py @@ -13,6 +13,7 @@ from werkzeug.exceptions import BadRequest, Forbidden, NotFound from configs import dify_config from controllers.common.app_access import resolve_app_access_filter from controllers.common.fields import RedirectUrlResponse, SimpleResultResponse +from controllers.common.rbac import AgentBehindApp, PlainApp, RBACCheck, Workspace from controllers.common.schema import ( query_params_from_model, query_params_from_request, @@ -21,11 +22,10 @@ from controllers.common.schema import ( register_schema_models, ) from controllers.console import console_ns -from controllers.console.app.wraps import agent_manage_required_for_agent_app, get_app_model, with_session +from controllers.console.app.wraps import get_app_model, with_session from controllers.console.workspace.models import LoadBalancingPayload from controllers.console.wraps import ( RBACPermission, - RBACResourceScope, account_initialization_required, cloud_edition_billing_resource_check, edit_permission_required, @@ -691,7 +691,7 @@ class AppListApi(Resource): @setup_required @login_required @account_initialization_required - @rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_CREATE_AND_MANAGEMENT, resource_required=False) + @rbac_permission_required(RBACCheck(RBACPermission.APP_CREATE_AND_MANAGEMENT, Workspace())) @cloud_edition_billing_resource_check("apps") @edit_permission_required @with_current_user @@ -868,7 +868,7 @@ class AppApi(Resource): @enterprise_license_required @with_current_user @with_current_tenant_id - @rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_VIEW_LAYOUT) + @rbac_permission_required(RBACCheck(RBACPermission.APP_VIEW_LAYOUT, PlainApp())) @with_session(write=False) @get_app_model(mode=None) def get(self, session: Session, current_tenant_id: str, current_user: Account, app_model: App): @@ -907,8 +907,9 @@ class AppApi(Resource): @login_required @account_initialization_required @edit_permission_required - @rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_EDIT) - @agent_manage_required_for_agent_app + @rbac_permission_required( + RBACCheck(RBACPermission.APP_EDIT, PlainApp()), RBACCheck(RBACPermission.AGENT_EDIT, AgentBehindApp()) + ) @with_session @get_app_model(mode=None) @model_validate(UpdateAppPayload) @@ -942,8 +943,9 @@ class AppApi(Resource): @login_required @account_initialization_required @edit_permission_required - @rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_DELETE) - @agent_manage_required_for_agent_app + @rbac_permission_required( + RBACCheck(RBACPermission.APP_DELETE, PlainApp()), RBACCheck(RBACPermission.AGENT_DELETE, AgentBehindApp()) + ) @with_session @get_app_model def delete(self, session: Session, app_model: App): @@ -967,8 +969,7 @@ class AppCopyApi(Resource): @login_required @account_initialization_required @edit_permission_required - @rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_CREATE_AND_MANAGEMENT) - @agent_manage_required_for_agent_app + @rbac_permission_required(RBACCheck(RBACPermission.APP_CREATE_AND_MANAGEMENT, PlainApp())) @with_current_user @with_current_tenant_id @get_app_model(mode=None) @@ -1045,8 +1046,10 @@ class AppExportApi(Resource): @login_required @account_initialization_required @edit_permission_required - @rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_IMPORT_EXPORT_DSL) - @agent_manage_required_for_agent_app + @rbac_permission_required( + RBACCheck(RBACPermission.APP_IMPORT_EXPORT_DSL, PlainApp()), + RBACCheck(RBACPermission.AGENT_IMPORT_EXPORT_DSL, AgentBehindApp()), + ) @get_app_model @model_validate(AppExportQuery) def get(self, req_data: AppExportQuery, app_model: App): @@ -1070,8 +1073,10 @@ class AppPublishToCreatorsPlatformApi(Resource): @login_required @account_initialization_required @edit_permission_required - @rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_IMPORT_EXPORT_DSL) - @agent_manage_required_for_agent_app + @rbac_permission_required( + RBACCheck(RBACPermission.APP_IMPORT_EXPORT_DSL, PlainApp()), + RBACCheck(RBACPermission.AGENT_RELEASE_AND_VERSION, AgentBehindApp()), + ) @with_current_user_id @get_app_model(mode=None) def post(self, current_user_id: str, app_model: App): @@ -1111,8 +1116,9 @@ class AppNameApi(Resource): @login_required @account_initialization_required @edit_permission_required - @rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_EDIT) - @agent_manage_required_for_agent_app + @rbac_permission_required( + RBACCheck(RBACPermission.APP_EDIT, PlainApp()), RBACCheck(RBACPermission.AGENT_EDIT, AgentBehindApp()) + ) @with_session @get_app_model(mode=None) @model_validate(AppNamePayload) @@ -1139,8 +1145,9 @@ class AppIconApi(Resource): @login_required @account_initialization_required @edit_permission_required - @rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_EDIT) - @agent_manage_required_for_agent_app + @rbac_permission_required( + RBACCheck(RBACPermission.APP_EDIT, PlainApp()), RBACCheck(RBACPermission.AGENT_EDIT, AgentBehindApp()) + ) @with_session @get_app_model(mode=None) @model_validate(AppIconPayload) @@ -1173,7 +1180,10 @@ class AppSiteStatus(Resource): @login_required @account_initialization_required @edit_permission_required - @agent_manage_required_for_agent_app(scene=RBACPermission.APP_RELEASE_AND_VERSION) + @rbac_permission_required( + RBACCheck(RBACPermission.APP_RELEASE_AND_VERSION, PlainApp()), + RBACCheck(RBACPermission.AGENT_ACCESS_POINT_MANAGE, AgentBehindApp()), + ) @with_session @get_app_model(mode=None) @model_validate(AppSiteStatusPayload) @@ -1200,8 +1210,10 @@ class AppApiStatus(Resource): @login_required @is_admin_or_owner_required @account_initialization_required - @rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_RELEASE_AND_VERSION) - @agent_manage_required_for_agent_app + @rbac_permission_required( + RBACCheck(RBACPermission.APP_RELEASE_AND_VERSION, PlainApp()), + RBACCheck(RBACPermission.AGENT_ACCESS_POINT_MANAGE, AgentBehindApp()), + ) @with_session @get_app_model(mode=None) @model_validate(AppApiStatusPayload) @@ -1230,7 +1242,7 @@ class AppTraceApi(Resource): @login_required @account_initialization_required @with_session - @rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_VIEW_LAYOUT) + @rbac_permission_required(RBACCheck(RBACPermission.APP_VIEW_LAYOUT, PlainApp())) @get_app_model def get(self, session: Session, app_model: App): """Get app trace""" @@ -1252,7 +1264,7 @@ class AppTraceApi(Resource): @login_required @account_initialization_required @edit_permission_required - @rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_TRACING_CONFIG) + @rbac_permission_required(RBACCheck(RBACPermission.APP_TRACING_CONFIG, PlainApp())) @get_app_model @model_validate(AppTracePayload) def post(self, req_data: AppTracePayload, app_model: App): diff --git a/api/controllers/console/app/app_import.py b/api/controllers/console/app/app_import.py index f8fa63a7fba..18de98f8d9a 100644 --- a/api/controllers/console/app/app_import.py +++ b/api/controllers/console/app/app_import.py @@ -4,11 +4,11 @@ from sqlalchemy.orm import Session from werkzeug.exceptions import Forbidden from configs import dify_config +from controllers.common.rbac import PlainApp, RBACCheck, Workspace from controllers.common.schema import register_enum_models, register_schema_models from controllers.console.app.wraps import get_app_model from controllers.console.wraps import ( RBACPermission, - RBACResourceScope, account_initialization_required, cloud_edition_billing_resource_check, edit_permission_required, @@ -82,7 +82,7 @@ class AppImportApi(Resource): @account_initialization_required @cloud_edition_billing_resource_check("apps") @edit_permission_required - @rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_IMPORT_EXPORT_DSL, resource_required=False) + @rbac_permission_required(RBACCheck(RBACPermission.APP_IMPORT_EXPORT_DSL, Workspace())) @with_current_user @model_validate(AppImportPayload) def post(self, req_data: AppImportPayload, current_user: Account | None = None): @@ -149,7 +149,7 @@ class AppImportConfirmApi(Resource): @login_required @account_initialization_required @edit_permission_required - @rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_IMPORT_EXPORT_DSL, resource_required=False) + @rbac_permission_required(RBACCheck(RBACPermission.APP_IMPORT_EXPORT_DSL, Workspace())) @with_current_user def post(self, current_user: Account | None = None, import_id: str = ""): current_user = current_user if current_user is not None else _current_user_and_tenant_id(None)[0] @@ -208,7 +208,7 @@ class AppImportCheckDependenciesApi(Resource): @get_app_model @account_initialization_required @edit_permission_required - @rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_VIEW_LAYOUT) + @rbac_permission_required(RBACCheck(RBACPermission.APP_VIEW_LAYOUT, PlainApp())) def get(self, app_model: App): with Session(db.engine, expire_on_commit=False) as session: import_service = AppDslService(session) diff --git a/api/controllers/console/app/audio.py b/api/controllers/console/app/audio.py index b476817cf08..f6c8cc43ed6 100644 --- a/api/controllers/console/app/audio.py +++ b/api/controllers/console/app/audio.py @@ -9,8 +9,8 @@ from werkzeug.datastructures import FileStorage from werkzeug.exceptions import HTTPException, InternalServerError import services +from controllers.common.rbac import PlainApp, RBACCheck, enforce_rbac_checks from controllers.common.schema import query_params_from_model, register_response_schema_models, register_schema_models -from controllers.common.wraps import enforce_rbac_access from controllers.console import console_ns from controllers.console.agent.app_helpers import resolve_agent_runtime_app_model from controllers.console.app.error import ( @@ -28,7 +28,6 @@ from controllers.console.app.error import ( from controllers.console.app.wraps import get_app_model, with_session from controllers.console.wraps import ( RBACPermission, - RBACResourceScope, account_initialization_required, edit_permission_required, model_validate, @@ -242,11 +241,10 @@ class AgentChatMessageAudioApi(Resource): agent_id=agent_id, ) # Agent routes expose Agent ids, while APP RBAC is keyed by the resolved runtime App id. - enforce_rbac_access( + enforce_rbac_checks( tenant_id=current_tenant_id, account_id=current_user.id, - resource_type=RBACResourceScope.APP, - scene=RBACPermission.APP_TEST_AND_RUN, + checks=[RBACCheck(RBACPermission.APP_TEST_AND_RUN, PlainApp())], path_args={"app_id": app_model.id}, ) agent_soul = AgentComposerService.load_agent_soul_for_debug( @@ -338,7 +336,7 @@ class TextModesApi(Resource): @setup_required @login_required @account_initialization_required - @rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_VIEW_LAYOUT) + @rbac_permission_required(RBACCheck(RBACPermission.APP_VIEW_LAYOUT, PlainApp())) @get_app_model @model_validate(TextToSpeechVoiceQuery) def get(self, req_data: TextToSpeechVoiceQuery, app_model: App): diff --git a/api/controllers/console/app/completion.py b/api/controllers/console/app/completion.py index 53509d00f26..61ce63e7d6b 100644 --- a/api/controllers/console/app/completion.py +++ b/api/controllers/console/app/completion.py @@ -12,6 +12,7 @@ from werkzeug.exceptions import BadRequest, InternalServerError, NotFound import services from controllers.common.fields import SimpleResultResponse +from controllers.common.rbac import AgentId, PlainApp, RBACCheck from controllers.common.schema import register_response_schema_models, register_schema_models from controllers.console import console_ns from controllers.console.agent.app_helpers import resolve_agent_runtime_app_model @@ -24,10 +25,9 @@ from controllers.console.app.error import ( ProviderNotInitializeError, ProviderQuotaExceededError, ) -from controllers.console.app.wraps import agent_manage_required_for_agent_app, get_app_model, with_session +from controllers.console.app.wraps import get_app_model, with_session from controllers.console.wraps import ( RBACPermission, - RBACResourceScope, account_initialization_required, edit_permission_required, model_validate, @@ -159,7 +159,7 @@ class CompletionMessageApi(Resource): @login_required @account_initialization_required @with_current_user - @rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_TEST_AND_RUN) + @rbac_permission_required(RBACCheck(RBACPermission.APP_TEST_AND_RUN, PlainApp())) @with_session @get_app_model(mode=AppMode.COMPLETION) @model_validate(CompletionMessagePayload) @@ -241,7 +241,7 @@ class ChatMessageApi(Resource): @edit_permission_required @with_current_user @with_current_tenant_id - @rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_TEST_AND_RUN) + @rbac_permission_required(RBACCheck(RBACPermission.APP_TEST_AND_RUN, PlainApp())) @with_session @get_app_model(mode=[AppMode.CHAT, AppMode.AGENT_CHAT, AppMode.AGENT]) def post(self, session: Session, current_tenant_id: str, current_user: Account, app_model: App): @@ -263,7 +263,7 @@ class AgentChatMessageApi(Resource): @login_required @account_initialization_required @edit_permission_required - @agent_manage_required_for_agent_app(scene=RBACPermission.APP_TEST_AND_RUN) + @rbac_permission_required(RBACCheck(RBACPermission.AGENT_TEST_AND_RUN, AgentId())) @with_current_user @with_current_tenant_id @with_session @@ -292,7 +292,7 @@ class AgentBuildChatFinalizeApi(Resource): @login_required @account_initialization_required @edit_permission_required - @agent_manage_required_for_agent_app(scene=RBACPermission.APP_TEST_AND_RUN) + @rbac_permission_required(RBACCheck(RBACPermission.AGENT_TEST_AND_RUN, AgentId())) @with_current_user @with_current_tenant_id @with_session @@ -333,6 +333,7 @@ class AgentChatMessageStopApi(Resource): @setup_required @login_required @account_initialization_required + @rbac_permission_required(RBACCheck(RBACPermission.AGENT_TEST_AND_RUN, AgentId())) @with_current_user_id @with_current_tenant_id @with_session(write=False) diff --git a/api/controllers/console/app/conversation.py b/api/controllers/console/app/conversation.py index 325f3ef22d7..b41b03f2e90 100644 --- a/api/controllers/console/app/conversation.py +++ b/api/controllers/console/app/conversation.py @@ -9,13 +9,13 @@ from sqlalchemy import func, or_ from sqlalchemy.orm import Session, selectinload from werkzeug.exceptions import NotFound +from controllers.common.rbac import PlainApp, RBACCheck from controllers.common.schema import query_params_from_model, register_response_schema_models, register_schema_models from controllers.common.session import with_session from controllers.console import console_ns from controllers.console.app.wraps import get_app_model from controllers.console.wraps import ( RBACPermission, - RBACResourceScope, account_initialization_required, edit_permission_required, model_validate, @@ -106,7 +106,7 @@ class CompletionConversationApi(Resource): @account_initialization_required @edit_permission_required @with_current_user - @rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_VIEW_LAYOUT) + @rbac_permission_required(RBACCheck(RBACPermission.APP_VIEW_LAYOUT, PlainApp())) @with_session(write=False) @get_app_model(mode=AppMode.COMPLETION) @model_validate(CompletionConversationQuery) @@ -187,7 +187,7 @@ class CompletionConversationDetailApi(Resource): @account_initialization_required @edit_permission_required @with_current_user - @rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_VIEW_LAYOUT) + @rbac_permission_required(RBACCheck(RBACPermission.APP_VIEW_LAYOUT, PlainApp())) @with_session @get_app_model(mode=AppMode.COMPLETION) def get(self, session: Session, current_user: Account, app_model: App, conversation_id: UUID): @@ -209,7 +209,7 @@ class CompletionConversationDetailApi(Resource): @login_required @account_initialization_required @edit_permission_required - @rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_EDIT) + @rbac_permission_required(RBACCheck(RBACPermission.APP_EDIT, PlainApp())) @with_current_user @with_session @get_app_model(mode=AppMode.COMPLETION) @@ -236,7 +236,7 @@ class ChatConversationApi(Resource): @account_initialization_required @edit_permission_required @with_current_user - @rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_VIEW_LAYOUT) + @rbac_permission_required(RBACCheck(RBACPermission.APP_VIEW_LAYOUT, PlainApp())) @with_session(write=False) @get_app_model(mode=[AppMode.CHAT, AppMode.AGENT_CHAT, AppMode.ADVANCED_CHAT, AppMode.AGENT]) @model_validate(ChatConversationQuery) @@ -356,7 +356,7 @@ class ChatConversationDetailApi(Resource): @account_initialization_required @edit_permission_required @with_current_user - @rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_VIEW_LAYOUT) + @rbac_permission_required(RBACCheck(RBACPermission.APP_VIEW_LAYOUT, PlainApp())) @with_session @get_app_model(mode=[AppMode.CHAT, AppMode.AGENT_CHAT, AppMode.ADVANCED_CHAT, AppMode.AGENT]) def get(self, session: Session, current_user: Account, app_model: App, conversation_id: UUID): @@ -378,7 +378,7 @@ class ChatConversationDetailApi(Resource): @login_required @account_initialization_required @edit_permission_required - @rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_EDIT) + @rbac_permission_required(RBACCheck(RBACPermission.APP_EDIT, PlainApp())) @with_current_user @with_session @get_app_model(mode=[AppMode.CHAT, AppMode.AGENT_CHAT, AppMode.ADVANCED_CHAT, AppMode.AGENT]) diff --git a/api/controllers/console/app/conversation_variables.py b/api/controllers/console/app/conversation_variables.py index ff0727039aa..00c9620d96e 100644 --- a/api/controllers/console/app/conversation_variables.py +++ b/api/controllers/console/app/conversation_variables.py @@ -8,12 +8,12 @@ from pydantic import BaseModel, Field, field_validator from sqlalchemy import select from sqlalchemy.orm import sessionmaker +from controllers.common.rbac import PlainApp, RBACCheck from controllers.common.schema import query_params_from_model, register_schema_models from controllers.console import console_ns from controllers.console.app.wraps import get_app_model from controllers.console.wraps import ( RBACPermission, - RBACResourceScope, account_initialization_required, model_validate, rbac_permission_required, @@ -99,7 +99,7 @@ class ConversationVariablesApi(Resource): @setup_required @login_required @account_initialization_required - @rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_CREATE_AND_MANAGEMENT) + @rbac_permission_required(RBACCheck(RBACPermission.APP_CREATE_AND_MANAGEMENT, PlainApp())) @get_app_model(mode=AppMode.ADVANCED_CHAT) @model_validate(ConversationVariablesQuery) def get(self, req_data: ConversationVariablesQuery, app_model: App): diff --git a/api/controllers/console/app/mcp_server.py b/api/controllers/console/app/mcp_server.py index 6d232451043..8a6ab77b3dd 100644 --- a/api/controllers/console/app/mcp_server.py +++ b/api/controllers/console/app/mcp_server.py @@ -7,12 +7,12 @@ from pydantic import BaseModel, Field, field_validator from sqlalchemy import select from werkzeug.exceptions import NotFound +from controllers.common.rbac import PlainApp, RBACCheck from controllers.common.schema import register_schema_models from controllers.console import console_ns from controllers.console.app.wraps import get_app_model from controllers.console.wraps import ( RBACPermission, - RBACResourceScope, account_initialization_required, edit_permission_required, model_validate, @@ -87,7 +87,7 @@ class AppMCPServerController(Resource): @login_required @account_initialization_required @setup_required - @rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_VIEW_LAYOUT) + @rbac_permission_required(RBACCheck(RBACPermission.APP_VIEW_LAYOUT, PlainApp())) @get_app_model def get(self, app_model: App): server = db.session.scalar(select(AppMCPServer).where(AppMCPServer.app_id == app_model.id).limit(1)) @@ -107,7 +107,7 @@ class AppMCPServerController(Resource): @login_required @setup_required @edit_permission_required - @rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_EDIT) + @rbac_permission_required(RBACCheck(RBACPermission.APP_EDIT, PlainApp())) @with_current_tenant_id @get_app_model @model_validate(MCPServerCreatePayload) @@ -142,7 +142,7 @@ class AppMCPServerController(Resource): @setup_required @account_initialization_required @edit_permission_required - @rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_EDIT) + @rbac_permission_required(RBACCheck(RBACPermission.APP_EDIT, PlainApp())) @get_app_model @model_validate(MCPServerUpdatePayload) def put(self, req_data: MCPServerUpdatePayload, app_model: App): @@ -190,7 +190,7 @@ class AppMCPServerRefreshController(Resource): @login_required @account_initialization_required @edit_permission_required - @rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_EDIT) + @rbac_permission_required(RBACCheck(RBACPermission.APP_EDIT, PlainApp())) @with_current_tenant_id @get_app_model def post(self, current_tenant_id: str, app_model: App): diff --git a/api/controllers/console/app/message.py b/api/controllers/console/app/message.py index 1c5a74e5172..5234ea9309e 100644 --- a/api/controllers/console/app/message.py +++ b/api/controllers/console/app/message.py @@ -11,6 +11,7 @@ from werkzeug.exceptions import InternalServerError, NotFound from controllers.common.controller_schemas import MessageFeedbackPayload as _MessageFeedbackPayloadBase from controllers.common.fields import SimpleResultResponse, TextFileResponse +from controllers.common.rbac import AgentId, PlainApp, RBACCheck from controllers.common.schema import query_params_from_model, register_response_schema_models, register_schema_models from controllers.common.session import with_session from controllers.console import console_ns @@ -21,11 +22,10 @@ from controllers.console.app.error import ( ProviderNotInitializeError, ProviderQuotaExceededError, ) -from controllers.console.app.wraps import agent_manage_required_for_agent_app, get_app_model +from controllers.console.app.wraps import get_app_model from controllers.console.explore.error import AppSuggestedQuestionsAfterAnswerDisabledError from controllers.console.wraps import ( RBACPermission, - RBACResourceScope, account_initialization_required, edit_permission_required, model_validate, @@ -155,7 +155,7 @@ class ChatMessageListApi(Resource): @setup_required @edit_permission_required @with_current_user - @rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_VIEW_LAYOUT) + @rbac_permission_required(RBACCheck(RBACPermission.APP_VIEW_LAYOUT, PlainApp())) @with_session(write=False) @get_app_model(mode=[AppMode.CHAT, AppMode.AGENT_CHAT, AppMode.ADVANCED_CHAT, AppMode.AGENT]) def get(self, session: Session, current_user: Account, app_model: App): @@ -174,7 +174,7 @@ class AgentChatMessageListApi(Resource): @account_initialization_required @setup_required @edit_permission_required - @agent_manage_required_for_agent_app(scene=RBACPermission.APP_VIEW_LAYOUT) + @rbac_permission_required(RBACCheck(RBACPermission.AGENT_TEST_AND_RUN, AgentId())) @with_current_user @with_current_tenant_id @with_session(write=False) @@ -217,6 +217,7 @@ class AgentMessageFeedbackApi(Resource): @setup_required @login_required @account_initialization_required + @rbac_permission_required(RBACCheck(RBACPermission.AGENT_TEST_AND_RUN, AgentId())) @with_current_user @with_current_tenant_id @with_session @@ -242,7 +243,7 @@ class MessageAnnotationCountApi(Resource): @setup_required @login_required @account_initialization_required - @rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_VIEW_LAYOUT) + @rbac_permission_required(RBACCheck(RBACPermission.APP_VIEW_LAYOUT, PlainApp())) @get_app_model def get(self, app_model: App): count = db.session.scalar( @@ -267,7 +268,7 @@ class MessageSuggestedQuestionApi(Resource): @login_required @account_initialization_required @with_current_user - @rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_VIEW_LAYOUT) + @rbac_permission_required(RBACCheck(RBACPermission.APP_VIEW_LAYOUT, PlainApp())) @with_session(write=False) @get_app_model(mode=[AppMode.CHAT, AppMode.AGENT_CHAT, AppMode.ADVANCED_CHAT, AppMode.AGENT]) def get(self, session: Session, current_user: Account, app_model: App, message_id: UUID): @@ -290,6 +291,7 @@ class AgentMessageSuggestedQuestionApi(Resource): @setup_required @login_required @account_initialization_required + @rbac_permission_required(RBACCheck(RBACPermission.AGENT_TEST_AND_RUN, AgentId())) @with_current_user @with_current_tenant_id @with_session(write=False) @@ -319,7 +321,7 @@ class MessageFeedbackExportApi(Resource): @setup_required @login_required @account_initialization_required - @rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_VIEW_LAYOUT) + @rbac_permission_required(RBACCheck(RBACPermission.APP_VIEW_LAYOUT, PlainApp())) @get_app_model @model_validate(FeedbackExportQuery) def get(self, req_data: FeedbackExportQuery, app_model: App): @@ -358,7 +360,7 @@ class MessageApi(Resource): @setup_required @login_required @account_initialization_required - @rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_VIEW_LAYOUT) + @rbac_permission_required(RBACCheck(RBACPermission.APP_VIEW_LAYOUT, PlainApp())) @with_session(write=False) @get_app_model def get(self, session: Session, app_model: App, message_id: UUID): @@ -375,6 +377,7 @@ class AgentMessageApi(Resource): @setup_required @login_required @account_initialization_required + @rbac_permission_required(RBACCheck(RBACPermission.AGENT_TEST_AND_RUN, AgentId())) @with_current_tenant_id @with_session(write=False) def get(self, session: Session, current_tenant_id: str, agent_id: UUID, message_id: UUID): diff --git a/api/controllers/console/app/model_config.py b/api/controllers/console/app/model_config.py index 15298366414..1b44c4c64ec 100644 --- a/api/controllers/console/app/model_config.py +++ b/api/controllers/console/app/model_config.py @@ -7,13 +7,13 @@ from pydantic import BaseModel, Field from sqlalchemy.orm import Session from controllers.common.fields import SimpleResultResponse +from controllers.common.rbac import PlainApp, RBACCheck from controllers.common.schema import register_response_schema_models, register_schema_models from controllers.common.session import with_session from controllers.console import console_ns from controllers.console.app.wraps import get_app_model from controllers.console.wraps import ( RBACPermission, - RBACResourceScope, account_initialization_required, edit_permission_required, rbac_permission_required, @@ -90,7 +90,7 @@ class ModelConfigResource(Resource): @setup_required @login_required @edit_permission_required - @rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_VIEW_LAYOUT) + @rbac_permission_required(RBACCheck(RBACPermission.APP_VIEW_LAYOUT, PlainApp())) @account_initialization_required @with_current_user_id @with_current_tenant_id diff --git a/api/controllers/console/app/ops_trace.py b/api/controllers/console/app/ops_trace.py index 9f9cdfb48f2..4dbe8bc8d07 100644 --- a/api/controllers/console/app/ops_trace.py +++ b/api/controllers/console/app/ops_trace.py @@ -4,6 +4,7 @@ from uuid import UUID from flask_restx import Resource from pydantic import BaseModel, Field +from controllers.common.rbac import PlainApp, RBACCheck from controllers.common.schema import query_params_from_model, register_response_schema_models, register_schema_models from controllers.console import console_ns from controllers.console.app.error import ( @@ -18,7 +19,6 @@ from controllers.console.app.error import ( from controllers.console.flask_admission import console_account_admission from controllers.console.wraps import ( RBACPermission, - RBACResourceScope, model_validate, ) from extensions.ext_application_services import application_services @@ -106,8 +106,7 @@ class TraceAppConfigApi(Resource): @console_ns.response(404, "Application not found") @console_ns.response(500, "Tracing configuration processing failed") @console_account_admission( - rbac_resource_scope=RBACResourceScope.APP, - rbac_permission=RBACPermission.APP_TRACING_CONFIG, + rbac_checks=[RBACCheck(RBACPermission.APP_TRACING_CONFIG, PlainApp())], ) @model_validate(TraceProviderQuery) def get( @@ -155,8 +154,7 @@ class TraceAppConfigApi(Resource): @console_ns.response(500, "Tracing configuration processing failed") @console_account_admission( allowed_roles=_APP_TRACING_CONFIG_EDIT_ROLES, - rbac_resource_scope=RBACResourceScope.APP, - rbac_permission=RBACPermission.APP_TRACING_CONFIG, + rbac_checks=[RBACCheck(RBACPermission.APP_TRACING_CONFIG, PlainApp())], ) @model_validate(TraceConfigPayload) def post( @@ -205,8 +203,7 @@ class TraceAppConfigApi(Resource): @console_ns.response(500, "Tracing configuration processing failed") @console_account_admission( allowed_roles=_APP_TRACING_CONFIG_EDIT_ROLES, - rbac_resource_scope=RBACResourceScope.APP, - rbac_permission=RBACPermission.APP_TRACING_CONFIG, + rbac_checks=[RBACCheck(RBACPermission.APP_TRACING_CONFIG, PlainApp())], ) @model_validate(TraceConfigPayload) def patch( @@ -251,8 +248,7 @@ class TraceAppConfigApi(Resource): @console_ns.response(500, "Tracing configuration processing failed") @console_account_admission( allowed_roles=_APP_TRACING_CONFIG_EDIT_ROLES, - rbac_resource_scope=RBACResourceScope.APP, - rbac_permission=RBACPermission.APP_TRACING_CONFIG, + rbac_checks=[RBACCheck(RBACPermission.APP_TRACING_CONFIG, PlainApp())], ) @model_validate(TraceProviderQuery) def delete( diff --git a/api/controllers/console/app/site.py b/api/controllers/console/app/site.py index 7d1d7efc616..c1e0636ac0f 100644 --- a/api/controllers/console/app/site.py +++ b/api/controllers/console/app/site.py @@ -5,13 +5,13 @@ from pydantic import BaseModel, Field, field_validator from werkzeug.exceptions import NotFound from constants.languages import supported_language +from controllers.common.rbac import AgentBehindApp, PlainApp, RBACCheck from controllers.common.schema import register_schema_models from controllers.console import console_ns from controllers.console.app.error import AppNotFoundError from controllers.console.flask_admission import console_account_admission from controllers.console.wraps import ( RBACPermission, - RBACResourceScope, model_validate, ) from extensions.ext_application_services import application_services @@ -105,9 +105,10 @@ class AppSite(Resource): @console_ns.response(404, "App not found") @console_account_admission( allowed_roles=_APP_SITE_EDIT_ROLES, - rbac_resource_scope=RBACResourceScope.APP, - rbac_permission=RBACPermission.APP_RELEASE_AND_VERSION, - agent_manage_fallback=True, + rbac_checks=[ + RBACCheck(RBACPermission.APP_RELEASE_AND_VERSION, PlainApp()), + RBACCheck(RBACPermission.AGENT_ACCESS_POINT_MANAGE, AgentBehindApp()), + ], ) @model_validate(AppSiteUpdatePayload) def post( @@ -136,9 +137,10 @@ class AppSiteAccessTokenReset(Resource): @console_ns.response(404, "App or site not found") @console_account_admission( allowed_roles=_APP_SITE_TOKEN_RESET_ROLES, - rbac_resource_scope=RBACResourceScope.APP, - rbac_permission=RBACPermission.APP_RELEASE_AND_VERSION, - agent_manage_fallback=True, + rbac_checks=[ + RBACCheck(RBACPermission.APP_RELEASE_AND_VERSION, PlainApp()), + RBACCheck(RBACPermission.AGENT_ACCESS_POINT_MANAGE, AgentBehindApp()), + ], ) def post(self, request_context: RequestContext, app_id: UUID): try: diff --git a/api/controllers/console/app/statistic.py b/api/controllers/console/app/statistic.py index 2ebf60bdcfe..195af05e5fa 100644 --- a/api/controllers/console/app/statistic.py +++ b/api/controllers/console/app/statistic.py @@ -5,11 +5,12 @@ from flask_restx import Resource from pydantic import BaseModel, Field, field_validator from werkzeug.exceptions import BadRequest +from controllers.common.rbac import PlainApp, RBACCheck from controllers.common.schema import query_params_from_model, register_response_schema_models, register_schema_models from controllers.console import console_ns from controllers.console.app.wraps import get_app_model from controllers.console.flask_admission import console_account_admission -from controllers.console.wraps import RBACPermission, RBACResourceScope, model_validate +from controllers.console.wraps import RBACPermission, model_validate from extensions.ext_application_services import application_services from fields.base import ResponseModel from libs.datetime_utils import parse_time_range @@ -158,8 +159,7 @@ class DailyMessageStatistic(Resource): console_ns.models[DailyMessageStatisticResponse.__name__], ) @console_account_admission( - rbac_resource_scope=RBACResourceScope.APP, - rbac_permission=RBACPermission.APP_MONITOR, + rbac_checks=[RBACCheck(RBACPermission.APP_MONITOR, PlainApp())], ) @get_app_model @model_validate(StatisticTimeRangeQuery) @@ -186,8 +186,7 @@ class DailyConversationStatistic(Resource): console_ns.models[DailyConversationStatisticResponse.__name__], ) @console_account_admission( - rbac_resource_scope=RBACResourceScope.APP, - rbac_permission=RBACPermission.APP_MONITOR, + rbac_checks=[RBACCheck(RBACPermission.APP_MONITOR, PlainApp())], ) @get_app_model @model_validate(StatisticTimeRangeQuery) @@ -214,8 +213,7 @@ class DailyTerminalsStatistic(Resource): console_ns.models[DailyTerminalStatisticResponse.__name__], ) @console_account_admission( - rbac_resource_scope=RBACResourceScope.APP, - rbac_permission=RBACPermission.APP_MONITOR, + rbac_checks=[RBACCheck(RBACPermission.APP_MONITOR, PlainApp())], ) @get_app_model @model_validate(StatisticTimeRangeQuery) @@ -242,8 +240,7 @@ class DailyTokenCostStatistic(Resource): console_ns.models[DailyTokenCostStatisticResponse.__name__], ) @console_account_admission( - rbac_resource_scope=RBACResourceScope.APP, - rbac_permission=RBACPermission.APP_MONITOR, + rbac_checks=[RBACCheck(RBACPermission.APP_MONITOR, PlainApp())], ) @get_app_model @model_validate(StatisticTimeRangeQuery) @@ -270,8 +267,7 @@ class AverageSessionInteractionStatistic(Resource): console_ns.models[AverageSessionInteractionStatisticResponse.__name__], ) @console_account_admission( - rbac_resource_scope=RBACResourceScope.APP, - rbac_permission=RBACPermission.APP_MONITOR, + rbac_checks=[RBACCheck(RBACPermission.APP_MONITOR, PlainApp())], ) @get_app_model(mode=[AppMode.CHAT, AppMode.AGENT_CHAT, AppMode.ADVANCED_CHAT, AppMode.AGENT]) @model_validate(StatisticTimeRangeQuery) @@ -298,8 +294,7 @@ class UserSatisfactionRateStatistic(Resource): console_ns.models[UserSatisfactionRateStatisticResponse.__name__], ) @console_account_admission( - rbac_resource_scope=RBACResourceScope.APP, - rbac_permission=RBACPermission.APP_MONITOR, + rbac_checks=[RBACCheck(RBACPermission.APP_MONITOR, PlainApp())], ) @get_app_model @model_validate(StatisticTimeRangeQuery) @@ -326,8 +321,7 @@ class AverageResponseTimeStatistic(Resource): console_ns.models[AverageResponseTimeStatisticResponse.__name__], ) @console_account_admission( - rbac_resource_scope=RBACResourceScope.APP, - rbac_permission=RBACPermission.APP_MONITOR, + rbac_checks=[RBACCheck(RBACPermission.APP_MONITOR, PlainApp())], ) @get_app_model(mode=AppMode.COMPLETION) @model_validate(StatisticTimeRangeQuery) @@ -354,8 +348,7 @@ class TokensPerSecondStatistic(Resource): console_ns.models[TokensPerSecondStatisticResponse.__name__], ) @console_account_admission( - rbac_resource_scope=RBACResourceScope.APP, - rbac_permission=RBACPermission.APP_MONITOR, + rbac_checks=[RBACCheck(RBACPermission.APP_MONITOR, PlainApp())], ) @get_app_model @model_validate(StatisticTimeRangeQuery) diff --git a/api/controllers/console/app/workflow.py b/api/controllers/console/app/workflow.py index c7e51f663a4..aa596e39a8e 100644 --- a/api/controllers/console/app/workflow.py +++ b/api/controllers/console/app/workflow.py @@ -25,6 +25,7 @@ from controllers.common.app_access import resolve_app_access_filter from controllers.common.controller_schemas import DefaultBlockConfigQuery, WorkflowListQuery, WorkflowUpdatePayload from controllers.common.errors import InvalidArgumentError from controllers.common.fields import GeneratedAppResponse, NewAppResponse, SimpleResultResponse +from controllers.common.rbac import PlainApp, RBACCheck from controllers.common.schema import ( query_params_from_model, register_response_schema_model, @@ -41,7 +42,6 @@ from controllers.console.app.permission_keys import get_app_permission_keys from controllers.console.app.wraps import get_app_model, with_session from controllers.console.wraps import ( RBACPermission, - RBACResourceScope, account_initialization_required, edit_permission_required, model_validate, @@ -582,7 +582,7 @@ class DraftWorkflowApi(Resource): @login_required @account_initialization_required @edit_permission_required - @rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_VIEW_LAYOUT) + @rbac_permission_required(RBACCheck(RBACPermission.APP_VIEW_LAYOUT, PlainApp())) @get_app_model(mode=[AppMode.ADVANCED_CHAT, AppMode.WORKFLOW]) def get(self, app_model: App): """ @@ -625,7 +625,7 @@ class DraftWorkflowApi(Resource): @console_ns.response(403, "Permission denied") @with_current_user @edit_permission_required - @rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_VIEW_LAYOUT) + @rbac_permission_required(RBACCheck(RBACPermission.APP_VIEW_LAYOUT, PlainApp())) def post(self, current_user: Account, app_model: App): """ Sync draft workflow @@ -704,7 +704,7 @@ class AdvancedChatDraftWorkflowRunApi(Resource): @setup_required @login_required @account_initialization_required - @rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_TEST_AND_RUN) + @rbac_permission_required(RBACCheck(RBACPermission.APP_TEST_AND_RUN, PlainApp())) @with_current_user @edit_permission_required @with_session @@ -760,7 +760,7 @@ class AdvancedChatDraftRunIterationNodeApi(Resource): @setup_required @login_required @account_initialization_required - @rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_TEST_AND_RUN) + @rbac_permission_required(RBACCheck(RBACPermission.APP_TEST_AND_RUN, PlainApp())) @get_app_model(mode=[AppMode.ADVANCED_CHAT]) @with_current_user @edit_permission_required @@ -809,7 +809,7 @@ class WorkflowDraftRunIterationNodeApi(Resource): @setup_required @login_required @account_initialization_required - @rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_TEST_AND_RUN) + @rbac_permission_required(RBACCheck(RBACPermission.APP_TEST_AND_RUN, PlainApp())) @get_app_model(mode=[AppMode.WORKFLOW]) @with_current_user @edit_permission_required @@ -854,7 +854,7 @@ class AdvancedChatDraftRunLoopNodeApi(Resource): @setup_required @login_required @account_initialization_required - @rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_TEST_AND_RUN) + @rbac_permission_required(RBACCheck(RBACPermission.APP_TEST_AND_RUN, PlainApp())) @get_app_model(mode=[AppMode.ADVANCED_CHAT]) @with_current_user @edit_permission_required @@ -902,7 +902,7 @@ class WorkflowDraftRunLoopNodeApi(Resource): @setup_required @login_required @account_initialization_required - @rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_TEST_AND_RUN) + @rbac_permission_required(RBACCheck(RBACPermission.APP_TEST_AND_RUN, PlainApp())) @get_app_model(mode=[AppMode.WORKFLOW]) @with_current_user @edit_permission_required @@ -982,7 +982,7 @@ class AdvancedChatDraftHumanInputFormPreviewApi(Resource): @get_app_model(mode=[AppMode.ADVANCED_CHAT]) @with_current_user @edit_permission_required - @rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_VIEW_LAYOUT) + @rbac_permission_required(RBACCheck(RBACPermission.APP_VIEW_LAYOUT, PlainApp())) @model_validate(HumanInputFormPreviewPayload) def post(self, args: HumanInputFormPreviewPayload, current_user: Account, app_model: App, node_id: str): """ @@ -1015,7 +1015,7 @@ class AdvancedChatDraftHumanInputFormRunApi(Resource): @setup_required @login_required @account_initialization_required - @rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_TEST_AND_RUN) + @rbac_permission_required(RBACCheck(RBACPermission.APP_TEST_AND_RUN, PlainApp())) @get_app_model(mode=[AppMode.ADVANCED_CHAT]) @with_current_user @edit_permission_required @@ -1050,7 +1050,7 @@ class WorkflowDraftHumanInputFormPreviewApi(Resource): @get_app_model(mode=[AppMode.WORKFLOW]) @with_current_user @edit_permission_required - @rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_VIEW_LAYOUT) + @rbac_permission_required(RBACCheck(RBACPermission.APP_VIEW_LAYOUT, PlainApp())) @model_validate(HumanInputFormPreviewPayload) def post(self, args: HumanInputFormPreviewPayload, current_user: Account, app_model: App, node_id: str): """ @@ -1083,7 +1083,7 @@ class WorkflowDraftHumanInputFormRunApi(Resource): @setup_required @login_required @account_initialization_required - @rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_TEST_AND_RUN) + @rbac_permission_required(RBACCheck(RBACPermission.APP_TEST_AND_RUN, PlainApp())) @get_app_model(mode=[AppMode.WORKFLOW]) @with_current_user @edit_permission_required @@ -1118,7 +1118,7 @@ class WorkflowDraftHumanInputDeliveryTestApi(Resource): @get_app_model(mode=[AppMode.WORKFLOW, AppMode.ADVANCED_CHAT]) @with_current_user @edit_permission_required - @rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_TEST_AND_RUN) + @rbac_permission_required(RBACCheck(RBACPermission.APP_TEST_AND_RUN, PlainApp())) @model_validate(HumanInputDeliveryTestPayload) def post(self, args: HumanInputDeliveryTestPayload, current_user: Account, app_model: App, node_id: str): """ @@ -1151,7 +1151,7 @@ class DraftWorkflowRunApi(Resource): @setup_required @login_required @account_initialization_required - @rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_TEST_AND_RUN) + @rbac_permission_required(RBACCheck(RBACPermission.APP_TEST_AND_RUN, PlainApp())) @with_current_user @edit_permission_required @with_session @@ -1194,7 +1194,7 @@ class WorkflowTaskStopApi(Resource): @login_required @account_initialization_required @edit_permission_required - @rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_TEST_AND_RUN) + @rbac_permission_required(RBACCheck(RBACPermission.APP_TEST_AND_RUN, PlainApp())) @get_app_model(mode=[AppMode.ADVANCED_CHAT, AppMode.WORKFLOW]) def post(self, app_model: App, task_id: str): """ @@ -1226,7 +1226,7 @@ class DraftWorkflowNodeRunApi(Resource): @setup_required @login_required @account_initialization_required - @rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_TEST_AND_RUN) + @rbac_permission_required(RBACCheck(RBACPermission.APP_TEST_AND_RUN, PlainApp())) @get_app_model(mode=[AppMode.ADVANCED_CHAT, AppMode.WORKFLOW]) @with_current_user @edit_permission_required @@ -1278,7 +1278,7 @@ class PublishedWorkflowApi(Resource): @login_required @account_initialization_required @edit_permission_required - @rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_VIEW_LAYOUT) + @rbac_permission_required(RBACCheck(RBACPermission.APP_VIEW_LAYOUT, PlainApp())) @get_app_model(mode=[AppMode.ADVANCED_CHAT, AppMode.WORKFLOW]) def get(self, app_model: App): """ @@ -1300,7 +1300,7 @@ class PublishedWorkflowApi(Resource): @setup_required @login_required @account_initialization_required - @rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_RELEASE_AND_VERSION) + @rbac_permission_required(RBACCheck(RBACPermission.APP_RELEASE_AND_VERSION, PlainApp())) @get_app_model(mode=[AppMode.ADVANCED_CHAT, AppMode.WORKFLOW]) @with_current_user @edit_permission_required @@ -1349,7 +1349,7 @@ class DefaultBlockConfigsApi(Resource): @login_required @account_initialization_required @edit_permission_required - @rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_VIEW_LAYOUT) + @rbac_permission_required(RBACCheck(RBACPermission.APP_VIEW_LAYOUT, PlainApp())) @get_app_model(mode=[AppMode.ADVANCED_CHAT, AppMode.WORKFLOW]) def get(self, app_model: App): """ @@ -1376,7 +1376,7 @@ class DefaultBlockConfigApi(Resource): @login_required @account_initialization_required @edit_permission_required - @rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_VIEW_LAYOUT) + @rbac_permission_required(RBACCheck(RBACPermission.APP_VIEW_LAYOUT, PlainApp())) @get_app_model(mode=[AppMode.ADVANCED_CHAT, AppMode.WORKFLOW]) @model_validate(DefaultBlockConfigQuery) def get(self, args: DefaultBlockConfigQuery, app_model: App, block_type: str): @@ -1416,7 +1416,7 @@ class ConvertToWorkflowApi(Resource): @with_current_user @with_current_tenant_id @edit_permission_required - @rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_EDIT) + @rbac_permission_required(RBACCheck(RBACPermission.APP_EDIT, PlainApp())) @model_validate(ConvertToWorkflowPayload) def post(self, payload: ConvertToWorkflowPayload, current_tenant_id: str, current_user: Account, app_model: App): """ @@ -1458,7 +1458,7 @@ class WorkflowFeaturesApi(Resource): @get_app_model(mode=[AppMode.ADVANCED_CHAT, AppMode.WORKFLOW]) @with_current_user @edit_permission_required - @rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_VIEW_LAYOUT) + @rbac_permission_required(RBACCheck(RBACPermission.APP_VIEW_LAYOUT, PlainApp())) @model_validate(WorkflowFeaturesPayload) def post(self, args: WorkflowFeaturesPayload, current_user: Account, app_model: App): features = args.features.model_dump(mode="json", exclude_unset=True) @@ -1485,7 +1485,7 @@ class PublishedAllWorkflowApi(Resource): @setup_required @login_required @account_initialization_required - @rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_VIEW_LAYOUT) + @rbac_permission_required(RBACCheck(RBACPermission.APP_VIEW_LAYOUT, PlainApp())) @get_app_model(mode=[AppMode.ADVANCED_CHAT, AppMode.WORKFLOW]) @with_current_user @edit_permission_required @@ -1538,7 +1538,7 @@ class DraftWorkflowRestoreApi(Resource): @get_app_model(mode=[AppMode.ADVANCED_CHAT, AppMode.WORKFLOW]) @with_current_user @edit_permission_required - @rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_RELEASE_AND_VERSION) + @rbac_permission_required(RBACCheck(RBACPermission.APP_RELEASE_AND_VERSION, PlainApp())) def post(self, current_user: Account, app_model: App, workflow_id: str): workflow_service = WorkflowService() @@ -1578,7 +1578,7 @@ class WorkflowByIdApi(Resource): @get_app_model(mode=[AppMode.ADVANCED_CHAT, AppMode.WORKFLOW]) @with_current_user @edit_permission_required - @rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_EDIT) + @rbac_permission_required(RBACCheck(RBACPermission.APP_EDIT, PlainApp())) @model_validate(WorkflowUpdatePayload) def patch(self, args: WorkflowUpdatePayload, current_user: Account, app_model: App, workflow_id: str): """ @@ -1620,7 +1620,7 @@ class WorkflowByIdApi(Resource): @get_app_model(mode=[AppMode.ADVANCED_CHAT, AppMode.WORKFLOW]) @with_current_user @edit_permission_required - @rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_EDIT) + @rbac_permission_required(RBACCheck(RBACPermission.APP_EDIT, PlainApp())) @console_ns.response(204, "Workflow deleted successfully") def delete(self, current_user: Account, app_model: App, workflow_id: str): """ @@ -1666,7 +1666,7 @@ class DraftWorkflowNodeLastRunApi(Resource): @setup_required @login_required @account_initialization_required - @rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_VIEW_LAYOUT) + @rbac_permission_required(RBACCheck(RBACPermission.APP_VIEW_LAYOUT, PlainApp())) @get_app_model(mode=[AppMode.ADVANCED_CHAT, AppMode.WORKFLOW]) def get(self, app_model: App, node_id: str): srv = WorkflowService() @@ -1711,7 +1711,7 @@ class DraftWorkflowTriggerRunApi(Resource): @setup_required @login_required @account_initialization_required - @rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_TEST_AND_RUN) + @rbac_permission_required(RBACCheck(RBACPermission.APP_TEST_AND_RUN, PlainApp())) @with_current_user @edit_permission_required @with_session @@ -1782,7 +1782,7 @@ class DraftWorkflowTriggerNodeApi(Resource): @setup_required @login_required @account_initialization_required - @rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_TEST_AND_RUN) + @rbac_permission_required(RBACCheck(RBACPermission.APP_TEST_AND_RUN, PlainApp())) @get_app_model(mode=[AppMode.WORKFLOW]) @with_current_user @edit_permission_required @@ -1865,7 +1865,7 @@ class DraftWorkflowTriggerRunAllApi(Resource): @account_initialization_required @with_current_user @edit_permission_required - @rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_TEST_AND_RUN) + @rbac_permission_required(RBACCheck(RBACPermission.APP_TEST_AND_RUN, PlainApp())) @with_session @get_app_model(mode=[AppMode.WORKFLOW]) @model_validate(DraftWorkflowTriggerRunAllPayload) diff --git a/api/controllers/console/app/workflow_app_log.py b/api/controllers/console/app/workflow_app_log.py index e74abd03f66..6271c1fe64a 100644 --- a/api/controllers/console/app/workflow_app_log.py +++ b/api/controllers/console/app/workflow_app_log.py @@ -5,15 +5,12 @@ from dateutil.parser import isoparse from flask_restx import Resource from pydantic import BaseModel, Field, field_validator +from controllers.common.rbac import PlainApp, RBACCheck from controllers.common.schema import query_params_from_model, register_schema_models from controllers.console import console_ns from controllers.console.app.wraps import get_app_model from controllers.console.flask_admission import console_account_admission -from controllers.console.wraps import ( - RBACPermission, - RBACResourceScope, - model_validate, -) +from controllers.console.wraps import RBACPermission, model_validate from extensions.ext_application_services import application_services from fields.base import ResponseModel from fields.end_user_fields import SimpleEndUser @@ -133,8 +130,7 @@ class WorkflowAppLogApi(Resource): console_ns.models[WorkflowAppLogPaginationResponse.__name__], ) @console_account_admission( - rbac_resource_scope=RBACResourceScope.APP, - rbac_permission=RBACPermission.APP_LOG_AND_ANNOTATION, + rbac_checks=[RBACCheck(RBACPermission.APP_LOG_AND_ANNOTATION, PlainApp())], ) @get_app_model(mode=[AppMode.WORKFLOW]) @model_validate(WorkflowAppLogQuery) diff --git a/api/controllers/console/app/workflow_draft_variable.py b/api/controllers/console/app/workflow_draft_variable.py index 6d52419f88c..32deabfae19 100644 --- a/api/controllers/console/app/workflow_draft_variable.py +++ b/api/controllers/console/app/workflow_draft_variable.py @@ -11,6 +11,7 @@ from sqlalchemy.orm import sessionmaker from controllers.common.errors import InvalidArgumentError, NotFoundError from controllers.common.fields import SimpleResultResponse +from controllers.common.rbac import PlainApp, RBACCheck from controllers.common.schema import query_params_from_model, register_response_schema_models, register_schema_models from controllers.console import console_ns from controllers.console.app.error import ( @@ -19,7 +20,6 @@ from controllers.console.app.error import ( from controllers.console.app.wraps import get_app_model from controllers.console.wraps import ( RBACPermission, - RBACResourceScope, account_initialization_required, edit_permission_required, model_validate, @@ -337,7 +337,7 @@ def _api_prerequisite[T, **P, R]( @login_required @account_initialization_required @edit_permission_required - @rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_VIEW_LAYOUT) + @rbac_permission_required(RBACCheck(RBACPermission.APP_VIEW_LAYOUT, PlainApp())) @get_app_model(mode=[AppMode.ADVANCED_CHAT, AppMode.WORKFLOW]) @with_current_user @wraps(f) @@ -359,7 +359,7 @@ class WorkflowVariableCollectionApi(Resource): ) @_api_prerequisite @marshal_with(workflow_draft_variable_list_without_value_model) - @rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_VIEW_LAYOUT) + @rbac_permission_required(RBACCheck(RBACPermission.APP_VIEW_LAYOUT, PlainApp())) @model_validate(WorkflowDraftVariableListQuery) def get(self, req_data: WorkflowDraftVariableListQuery, current_user: Account, app_model: App): """ @@ -424,7 +424,7 @@ class NodeVariableCollectionApi(Resource): @console_ns.response(200, "Node variables retrieved successfully", workflow_draft_variable_list_model) @_api_prerequisite @marshal_with(workflow_draft_variable_list_model) - @rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_VIEW_LAYOUT) + @rbac_permission_required(RBACCheck(RBACPermission.APP_VIEW_LAYOUT, PlainApp())) def get(self, current_user: Account, app_model: App, node_id: str): validate_node_id(node_id) with sessionmaker(bind=db.engine, expire_on_commit=False).begin() as session: @@ -459,7 +459,7 @@ class VariableApi(Resource): @console_ns.response(404, "Variable not found") @_api_prerequisite @marshal_with(workflow_draft_variable_model) - @rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_VIEW_LAYOUT) + @rbac_permission_required(RBACCheck(RBACPermission.APP_VIEW_LAYOUT, PlainApp())) def get(self, current_user: Account, app_model: App, variable_id: UUID): draft_var_srv = WorkflowDraftVariableService( session=db.session(), @@ -638,7 +638,7 @@ class ConversationVariableCollectionApi(Resource): @console_ns.response(404, "Draft workflow not found") @_api_prerequisite @marshal_with(workflow_draft_variable_list_model) - @rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_VIEW_LAYOUT) + @rbac_permission_required(RBACCheck(RBACPermission.APP_VIEW_LAYOUT, PlainApp())) def get(self, current_user: Account, app_model: App): # NOTE(QuantumGhost): Prefill conversation variables into the draft variables table # so their IDs can be returned to the caller. @@ -664,7 +664,7 @@ class ConversationVariableCollectionApi(Resource): @login_required @account_initialization_required @edit_permission_required - @rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_EDIT) + @rbac_permission_required(RBACCheck(RBACPermission.APP_EDIT, PlainApp())) @with_current_user @get_app_model(mode=AppMode.ADVANCED_CHAT) @model_validate(ConversationVariableUpdatePayload) @@ -697,7 +697,7 @@ class SystemVariableCollectionApi(Resource): @console_ns.response(200, "System variables retrieved successfully", workflow_draft_variable_list_model) @_api_prerequisite @marshal_with(workflow_draft_variable_list_model) - @rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_VIEW_LAYOUT) + @rbac_permission_required(RBACCheck(RBACPermission.APP_VIEW_LAYOUT, PlainApp())) def get(self, current_user: Account, app_model: App): return _get_variable_list(app_model, SYSTEM_VARIABLE_NODE_ID, current_user.id) @@ -714,7 +714,7 @@ class EnvironmentVariableCollectionApi(Resource): ) @console_ns.response(404, "Draft workflow not found") @_api_prerequisite - @rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_VIEW_LAYOUT) + @rbac_permission_required(RBACCheck(RBACPermission.APP_VIEW_LAYOUT, PlainApp())) def get(self, _current_user: Account, app_model: App): """ Get draft workflow @@ -759,7 +759,7 @@ class EnvironmentVariableCollectionApi(Resource): @login_required @account_initialization_required @edit_permission_required - @rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_EDIT) + @rbac_permission_required(RBACCheck(RBACPermission.APP_EDIT, PlainApp())) @with_current_user @get_app_model(mode=[AppMode.ADVANCED_CHAT, AppMode.WORKFLOW]) @model_validate(EnvironmentVariableUpdatePayload) diff --git a/api/controllers/console/app/workflow_node_output_inspector.py b/api/controllers/console/app/workflow_node_output_inspector.py index ea45a718a02..ae16bbd3865 100644 --- a/api/controllers/console/app/workflow_node_output_inspector.py +++ b/api/controllers/console/app/workflow_node_output_inspector.py @@ -31,12 +31,12 @@ from flask import Response from flask_restx import Resource from controllers.common.fields import EventStreamResponse +from controllers.common.rbac import PlainApp, RBACCheck from controllers.common.schema import register_response_schema_models from controllers.console import console_ns from controllers.console.app.wraps import get_app_model from controllers.console.wraps import ( RBACPermission, - RBACResourceScope, account_initialization_required, rbac_permission_required, setup_required, @@ -157,7 +157,7 @@ class WorkflowDraftRunNodeOutputsApi(Resource): @setup_required @login_required @account_initialization_required - @rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_VIEW_LAYOUT) + @rbac_permission_required(RBACCheck(RBACPermission.APP_VIEW_LAYOUT, PlainApp())) @get_app_model(mode=[AppMode.ADVANCED_CHAT, AppMode.WORKFLOW]) def get(self, app_model: App, run_id: UUID): return _serve_snapshot(app_model, run_id) @@ -181,7 +181,7 @@ class WorkflowDraftRunNodeOutputDetailApi(Resource): @setup_required @login_required @account_initialization_required - @rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_VIEW_LAYOUT) + @rbac_permission_required(RBACCheck(RBACPermission.APP_VIEW_LAYOUT, PlainApp())) @get_app_model(mode=[AppMode.ADVANCED_CHAT, AppMode.WORKFLOW]) def get(self, app_model: App, run_id: UUID, node_id: str): return _serve_node_detail(app_model, run_id, node_id) @@ -208,7 +208,7 @@ class WorkflowDraftRunNodeOutputPreviewApi(Resource): @setup_required @login_required @account_initialization_required - @rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_VIEW_LAYOUT) + @rbac_permission_required(RBACCheck(RBACPermission.APP_VIEW_LAYOUT, PlainApp())) @get_app_model(mode=[AppMode.ADVANCED_CHAT, AppMode.WORKFLOW]) def get(self, app_model: App, run_id: UUID, node_id: str, output_name: str): return _serve_output_preview(app_model, run_id, node_id, output_name) @@ -353,7 +353,7 @@ class WorkflowDraftRunNodeOutputEventsApi(Resource): @setup_required @login_required @account_initialization_required - @rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_VIEW_LAYOUT) + @rbac_permission_required(RBACCheck(RBACPermission.APP_VIEW_LAYOUT, PlainApp())) @get_app_model(mode=[AppMode.ADVANCED_CHAT, AppMode.WORKFLOW]) def get(self, app_model: App, run_id: UUID): return Response( @@ -384,7 +384,7 @@ class WorkflowPublishedRunNodeOutputsApi(Resource): @setup_required @login_required @account_initialization_required - @rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_VIEW_LAYOUT) + @rbac_permission_required(RBACCheck(RBACPermission.APP_VIEW_LAYOUT, PlainApp())) @get_app_model(mode=[AppMode.ADVANCED_CHAT, AppMode.WORKFLOW]) def get(self, app_model: App, run_id: UUID): return _serve_snapshot(app_model, run_id) @@ -408,7 +408,7 @@ class WorkflowPublishedRunNodeOutputDetailApi(Resource): @setup_required @login_required @account_initialization_required - @rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_VIEW_LAYOUT) + @rbac_permission_required(RBACCheck(RBACPermission.APP_VIEW_LAYOUT, PlainApp())) @get_app_model(mode=[AppMode.ADVANCED_CHAT, AppMode.WORKFLOW]) def get(self, app_model: App, run_id: UUID, node_id: str): return _serve_node_detail(app_model, run_id, node_id) @@ -436,7 +436,7 @@ class WorkflowPublishedRunNodeOutputPreviewApi(Resource): @setup_required @login_required @account_initialization_required - @rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_VIEW_LAYOUT) + @rbac_permission_required(RBACCheck(RBACPermission.APP_VIEW_LAYOUT, PlainApp())) @get_app_model(mode=[AppMode.ADVANCED_CHAT, AppMode.WORKFLOW]) def get(self, app_model: App, run_id: UUID, node_id: str, output_name: str): return _serve_output_preview(app_model, run_id, node_id, output_name) @@ -458,7 +458,7 @@ class WorkflowPublishedRunNodeOutputEventsApi(Resource): @setup_required @login_required @account_initialization_required - @rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_VIEW_LAYOUT) + @rbac_permission_required(RBACCheck(RBACPermission.APP_VIEW_LAYOUT, PlainApp())) @get_app_model(mode=[AppMode.ADVANCED_CHAT, AppMode.WORKFLOW]) def get(self, app_model: App, run_id: UUID): return Response( diff --git a/api/controllers/console/app/workflow_run.py b/api/controllers/console/app/workflow_run.py index 8a4b179439c..66873a9b523 100644 --- a/api/controllers/console/app/workflow_run.py +++ b/api/controllers/console/app/workflow_run.py @@ -6,13 +6,13 @@ from pydantic import BaseModel, Field, field_validator from configs import dify_config from controllers.common.errors import NotFoundError +from controllers.common.rbac import PlainApp, RBACCheck from controllers.common.schema import query_params_from_model, register_response_schema_models, register_schema_models from controllers.console import console_ns from controllers.console.app.wraps import get_app_model from controllers.console.flask_admission import console_account_admission from controllers.console.wraps import ( RBACPermission, - RBACResourceScope, model_validate, ) from extensions.ext_application_services import application_services @@ -143,8 +143,7 @@ class AdvancedChatAppWorkflowRunListApi(Resource): console_ns.models[AdvancedChatWorkflowRunPaginationResponse.__name__], ) @console_account_admission( - rbac_resource_scope=RBACResourceScope.APP, - rbac_permission=RBACPermission.APP_CREATE_AND_MANAGEMENT, + rbac_checks=[RBACCheck(RBACPermission.APP_CREATE_AND_MANAGEMENT, PlainApp())], ) @get_app_model(mode=[AppMode.ADVANCED_CHAT]) @model_validate(WorkflowRunListQuery) @@ -174,8 +173,7 @@ class AdvancedChatAppWorkflowRunCountApi(Resource): console_ns.models[WorkflowRunCountResponse.__name__], ) @console_account_admission( - rbac_resource_scope=RBACResourceScope.APP, - rbac_permission=RBACPermission.APP_CREATE_AND_MANAGEMENT, + rbac_checks=[RBACCheck(RBACPermission.APP_CREATE_AND_MANAGEMENT, PlainApp())], ) @get_app_model(mode=[AppMode.ADVANCED_CHAT]) @model_validate(WorkflowRunCountQuery) @@ -206,8 +204,7 @@ class WorkflowRunListApi(Resource): console_ns.models[WorkflowRunPaginationResponse.__name__], ) @console_account_admission( - rbac_resource_scope=RBACResourceScope.APP, - rbac_permission=RBACPermission.APP_CREATE_AND_MANAGEMENT, + rbac_checks=[RBACCheck(RBACPermission.APP_CREATE_AND_MANAGEMENT, PlainApp())], ) @get_app_model(mode=[AppMode.ADVANCED_CHAT, AppMode.WORKFLOW]) @model_validate(WorkflowRunListQuery) @@ -237,8 +234,7 @@ class WorkflowRunCountApi(Resource): console_ns.models[WorkflowRunCountResponse.__name__], ) @console_account_admission( - rbac_resource_scope=RBACResourceScope.APP, - rbac_permission=RBACPermission.APP_CREATE_AND_MANAGEMENT, + rbac_checks=[RBACCheck(RBACPermission.APP_CREATE_AND_MANAGEMENT, PlainApp())], ) @get_app_model(mode=[AppMode.ADVANCED_CHAT, AppMode.WORKFLOW]) @model_validate(WorkflowRunCountQuery) @@ -269,8 +265,7 @@ class WorkflowRunDetailApi(Resource): ) @console_ns.response(404, "Workflow run not found") @console_account_admission( - rbac_resource_scope=RBACResourceScope.APP, - rbac_permission=RBACPermission.APP_CREATE_AND_MANAGEMENT, + rbac_checks=[RBACCheck(RBACPermission.APP_CREATE_AND_MANAGEMENT, PlainApp())], ) @get_app_model(mode=[AppMode.ADVANCED_CHAT, AppMode.WORKFLOW]) def get(self, request_context: RequestContext, app_model: App, run_id: UUID): @@ -300,8 +295,7 @@ class WorkflowRunNodeExecutionListApi(Resource): ) @console_ns.response(404, "Workflow run not found") @console_account_admission( - rbac_resource_scope=RBACResourceScope.APP, - rbac_permission=RBACPermission.APP_CREATE_AND_MANAGEMENT, + rbac_checks=[RBACCheck(RBACPermission.APP_CREATE_AND_MANAGEMENT, PlainApp())], ) @get_app_model(mode=[AppMode.ADVANCED_CHAT, AppMode.WORKFLOW]) def get(self, request_context: RequestContext, app_model: App, run_id: UUID): diff --git a/api/controllers/console/app/workflow_statistic.py b/api/controllers/console/app/workflow_statistic.py index 48661472584..af5f057dccf 100644 --- a/api/controllers/console/app/workflow_statistic.py +++ b/api/controllers/console/app/workflow_statistic.py @@ -4,13 +4,13 @@ from flask_restx import Resource from pydantic import BaseModel, Field, field_validator from werkzeug.exceptions import BadRequest +from controllers.common.rbac import PlainApp, RBACCheck from controllers.common.schema import query_params_from_model, register_response_schema_models, register_schema_models from controllers.console import console_ns from controllers.console.app.wraps import get_app_model from controllers.console.flask_admission import console_account_admission from controllers.console.wraps import ( RBACPermission, - RBACResourceScope, model_validate, ) from extensions.ext_application_services import application_services @@ -106,8 +106,7 @@ class WorkflowDailyRunsStatistic(Resource): console_ns.models[WorkflowDailyRunsStatisticResponse.__name__], ) @console_account_admission( - rbac_resource_scope=RBACResourceScope.APP, - rbac_permission=RBACPermission.APP_MONITOR, + rbac_checks=[RBACCheck(RBACPermission.APP_MONITOR, PlainApp())], ) @get_app_model @model_validate(WorkflowStatisticQuery) @@ -136,8 +135,7 @@ class WorkflowDailyTerminalsStatistic(Resource): console_ns.models[WorkflowDailyTerminalsStatisticResponse.__name__], ) @console_account_admission( - rbac_resource_scope=RBACResourceScope.APP, - rbac_permission=RBACPermission.APP_MONITOR, + rbac_checks=[RBACCheck(RBACPermission.APP_MONITOR, PlainApp())], ) @get_app_model @model_validate(WorkflowStatisticQuery) @@ -166,8 +164,7 @@ class WorkflowDailyTokenCostStatistic(Resource): console_ns.models[WorkflowDailyTokenCostStatisticResponse.__name__], ) @console_account_admission( - rbac_resource_scope=RBACResourceScope.APP, - rbac_permission=RBACPermission.APP_MONITOR, + rbac_checks=[RBACCheck(RBACPermission.APP_MONITOR, PlainApp())], ) @get_app_model @model_validate(WorkflowStatisticQuery) @@ -196,8 +193,7 @@ class WorkflowAverageAppInteractionStatistic(Resource): console_ns.models[WorkflowAverageAppInteractionStatisticResponse.__name__], ) @console_account_admission( - rbac_resource_scope=RBACResourceScope.APP, - rbac_permission=RBACPermission.APP_MONITOR, + rbac_checks=[RBACCheck(RBACPermission.APP_MONITOR, PlainApp())], ) @get_app_model(mode=[AppMode.WORKFLOW]) @model_validate(WorkflowStatisticQuery) diff --git a/api/controllers/console/app/workflow_trigger.py b/api/controllers/console/app/workflow_trigger.py index 4f544db7c46..98260b3734d 100644 --- a/api/controllers/console/app/workflow_trigger.py +++ b/api/controllers/console/app/workflow_trigger.py @@ -8,6 +8,7 @@ from sqlalchemy.orm import sessionmaker from werkzeug.exceptions import NotFound from configs import dify_config +from controllers.common.rbac import PlainApp, RBACCheck from controllers.common.schema import query_params_from_model, register_schema_models from extensions.ext_database import db from fields.base import ResponseModel @@ -21,7 +22,6 @@ from .. import console_ns from ..app.wraps import get_app_model from ..wraps import ( RBACPermission, - RBACResourceScope, account_initialization_required, edit_permission_required, model_validate, @@ -100,7 +100,7 @@ class WebhookTriggerApi(Resource): @login_required @account_initialization_required @console_ns.response(200, "Success", console_ns.models[WebhookTriggerResponse.__name__]) - @rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_VIEW_LAYOUT) + @rbac_permission_required(RBACCheck(RBACPermission.APP_VIEW_LAYOUT, PlainApp())) @get_app_model(mode=AppMode.WORKFLOW) @model_validate(Parser) def get(self, req_data: Parser, app_model: App): @@ -134,7 +134,7 @@ class AppTriggersApi(Resource): @account_initialization_required @console_ns.response(200, "Success", console_ns.models[WorkflowTriggerListResponse.__name__]) @with_current_tenant_id - @rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_VIEW_LAYOUT) + @rbac_permission_required(RBACCheck(RBACPermission.APP_VIEW_LAYOUT, PlainApp())) @get_app_model(mode=AppMode.WORKFLOW) def get(self, current_tenant_id: str, app_model: App): """Get app triggers list""" @@ -171,7 +171,7 @@ class AppTriggerEnableApi(Resource): @login_required @account_initialization_required @edit_permission_required - @rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_EDIT) + @rbac_permission_required(RBACCheck(RBACPermission.APP_EDIT, PlainApp())) @console_ns.response(200, "Success", console_ns.models[WorkflowTriggerResponse.__name__]) @with_current_tenant_id @get_app_model(mode=AppMode.WORKFLOW) diff --git a/api/controllers/console/app/wraps.py b/api/controllers/console/app/wraps.py index 8e18c6af01e..2c8f66bc053 100644 --- a/api/controllers/console/app/wraps.py +++ b/api/controllers/console/app/wraps.py @@ -11,35 +11,37 @@ from functools import wraps from typing import cast, overload from sqlalchemy import select -from sqlalchemy.orm import Session -from werkzeug.exceptions import Forbidden +from sqlalchemy.orm import Session, scoped_session -from configs import dify_config from controllers.common.session import with_session -from controllers.common.wraps import RBACPermission, RBACResourceScope, _extract_resource_id, enforce_rbac_access from controllers.console.app.error import AppNotFoundError from extensions.ext_application_services import application_services from extensions.ext_database import db from libs.login import current_account_with_tenant from models import App, AppMode -from models.agent import Agent, AgentScope +from models.agent import AgentScope from services.app_service import AppService __all__ = [ - "agent_manage_required_for_agent_app", - "enforce_agent_manage_or_app_scene", "get_app_model", "get_previewable_app_model", "with_session", ] +def _is_hidden_backing_app(app_model: App, session: Session | scoped_session) -> bool: + binding = app_model.agent_app_binding_with_session(session=session, include_archived=True) + return binding is not None and binding.scope == AgentScope.WORKFLOW_ONLY + + def _load_app_model(session: Session, app_id: str) -> App | None: """Load the tenant-scoped app row with the request session owned by `with_session`.""" _, current_tenant_id = current_account_with_tenant() app_model = session.scalar( select(App).where(App.id == app_id, App.tenant_id == current_tenant_id, App.status == "normal").limit(1) ) + if app_model is not None and _is_hidden_backing_app(app_model, session): + return None return app_model @@ -49,6 +51,8 @@ def _load_app_model_from_scoped_session(app_id: str) -> App | None: app_model = db.session.scalar( select(App).where(App.id == app_id, App.tenant_id == current_tenant_id, App.status == "normal").limit(1) ) + if app_model is not None and _is_hidden_backing_app(app_model, db.session): + return None return app_model @@ -59,118 +63,6 @@ def _load_previewable_app_model(session: Session, app_id: str) -> App | None: return AppService.get_normal_app_by_id(app_id, session) -def _agent_app_binding(app_id: str) -> Agent | None: - app_model = _load_app_model_from_scoped_session(app_id) - if app_model is None: - return None - return app_model.agent_app_binding_with_session(session=db.session(), include_archived=True) - - -def _reject_hidden_agent_backing_app(path_args: dict[str, object]) -> None: - raw_app_id = path_args.get("app_id") or path_args.get("resource_id") - if raw_app_id is None: - return - binding = _agent_app_binding(str(raw_app_id)) - if binding is not None and binding.scope == AgentScope.WORKFLOW_ONLY: - raise AppNotFoundError() - - -def enforce_agent_manage_or_app_scene( - *, - tenant_id: str, - account_id: str, - scene: RBACPermission, - path_args: dict[str, object], -) -> None: - # Must run before the RBAC_ENABLED check below: a hidden workflow-only - # backing App has to stay unreachable regardless of RBAC_ENABLED. - _reject_hidden_agent_backing_app(path_args) - - if not dify_config.RBAC_ENABLED: - return - - binding = _agent_app_binding(_extract_resource_id(RBACResourceScope.APP, tenant_id, path_args)) - - if binding is not None: - if binding.scope == AgentScope.WORKFLOW_ONLY: - raise AppNotFoundError() - try: - enforce_rbac_access( - tenant_id=tenant_id, - account_id=account_id, - resource_type=RBACResourceScope.WORKSPACE, - scene=RBACPermission.AGENT_MANAGE, - resource_required=False, - ) - return - except Forbidden: - pass # not an agent.manage holder — fall through to the normal scene check - - enforce_rbac_access( - tenant_id=tenant_id, - account_id=account_id, - resource_type=RBACResourceScope.APP, - scene=scene, - path_args=path_args, - ) - - -@overload -def agent_manage_required_for_agent_app[**P, R](view: Callable[P, R]) -> Callable[P, R]: ... - - -@overload -def agent_manage_required_for_agent_app[**P, R]( - view: None = None, *, scene: RBACPermission | None = None -) -> Callable[[Callable[P, R]], Callable[P, R]]: ... - - -def agent_manage_required_for_agent_app[**P, R]( - view: Callable[P, R] | None = None, *, scene: RBACPermission | None = None -) -> Callable[P, R] | Callable[[Callable[P, R]], Callable[P, R]]: - # Must sit above get_app_model in the decorator stack — get_app_model - # deletes app_id from kwargs, and this decorator needs it. - # TODO: this is a workaround, remove this after ACL for agent app is available - def decorator(view_func: Callable[P, R]) -> Callable[P, R]: - @wraps(view_func) - def decorated(*args: P.args, **kwargs: P.kwargs) -> R: - if scene is not None: - if not dify_config.RBAC_ENABLED: - _reject_hidden_agent_backing_app(kwargs) - return view_func(*args, **kwargs) - current_user, current_tenant_id = current_account_with_tenant() - enforce_agent_manage_or_app_scene( - tenant_id=current_tenant_id, - account_id=current_user.id, - scene=scene, - path_args=kwargs, - ) - return view_func(*args, **kwargs) - - raw_app_id = kwargs.get("app_id") or kwargs.get("resource_id") - if raw_app_id is not None: - binding = _agent_app_binding(str(raw_app_id)) - if binding is not None: - if binding.scope == AgentScope.WORKFLOW_ONLY: - raise AppNotFoundError() - if dify_config.RBAC_ENABLED: - current_user, current_tenant_id = current_account_with_tenant() - enforce_rbac_access( - tenant_id=current_tenant_id, - account_id=current_user.id, - resource_type=RBACResourceScope.WORKSPACE, - scene=RBACPermission.AGENT_MANAGE, - resource_required=False, - ) - return view_func(*args, **kwargs) - - return decorated - - if view is None: - return decorator - return decorator(view) - - def _get_injected_session(args: tuple[object, ...]) -> Session | None: """Return the request session inserted by `with_session`, if this handler has been migrated.""" if len(args) < 2: diff --git a/api/controllers/console/auth/data_source_bearer_auth.py b/api/controllers/console/auth/data_source_bearer_auth.py index 808fae53b91..f42aca2997e 100644 --- a/api/controllers/console/auth/data_source_bearer_auth.py +++ b/api/controllers/console/auth/data_source_bearer_auth.py @@ -5,6 +5,7 @@ from flask_restx import Resource from pydantic import BaseModel, ConfigDict, Field from controllers.common.fields import SimpleResultResponse +from controllers.common.rbac import RBACCheck, Workspace from controllers.common.schema import register_response_schema_models, register_schema_models from controllers.console import console_ns from controllers.console.auth.error import ( @@ -14,7 +15,7 @@ from controllers.console.auth.error import ( InvalidDataSourceApiKeyAuthCredentialsRequestError, ) from controllers.console.flask_admission import console_account_admission -from controllers.console.wraps import RBACPermission, RBACResourceScope, model_validate +from controllers.console.wraps import RBACPermission, model_validate from extensions.ext_application_services import application_services from fields.base import ResponseModel from libs.helper import dump_response @@ -104,9 +105,7 @@ class ApiKeyAuthDataSourceBinding(Resource): @console_ns.expect(console_ns.models[ApiKeyAuthBindingPayload.__name__]) @console_account_admission( allowed_roles=_ADMIN_OR_OWNER_ROLES, - rbac_resource_scope=RBACResourceScope.WORKSPACE, - rbac_permission=RBACPermission.CREDENTIAL_CREATE, - rbac_resource_required=False, + rbac_checks=[RBACCheck(RBACPermission.CREDENTIAL_CREATE, Workspace())], ) @model_validate(ApiKeyAuthBindingPayload) def post(self, req_data: ApiKeyAuthBindingPayload, request_context: RequestContext): @@ -138,9 +137,7 @@ class ApiKeyAuthDataSourceBindingDelete(Resource): @console_ns.response(HTTPStatus.NO_CONTENT, "Binding deleted successfully") @console_account_admission( allowed_roles=_ADMIN_OR_OWNER_ROLES, - rbac_resource_scope=RBACResourceScope.WORKSPACE, - rbac_permission=RBACPermission.CREDENTIAL_MANAGE, - rbac_resource_required=False, + rbac_checks=[RBACCheck(RBACPermission.CREDENTIAL_MANAGE, Workspace())], ) def delete(self, request_context: RequestContext, binding_id: UUID): application_services().data_source_api_key_auth.delete_binding(request_context, str(binding_id)) diff --git a/api/controllers/console/auth/data_source_oauth.py b/api/controllers/console/auth/data_source_oauth.py index 792e698fab9..8c4147ea7fa 100644 --- a/api/controllers/console/auth/data_source_oauth.py +++ b/api/controllers/console/auth/data_source_oauth.py @@ -9,6 +9,7 @@ from pydantic import BaseModel, Field, ValidationError from configs import dify_config from controllers.common.fields import RedirectResponse +from controllers.common.rbac import RBACCheck, Workspace from controllers.common.schema import ( query_params_from_model, query_params_from_request, @@ -17,7 +18,7 @@ from controllers.common.schema import ( register_schema_models, ) from controllers.console.flask_admission import console_account_admission -from core.rbac import RBACPermission, RBACResourceScope +from core.rbac import RBACPermission from extensions.ext_application_services import application_services from fields.base import ResponseModel from libs.helper import dump_response @@ -111,9 +112,7 @@ class OAuthDataSource(Resource): @console_ns.response(HTTPStatus.FORBIDDEN, "Admin privileges required") @console_account_admission( allowed_roles=_ADMIN_OR_OWNER_ROLES, - rbac_resource_scope=RBACResourceScope.WORKSPACE, - rbac_permission=RBACPermission.CREDENTIAL_MANAGE, - rbac_resource_required=False, + rbac_checks=[RBACCheck(RBACPermission.CREDENTIAL_MANAGE, Workspace())], ) def get(self, request_context: RequestContext, provider: str): try: diff --git a/api/controllers/console/datasets/data_source.py b/api/controllers/console/datasets/data_source.py index 62a7ea84415..f5506e36850 100644 --- a/api/controllers/console/datasets/data_source.py +++ b/api/controllers/console/datasets/data_source.py @@ -12,6 +12,7 @@ from sqlalchemy.orm import Session from werkzeug.exceptions import NotFound from controllers.common.fields import SimpleResultResponse, TextContentResponse +from controllers.common.rbac import DatasetId, RBACCheck, Workspace from controllers.common.schema import query_params_from_model, register_response_schema_models, register_schema_models from controllers.common.session import with_session from core.datasource.entities.datasource_entities import DatasourceProviderType, OnlineDocumentPagesMessage @@ -34,7 +35,6 @@ from tasks.document_indexing_sync_task import document_indexing_sync_task from .. import console_ns from ..wraps import ( RBACPermission, - RBACResourceScope, account_initialization_required, is_admin_or_owner_required, model_validate, @@ -140,7 +140,7 @@ class DataSourceApi(Resource): @setup_required @login_required @is_admin_or_owner_required - @rbac_permission_required(RBACResourceScope.WORKSPACE, RBACPermission.CREDENTIAL_MANAGE, resource_required=False) + @rbac_permission_required(RBACCheck(RBACPermission.CREDENTIAL_MANAGE, Workspace())) @account_initialization_required @console_ns.response(200, "Success", console_ns.models[DataSourceIntegrateListResponse.__name__]) @with_current_tenant_id @@ -191,7 +191,7 @@ class DataSourceApi(Resource): @setup_required @login_required @is_admin_or_owner_required - @rbac_permission_required(RBACResourceScope.WORKSPACE, RBACPermission.CREDENTIAL_MANAGE, resource_required=False) + @rbac_permission_required(RBACCheck(RBACPermission.CREDENTIAL_MANAGE, Workspace())) @account_initialization_required @console_ns.response(200, "Success", console_ns.models[SimpleResultResponse.__name__]) @with_current_tenant_id @@ -420,7 +420,7 @@ class DataSourceNotionDatasetSyncApi(Resource): @login_required @account_initialization_required @console_ns.response(200, "Success", console_ns.models[SimpleResultResponse.__name__]) - @rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_CREATE_AND_MANAGEMENT) + @rbac_permission_required(RBACCheck(RBACPermission.DATASET_CREATE_AND_MANAGEMENT, DatasetId())) @with_session(write=False) def get(self, session: Session, dataset_id: UUID) -> tuple[dict[str, str], int]: dataset_id_str = str(dataset_id) @@ -440,7 +440,7 @@ class DataSourceNotionDocumentSyncApi(Resource): @login_required @account_initialization_required @console_ns.response(200, "Success", console_ns.models[SimpleResultResponse.__name__]) - @rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_CREATE_AND_MANAGEMENT) + @rbac_permission_required(RBACCheck(RBACPermission.DATASET_CREATE_AND_MANAGEMENT, DatasetId())) @with_session(write=False) def get(self, session: Session, dataset_id: UUID, document_id: UUID) -> tuple[dict[str, str], int]: dataset_id_str = str(dataset_id) diff --git a/api/controllers/console/datasets/datasets.py b/api/controllers/console/datasets/datasets.py index 9a2711f4732..b85ea3bc9eb 100644 --- a/api/controllers/console/datasets/datasets.py +++ b/api/controllers/console/datasets/datasets.py @@ -13,6 +13,7 @@ from werkzeug.exceptions import Forbidden, NotFound import services from configs import dify_config from controllers.common.fields import ApiBaseUrlResponse, SimpleResultResponse, UsageCheckResponse +from controllers.common.rbac import DatasetId, RBACCheck, Workspace from controllers.common.schema import query_params_from_model, register_response_schema_models, register_schema_models from controllers.common.session import with_session from controllers.console import console_ns @@ -21,7 +22,6 @@ from controllers.console.app.error import ProviderNotInitializeError from controllers.console.datasets.error import DatasetInUseError, DatasetNameDuplicateError, IndexingEstimateError from controllers.console.wraps import ( RBACPermission, - RBACResourceScope, account_initialization_required, cloud_edition_billing_rate_limit_check, enterprise_license_required, @@ -589,9 +589,7 @@ class DatasetListApi(Resource): @setup_required @login_required @account_initialization_required - @rbac_permission_required( - RBACResourceScope.DATASET, RBACPermission.DATASET_CREATE_AND_MANAGEMENT, resource_required=False - ) + @rbac_permission_required(RBACCheck(RBACPermission.DATASET_CREATE_AND_MANAGEMENT, Workspace())) @cloud_edition_billing_rate_limit_check("knowledge") @with_current_user @with_current_tenant_id @@ -662,7 +660,7 @@ class DatasetApi(Resource): @setup_required @login_required @account_initialization_required - @rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_READONLY) + @rbac_permission_required(RBACCheck(RBACPermission.DATASET_READONLY, DatasetId())) @with_current_user @with_current_tenant_id @with_session(write=False) @@ -731,7 +729,7 @@ class DatasetApi(Resource): @cloud_edition_billing_rate_limit_check("knowledge") @with_current_user @with_current_tenant_id - @rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_EDIT) + @rbac_permission_required(RBACCheck(RBACPermission.DATASET_EDIT, DatasetId())) @with_session @model_validate(DatasetUpdatePayload) def patch( @@ -798,7 +796,7 @@ class DatasetApi(Resource): @cloud_edition_billing_rate_limit_check("knowledge") @console_ns.response(204, "Dataset deleted successfully") @with_current_user - @rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_EDIT) + @rbac_permission_required(RBACCheck(RBACPermission.DATASET_EDIT, DatasetId())) @with_session def delete(self, session: Session, current_user: Account, dataset_id: UUID): dataset_id_str = str(dataset_id) @@ -831,7 +829,7 @@ class DatasetUseCheckApi(Resource): @account_initialization_required @with_current_user @with_current_tenant_id - @rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_READONLY) + @rbac_permission_required(RBACCheck(RBACPermission.DATASET_READONLY, DatasetId())) @with_session(write=False) def get(self, session: Session, current_tenant_id: str, current_user: Account, dataset_id: UUID): dataset = _get_accessible_dataset(dataset_id, current_tenant_id, current_user, session) @@ -853,7 +851,7 @@ class DatasetQueryApi(Resource): @login_required @account_initialization_required @with_current_user - @rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_READONLY) + @rbac_permission_required(RBACCheck(RBACPermission.DATASET_READONLY, DatasetId())) @with_session(write=False) def get(self, session: Session, current_user: Account, dataset_id: UUID): dataset_id_str = str(dataset_id) @@ -1009,7 +1007,7 @@ class DatasetRelatedAppListApi(Resource): @login_required @account_initialization_required @with_current_user - @rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_READONLY) + @rbac_permission_required(RBACCheck(RBACPermission.DATASET_READONLY, DatasetId())) @with_session(write=False) def get(self, session: Session, current_user: Account, dataset_id: UUID): dataset_id_str = str(dataset_id) @@ -1048,7 +1046,7 @@ class DatasetIndexingStatusApi(Resource): @account_initialization_required @with_current_user @with_current_tenant_id - @rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_READONLY) + @rbac_permission_required(RBACCheck(RBACPermission.DATASET_READONLY, DatasetId())) @with_session(write=False) def get(self, session: Session, current_tenant_id: str, current_user: Account, dataset_id: UUID): dataset = _get_accessible_dataset(dataset_id, current_tenant_id, current_user, session) @@ -1112,7 +1110,7 @@ class DatasetApiKeyApi(Resource): @setup_required @login_required @is_admin_or_owner_required - @rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_API_KEY_MANAGE, resource_required=False) + @rbac_permission_required(RBACCheck(RBACPermission.DATASET_API_KEY_MANAGE, Workspace())) @account_initialization_required @with_current_tenant_id @with_session(write=False) @@ -1130,7 +1128,7 @@ class DatasetApiKeyApi(Resource): @setup_required @login_required @is_admin_or_owner_required - @rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_API_KEY_MANAGE, resource_required=False) + @rbac_permission_required(RBACCheck(RBACPermission.DATASET_API_KEY_MANAGE, Workspace())) @account_initialization_required @with_current_tenant_id @with_session @@ -1191,7 +1189,7 @@ class DatasetApiDeleteApi(Resource): @setup_required @login_required @is_admin_or_owner_required - @rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_API_KEY_MANAGE, resource_required=False) + @rbac_permission_required(RBACCheck(RBACPermission.DATASET_API_KEY_MANAGE, Workspace())) @account_initialization_required @with_current_tenant_id @with_session @@ -1228,7 +1226,7 @@ class DatasetEnableApiApi(Resource): @console_ns.response(200, "Success", console_ns.models[SimpleResultResponse.__name__]) @with_current_user @with_current_tenant_id - @rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_EDIT) + @rbac_permission_required(RBACCheck(RBACPermission.DATASET_EDIT, DatasetId())) @with_session def post(self, session: Session, current_tenant_id: str, current_user: Account, dataset_id: UUID, status: str): dataset = _get_accessible_dataset(dataset_id, current_tenant_id, current_user, session) @@ -1303,7 +1301,7 @@ class DatasetErrorDocs(Resource): @account_initialization_required @with_current_user @with_current_tenant_id - @rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_READONLY) + @rbac_permission_required(RBACCheck(RBACPermission.DATASET_READONLY, DatasetId())) @with_session(write=False) def get(self, session: Session, current_tenant_id: str, current_user: Account, dataset_id: UUID): dataset = _get_accessible_dataset(dataset_id, current_tenant_id, current_user, session) @@ -1330,7 +1328,7 @@ class DatasetPermissionUserListApi(Resource): @login_required @account_initialization_required @with_current_user - @rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_READONLY) + @rbac_permission_required(RBACCheck(RBACPermission.DATASET_READONLY, DatasetId())) @with_session(write=False) def get(self, session: Session, current_user: Account, dataset_id: UUID): dataset_id_str = str(dataset_id) @@ -1363,7 +1361,7 @@ class DatasetAutoDisableLogApi(Resource): @account_initialization_required @with_current_user @with_current_tenant_id - @rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_READONLY) + @rbac_permission_required(RBACCheck(RBACPermission.DATASET_READONLY, DatasetId())) @with_session(write=False) def get(self, session: Session, current_tenant_id: str, current_user: Account, dataset_id: UUID): dataset = _get_accessible_dataset(dataset_id, current_tenant_id, current_user, session) diff --git a/api/controllers/console/datasets/datasets_document.py b/api/controllers/console/datasets/datasets_document.py index a514d913550..dd02a4011d8 100644 --- a/api/controllers/console/datasets/datasets_document.py +++ b/api/controllers/console/datasets/datasets_document.py @@ -19,10 +19,11 @@ import services from configs import dify_config from controllers.common.controller_schemas import DocumentBatchDownloadZipPayload from controllers.common.fields import SimpleResultMessageResponse, SimpleResultResponse, UrlResponse +from controllers.common.rbac import DatasetId, RBACCheck from controllers.common.schema import register_response_schema_models, register_schema_models from controllers.common.session import with_session from controllers.console import console_ns -from controllers.console.wraps import RBACPermission, RBACResourceScope, model_validate, rbac_permission_required +from controllers.console.wraps import RBACPermission, model_validate, rbac_permission_required from core.entities.knowledge_entities import IndexingEstimate from core.errors.error import ( LLMBadRequestError, @@ -405,7 +406,7 @@ class DatasetDocumentListApi(Resource): @account_initialization_required @with_current_user @with_current_tenant_id - @rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_CREATE_AND_MANAGEMENT) + @rbac_permission_required(RBACCheck(RBACPermission.DATASET_CREATE_AND_MANAGEMENT, DatasetId())) @with_session(write=False) def get(self, session: Session, current_tenant_id: str, current_user: Account, dataset_id: UUID): dataset_id_str = str(dataset_id) @@ -534,7 +535,7 @@ class DatasetDocumentListApi(Resource): @console_ns.expect(console_ns.models[KnowledgeConfig.__name__]) @console_ns.response(200, "Documents created successfully", console_ns.models[DatasetAndDocumentResponse.__name__]) @with_current_user - @rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_EDIT) + @rbac_permission_required(RBACCheck(RBACPermission.DATASET_EDIT, DatasetId())) @with_session def post(self, session: Session, current_user: Account, dataset_id: UUID): dataset_id_str = str(dataset_id) @@ -585,7 +586,7 @@ class DatasetDocumentListApi(Resource): @console_ns.response(204, "Documents deleted successfully") @with_current_user @with_current_tenant_id - @rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_EDIT) + @rbac_permission_required(RBACCheck(RBACPermission.DATASET_EDIT, DatasetId())) @with_session def delete( self, @@ -719,7 +720,7 @@ class DocumentIndexingEstimateApi(DocumentResource): @account_initialization_required @with_current_user @with_current_tenant_id - @rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_CREATE_AND_MANAGEMENT) + @rbac_permission_required(RBACCheck(RBACPermission.DATASET_CREATE_AND_MANAGEMENT, DatasetId())) @with_session def get(self, session: Session, current_tenant_id: str, current_user: Account, dataset_id: UUID, document_id: UUID): dataset_id_str = str(dataset_id) @@ -811,7 +812,7 @@ class DocumentBatchIndexingEstimateApi(DocumentResource): @account_initialization_required @with_current_user @with_current_tenant_id - @rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_CREATE_AND_MANAGEMENT) + @rbac_permission_required(RBACCheck(RBACPermission.DATASET_CREATE_AND_MANAGEMENT, DatasetId())) @with_session def get(self, session: Session, current_tenant_id: str, current_user: Account, dataset_id: UUID, batch: str): dataset_id_str = str(dataset_id) @@ -933,7 +934,7 @@ class DocumentBatchIndexingStatusApi(DocumentResource): @login_required @account_initialization_required @with_current_user - @rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_CREATE_AND_MANAGEMENT) + @rbac_permission_required(RBACCheck(RBACPermission.DATASET_CREATE_AND_MANAGEMENT, DatasetId())) @with_session(write=False) def get(self, session: Session, current_user: Account, dataset_id: UUID, batch: str): dataset_id_str = str(dataset_id) @@ -993,7 +994,7 @@ class DocumentIndexingStatusApi(DocumentResource): @account_initialization_required @with_current_user @with_current_tenant_id - @rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_CREATE_AND_MANAGEMENT) + @rbac_permission_required(RBACCheck(RBACPermission.DATASET_CREATE_AND_MANAGEMENT, DatasetId())) @with_session(write=False) def get(self, session: Session, current_tenant_id: str, current_user: Account, dataset_id: UUID, document_id: UUID): dataset_id_str = str(dataset_id) @@ -1059,7 +1060,7 @@ class DocumentApi(DocumentResource): @account_initialization_required @with_current_user @with_current_tenant_id - @rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_CREATE_AND_MANAGEMENT) + @rbac_permission_required(RBACCheck(RBACPermission.DATASET_CREATE_AND_MANAGEMENT, DatasetId())) @with_session(write=False) def get(self, session: Session, current_tenant_id: str, current_user: Account, dataset_id: UUID, document_id: UUID): dataset_id_str = str(dataset_id) @@ -1133,7 +1134,7 @@ class DocumentApi(DocumentResource): @console_ns.response(204, "Document deleted successfully") @with_current_user @with_current_tenant_id - @rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_EDIT) + @rbac_permission_required(RBACCheck(RBACPermission.DATASET_EDIT, DatasetId())) @with_session def delete( self, session: Session, current_tenant_id: str, current_user: Account, dataset_id: UUID, document_id: UUID @@ -1169,7 +1170,7 @@ class DocumentDownloadApi(DocumentResource): @cloud_edition_billing_rate_limit_check("knowledge") @with_current_user @with_current_tenant_id - @rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_DOCUMENT_DOWNLOAD) + @rbac_permission_required(RBACCheck(RBACPermission.DATASET_DOCUMENT_DOWNLOAD, DatasetId())) @with_session(write=False) def get( self, session: Session, current_tenant_id: str, current_user: Account, dataset_id: UUID, document_id: UUID @@ -1193,7 +1194,7 @@ class DocumentBatchDownloadZipApi(DocumentResource): @console_ns.expect(console_ns.models[DocumentBatchDownloadZipPayload.__name__]) @with_current_user @with_current_tenant_id - @rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_EDIT) + @rbac_permission_required(RBACCheck(RBACPermission.DATASET_EDIT, DatasetId())) @with_session(write=False) def post(self, session: Session, current_tenant_id: str, current_user: Account, dataset_id: UUID): """Stream a ZIP archive containing the requested uploaded documents.""" @@ -1245,7 +1246,7 @@ class DocumentProcessingApi(DocumentResource): @cloud_edition_billing_rate_limit_check("knowledge") @with_current_user @with_current_tenant_id - @rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_EDIT) + @rbac_permission_required(RBACCheck(RBACPermission.DATASET_EDIT, DatasetId())) @with_session def patch( self, @@ -1302,7 +1303,7 @@ class DocumentMetadataApi(DocumentResource): @account_initialization_required @with_current_user @with_current_tenant_id - @rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_EDIT) + @rbac_permission_required(RBACCheck(RBACPermission.DATASET_EDIT, DatasetId())) @with_session @model_validate(DocumentMetadataUpdatePayload) def put( @@ -1361,7 +1362,7 @@ class DocumentStatusApi(DocumentResource): @cloud_edition_billing_rate_limit_check("knowledge") @console_ns.response(200, "Success", console_ns.models[SimpleResultResponse.__name__]) @with_current_user - @rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_EDIT) + @rbac_permission_required(RBACCheck(RBACPermission.DATASET_EDIT, DatasetId())) @with_session def patch( self, @@ -1407,7 +1408,7 @@ class DocumentPauseApi(DocumentResource): @console_ns.response(204, "Document paused successfully") @with_current_user @with_current_tenant_id - @rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_EDIT) + @rbac_permission_required(RBACCheck(RBACPermission.DATASET_EDIT, DatasetId())) @with_session def patch( self, @@ -1447,7 +1448,7 @@ class DocumentRecoverApi(DocumentResource): @console_ns.response(204, "Document resumed successfully") @with_current_user @with_current_tenant_id - @rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_EDIT) + @rbac_permission_required(RBACCheck(RBACPermission.DATASET_EDIT, DatasetId())) @with_session def patch( self, @@ -1487,7 +1488,7 @@ class DocumentRetryApi(DocumentResource): @console_ns.response(204, "Documents retry started successfully") @with_current_user @with_current_tenant_id - @rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_EDIT) + @rbac_permission_required(RBACCheck(RBACPermission.DATASET_EDIT, DatasetId())) @with_session @model_validate(DocumentRetryPayload) def post( @@ -1552,7 +1553,7 @@ class DocumentRenameApi(DocumentResource): @console_ns.response(200, "Document renamed successfully", console_ns.models[DocumentResponse.__name__]) @console_ns.expect(console_ns.models[DocumentRenamePayload.__name__]) @with_current_user - @rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_EDIT) + @rbac_permission_required(RBACCheck(RBACPermission.DATASET_EDIT, DatasetId())) @with_session @model_validate(DocumentRenamePayload) def post( @@ -1587,7 +1588,7 @@ class WebsiteDocumentSyncApi(DocumentResource): @console_ns.response(200, "Success", console_ns.models[SimpleResultResponse.__name__]) @with_current_user @with_current_tenant_id - @rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_EDIT) + @rbac_permission_required(RBACCheck(RBACPermission.DATASET_EDIT, DatasetId())) @with_session def get( self, @@ -1629,7 +1630,7 @@ class DocumentPipelineExecutionLogApi(DocumentResource): @account_initialization_required @with_current_user @with_current_tenant_id - @rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_CREATE_AND_MANAGEMENT) + @rbac_permission_required(RBACCheck(RBACPermission.DATASET_CREATE_AND_MANAGEMENT, DatasetId())) @with_session(write=False) def get( self, @@ -1681,7 +1682,7 @@ class DocumentGenerateSummaryApi(Resource): @account_initialization_required @cloud_edition_billing_rate_limit_check("knowledge") @with_current_user - @rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_EDIT) + @rbac_permission_required(RBACCheck(RBACPermission.DATASET_EDIT, DatasetId())) @with_session @model_validate(GenerateSummaryPayload) def post(self, req_data: GenerateSummaryPayload, session: Session, current_user: Account, dataset_id: UUID): @@ -1782,7 +1783,7 @@ class DocumentSummaryStatusApi(DocumentResource): @login_required @account_initialization_required @with_current_user - @rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_CREATE_AND_MANAGEMENT) + @rbac_permission_required(RBACCheck(RBACPermission.DATASET_CREATE_AND_MANAGEMENT, DatasetId())) @with_session(write=False) def get(self, session: Session, current_user: Account, dataset_id: UUID, document_id: UUID): """ diff --git a/api/controllers/console/datasets/datasets_segments.py b/api/controllers/console/datasets/datasets_segments.py index 35b2256a118..fdaa47c13f5 100644 --- a/api/controllers/console/datasets/datasets_segments.py +++ b/api/controllers/console/datasets/datasets_segments.py @@ -15,6 +15,7 @@ import services from configs import dify_config from controllers.common.controller_schemas import ChildChunkCreatePayload, ChildChunkUpdatePayload from controllers.common.fields import SimpleResultResponse +from controllers.common.rbac import DatasetId, RBACCheck from controllers.common.schema import ( query_params_from_model, query_params_from_request, @@ -31,7 +32,6 @@ from controllers.console.datasets.error import ( ) from controllers.console.wraps import ( RBACPermission, - RBACResourceScope, account_initialization_required, cloud_edition_billing_knowledge_limit_check, cloud_edition_billing_rate_limit_check, @@ -191,7 +191,7 @@ class DatasetDocumentSegmentListApi(Resource): @account_initialization_required @with_current_user @with_current_tenant_id - @rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_READONLY) + @rbac_permission_required(RBACCheck(RBACPermission.DATASET_READONLY, DatasetId())) @with_session(write=False) def get(self, session: Session, current_tenant_id: str, current_user: Account, dataset_id: UUID, document_id: UUID): dataset_id_str = str(dataset_id) @@ -302,7 +302,7 @@ class DatasetDocumentSegmentListApi(Resource): @console_ns.doc(params=query_params_from_model(SegmentIdListQuery)) @console_ns.response(204, "Segments deleted successfully") @with_current_user - @rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_EDIT) + @rbac_permission_required(RBACCheck(RBACPermission.DATASET_EDIT, DatasetId())) @with_session def delete(self, session: Session, current_user: Account, dataset_id: UUID, document_id: UUID): # check dataset @@ -342,7 +342,7 @@ class DatasetDocumentSegmentApi(Resource): @console_ns.response(200, "Success", console_ns.models[SimpleResultResponse.__name__]) @with_current_user @with_current_tenant_id - @rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_EDIT) + @rbac_permission_required(RBACCheck(RBACPermission.DATASET_EDIT, DatasetId())) @with_session def patch( self, @@ -413,7 +413,7 @@ class DatasetDocumentSegmentAddApi(Resource): @console_ns.response(200, "Segment created successfully", console_ns.models[SegmentDetailResponse.__name__]) @with_current_user @with_current_tenant_id - @rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_EDIT) + @rbac_permission_required(RBACCheck(RBACPermission.DATASET_EDIT, DatasetId())) @with_session @model_validate(SegmentCreatePayload) def post( @@ -485,7 +485,7 @@ class DatasetDocumentSegmentUpdateApi(Resource): @console_ns.response(200, "Segment updated successfully", console_ns.models[SegmentDetailResponse.__name__]) @with_current_user @with_current_tenant_id - @rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_EDIT) + @rbac_permission_required(RBACCheck(RBACPermission.DATASET_EDIT, DatasetId())) @with_session @model_validate(SegmentUpdatePayload) def patch( @@ -566,7 +566,7 @@ class DatasetDocumentSegmentUpdateApi(Resource): @console_ns.response(204, "Segment deleted successfully") @with_current_user @with_current_tenant_id - @rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_EDIT) + @rbac_permission_required(RBACCheck(RBACPermission.DATASET_EDIT, DatasetId())) @with_session def delete( self, @@ -617,7 +617,7 @@ class DatasetDocumentSegmentBatchImportApi(Resource): @console_ns.expect(console_ns.models[BatchImportPayload.__name__]) @with_current_user @with_current_tenant_id - @rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_EDIT) + @rbac_permission_required(RBACCheck(RBACPermission.DATASET_EDIT, DatasetId())) @with_session @model_validate(BatchImportPayload) def post( @@ -676,7 +676,7 @@ class DatasetDocumentSegmentBatchImportApi(Resource): @setup_required @login_required @account_initialization_required - @rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_READONLY) + @rbac_permission_required(RBACCheck(RBACPermission.DATASET_READONLY, DatasetId())) def get(self, job_id=None, dataset_id: UUID | None = None, document_id: UUID | None = None): if job_id is None: raise NotFound("The job does not exist.") @@ -703,7 +703,7 @@ class ChildChunkAddApi(Resource): @console_ns.response(200, "Child chunk created successfully", console_ns.models[ChildChunkDetailResponse.__name__]) @with_current_user @with_current_tenant_id - @rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_EDIT) + @rbac_permission_required(RBACCheck(RBACPermission.DATASET_EDIT, DatasetId())) @with_session @model_validate(ChildChunkCreatePayload) def post( @@ -764,7 +764,7 @@ class ChildChunkAddApi(Resource): @login_required @account_initialization_required @with_current_tenant_id - @rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_READONLY) + @rbac_permission_required(RBACCheck(RBACPermission.DATASET_READONLY, DatasetId())) @with_session(write=False) def get(self, session: Session, current_tenant_id: str, dataset_id: UUID, document_id: UUID, segment_id: UUID): # check dataset @@ -819,7 +819,7 @@ class ChildChunkAddApi(Resource): @console_ns.expect(console_ns.models[ChildChunkBatchUpdatePayload.__name__]) @with_current_user @with_current_tenant_id - @rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_EDIT) + @rbac_permission_required(RBACCheck(RBACPermission.DATASET_EDIT, DatasetId())) @with_session @model_validate(ChildChunkBatchUpdatePayload) def patch( @@ -873,7 +873,7 @@ class ChildChunkUpdateApi(Resource): @console_ns.response(204, "Child chunk deleted successfully") @with_current_user @with_current_tenant_id - @rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_EDIT) + @rbac_permission_required(RBACCheck(RBACPermission.DATASET_EDIT, DatasetId())) @with_session def delete( self, @@ -926,7 +926,7 @@ class ChildChunkUpdateApi(Resource): @console_ns.response(200, "Child chunk updated successfully", console_ns.models[ChildChunkDetailResponse.__name__]) @with_current_user @with_current_tenant_id - @rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_EDIT) + @rbac_permission_required(RBACCheck(RBACPermission.DATASET_EDIT, DatasetId())) @with_session @model_validate(ChildChunkUpdatePayload) def patch( diff --git a/api/controllers/console/datasets/external.py b/api/controllers/console/datasets/external.py index 51a9cebb2f5..371b9d537c0 100644 --- a/api/controllers/console/datasets/external.py +++ b/api/controllers/console/datasets/external.py @@ -12,6 +12,7 @@ from werkzeug.exceptions import Forbidden, InternalServerError, NotFound import services from controllers.common.fields import UsageCountResponse +from controllers.common.rbac import DatasetId, RBACCheck, Workspace from controllers.common.schema import ( query_params_from_model, register_response_schema_models, @@ -22,7 +23,6 @@ from controllers.console import console_ns from controllers.console.datasets.error import DatasetNameDuplicateError from controllers.console.wraps import ( RBACPermission, - RBACResourceScope, account_initialization_required, edit_permission_required, model_validate, @@ -350,9 +350,7 @@ class ExternalDatasetCreateApi(Resource): @login_required @account_initialization_required @edit_permission_required - @rbac_permission_required( - RBACResourceScope.DATASET, RBACPermission.DATASET_EXTERNAL_CONNECT, resource_required=False - ) + @rbac_permission_required(RBACCheck(RBACPermission.DATASET_EXTERNAL_CONNECT, Workspace())) @with_current_user @with_current_tenant_id @with_session @@ -411,7 +409,7 @@ class ExternalKnowledgeHitTestingApi(Resource): @login_required @account_initialization_required @with_current_user - @rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_PIPELINE_TEST) + @rbac_permission_required(RBACCheck(RBACPermission.DATASET_PIPELINE_TEST, DatasetId())) @with_session @model_validate(ExternalHitTestingPayload) def post(self, req_data: ExternalHitTestingPayload, session: Session, current_user: Account, dataset_id: UUID): diff --git a/api/controllers/console/datasets/hit_testing.py b/api/controllers/console/datasets/hit_testing.py index 8018343b01f..3cc0066009f 100644 --- a/api/controllers/console/datasets/hit_testing.py +++ b/api/controllers/console/datasets/hit_testing.py @@ -5,9 +5,10 @@ from uuid import UUID from flask_restx import Resource from sqlalchemy.orm import Session +from controllers.common.rbac import DatasetId, RBACCheck from controllers.common.schema import register_response_schema_models, register_schema_models from controllers.console.app.wraps import with_session -from controllers.console.wraps import RBACPermission, RBACResourceScope, rbac_permission_required +from controllers.console.wraps import RBACPermission, rbac_permission_required from fields.hit_testing_fields import HitTestingResponse from libs.helper import dump_response from libs.login import login_required @@ -46,7 +47,7 @@ class HitTestingApi(Resource, DatasetsHitTestingBase): @cloud_edition_billing_rate_limit_check("knowledge") @with_current_tenant_id @with_current_user - @rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_PIPELINE_TEST) + @rbac_permission_required(RBACCheck(RBACPermission.DATASET_PIPELINE_TEST, DatasetId())) @with_session def post( self, session: Session, current_user: Account, current_tenant_id: str, dataset_id: UUID diff --git a/api/controllers/console/datasets/metadata.py b/api/controllers/console/datasets/metadata.py index c076d455f49..f0cec5c014d 100644 --- a/api/controllers/console/datasets/metadata.py +++ b/api/controllers/console/datasets/metadata.py @@ -8,12 +8,12 @@ from werkzeug.exceptions import Forbidden, NotFound import services from configs import dify_config from controllers.common.controller_schemas import MetadataUpdatePayload +from controllers.common.rbac import DatasetId, RBACCheck from controllers.common.schema import register_response_schema_models, register_schema_models from controllers.common.session import with_session from controllers.console import console_ns from controllers.console.wraps import ( RBACPermission, - RBACResourceScope, account_initialization_required, enterprise_license_required, model_validate, @@ -61,7 +61,7 @@ class DatasetMetadataCreateApi(Resource): @console_ns.expect(console_ns.models[MetadataArgs.__name__]) @with_current_user @with_current_tenant_id - @rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_EDIT) + @rbac_permission_required(RBACCheck(RBACPermission.DATASET_EDIT, DatasetId())) @with_session @model_validate(MetadataArgs) def post( @@ -92,7 +92,7 @@ class DatasetMetadataCreateApi(Resource): ) @with_current_user @with_current_tenant_id - @rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_CREATE_AND_MANAGEMENT) + @rbac_permission_required(RBACCheck(RBACPermission.DATASET_CREATE_AND_MANAGEMENT, DatasetId())) @with_session(write=False) def get(self, session: Session, current_tenant_id: str, current_user: Account, dataset_id: UUID): dataset_id_str = str(dataset_id) @@ -118,7 +118,7 @@ class DatasetMetadataApi(Resource): @console_ns.expect(console_ns.models[MetadataUpdatePayload.__name__]) @with_current_user @with_current_tenant_id - @rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_EDIT) + @rbac_permission_required(RBACCheck(RBACPermission.DATASET_EDIT, DatasetId())) @with_session @model_validate(MetadataUpdatePayload) def patch( @@ -149,7 +149,7 @@ class DatasetMetadataApi(Resource): @console_ns.response(204, "Metadata deleted successfully") @with_current_user @with_current_tenant_id - @rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_EDIT) + @rbac_permission_required(RBACCheck(RBACPermission.DATASET_EDIT, DatasetId())) @with_session def delete( self, @@ -195,7 +195,7 @@ class DatasetMetadataBuiltInFieldActionApi(Resource): @enterprise_license_required @console_ns.response(204, "Action completed successfully") @with_current_user - @rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_EDIT) + @rbac_permission_required(RBACCheck(RBACPermission.DATASET_EDIT, DatasetId())) @with_session def post(self, session: Session, current_user: Account, dataset_id: UUID, action: Literal["enable", "disable"]): dataset_id_str = str(dataset_id) @@ -227,7 +227,7 @@ class DocumentMetadataEditApi(Resource): @console_ns.response(404, "Dataset, document, or metadata not found") @with_current_user @with_current_tenant_id - @rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_EDIT) + @rbac_permission_required(RBACCheck(RBACPermission.DATASET_EDIT, DatasetId())) @with_session @model_validate(MetadataOperationData) def post( diff --git a/api/controllers/console/datasets/rag_pipeline/datasource_auth.py b/api/controllers/console/datasets/rag_pipeline/datasource_auth.py index 95f19e11fd8..672a8a3d83a 100644 --- a/api/controllers/console/datasets/rag_pipeline/datasource_auth.py +++ b/api/controllers/console/datasets/rag_pipeline/datasource_auth.py @@ -7,11 +7,11 @@ from werkzeug.exceptions import Forbidden, NotFound from configs import dify_config from controllers.common.fields import SimpleResultResponse +from controllers.common.rbac import RBACCheck, Workspace from controllers.common.schema import query_params_from_model, register_response_schema_models, register_schema_models from controllers.console import console_ns from controllers.console.wraps import ( RBACPermission, - RBACResourceScope, account_initialization_required, edit_permission_required, model_validate, @@ -167,7 +167,7 @@ class DatasourcePluginOAuthAuthorizationUrl(Resource): @login_required @account_initialization_required @edit_permission_required - @rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.CREDENTIAL_MANAGE, resource_required=False) + @rbac_permission_required(RBACCheck(RBACPermission.CREDENTIAL_MANAGE, Workspace())) @with_current_user @with_current_tenant_id def get(self, current_tenant_id: str, current_user: Account, provider_id: str): @@ -310,7 +310,7 @@ class DatasourceAuth(Resource): @login_required @account_initialization_required @edit_permission_required - @rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.CREDENTIAL_CREATE, resource_required=False) + @rbac_permission_required(RBACCheck(RBACPermission.CREDENTIAL_CREATE, Workspace())) @with_current_tenant_id @model_validate(DatasourceCredentialPayload) def post(self, req_data: DatasourceCredentialPayload, current_tenant_id: str, provider_id: str): @@ -337,7 +337,7 @@ class DatasourceAuth(Resource): @login_required @account_initialization_required @edit_permission_required - @rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.CREDENTIAL_MANAGE, resource_required=False) + @rbac_permission_required(RBACCheck(RBACPermission.CREDENTIAL_MANAGE, Workspace())) @with_current_user @with_current_tenant_id def get(self, current_tenant_id: str, user: Account, provider_id: str): @@ -362,7 +362,7 @@ class DatasourceAuthDeleteApi(Resource): @login_required @account_initialization_required @edit_permission_required - @rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.CREDENTIAL_MANAGE, resource_required=False) + @rbac_permission_required(RBACCheck(RBACPermission.CREDENTIAL_MANAGE, Workspace())) @with_current_tenant_id @model_validate(DatasourceCredentialDeletePayload) def post(self, req_data: DatasourceCredentialDeletePayload, current_tenant_id: str, provider_id: str): @@ -391,7 +391,7 @@ class DatasourceAuthUpdateApi(Resource): @login_required @account_initialization_required @edit_permission_required - @rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.CREDENTIAL_MANAGE, resource_required=False) + @rbac_permission_required(RBACCheck(RBACPermission.CREDENTIAL_MANAGE, Workspace())) @with_current_tenant_id @model_validate(DatasourceCredentialUpdatePayload) def post(self, req_data: DatasourceCredentialUpdatePayload, current_tenant_id: str, provider_id: str): @@ -459,7 +459,7 @@ class DatasourceAuthOauthCustomClient(Resource): @login_required @account_initialization_required @edit_permission_required - @rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.CREDENTIAL_MANAGE, resource_required=False) + @rbac_permission_required(RBACCheck(RBACPermission.CREDENTIAL_MANAGE, Workspace())) @with_current_tenant_id @model_validate(DatasourceCustomClientPayload) def post(self, req_data: DatasourceCustomClientPayload, current_tenant_id: str, provider_id: str): @@ -477,7 +477,7 @@ class DatasourceAuthOauthCustomClient(Resource): @login_required @account_initialization_required @edit_permission_required - @rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.CREDENTIAL_MANAGE, resource_required=False) + @rbac_permission_required(RBACCheck(RBACPermission.CREDENTIAL_MANAGE, Workspace())) @console_ns.response(200, "Success", console_ns.models[SimpleResultResponse.__name__]) @with_current_tenant_id def delete(self, current_tenant_id: str, provider_id: str): @@ -498,7 +498,7 @@ class DatasourceAuthDefaultApi(Resource): @login_required @account_initialization_required @edit_permission_required - @rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.CREDENTIAL_MANAGE, resource_required=False) + @rbac_permission_required(RBACCheck(RBACPermission.CREDENTIAL_MANAGE, Workspace())) @with_current_tenant_id @model_validate(DatasourceDefaultPayload) def post(self, req_data: DatasourceDefaultPayload, current_tenant_id: str, provider_id: str): @@ -520,7 +520,7 @@ class DatasourceUpdateProviderNameApi(Resource): @login_required @account_initialization_required @edit_permission_required - @rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.CREDENTIAL_MANAGE, resource_required=False) + @rbac_permission_required(RBACCheck(RBACPermission.CREDENTIAL_MANAGE, Workspace())) @with_current_tenant_id @model_validate(DatasourceUpdateNamePayload) def post(self, req_data: DatasourceUpdateNamePayload, current_tenant_id: str, provider_id: str): diff --git a/api/controllers/console/datasets/rag_pipeline/rag_pipeline.py b/api/controllers/console/datasets/rag_pipeline/rag_pipeline.py index d2a80cadf26..a3d49f149b2 100644 --- a/api/controllers/console/datasets/rag_pipeline/rag_pipeline.py +++ b/api/controllers/console/datasets/rag_pipeline/rag_pipeline.py @@ -8,6 +8,7 @@ from werkzeug.exceptions import Forbidden, NotFound from configs import dify_config from controllers.common.fields import SimpleDataResponse +from controllers.common.rbac import DatasetByPipeline, RBACCheck from controllers.common.schema import ( JsonResponseWithStatus, query_params_from_model, @@ -19,7 +20,6 @@ from controllers.console.app.wraps import with_session from controllers.console.datasets.wraps import get_rag_pipeline from controllers.console.wraps import ( RBACPermission, - RBACResourceScope, account_initialization_required, enterprise_license_required, knowledge_pipeline_publish_enabled, @@ -223,7 +223,7 @@ class PublishCustomizedPipelineTemplateApi(Resource): @knowledge_pipeline_publish_enabled @with_current_user @get_rag_pipeline - @rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_PIPELINE_RELEASE) + @rbac_permission_required(RBACCheck(RBACPermission.DATASET_PIPELINE_RELEASE, DatasetByPipeline())) @model_validate(CustomizedPipelineTemplatePayload) def post( self, diff --git a/api/controllers/console/datasets/rag_pipeline/rag_pipeline_draft_variable.py b/api/controllers/console/datasets/rag_pipeline/rag_pipeline_draft_variable.py index b272506052d..b6953cd0696 100644 --- a/api/controllers/console/datasets/rag_pipeline/rag_pipeline_draft_variable.py +++ b/api/controllers/console/datasets/rag_pipeline/rag_pipeline_draft_variable.py @@ -10,6 +10,7 @@ from pydantic import BaseModel, Field from sqlalchemy.orm import sessionmaker from controllers.common.errors import InvalidArgumentError, NotFoundError +from controllers.common.rbac import DatasetByPipeline, RBACCheck from controllers.common.schema import query_params_from_model, register_schema_models from controllers.console import console_ns from controllers.console.app.error import ( @@ -26,7 +27,6 @@ from controllers.console.app.workflow_draft_variable import ( from controllers.console.datasets.wraps import get_rag_pipeline from controllers.console.wraps import ( RBACPermission, - RBACResourceScope, account_initialization_required, edit_permission_required, model_validate, @@ -82,7 +82,7 @@ def _api_prerequisite[T, **P, R]( @account_initialization_required @get_rag_pipeline @edit_permission_required - @rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_EDIT) + @rbac_permission_required(RBACCheck(RBACPermission.DATASET_EDIT, DatasetByPipeline())) @with_current_user @wraps(f) def wrapper(self: T, current_user: Account, *args: P.args, **kwargs: P.kwargs) -> R | Response: diff --git a/api/controllers/console/datasets/rag_pipeline/rag_pipeline_import.py b/api/controllers/console/datasets/rag_pipeline/rag_pipeline_import.py index d55b5b6a366..5af74390ab1 100644 --- a/api/controllers/console/datasets/rag_pipeline/rag_pipeline_import.py +++ b/api/controllers/console/datasets/rag_pipeline/rag_pipeline_import.py @@ -3,6 +3,7 @@ from pydantic import BaseModel, Field from sqlalchemy.orm import Session from controllers.common.fields import SimpleDataResponse +from controllers.common.rbac import DatasetByPipeline, RBACCheck, Workspace from controllers.common.schema import ( JsonResponseWithStatus, query_params_from_model, @@ -13,7 +14,6 @@ from controllers.console import console_ns from controllers.console.datasets.wraps import get_rag_pipeline from controllers.console.wraps import ( RBACPermission, - RBACResourceScope, account_initialization_required, edit_permission_required, model_validate, @@ -81,9 +81,7 @@ class RagPipelineImportApi(Resource): @login_required @account_initialization_required @edit_permission_required - @rbac_permission_required( - RBACResourceScope.DATASET, RBACPermission.DATASET_CREATE_AND_MANAGEMENT, resource_required=False - ) + @rbac_permission_required(RBACCheck(RBACPermission.DATASET_CREATE_AND_MANAGEMENT, Workspace())) @with_current_user @model_validate(RagPipelineImportPayload) def post(self, req_data: RagPipelineImportPayload, current_user: Account) -> JsonResponseWithStatus: @@ -128,9 +126,7 @@ class RagPipelineImportConfirmApi(Resource): @login_required @account_initialization_required @edit_permission_required - @rbac_permission_required( - RBACResourceScope.DATASET, RBACPermission.DATASET_CREATE_AND_MANAGEMENT, resource_required=False - ) + @rbac_permission_required(RBACCheck(RBACPermission.DATASET_CREATE_AND_MANAGEMENT, Workspace())) @with_current_user def post(self, current_user: Account, import_id: str) -> JsonResponseWithStatus: with Session(db.engine, expire_on_commit=False) as session: @@ -160,7 +156,7 @@ class RagPipelineImportCheckDependenciesApi(Resource): @get_rag_pipeline @account_initialization_required @edit_permission_required - @rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_READONLY) + @rbac_permission_required(RBACCheck(RBACPermission.DATASET_READONLY, DatasetByPipeline())) def get(self, pipeline: Pipeline) -> JsonResponseWithStatus: with Session(db.engine, expire_on_commit=False) as session: import_service = RagPipelineDslService(session) @@ -178,7 +174,7 @@ class RagPipelineExportApi(Resource): @get_rag_pipeline @account_initialization_required @edit_permission_required - @rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_IMPORT_EXPORT_DSL) + @rbac_permission_required(RBACCheck(RBACPermission.DATASET_IMPORT_EXPORT_DSL, DatasetByPipeline())) @model_validate(IncludeSecretQuery) def get(self, req_data: IncludeSecretQuery, pipeline: Pipeline) -> JsonResponseWithStatus: # Add include_secret params diff --git a/api/controllers/console/datasets/rag_pipeline/rag_pipeline_workflow.py b/api/controllers/console/datasets/rag_pipeline/rag_pipeline_workflow.py index 432e83cc099..a8e98298eb5 100644 --- a/api/controllers/console/datasets/rag_pipeline/rag_pipeline_workflow.py +++ b/api/controllers/console/datasets/rag_pipeline/rag_pipeline_workflow.py @@ -13,6 +13,7 @@ import services from configs import dify_config from controllers.common.controller_schemas import DefaultBlockConfigQuery, WorkflowListQuery, WorkflowUpdatePayload from controllers.common.fields import SimpleResultResponse +from controllers.common.rbac import DatasetByPipeline, DatasetId, RBACCheck from controllers.common.schema import query_params_from_model, register_response_schema_models, register_schema_models from controllers.console import console_ns from controllers.console.app.error import ( @@ -32,7 +33,6 @@ from controllers.console.app.wraps import with_session from controllers.console.datasets.wraps import get_rag_pipeline, load_rag_pipeline from controllers.console.wraps import ( RBACPermission, - RBACResourceScope, account_initialization_required, edit_permission_required, model_validate, @@ -197,7 +197,7 @@ class DraftRagPipelineApi(Resource): @account_initialization_required @get_rag_pipeline @edit_permission_required - @rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_EDIT) + @rbac_permission_required(RBACCheck(RBACPermission.DATASET_EDIT, DatasetByPipeline())) def get(self, pipeline: Pipeline): """ Get draft rag pipeline's workflow @@ -219,7 +219,7 @@ class DraftRagPipelineApi(Resource): @with_current_user @get_rag_pipeline @edit_permission_required - @rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_EDIT) + @rbac_permission_required(RBACCheck(RBACPermission.DATASET_EDIT, DatasetByPipeline())) @console_ns.expect(console_ns.models[DraftWorkflowSyncPayload.__name__]) @console_ns.response(200, "Success", console_ns.models[RagPipelineWorkflowSyncResponse.__name__]) def post(self, current_user: Account, pipeline: Pipeline): @@ -280,7 +280,7 @@ class RagPipelineDraftRunIterationNodeApi(Resource): @with_current_user @get_rag_pipeline @edit_permission_required - @rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_EDIT) + @rbac_permission_required(RBACCheck(RBACPermission.DATASET_EDIT, DatasetByPipeline())) @model_validate(NodeRunPayload) def post(self, req_data: NodeRunPayload, current_user: Account, pipeline: Pipeline, node_id: str): """ @@ -313,7 +313,7 @@ class RagPipelineDraftRunLoopNodeApi(Resource): @login_required @account_initialization_required @edit_permission_required - @rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_EDIT) + @rbac_permission_required(RBACCheck(RBACPermission.DATASET_EDIT, DatasetByPipeline())) @with_current_user @get_rag_pipeline @model_validate(NodeRunPayload) @@ -348,7 +348,7 @@ class DraftRagPipelineRunApi(Resource): @login_required @account_initialization_required @edit_permission_required - @rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_EDIT) + @rbac_permission_required(RBACCheck(RBACPermission.DATASET_EDIT, DatasetByPipeline())) @with_current_user @with_session @model_validate(DraftWorkflowRunPayload) @@ -382,7 +382,7 @@ class PublishedRagPipelineRunApi(Resource): @login_required @account_initialization_required @edit_permission_required - @rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_EDIT) + @rbac_permission_required(RBACCheck(RBACPermission.DATASET_EDIT, DatasetByPipeline())) @with_current_user @with_session @model_validate(PublishedWorkflowRunPayload) @@ -417,7 +417,7 @@ class RagPipelinePublishedDatasourceNodeRunApi(Resource): @login_required @account_initialization_required @edit_permission_required - @rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_EDIT) + @rbac_permission_required(RBACCheck(RBACPermission.DATASET_EDIT, DatasetByPipeline())) @with_current_user @get_rag_pipeline @model_validate(DatasourceNodeRunPayload) @@ -449,7 +449,7 @@ class RagPipelineDraftDatasourceNodeRunApi(Resource): @setup_required @login_required @edit_permission_required - @rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_EDIT) + @rbac_permission_required(RBACCheck(RBACPermission.DATASET_EDIT, DatasetByPipeline())) @account_initialization_required @with_current_user @get_rag_pipeline @@ -486,7 +486,7 @@ class RagPipelineDraftNodeRunApi(Resource): @setup_required @login_required @edit_permission_required - @rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_EDIT) + @rbac_permission_required(RBACCheck(RBACPermission.DATASET_EDIT, DatasetByPipeline())) @account_initialization_required @with_current_user @get_rag_pipeline @@ -516,7 +516,7 @@ class RagPipelineTaskStopApi(Resource): @setup_required @login_required @edit_permission_required - @rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_EDIT) + @rbac_permission_required(RBACCheck(RBACPermission.DATASET_EDIT, DatasetByPipeline())) @account_initialization_required @with_current_user @get_rag_pipeline @@ -540,7 +540,7 @@ class PublishedRagPipelineApi(Resource): @login_required @account_initialization_required @edit_permission_required - @rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_EDIT) + @rbac_permission_required(RBACCheck(RBACPermission.DATASET_EDIT, DatasetByPipeline())) @get_rag_pipeline def get(self, pipeline: Pipeline): """ @@ -565,7 +565,7 @@ class PublishedRagPipelineApi(Resource): @login_required @account_initialization_required @edit_permission_required - @rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_EDIT) + @rbac_permission_required(RBACCheck(RBACPermission.DATASET_EDIT, DatasetByPipeline())) @with_current_user @get_rag_pipeline def post(self, current_user: Account, pipeline: Pipeline): @@ -600,7 +600,7 @@ class DefaultRagPipelineBlockConfigsApi(Resource): @login_required @account_initialization_required @edit_permission_required - @rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_EDIT) + @rbac_permission_required(RBACCheck(RBACPermission.DATASET_EDIT, DatasetByPipeline())) @get_rag_pipeline def get(self, pipeline: Pipeline): """ @@ -623,7 +623,7 @@ class DefaultRagPipelineBlockConfigApi(Resource): @login_required @account_initialization_required @edit_permission_required - @rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_EDIT) + @rbac_permission_required(RBACCheck(RBACPermission.DATASET_EDIT, DatasetByPipeline())) @get_rag_pipeline @model_validate(DefaultBlockConfigQuery) def get(self, req_data: DefaultBlockConfigQuery, pipeline: Pipeline, block_type: str): @@ -656,7 +656,7 @@ class PublishedAllRagPipelineApi(Resource): @login_required @account_initialization_required @edit_permission_required - @rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_EDIT) + @rbac_permission_required(RBACCheck(RBACPermission.DATASET_EDIT, DatasetByPipeline())) @with_current_user @get_rag_pipeline @model_validate(WorkflowListQuery) @@ -702,7 +702,7 @@ class RagPipelineDraftWorkflowRestoreApi(Resource): @login_required @account_initialization_required @edit_permission_required - @rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_EDIT) + @rbac_permission_required(RBACCheck(RBACPermission.DATASET_EDIT, DatasetByPipeline())) @with_current_user @get_rag_pipeline def post(self, current_user: Account, pipeline: Pipeline, workflow_id: str): @@ -737,7 +737,7 @@ class RagPipelineByIdApi(Resource): @login_required @account_initialization_required @edit_permission_required - @rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_EDIT) + @rbac_permission_required(RBACCheck(RBACPermission.DATASET_EDIT, DatasetByPipeline())) @with_current_user @get_rag_pipeline @console_ns.expect(console_ns.models[WorkflowUpdatePayload.__name__]) @@ -773,7 +773,7 @@ class RagPipelineByIdApi(Resource): @login_required @account_initialization_required @edit_permission_required - @rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_EDIT) + @rbac_permission_required(RBACCheck(RBACPermission.DATASET_EDIT, DatasetByPipeline())) @with_current_user @get_rag_pipeline def delete(self, current_user: Account, pipeline: Pipeline, workflow_id: str): @@ -816,7 +816,7 @@ class PublishedRagPipelineSecondStepApi(Resource): @account_initialization_required @get_rag_pipeline @edit_permission_required - @rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_EDIT) + @rbac_permission_required(RBACCheck(RBACPermission.DATASET_EDIT, DatasetByPipeline())) @model_validate(NodeIdQuery) def get(self, req_data: NodeIdQuery, pipeline: Pipeline): """ @@ -839,7 +839,7 @@ class PublishedRagPipelineFirstStepApi(Resource): @account_initialization_required @get_rag_pipeline @edit_permission_required - @rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_EDIT) + @rbac_permission_required(RBACCheck(RBACPermission.DATASET_EDIT, DatasetByPipeline())) @model_validate(NodeIdQuery) def get(self, req_data: NodeIdQuery, pipeline: Pipeline): """ @@ -862,7 +862,7 @@ class DraftRagPipelineFirstStepApi(Resource): @account_initialization_required @get_rag_pipeline @edit_permission_required - @rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_EDIT) + @rbac_permission_required(RBACCheck(RBACPermission.DATASET_EDIT, DatasetByPipeline())) @model_validate(NodeIdQuery) def get(self, req_data: NodeIdQuery, pipeline: Pipeline): """ @@ -885,7 +885,7 @@ class DraftRagPipelineSecondStepApi(Resource): @account_initialization_required @get_rag_pipeline @edit_permission_required - @rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_EDIT) + @rbac_permission_required(RBACCheck(RBACPermission.DATASET_EDIT, DatasetByPipeline())) @model_validate(NodeIdQuery) def get(self, req_data: NodeIdQuery, pipeline: Pipeline): """ @@ -1035,7 +1035,7 @@ class RagPipelineTransformApi(Resource): @account_initialization_required @with_current_user @with_current_tenant_id - @rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_EDIT) + @rbac_permission_required(RBACCheck(RBACPermission.DATASET_EDIT, DatasetId())) @with_session def post(self, session: Session, current_tenant_id: str, current_user: Account, dataset_id: UUID): dataset = DatasetService.get_dataset_for_tenant(str(dataset_id), current_tenant_id, session=session) @@ -1070,7 +1070,7 @@ class RagPipelineDatasourceVariableApi(Resource): @with_current_user @get_rag_pipeline @edit_permission_required - @rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_EDIT) + @rbac_permission_required(RBACCheck(RBACPermission.DATASET_EDIT, DatasetByPipeline())) @model_validate(DatasourceVariablesPayload) def post(self, req_data: DatasourceVariablesPayload, current_user: Account, pipeline: Pipeline): """ diff --git a/api/controllers/console/flask_admission.py b/api/controllers/console/flask_admission.py index b8e3cc1ce16..d9017bf1736 100644 --- a/api/controllers/console/flask_admission.py +++ b/api/controllers/console/flask_admission.py @@ -1,6 +1,6 @@ """Flask adapter for Console API admission.""" -from collections.abc import Callable +from collections.abc import Callable, Sequence from functools import wraps from typing import Concatenate @@ -8,8 +8,7 @@ from flask import Response, abort, request from werkzeug.exceptions import Forbidden from configs import dify_config -from controllers.common.wraps import enforce_rbac_access -from controllers.console.app.wraps import enforce_agent_manage_or_app_scene +from controllers.common.rbac import RBAC_CHECKS_ATTR, RBACCheck, enforce_rbac_checks from controllers.console.wraps import ( account_initialization_required, enable_change_email, @@ -17,11 +16,9 @@ from controllers.console.wraps import ( setup_required, ) from core.logging.context import get_request_id, get_trace_id -from core.rbac import RBACPermission, RBACResourceScope from enums import DeploymentEdition from libs.login import current_account_with_tenant, login_required from machinery.context import RequestContext -from machinery.errors import AdmissionConfigurationError from models.account import TenantAccountRole from services.system_feature_service import SystemFeatureService @@ -50,10 +47,7 @@ def console_account_admission[T, **P, R]( require_initialized: bool = True, require_valid_enterprise_license: bool = False, allowed_roles: frozenset[TenantAccountRole] | None = None, - rbac_resource_scope: RBACResourceScope | None = None, - rbac_permission: RBACPermission | None = None, - rbac_resource_required: bool = True, - agent_manage_fallback: bool = False, + rbac_checks: Sequence[RBACCheck] | None = None, ) -> Callable[ [Callable[Concatenate[T, RequestContext, P], R]], Callable[Concatenate[T, P], R | Response], @@ -66,40 +60,23 @@ def console_account_admission[T, **P, R]( context construction. """ - if (rbac_resource_scope is None) != (rbac_permission is None): - raise AdmissionConfigurationError("RBAC resource scope and permission must be configured together") - if agent_manage_fallback and rbac_resource_scope != RBACResourceScope.APP: - raise AdmissionConfigurationError("agent_manage_fallback requires rbac_resource_scope=RBACResourceScope.APP") - if agent_manage_fallback and not rbac_resource_required: - raise AdmissionConfigurationError("agent_manage_fallback requires rbac_resource_required=True") - def decorator( view: Callable[Concatenate[T, RequestContext, P], R], ) -> Callable[Concatenate[T, P], R | Response]: - @wraps(view) + @wraps(view, updated=()) def inject_request_context(self: T, /, *args: P.args, **kwargs: P.kwargs) -> R: account_with_tenant = current_account_with_tenant() account = account_with_tenant.account tenant_id = account_with_tenant.tenant_id if allowed_roles is not None and not dify_config.RBAC_ENABLED and account.role not in allowed_roles: raise Forbidden() - if rbac_resource_scope is not None and rbac_permission is not None: - if agent_manage_fallback: - enforce_agent_manage_or_app_scene( - tenant_id=tenant_id, - account_id=account.id, - scene=rbac_permission, - path_args=kwargs, - ) - else: - enforce_rbac_access( - tenant_id=tenant_id, - account_id=account.id, - resource_type=rbac_resource_scope, - scene=rbac_permission, - resource_required=rbac_resource_required, - path_args=kwargs, - ) + if rbac_checks is not None: + enforce_rbac_checks( + tenant_id=tenant_id, + account_id=account.id, + checks=rbac_checks, + path_args=kwargs, + ) request_context = RequestContext( account_id=account.id, active_workspace_id=tenant_id, @@ -108,6 +85,9 @@ def console_account_admission[T, **P, R]( ) return view(self, request_context, *args, **kwargs) + if rbac_checks is not None: + setattr(inject_request_context, RBAC_CHECKS_ATTR, rbac_checks) + admitted: Callable[Concatenate[T, P], R | Response] = inject_request_context if require_change_email_enabled: admitted = enable_change_email(admitted) diff --git a/api/controllers/console/snippets/snippet_workflow.py b/api/controllers/console/snippets/snippet_workflow.py index c293dc60e77..c90d75384f8 100644 --- a/api/controllers/console/snippets/snippet_workflow.py +++ b/api/controllers/console/snippets/snippet_workflow.py @@ -10,6 +10,7 @@ from werkzeug.exceptions import BadRequest, InternalServerError, NotFound from controllers.common.controller_schemas import WorkflowUpdatePayload from controllers.common.fields import GeneratedAppResponse, SimpleResultResponse +from controllers.common.rbac import RBACCheck, Workspace from controllers.common.schema import query_params_from_model, register_response_schema_models, register_schema_models from controllers.console import console_ns from controllers.console.app.error import DraftWorkflowNotExist, DraftWorkflowNotSync @@ -34,7 +35,6 @@ from controllers.console.snippets.payloads import ( ) from controllers.console.wraps import ( RBACPermission, - RBACResourceScope, account_initialization_required, edit_permission_required, model_validate, @@ -205,9 +205,7 @@ class SnippetDraftWorkflowApi(Resource): @with_current_user @get_snippet @edit_permission_required - @rbac_permission_required( - RBACResourceScope.WORKSPACE, RBACPermission.SNIPPETS_CREATE_AND_MODIFY, resource_required=False - ) + @rbac_permission_required(RBACCheck(RBACPermission.SNIPPETS_CREATE_AND_MODIFY, Workspace())) @model_validate(SnippetDraftSyncPayload) def post(self, req_data: SnippetDraftSyncPayload, current_user: Account, snippet: CustomizedSnippet): """Sync draft workflow for snippet.""" @@ -294,9 +292,7 @@ class SnippetPublishedWorkflowApi(Resource): @with_current_user @get_snippet @edit_permission_required - @rbac_permission_required( - RBACResourceScope.WORKSPACE, RBACPermission.SNIPPETS_CREATE_AND_MODIFY, resource_required=False - ) + @rbac_permission_required(RBACCheck(RBACPermission.SNIPPETS_CREATE_AND_MODIFY, Workspace())) def post(self, current_user: Account, snippet: CustomizedSnippet): """Publish snippet workflow.""" snippet_service = _snippet_service() @@ -355,9 +351,7 @@ class SnippetPublishedAllWorkflowApi(Resource): @account_initialization_required @get_snippet @edit_permission_required - @rbac_permission_required( - RBACResourceScope.WORKSPACE, RBACPermission.SNIPPETS_CREATE_AND_MODIFY, resource_required=False - ) + @rbac_permission_required(RBACCheck(RBACPermission.SNIPPETS_CREATE_AND_MODIFY, Workspace())) @model_validate(SnippetWorkflowListQuery) def get(self, req_data: SnippetWorkflowListQuery, snippet: CustomizedSnippet): """Get all published workflow versions for snippet.""" @@ -399,9 +393,7 @@ class SnippetDraftWorkflowRestoreApi(Resource): @with_current_user @get_snippet @edit_permission_required - @rbac_permission_required( - RBACResourceScope.WORKSPACE, RBACPermission.SNIPPETS_CREATE_AND_MODIFY, resource_required=False - ) + @rbac_permission_required(RBACCheck(RBACPermission.SNIPPETS_CREATE_AND_MODIFY, Workspace())) def post(self, current_user: Account, snippet: CustomizedSnippet, workflow_id: str): """Restore a published snippet workflow version into the draft workflow.""" snippet_service = _snippet_service() @@ -441,9 +433,7 @@ class SnippetWorkflowByIdApi(Resource): @with_current_user @get_snippet @edit_permission_required - @rbac_permission_required( - RBACResourceScope.WORKSPACE, RBACPermission.SNIPPETS_CREATE_AND_MODIFY, resource_required=False - ) + @rbac_permission_required(RBACCheck(RBACPermission.SNIPPETS_CREATE_AND_MODIFY, Workspace())) @model_validate(WorkflowUpdatePayload) def patch( self, @@ -487,9 +477,7 @@ class SnippetWorkflowByIdApi(Resource): @account_initialization_required @get_snippet @edit_permission_required - @rbac_permission_required( - RBACResourceScope.WORKSPACE, RBACPermission.SNIPPETS_CREATE_AND_MODIFY, resource_required=False - ) + @rbac_permission_required(RBACCheck(RBACPermission.SNIPPETS_CREATE_AND_MODIFY, Workspace())) def delete(self, snippet: CustomizedSnippet, workflow_id: str): """Delete a published snippet workflow version.""" snippet_service = _snippet_service() diff --git a/api/controllers/console/tag/tags.py b/api/controllers/console/tag/tags.py index db336df5c58..7649ada8741 100644 --- a/api/controllers/console/tag/tags.py +++ b/api/controllers/console/tag/tags.py @@ -7,13 +7,12 @@ from werkzeug.exceptions import Forbidden, NotFound from configs import dify_config from controllers.common.fields import SimpleResultResponse +from controllers.common.rbac import RBACCheck, Workspace, enforce_rbac_checks from controllers.common.schema import query_params_from_model, register_response_schema_models, register_schema_models -from controllers.common.wraps import enforce_rbac_access from controllers.console import console_ns from controllers.console.flask_admission import console_account_admission from controllers.console.wraps import ( RBACPermission, - RBACResourceScope, model_validate, ) from extensions.ext_application_services import application_services @@ -102,12 +101,10 @@ def _enforce_snippet_tag_rbac_if_needed(tag_type: TagType | str | None, context: if not dify_config.RBAC_ENABLED: return - enforce_rbac_access( + enforce_rbac_checks( tenant_id=context.active_workspace_id, account_id=context.account_id, - resource_type=RBACResourceScope.WORKSPACE, - scene=RBACPermission.SNIPPETS_CREATE_AND_MODIFY, - resource_required=False, + checks=[RBACCheck(RBACPermission.SNIPPETS_CREATE_AND_MODIFY, Workspace())], ) diff --git a/api/controllers/console/workspace/endpoint.py b/api/controllers/console/workspace/endpoint.py index ef87c6c4571..001327047e6 100644 --- a/api/controllers/console/workspace/endpoint.py +++ b/api/controllers/console/workspace/endpoint.py @@ -15,11 +15,11 @@ from flask_restx import Resource from pydantic import BaseModel, Field from controllers.common.fields import SuccessResponse +from controllers.common.rbac import RBACCheck, Workspace from controllers.common.schema import query_params_from_model, register_response_schema_models, register_schema_models from controllers.console import console_ns from controllers.console.wraps import ( RBACPermission, - RBACResourceScope, account_initialization_required, is_admin_or_owner_required, model_validate, @@ -235,7 +235,7 @@ class EndpointCollectionApi(Resource): @setup_required @login_required @is_admin_or_owner_required - @rbac_permission_required(RBACResourceScope.WORKSPACE, RBACPermission.PLUGIN_MODEL_CONFIG, resource_required=False) + @rbac_permission_required(RBACCheck(RBACPermission.PLUGIN_MODEL_CONFIG, Workspace())) @account_initialization_required @with_current_user_id @with_current_tenant_id @@ -267,7 +267,7 @@ class DeprecatedEndpointCreateApi(Resource): @setup_required @login_required @is_admin_or_owner_required - @rbac_permission_required(RBACResourceScope.WORKSPACE, RBACPermission.PLUGIN_MODEL_CONFIG, resource_required=False) + @rbac_permission_required(RBACCheck(RBACPermission.PLUGIN_MODEL_CONFIG, Workspace())) @account_initialization_required @with_current_user_id @with_current_tenant_id @@ -291,7 +291,7 @@ class EndpointListApi(Resource): @setup_required @login_required @is_admin_or_owner_required - @rbac_permission_required(RBACResourceScope.WORKSPACE, RBACPermission.PLUGIN_MODEL_CONFIG, resource_required=False) + @rbac_permission_required(RBACCheck(RBACPermission.PLUGIN_MODEL_CONFIG, Workspace())) @account_initialization_required @with_current_user_id @with_current_tenant_id @@ -321,7 +321,7 @@ class EndpointListForSinglePluginApi(Resource): @setup_required @login_required @is_admin_or_owner_required - @rbac_permission_required(RBACResourceScope.WORKSPACE, RBACPermission.PLUGIN_MODEL_CONFIG, resource_required=False) + @rbac_permission_required(RBACCheck(RBACPermission.PLUGIN_MODEL_CONFIG, Workspace())) @account_initialization_required @with_current_user_id @with_current_tenant_id @@ -355,7 +355,7 @@ class EndpointItemApi(Resource): @setup_required @login_required @is_admin_or_owner_required - @rbac_permission_required(RBACResourceScope.WORKSPACE, RBACPermission.PLUGIN_MODEL_CONFIG, resource_required=False) + @rbac_permission_required(RBACCheck(RBACPermission.PLUGIN_MODEL_CONFIG, Workspace())) @account_initialization_required @with_current_user_id @with_current_tenant_id @@ -377,7 +377,7 @@ class EndpointItemApi(Resource): @setup_required @login_required @is_admin_or_owner_required - @rbac_permission_required(RBACResourceScope.WORKSPACE, RBACPermission.PLUGIN_MODEL_CONFIG, resource_required=False) + @rbac_permission_required(RBACCheck(RBACPermission.PLUGIN_MODEL_CONFIG, Workspace())) @account_initialization_required @with_current_user_id @with_current_tenant_id @@ -410,7 +410,7 @@ class DeprecatedEndpointDeleteApi(Resource): @setup_required @login_required @is_admin_or_owner_required - @rbac_permission_required(RBACResourceScope.WORKSPACE, RBACPermission.PLUGIN_MODEL_CONFIG, resource_required=False) + @rbac_permission_required(RBACCheck(RBACPermission.PLUGIN_MODEL_CONFIG, Workspace())) @account_initialization_required @with_current_user_id @with_current_tenant_id @@ -443,7 +443,7 @@ class DeprecatedEndpointUpdateApi(Resource): @setup_required @login_required @is_admin_or_owner_required - @rbac_permission_required(RBACResourceScope.WORKSPACE, RBACPermission.PLUGIN_MODEL_CONFIG, resource_required=False) + @rbac_permission_required(RBACCheck(RBACPermission.PLUGIN_MODEL_CONFIG, Workspace())) @account_initialization_required @with_current_user_id @with_current_tenant_id @@ -468,7 +468,7 @@ class EndpointEnableApi(Resource): @setup_required @login_required @is_admin_or_owner_required - @rbac_permission_required(RBACResourceScope.WORKSPACE, RBACPermission.PLUGIN_MODEL_CONFIG, resource_required=False) + @rbac_permission_required(RBACCheck(RBACPermission.PLUGIN_MODEL_CONFIG, Workspace())) @account_initialization_required @with_current_user_id @with_current_tenant_id @@ -493,7 +493,7 @@ class EndpointDisableApi(Resource): @setup_required @login_required @is_admin_or_owner_required - @rbac_permission_required(RBACResourceScope.WORKSPACE, RBACPermission.PLUGIN_MODEL_CONFIG, resource_required=False) + @rbac_permission_required(RBACCheck(RBACPermission.PLUGIN_MODEL_CONFIG, Workspace())) @account_initialization_required @with_current_user_id @with_current_tenant_id diff --git a/api/controllers/console/workspace/model_providers.py b/api/controllers/console/workspace/model_providers.py index fe844204d27..dd67b9ec131 100644 --- a/api/controllers/console/workspace/model_providers.py +++ b/api/controllers/console/workspace/model_providers.py @@ -7,12 +7,12 @@ from pydantic import BaseModel, Field, field_validator from sqlalchemy.orm import Session from controllers.common.fields import SimpleResultResponse, ValidationResultResponse +from controllers.common.rbac import RBACCheck, Workspace from controllers.common.schema import query_params_from_model, register_response_schema_models, register_schema_models from controllers.common.session import with_session from controllers.console import console_ns from controllers.console.wraps import ( RBACPermission, - RBACResourceScope, account_initialization_required, is_admin_or_owner_required, model_validate, @@ -208,7 +208,7 @@ class ModelProviderCredentialApi(Resource): @setup_required @login_required @is_admin_or_owner_required - @rbac_permission_required(RBACResourceScope.WORKSPACE, RBACPermission.CREDENTIAL_MANAGE, resource_required=False) + @rbac_permission_required(RBACCheck(RBACPermission.CREDENTIAL_MANAGE, Workspace())) @account_initialization_required @with_current_tenant_id @model_validate(ParserCredentialId) @@ -226,7 +226,7 @@ class ModelProviderCredentialApi(Resource): @setup_required @login_required @is_admin_or_owner_required - @rbac_permission_required(RBACResourceScope.WORKSPACE, RBACPermission.CREDENTIAL_CREATE, resource_required=False) + @rbac_permission_required(RBACCheck(RBACPermission.CREDENTIAL_CREATE, Workspace())) @account_initialization_required @with_current_tenant_id @model_validate(ParserCredentialCreate) @@ -250,7 +250,7 @@ class ModelProviderCredentialApi(Resource): @setup_required @login_required @is_admin_or_owner_required - @rbac_permission_required(RBACResourceScope.WORKSPACE, RBACPermission.CREDENTIAL_MANAGE, resource_required=False) + @rbac_permission_required(RBACCheck(RBACPermission.CREDENTIAL_MANAGE, Workspace())) @account_initialization_required @with_current_tenant_id @model_validate(ParserCredentialUpdate) @@ -275,7 +275,7 @@ class ModelProviderCredentialApi(Resource): @setup_required @login_required @is_admin_or_owner_required - @rbac_permission_required(RBACResourceScope.WORKSPACE, RBACPermission.CREDENTIAL_MANAGE, resource_required=False) + @rbac_permission_required(RBACCheck(RBACPermission.CREDENTIAL_MANAGE, Workspace())) @account_initialization_required @with_current_tenant_id @model_validate(ParserCredentialDelete) @@ -295,7 +295,7 @@ class ModelProviderCredentialSwitchApi(Resource): @setup_required @login_required @is_admin_or_owner_required - @rbac_permission_required(RBACResourceScope.WORKSPACE, RBACPermission.CREDENTIAL_USE, resource_required=False) + @rbac_permission_required(RBACCheck(RBACPermission.CREDENTIAL_USE, Workspace())) @account_initialization_required @with_current_tenant_id @model_validate(ParserCredentialSwitch) @@ -372,7 +372,7 @@ class PreferredProviderTypeUpdateApi(Resource): @setup_required @login_required @is_admin_or_owner_required - @rbac_permission_required(RBACResourceScope.WORKSPACE, RBACPermission.CREDENTIAL_USE, resource_required=False) + @rbac_permission_required(RBACCheck(RBACPermission.CREDENTIAL_USE, Workspace())) @account_initialization_required @with_current_tenant_id @model_validate(ParserPreferredProviderType) diff --git a/api/controllers/console/workspace/models.py b/api/controllers/console/workspace/models.py index 02b5a68f9c8..23b38bca57f 100644 --- a/api/controllers/console/workspace/models.py +++ b/api/controllers/console/workspace/models.py @@ -5,6 +5,7 @@ from flask_restx import Resource from pydantic import BaseModel, Field, field_validator from controllers.common.fields import SimpleResultResponse, ValidationResultResponse +from controllers.common.rbac import RBACCheck, Workspace from controllers.common.schema import ( query_params_from_model, register_enum_models, @@ -14,7 +15,6 @@ from controllers.common.schema import ( from controllers.console import console_ns from controllers.console.wraps import ( RBACPermission, - RBACResourceScope, account_initialization_required, is_admin_or_owner_required, model_validate, @@ -227,7 +227,7 @@ class DefaultModelApi(Resource): @setup_required @login_required @is_admin_or_owner_required - @rbac_permission_required(RBACResourceScope.WORKSPACE, RBACPermission.PLUGIN_PREFERENCES, resource_required=False) + @rbac_permission_required(RBACCheck(RBACPermission.PLUGIN_PREFERENCES, Workspace())) @account_initialization_required @with_current_tenant_id @model_validate(ParserPostDefault) @@ -276,7 +276,7 @@ class ModelProviderModelApi(Resource): @setup_required @login_required @is_admin_or_owner_required - @rbac_permission_required(RBACResourceScope.WORKSPACE, RBACPermission.PLUGIN_PREFERENCES, resource_required=False) + @rbac_permission_required(RBACCheck(RBACPermission.PLUGIN_PREFERENCES, Workspace())) @account_initialization_required @with_current_tenant_id @model_validate(ParserPostModels) @@ -325,7 +325,7 @@ class ModelProviderModelApi(Resource): @setup_required @login_required @is_admin_or_owner_required - @rbac_permission_required(RBACResourceScope.WORKSPACE, RBACPermission.PLUGIN_PREFERENCES, resource_required=False) + @rbac_permission_required(RBACCheck(RBACPermission.PLUGIN_PREFERENCES, Workspace())) @account_initialization_required @with_current_tenant_id @model_validate(ParserDeleteModels) @@ -350,7 +350,7 @@ class ModelProviderModelCredentialApi(Resource): @setup_required @login_required @is_admin_or_owner_required - @rbac_permission_required(RBACResourceScope.WORKSPACE, RBACPermission.CREDENTIAL_MANAGE, resource_required=False) + @rbac_permission_required(RBACCheck(RBACPermission.CREDENTIAL_MANAGE, Workspace())) @account_initialization_required @with_current_user @with_current_tenant_id @@ -413,7 +413,7 @@ class ModelProviderModelCredentialApi(Resource): @setup_required @login_required @is_admin_or_owner_required - @rbac_permission_required(RBACResourceScope.WORKSPACE, RBACPermission.CREDENTIAL_CREATE, resource_required=False) + @rbac_permission_required(RBACCheck(RBACPermission.CREDENTIAL_CREATE, Workspace())) @account_initialization_required @with_current_tenant_id @model_validate(ParserCreateCredential) @@ -446,7 +446,7 @@ class ModelProviderModelCredentialApi(Resource): @setup_required @login_required @is_admin_or_owner_required - @rbac_permission_required(RBACResourceScope.WORKSPACE, RBACPermission.CREDENTIAL_MANAGE, resource_required=False) + @rbac_permission_required(RBACCheck(RBACPermission.CREDENTIAL_MANAGE, Workspace())) @account_initialization_required @with_current_tenant_id @model_validate(ParserUpdateCredential) @@ -474,7 +474,7 @@ class ModelProviderModelCredentialApi(Resource): @setup_required @login_required @is_admin_or_owner_required - @rbac_permission_required(RBACResourceScope.WORKSPACE, RBACPermission.CREDENTIAL_MANAGE, resource_required=False) + @rbac_permission_required(RBACCheck(RBACPermission.CREDENTIAL_MANAGE, Workspace())) @account_initialization_required @with_current_tenant_id @model_validate(ParserDeleteCredential) @@ -499,7 +499,7 @@ class ModelProviderModelCredentialSwitchApi(Resource): @setup_required @login_required @is_admin_or_owner_required - @rbac_permission_required(RBACResourceScope.WORKSPACE, RBACPermission.CREDENTIAL_USE, resource_required=False) + @rbac_permission_required(RBACCheck(RBACPermission.CREDENTIAL_USE, Workspace())) @account_initialization_required @with_current_tenant_id @model_validate(ParserSwitch) @@ -526,7 +526,7 @@ class ModelProviderModelEnableApi(Resource): @login_required @account_initialization_required @with_current_tenant_id - @rbac_permission_required(RBACResourceScope.WORKSPACE, RBACPermission.PLUGIN_PREFERENCES, resource_required=False) + @rbac_permission_required(RBACCheck(RBACPermission.PLUGIN_PREFERENCES, Workspace())) @model_validate(ParserDeleteModels) def patch(self, req_data: ParserDeleteModels, tenant_id: str, provider: str): @@ -548,7 +548,7 @@ class ModelProviderModelDisableApi(Resource): @login_required @account_initialization_required @with_current_tenant_id - @rbac_permission_required(RBACResourceScope.WORKSPACE, RBACPermission.PLUGIN_PREFERENCES, resource_required=False) + @rbac_permission_required(RBACCheck(RBACPermission.PLUGIN_PREFERENCES, Workspace())) @model_validate(ParserDeleteModels) def patch(self, req_data: ParserDeleteModels, tenant_id: str, provider: str): diff --git a/api/controllers/console/workspace/plugin.py b/api/controllers/console/workspace/plugin.py index 236d6ac1f24..2f35e42d334 100644 --- a/api/controllers/console/workspace/plugin.py +++ b/api/controllers/console/workspace/plugin.py @@ -11,6 +11,7 @@ from werkzeug.exceptions import Forbidden from configs import dify_config from controllers.common.fields import BinaryFileResponse, SuccessResponse +from controllers.common.rbac import RBACCheck, Workspace from controllers.common.schema import ( query_params_from_model, register_enum_models, @@ -21,7 +22,6 @@ from controllers.console import console_ns from controllers.console.workspace import plugin_permission_required from controllers.console.wraps import ( RBACPermission, - RBACResourceScope, account_initialization_required, is_admin_or_owner_required, model_validate, @@ -570,7 +570,7 @@ class PluginDebuggingKeyApi(Resource): @setup_required @login_required @account_initialization_required - @rbac_permission_required(RBACResourceScope.WORKSPACE, RBACPermission.PLUGIN_DEBUG, resource_required=False) + @rbac_permission_required(RBACCheck(RBACPermission.PLUGIN_DEBUG, Workspace())) @plugin_permission_required(debug_required=True) @with_current_tenant_id def get(self, tenant_id: str): @@ -750,7 +750,7 @@ class PluginUploadFromPkgApi(Resource): @setup_required @login_required @account_initialization_required - @rbac_permission_required(RBACResourceScope.WORKSPACE, RBACPermission.PLUGIN_INSTALL, resource_required=False) + @rbac_permission_required(RBACCheck(RBACPermission.PLUGIN_INSTALL, Workspace())) @plugin_permission_required(install_required=True) @with_current_tenant_id def post(self, tenant_id: str): @@ -771,7 +771,7 @@ class PluginUploadFromGithubApi(Resource): @setup_required @login_required @account_initialization_required - @rbac_permission_required(RBACResourceScope.WORKSPACE, RBACPermission.PLUGIN_INSTALL, resource_required=False) + @rbac_permission_required(RBACCheck(RBACPermission.PLUGIN_INSTALL, Workspace())) @plugin_permission_required(install_required=True) @with_current_tenant_id @model_validate(ParserGithubUpload) @@ -793,7 +793,7 @@ class PluginUploadFromBundleApi(Resource): @setup_required @login_required @account_initialization_required - @rbac_permission_required(RBACResourceScope.WORKSPACE, RBACPermission.PLUGIN_INSTALL, resource_required=False) + @rbac_permission_required(RBACCheck(RBACPermission.PLUGIN_INSTALL, Workspace())) @plugin_permission_required(install_required=True) @with_current_tenant_id def post(self, tenant_id: str): @@ -814,7 +814,7 @@ class PluginInstallFromPkgApi(Resource): @setup_required @login_required @account_initialization_required - @rbac_permission_required(RBACResourceScope.WORKSPACE, RBACPermission.PLUGIN_INSTALL, resource_required=False) + @rbac_permission_required(RBACCheck(RBACPermission.PLUGIN_INSTALL, Workspace())) @plugin_permission_required(install_required=True) @with_current_tenant_id @model_validate(ParserPluginIdentifiers) @@ -835,7 +835,7 @@ class PluginInstallFromGithubApi(Resource): @setup_required @login_required @account_initialization_required - @rbac_permission_required(RBACResourceScope.WORKSPACE, RBACPermission.PLUGIN_INSTALL, resource_required=False) + @rbac_permission_required(RBACCheck(RBACPermission.PLUGIN_INSTALL, Workspace())) @plugin_permission_required(install_required=True) @with_current_tenant_id @model_validate(ParserGithubInstall) @@ -862,7 +862,7 @@ class PluginInstallFromMarketplaceApi(Resource): @setup_required @login_required @account_initialization_required - @rbac_permission_required(RBACResourceScope.WORKSPACE, RBACPermission.PLUGIN_INSTALL, resource_required=False) + @rbac_permission_required(RBACCheck(RBACPermission.PLUGIN_INSTALL, Workspace())) @plugin_permission_required(install_required=True) @with_current_tenant_id @model_validate(ParserPluginIdentifiers) @@ -883,7 +883,7 @@ class PluginFetchMarketplacePkgApi(Resource): @setup_required @login_required @account_initialization_required - @rbac_permission_required(RBACResourceScope.WORKSPACE, RBACPermission.PLUGIN_INSTALL, resource_required=False) + @rbac_permission_required(RBACCheck(RBACPermission.PLUGIN_INSTALL, Workspace())) @plugin_permission_required(install_required=True) @with_current_tenant_id @model_validate(ParserPluginIdentifierQuery) @@ -909,7 +909,7 @@ class PluginFetchManifestApi(Resource): @setup_required @login_required @account_initialization_required - @rbac_permission_required(RBACResourceScope.WORKSPACE, RBACPermission.PLUGIN_INSTALL, resource_required=False) + @rbac_permission_required(RBACCheck(RBACPermission.PLUGIN_INSTALL, Workspace())) @plugin_permission_required(install_required=True) @with_current_tenant_id @model_validate(ParserPluginIdentifierQuery) @@ -1014,7 +1014,7 @@ class PluginUpgradeFromMarketplaceApi(Resource): @setup_required @login_required @account_initialization_required - @rbac_permission_required(RBACResourceScope.WORKSPACE, RBACPermission.PLUGIN_MODEL_CONFIG, resource_required=False) + @rbac_permission_required(RBACCheck(RBACPermission.PLUGIN_MODEL_CONFIG, Workspace())) @plugin_permission_required(install_required=True) @with_current_tenant_id @model_validate(ParserMarketplaceUpgrade) @@ -1037,7 +1037,7 @@ class PluginUpgradeFromGithubApi(Resource): @setup_required @login_required @account_initialization_required - @rbac_permission_required(RBACResourceScope.WORKSPACE, RBACPermission.PLUGIN_MODEL_CONFIG, resource_required=False) + @rbac_permission_required(RBACCheck(RBACPermission.PLUGIN_MODEL_CONFIG, Workspace())) @plugin_permission_required(install_required=True) @with_current_tenant_id @model_validate(ParserGithubUpgrade) @@ -1065,7 +1065,7 @@ class PluginUninstallApi(Resource): @setup_required @login_required @account_initialization_required - @rbac_permission_required(RBACResourceScope.WORKSPACE, RBACPermission.PLUGIN_DELETE, resource_required=False) + @rbac_permission_required(RBACCheck(RBACPermission.PLUGIN_DELETE, Workspace())) @plugin_permission_required(install_required=True) @with_current_tenant_id @model_validate(ParserUninstall) @@ -1138,7 +1138,7 @@ class PluginFetchDynamicSelectOptionsApi(Resource): @setup_required @login_required @is_admin_or_owner_required - @rbac_permission_required(RBACResourceScope.WORKSPACE, RBACPermission.PLUGIN_MODEL_CONFIG, resource_required=False) + @rbac_permission_required(RBACCheck(RBACPermission.PLUGIN_MODEL_CONFIG, Workspace())) @account_initialization_required @with_current_user @with_current_tenant_id @@ -1169,7 +1169,7 @@ class PluginFetchDynamicSelectOptionsWithCredentialsApi(Resource): @setup_required @login_required @is_admin_or_owner_required - @rbac_permission_required(RBACResourceScope.WORKSPACE, RBACPermission.CREDENTIAL_MANAGE, resource_required=False) + @rbac_permission_required(RBACCheck(RBACPermission.CREDENTIAL_MANAGE, Workspace())) @account_initialization_required @with_current_user @with_current_tenant_id @@ -1201,7 +1201,7 @@ class PluginChangeAutoUpgradeApi(Resource): @setup_required @login_required @account_initialization_required - @rbac_permission_required(RBACResourceScope.WORKSPACE, RBACPermission.PLUGIN_PREFERENCES, resource_required=False) + @rbac_permission_required(RBACCheck(RBACPermission.PLUGIN_PREFERENCES, Workspace())) @with_current_user @with_current_tenant_id @model_validate(ParserAutoUpgradeChange) @@ -1256,7 +1256,7 @@ class PluginAutoUpgradeExcludePluginApi(Resource): @setup_required @login_required @account_initialization_required - @rbac_permission_required(RBACResourceScope.WORKSPACE, RBACPermission.PLUGIN_PREFERENCES, resource_required=False) + @rbac_permission_required(RBACCheck(RBACPermission.PLUGIN_PREFERENCES, Workspace())) @with_current_tenant_id @model_validate(ParserExcludePlugin) def post(self, req_data: ParserExcludePlugin, tenant_id: str): diff --git a/api/controllers/console/workspace/rbac.py b/api/controllers/console/workspace/rbac.py index 96b882871bf..9a036af6122 100644 --- a/api/controllers/console/workspace/rbac.py +++ b/api/controllers/console/workspace/rbac.py @@ -1,5 +1,7 @@ from __future__ import annotations +from collections.abc import Callable, Mapping +from dataclasses import dataclass from typing import Any, Literal from flask import request @@ -9,9 +11,10 @@ from sqlalchemy import select from werkzeug.exceptions import NotFound from configs import dify_config +from controllers.common.rbac import RBACCheck, Workspace from controllers.common.schema import query_params_from_model, register_response_schema_models, register_schema_models from controllers.console import console_ns -from controllers.console.wraps import RBACPermission, RBACResourceScope, model_validate, rbac_permission_required +from controllers.console.wraps import RBACPermission, model_validate, rbac_permission_required from core.db.session_factory import session_factory from enums import DeploymentEdition from extensions.ext_database import db @@ -50,6 +53,7 @@ register_response_schema_models( svc.MyPermissionsResponse, svc.AppAccessMatrix, svc.DatasetAccessMatrix, + svc.AgentAccessMatrix, svc.WorkspaceAccessMatrix, svc.ResourceWhitelist, svc.ResourceWhitelistConfig, @@ -282,24 +286,6 @@ class RBACWorkspaceCatalogApi(Resource): return _dump(svc.RBACService.Catalog.workspace(tenant_id, account_id)) -@console_ns.route("/workspaces/current/rbac/role-permissions/catalog/app") -class RBACAppCatalogApi(Resource): - @login_required - @console_ns.response(200, "Success", console_ns.models[svc.PermissionCatalogResponse.__name__]) - def get(self): - tenant_id, account_id = _current_ids() - return _dump(svc.RBACService.Catalog.app(tenant_id, account_id)) - - -@console_ns.route("/workspaces/current/rbac/role-permissions/catalog/dataset") -class RBACDatasetCatalogApi(Resource): - @login_required - @console_ns.response(200, "Success", console_ns.models[svc.PermissionCatalogResponse.__name__]) - def get(self): - tenant_id, account_id = _current_ids() - return _dump(svc.RBACService.Catalog.dataset(tenant_id, account_id)) - - # --------------------------------------------------------------------------- # Roles. # --------------------------------------------------------------------------- @@ -323,9 +309,7 @@ class _RoleUpsertRequest(BaseModel): @console_ns.route("/workspaces/current/rbac/roles") class RBACRolesApi(Resource): @login_required - @rbac_permission_required( - RBACResourceScope.WORKSPACE, RBACPermission.WORKSPACE_ROLE_MANAGE, resource_required=False - ) + @rbac_permission_required(RBACCheck(RBACPermission.WORKSPACE_ROLE_MANAGE, Workspace())) @console_ns.response(200, "Success", console_ns.models[_RBACRoleList.__name__]) @model_validate(_RolesListQuery) def get(self, req_data: _RolesListQuery): @@ -349,9 +333,7 @@ class RBACRolesApi(Resource): return _dump(result) @login_required - @rbac_permission_required( - RBACResourceScope.WORKSPACE, RBACPermission.WORKSPACE_ROLE_MANAGE, resource_required=False - ) + @rbac_permission_required(RBACCheck(RBACPermission.WORKSPACE_ROLE_MANAGE, Workspace())) @console_ns.response(201, "Role created", console_ns.models[svc.RBACRole.__name__]) def post(self): tenant_id, account_id = _current_ids() @@ -363,9 +345,7 @@ class RBACRolesApi(Resource): @console_ns.route("/workspaces/current/rbac/roles/") class RBACRoleItemApi(Resource): @login_required - @rbac_permission_required( - RBACResourceScope.WORKSPACE, RBACPermission.WORKSPACE_ROLE_MANAGE, resource_required=False - ) + @rbac_permission_required(RBACCheck(RBACPermission.WORKSPACE_ROLE_MANAGE, Workspace())) @console_ns.response(200, "Success", console_ns.models[svc.RBACRole.__name__]) def get(self, role_id): tenant_id, account_id = _current_ids() @@ -379,9 +359,7 @@ class RBACRoleItemApi(Resource): ) @login_required - @rbac_permission_required( - RBACResourceScope.WORKSPACE, RBACPermission.WORKSPACE_ROLE_MANAGE, resource_required=False - ) + @rbac_permission_required(RBACCheck(RBACPermission.WORKSPACE_ROLE_MANAGE, Workspace())) @console_ns.response(200, "Success", console_ns.models[svc.RBACRole.__name__]) def put(self, role_id): tenant_id, account_id = _current_ids() @@ -390,9 +368,7 @@ class RBACRoleItemApi(Resource): return _dump(role) @login_required - @rbac_permission_required( - RBACResourceScope.WORKSPACE, RBACPermission.WORKSPACE_ROLE_MANAGE, resource_required=False - ) + @rbac_permission_required(RBACCheck(RBACPermission.WORKSPACE_ROLE_MANAGE, Workspace())) @console_ns.response(200, "Success", console_ns.models[svc.RBACRole.__name__]) def delete(self, role_id): tenant_id, account_id = _current_ids() @@ -403,9 +379,7 @@ class RBACRoleItemApi(Resource): @console_ns.route("/workspaces/current/rbac/roles//copy") class RBACRoleCopyApi(Resource): @login_required - @rbac_permission_required( - RBACResourceScope.WORKSPACE, RBACPermission.WORKSPACE_ROLE_MANAGE, resource_required=False - ) + @rbac_permission_required(RBACCheck(RBACPermission.WORKSPACE_ROLE_MANAGE, Workspace())) @console_ns.response(201, "Role copied", console_ns.models[svc.RBACRole.__name__]) def post(self, role_id): tenant_id, account_id = _current_ids() @@ -430,9 +404,7 @@ class _AccessPolicyUpdateRequest(BaseModel): @console_ns.route("/workspaces/current/rbac/access-policies") class RBACAccessPoliciesApi(Resource): @login_required - @rbac_permission_required( - RBACResourceScope.WORKSPACE, RBACPermission.WORKSPACE_ROLE_MANAGE, resource_required=False - ) + @rbac_permission_required(RBACCheck(RBACPermission.WORKSPACE_ROLE_MANAGE, Workspace())) @console_ns.response(200, "Success", console_ns.models[_AccessPolicyList.__name__]) def get(self): tenant_id, account_id = _current_ids() @@ -449,9 +421,7 @@ class RBACAccessPoliciesApi(Resource): ) @login_required - @rbac_permission_required( - RBACResourceScope.WORKSPACE, RBACPermission.WORKSPACE_ROLE_MANAGE, resource_required=False - ) + @rbac_permission_required(RBACCheck(RBACPermission.WORKSPACE_ROLE_MANAGE, Workspace())) @console_ns.response(201, "Policy created", console_ns.models[svc.AccessPolicy.__name__]) def post(self): tenant_id, account_id = _current_ids() @@ -472,18 +442,14 @@ class RBACAccessPoliciesApi(Resource): @console_ns.route("/workspaces/current/rbac/access-policies/") class RBACAccessPolicyItemApi(Resource): @login_required - @rbac_permission_required( - RBACResourceScope.WORKSPACE, RBACPermission.WORKSPACE_ROLE_MANAGE, resource_required=False - ) + @rbac_permission_required(RBACCheck(RBACPermission.WORKSPACE_ROLE_MANAGE, Workspace())) @console_ns.response(200, "Success", console_ns.models[svc.AccessPolicy.__name__]) def get(self, policy_id): tenant_id, account_id = _current_ids() return _dump(svc.RBACService.AccessPolicies.get(tenant_id, account_id, str(policy_id))) @login_required - @rbac_permission_required( - RBACResourceScope.WORKSPACE, RBACPermission.WORKSPACE_ROLE_MANAGE, resource_required=False - ) + @rbac_permission_required(RBACCheck(RBACPermission.WORKSPACE_ROLE_MANAGE, Workspace())) @console_ns.response(200, "Success", console_ns.models[svc.AccessPolicy.__name__]) def put(self, policy_id): tenant_id, account_id = _current_ids() @@ -501,9 +467,7 @@ class RBACAccessPolicyItemApi(Resource): return _dump(policy) @login_required - @rbac_permission_required( - RBACResourceScope.WORKSPACE, RBACPermission.WORKSPACE_ROLE_MANAGE, resource_required=False - ) + @rbac_permission_required(RBACCheck(RBACPermission.WORKSPACE_ROLE_MANAGE, Workspace())) @console_ns.response(200, "Success", console_ns.models[svc.AccessPolicy.__name__]) def delete(self, policy_id): tenant_id, account_id = _current_ids() @@ -514,9 +478,7 @@ class RBACAccessPolicyItemApi(Resource): @console_ns.route("/workspaces/current/rbac/access-policies//copy") class RBACAccessPolicyCopyApi(Resource): @login_required - @rbac_permission_required( - RBACResourceScope.WORKSPACE, RBACPermission.WORKSPACE_ROLE_MANAGE, resource_required=False - ) + @rbac_permission_required(RBACCheck(RBACPermission.WORKSPACE_ROLE_MANAGE, Workspace())) @console_ns.response(201, "Policy copied", console_ns.models[svc.AccessPolicy.__name__]) def post(self, policy_id): tenant_id, account_id = _current_ids() @@ -527,9 +489,7 @@ class RBACAccessPolicyCopyApi(Resource): @console_ns.route("/workspaces/current/rbac/access-policy-bindings//lock") class RBACAccessPolicyBindingLockApi(Resource): @login_required - @rbac_permission_required( - RBACResourceScope.WORKSPACE, RBACPermission.WORKSPACE_ROLE_MANAGE, resource_required=False - ) + @rbac_permission_required(RBACCheck(RBACPermission.WORKSPACE_ROLE_MANAGE, Workspace())) @console_ns.response(200, "Success", console_ns.models[svc.AccessPolicyBindingState.__name__]) def put(self, binding_id): tenant_id, account_id = _current_ids() @@ -539,20 +499,13 @@ class RBACAccessPolicyBindingLockApi(Resource): @console_ns.route("/workspaces/current/rbac/access-policy-bindings//unlock") class RBACAccessPolicyBindingUnlockApi(Resource): @login_required - @rbac_permission_required( - RBACResourceScope.WORKSPACE, RBACPermission.WORKSPACE_ROLE_MANAGE, resource_required=False - ) + @rbac_permission_required(RBACCheck(RBACPermission.WORKSPACE_ROLE_MANAGE, Workspace())) @console_ns.response(200, "Success", console_ns.models[svc.AccessPolicyBindingState.__name__]) def put(self, binding_id): tenant_id, account_id = _current_ids() return _dump(svc.RBACService.AccessPolicyBindings.unlock(tenant_id, account_id, str(binding_id))) -# --------------------------------------------------------------------------- -# Per-app access (App Access Config). -# --------------------------------------------------------------------------- - - class _ResourceAccessScopeRequest(BaseModel): automatic_include_workspace_members: bool @@ -611,346 +564,337 @@ class RBACMyPermissionsApi(Resource): account_id, app_id=request.args.get("app_id") or None, dataset_id=request.args.get("dataset_id") or None, + agent_id=request.args.get("agent_id") or None, session=db.session(), ) ) -@console_ns.route("/workspaces/current/rbac/apps//access-policy") -class RBACAppMatrixApi(Resource): - @login_required - @console_ns.doc(params=query_params_from_model(_AccessControlLanguageQuery)) - @console_ns.response(200, "Success", console_ns.models[svc.AppAccessMatrix.__name__]) - def get(self, app_id): - tenant_id, account_id = _current_ids() - result = svc.RBACService.AppAccess.matrix(tenant_id, account_id, str(app_id)) - _hydrate_access_matrix_account_names(result.items) - return _dump(result) - - -@console_ns.route("/workspaces/current/rbac/apps//whitelist") -class RBACAppWhitelistApi(Resource): - @login_required - @console_ns.response(200, "Success", console_ns.models[svc.ResourceWhitelist.__name__]) - def get(self, app_id): - tenant_id, account_id = _current_ids() - return _dump(svc.RBACService.AppAccess.whitelist(tenant_id, account_id, str(app_id))) - - @login_required - @console_ns.expect(console_ns.models[_ResourceAccessScopeRequest.__name__]) - @console_ns.response(200, "Success", console_ns.models[svc.ResourceWhitelist.__name__]) - def put(self, app_id): - tenant_id, account_id = _current_ids() - request = _payload(_ResourceAccessScopeRequest) - result = svc.RBACService.AppAccess.replace_whitelist( - tenant_id, - account_id, - str(app_id), - svc.ReplaceMemberBindings(automatic_include_workspace_members=request.automatic_include_workspace_members), - ) - if request.automatic_include_workspace_members: - initialize_created_app_rbac_access_task.delay(tenant_id, account_id, app_id=str(app_id)) - return _dump(result) - - -@console_ns.route("/workspaces/current/rbac/apps//whitelist_config") -class RBACAppWhitelistConfigApi(Resource): - @login_required - @console_ns.response(200, "Success", console_ns.models[svc.ResourceWhitelistConfig.__name__]) - def get(self, app_id): - tenant_id, account_id = _current_ids() - return _dump(svc.RBACService.AppAccess.whitelist_config(tenant_id, account_id, str(app_id))) - - -@console_ns.route("/workspaces/current/rbac/apps//user-access-policies") -class RBACAppUserAccessPoliciesApi(Resource): - @login_required - @console_ns.doc(params=query_params_from_model(_ResourceUserAccessPoliciesQuery)) - @console_ns.response(200, "Success", console_ns.models[svc.ResourceUserAccessPoliciesResponse.__name__]) - def get(self, app_id): - tenant_id, account_id = _current_ids() - options = _pagination_options() - result = svc.RBACService.AppAccess.user_access_policies(tenant_id, account_id, str(app_id), options=options) - _move_resource_maintainer_first(result.data, svc.app_maintainer_id(tenant_id, str(app_id))) - _hydrate_resource_user_account_names(result.data) - return _dump(result) - - -@console_ns.route("/workspaces/current/rbac/apps//users//access-policies") -class RBACAppUserAccessPolicyAssignmentApi(Resource): - @login_required - @console_ns.expect(console_ns.models[svc.ReplaceUserAccessPolicies.__name__]) - @console_ns.response(200, "Success", console_ns.models[svc.ReplaceUserAccessPoliciesResponse.__name__]) - def put(self, app_id, target_account_id): - tenant_id, account_id = _current_ids() - payload = _payload(svc.ReplaceUserAccessPolicies) - return _dump( - svc.RBACService.AppAccess.replace_user_access_policies( - tenant_id, - account_id, - app_id, - target_account_id, - payload, - ) - ) - - -@console_ns.route("/workspaces/current/rbac/apps//access-policies//role-bindings") -class RBACAppRoleBindingsApi(Resource): - @login_required - @console_ns.response(200, "Success", console_ns.models[svc.RoleBindingsResponse.__name__]) - def get(self, app_id, policy_id): - tenant_id, account_id = _current_ids() - return _dump(svc.RBACService.AppAccess.list_role_bindings(tenant_id, account_id, str(app_id), str(policy_id))) - - -@console_ns.route("/workspaces/current/rbac/apps//access-policies//member-bindings") -class RBACAppMemberBindingsApi(Resource): - @login_required - @console_ns.response(200, "Success", console_ns.models[svc.MemberBindingsResponse.__name__]) - def get(self, app_id, policy_id): - tenant_id, account_id = _current_ids() - return _dump(svc.RBACService.AppAccess.list_member_bindings(tenant_id, account_id, str(app_id), str(policy_id))) - - @login_required - @console_ns.expect(console_ns.models[_DeleteMemberBindingsRequest.__name__]) - @console_ns.response(200, "Success", console_ns.models[svc.MemberBindingsResponse.__name__]) - def delete(self, app_id, policy_id): - tenant_id, account_id = _current_ids() - request_body = _payload(_DeleteMemberBindingsRequest) - svc.RBACService.AppAccess.delete_member_bindings( - tenant_id, - account_id, - str(app_id), - str(policy_id), - svc.DeleteMemberBindings(account_ids=request_body.account_ids), - ) - return {"result": "success"} - - -# --------------------------------------------------------------------------- -# Per-dataset access (Knowledge Base Access Config). -# --------------------------------------------------------------------------- - - -@console_ns.route("/workspaces/current/rbac/datasets//access-policy") -class RBACDatasetMatrixApi(Resource): - @login_required - @console_ns.doc(params=query_params_from_model(_AccessControlLanguageQuery)) - @console_ns.response(200, "Success", console_ns.models[svc.DatasetAccessMatrix.__name__]) - def get(self, dataset_id): - tenant_id, account_id = _current_ids() - result = svc.RBACService.DatasetAccess.matrix(tenant_id, account_id, str(dataset_id)) - _hydrate_access_matrix_account_names(result.items) - return _dump(result) - - -@console_ns.route("/workspaces/current/rbac/datasets//whitelist_config") -class RBACDatasetWhitelistConfigApi(Resource): - @login_required - @console_ns.response(200, "Success", console_ns.models[svc.ResourceWhitelistConfig.__name__]) - def get(self, dataset_id): - tenant_id, account_id = _current_ids() - return _dump(svc.RBACService.DatasetAccess.whitelist_config(tenant_id, account_id, str(dataset_id))) - - -@console_ns.route("/workspaces/current/rbac/datasets//whitelist") -class RBACDatasetWhitelistApi(Resource): - @login_required - @console_ns.response(200, "Success", console_ns.models[svc.ResourceWhitelist.__name__]) - def get(self, dataset_id): - tenant_id, account_id = _current_ids() - return _dump(svc.RBACService.DatasetAccess.whitelist(tenant_id, account_id, str(dataset_id))) - - @login_required - @console_ns.expect(console_ns.models[_ResourceAccessScopeRequest.__name__]) - @console_ns.response(200, "Success", console_ns.models[svc.ResourceWhitelist.__name__]) - def put(self, dataset_id): - tenant_id, account_id = _current_ids() - request = _payload(_ResourceAccessScopeRequest) - result = svc.RBACService.DatasetAccess.replace_whitelist( - tenant_id, - account_id, - str(dataset_id), - svc.ReplaceMemberBindings(automatic_include_workspace_members=request.automatic_include_workspace_members), - ) - # Widening the scope only records it: the members still need the default access policy - # before they can reach the dataset, same as the app whitelist route above. - if request.automatic_include_workspace_members: - initialize_created_app_rbac_access_task.delay(tenant_id, account_id, dataset_id=str(dataset_id)) - return _dump(result) - - -@console_ns.route("/workspaces/current/rbac/datasets//user-access-policies") -class RBACDatasetUserAccessPoliciesApi(Resource): - @login_required - @console_ns.doc(params=query_params_from_model(_ResourceUserAccessPoliciesQuery)) - @console_ns.response(200, "Success", console_ns.models[svc.ResourceUserAccessPoliciesResponse.__name__]) - def get(self, dataset_id): - tenant_id, account_id = _current_ids() - options = _pagination_options() - result = svc.RBACService.DatasetAccess.user_access_policies( - tenant_id, account_id, str(dataset_id), options=options - ) - _move_resource_maintainer_first(result.data, svc.dataset_maintainer_id(tenant_id, str(dataset_id))) - _hydrate_resource_user_account_names(result.data) - return _dump(result) - - -@console_ns.route("/workspaces/current/rbac/datasets//users//access-policies") -class RBACDatasetUserAccessPolicyAssignmentApi(Resource): - @login_required - @console_ns.expect(console_ns.models[svc.ReplaceUserAccessPolicies.__name__]) - @console_ns.response(200, "Success", console_ns.models[svc.ReplaceUserAccessPoliciesResponse.__name__]) - def put(self, dataset_id, target_account_id): - tenant_id, account_id = _current_ids() - payload = _payload(svc.ReplaceUserAccessPolicies) - return _dump( - svc.RBACService.DatasetAccess.replace_user_access_policies( - tenant_id, - account_id, - str(dataset_id), - str(target_account_id), - payload, - ) - ) - - -@console_ns.route("/workspaces/current/rbac/datasets//access-policies//role-bindings") -class RBACDatasetRoleBindingsApi(Resource): - @login_required - @console_ns.response(200, "Success", console_ns.models[svc.RoleBindingsResponse.__name__]) - def get(self, dataset_id, policy_id): - tenant_id, account_id = _current_ids() - return _dump( - svc.RBACService.DatasetAccess.list_role_bindings(tenant_id, account_id, str(dataset_id), str(policy_id)) - ) - - -@console_ns.route( - "/workspaces/current/rbac/datasets//access-policies//member-bindings" +type _ResourceAccessClient = ( + type[svc.RBACService.AppAccess] | type[svc.RBACService.DatasetAccess] | type[svc.RBACService.AgentAccess] ) -class RBACDatasetMemberBindingsApi(Resource): - @login_required - @console_ns.response(200, "Success", console_ns.models[svc.MemberBindingsResponse.__name__]) - def get(self, dataset_id, policy_id): - tenant_id, account_id = _current_ids() - return _dump( - svc.RBACService.DatasetAccess.list_member_bindings(tenant_id, account_id, str(dataset_id), str(policy_id)) - ) - - @login_required - @console_ns.expect(console_ns.models[_DeleteMemberBindingsRequest.__name__]) - @console_ns.response(200, "Success", console_ns.models[svc.MemberBindingsResponse.__name__]) - def delete(self, dataset_id, policy_id): - tenant_id, account_id = _current_ids() - request_body = _payload(_DeleteMemberBindingsRequest) - svc.RBACService.DatasetAccess.delete_member_bindings( - tenant_id, - account_id, - str(dataset_id), - str(policy_id), - svc.DeleteMemberBindings(account_ids=request_body.account_ids), - ) - return {"result": "success"} -@console_ns.route("/workspaces/current/rbac/workspace/apps/access-policy") -class RBACWorkspaceAppMatrixApi(Resource): - @login_required - @console_ns.response(200, "Success", console_ns.models[svc.WorkspaceAccessMatrix.__name__]) - def get(self): - tenant_id, account_id = _current_ids() - options = _pagination_options() - result = svc.RBACService.WorkspaceAccess.app_matrix(tenant_id, account_id, options=options) - _hydrate_access_matrix_account_names(result.items) - return _dump(result) +@dataclass(frozen=True) +class _ResourceAccessRoutes: + resource_type: svc.RBACResourceType + class_prefix: str + matrix_model: type[BaseModel] + access: _ResourceAccessClient + catalog: Callable[[str, str], svc.PermissionCatalogResponse] + workspace_matrix: Callable[[str, str, svc.ListOption], svc.WorkspaceAccessMatrix] + workspace_role_bindings: Callable[[str, str, str], svc.RoleBindingsResponse] + workspace_member_bindings: Callable[[str, str, str], svc.MemberBindingsResponse] + replace_workspace_bindings: Callable[[str, str, str, svc.ReplaceBindings], svc.AccessMatrixItem] + maintainer_id: Callable[[str, str], str | None] | None = None + + @property + def url_segment(self) -> str: + return self.resource_type.route.segment + + @property + def id_param(self) -> str: + return self.resource_type.route.id_param -@console_ns.route("/workspaces/current/rbac/workspace/apps/access-policies//role-bindings") -class RBACWorkspaceAppRoleBindingsApi(Resource): - @login_required - @console_ns.response(200, "Success", console_ns.models[svc.RoleBindingsResponse.__name__]) - def get(self, policy_id): - tenant_id, account_id = _current_ids() - return _dump(svc.RBACService.WorkspaceAccess.list_app_role_bindings(tenant_id, account_id, str(policy_id))) +@dataclass(frozen=True) +class _ResourceAccessApis: + catalog: type[Resource] + matrix: type[Resource] + whitelist: type[Resource] + whitelist_config: type[Resource] + user_access_policies: type[Resource] + user_access_policy_assignment: type[Resource] + role_bindings: type[Resource] + member_bindings: type[Resource] + workspace_matrix: type[Resource] + workspace_role_bindings: type[Resource] + workspace_bindings: type[Resource] + workspace_member_bindings: type[Resource] -@console_ns.route("/workspaces/current/rbac/workspace/apps/access-policies//bindings") -class RBACWorkspaceAppBindingsApi(Resource): - @login_required - @console_ns.expect(console_ns.models[_ReplaceBindingsRequest.__name__]) - @console_ns.response(200, "Success", console_ns.models[svc.AccessMatrixItem.__name__]) - def put(self, policy_id): - tenant_id, account_id = _current_ids() - request = _payload(_ReplaceBindingsRequest) - return _dump( - svc.RBACService.WorkspaceAccess.replace_app_bindings( +_RESOURCE_ACCESS_ROUTES = ( + _ResourceAccessRoutes( + resource_type=svc.RBACResourceType.APP, + class_prefix="App", + matrix_model=svc.AppAccessMatrix, + access=svc.RBACService.AppAccess, + catalog=lambda tenant_id, account_id: svc.RBACService.Catalog.app(tenant_id, account_id), + workspace_matrix=lambda tenant_id, account_id, options: svc.RBACService.WorkspaceAccess.app_matrix( + tenant_id, account_id, options=options + ), + workspace_role_bindings=lambda tenant_id, account_id, policy_id: ( + svc.RBACService.WorkspaceAccess.list_app_role_bindings(tenant_id, account_id, policy_id) + ), + workspace_member_bindings=lambda tenant_id, account_id, policy_id: ( + svc.RBACService.WorkspaceAccess.list_app_member_bindings(tenant_id, account_id, policy_id) + ), + replace_workspace_bindings=lambda tenant_id, account_id, policy_id, payload: ( + svc.RBACService.WorkspaceAccess.replace_app_bindings(tenant_id, account_id, policy_id, payload) + ), + maintainer_id=lambda tenant_id, resource_id: svc.app_maintainer_id(tenant_id, resource_id), + ), + _ResourceAccessRoutes( + resource_type=svc.RBACResourceType.DATASET, + class_prefix="Dataset", + matrix_model=svc.DatasetAccessMatrix, + access=svc.RBACService.DatasetAccess, + catalog=lambda tenant_id, account_id: svc.RBACService.Catalog.dataset(tenant_id, account_id), + workspace_matrix=lambda tenant_id, account_id, options: svc.RBACService.WorkspaceAccess.dataset_matrix( + tenant_id, account_id, options=options + ), + workspace_role_bindings=lambda tenant_id, account_id, policy_id: ( + svc.RBACService.WorkspaceAccess.list_dataset_role_bindings(tenant_id, account_id, policy_id) + ), + workspace_member_bindings=lambda tenant_id, account_id, policy_id: ( + svc.RBACService.WorkspaceAccess.list_dataset_member_bindings(tenant_id, account_id, policy_id) + ), + replace_workspace_bindings=lambda tenant_id, account_id, policy_id, payload: ( + svc.RBACService.WorkspaceAccess.replace_dataset_bindings(tenant_id, account_id, policy_id, payload) + ), + maintainer_id=lambda tenant_id, resource_id: svc.dataset_maintainer_id(tenant_id, resource_id), + ), + _ResourceAccessRoutes( + resource_type=svc.RBACResourceType.AGENT, + class_prefix="Agent", + matrix_model=svc.AgentAccessMatrix, + access=svc.RBACService.AgentAccess, + catalog=lambda tenant_id, account_id: svc.RBACService.Catalog.agent(tenant_id, account_id), + workspace_matrix=lambda tenant_id, account_id, options: svc.RBACService.WorkspaceAccess.agent_matrix( + tenant_id, account_id, options=options + ), + workspace_role_bindings=lambda tenant_id, account_id, policy_id: ( + svc.RBACService.WorkspaceAccess.list_agent_role_bindings(tenant_id, account_id, policy_id) + ), + workspace_member_bindings=lambda tenant_id, account_id, policy_id: ( + svc.RBACService.WorkspaceAccess.list_agent_member_bindings(tenant_id, account_id, policy_id) + ), + replace_workspace_bindings=lambda tenant_id, account_id, policy_id, payload: ( + svc.RBACService.WorkspaceAccess.replace_agent_bindings(tenant_id, account_id, policy_id, payload) + ), + ), +) + + +def _build_resource_access_apis(spec: _ResourceAccessRoutes) -> _ResourceAccessApis: + id_param = spec.id_param + resource_prefix = f"/workspaces/current/rbac/{spec.url_segment}/" + workspace_prefix = f"/workspaces/current/rbac/workspace/{spec.url_segment}" + + def resource_id(path_params: Mapping[str, object]) -> str: + return str(path_params[id_param]) + + def register(resource: type[Resource], name: str, url: str) -> type[Resource]: + resource.__name__ = name + resource.__qualname__ = name + console_ns.route(url)(resource) + return resource + + class CatalogApi(Resource): + @login_required + @console_ns.response(200, "Success", console_ns.models[svc.PermissionCatalogResponse.__name__]) + def get(self): + tenant_id, account_id = _current_ids() + return _dump(spec.catalog(tenant_id, account_id)) + + class MatrixApi(Resource): + @login_required + @console_ns.doc(params=query_params_from_model(_AccessControlLanguageQuery)) + @console_ns.response(200, "Success", console_ns.models[spec.matrix_model.__name__]) + def get(self, **path_params): + tenant_id, account_id = _current_ids() + result = spec.access.matrix(tenant_id, account_id, resource_id(path_params)) + _hydrate_access_matrix_account_names(result.items) + return _dump(result) + + class WhitelistApi(Resource): + @login_required + @console_ns.response(200, "Success", console_ns.models[svc.ResourceWhitelist.__name__]) + def get(self, **path_params): + tenant_id, account_id = _current_ids() + return _dump(spec.access.whitelist(tenant_id, account_id, resource_id(path_params))) + + @login_required + @console_ns.expect(console_ns.models[_ResourceAccessScopeRequest.__name__]) + @console_ns.response(200, "Success", console_ns.models[svc.ResourceWhitelist.__name__]) + def put(self, **path_params): + tenant_id, account_id = _current_ids() + target_id = resource_id(path_params) + scope = _payload(_ResourceAccessScopeRequest) + result = spec.access.replace_whitelist( tenant_id, account_id, - str(policy_id), - svc.ReplaceBindings(role_ids=list(request.role_ids), account_ids=list(request.account_ids)), + target_id, + svc.ReplaceMemberBindings( + automatic_include_workspace_members=scope.automatic_include_workspace_members + ), ) - ) + if scope.automatic_include_workspace_members: + initialize_created_app_rbac_access_task.delay(tenant_id, account_id, **{id_param: target_id}) + return _dump(result) + class WhitelistConfigApi(Resource): + @login_required + @console_ns.response(200, "Success", console_ns.models[svc.ResourceWhitelistConfig.__name__]) + def get(self, **path_params): + tenant_id, account_id = _current_ids() + return _dump(spec.access.whitelist_config(tenant_id, account_id, resource_id(path_params))) -@console_ns.route("/workspaces/current/rbac/workspace/apps/access-policies//member-bindings") -class RBACWorkspaceAppMemberBindingsApi(Resource): - @login_required - @console_ns.response(200, "Success", console_ns.models[svc.MemberBindingsResponse.__name__]) - def get(self, policy_id): - tenant_id, account_id = _current_ids() - return _dump(svc.RBACService.WorkspaceAccess.list_app_member_bindings(tenant_id, account_id, str(policy_id))) + class UserAccessPoliciesApi(Resource): + @login_required + @console_ns.doc(params=query_params_from_model(_ResourceUserAccessPoliciesQuery)) + @console_ns.response(200, "Success", console_ns.models[svc.ResourceUserAccessPoliciesResponse.__name__]) + def get(self, **path_params): + tenant_id, account_id = _current_ids() + target_id = resource_id(path_params) + options = _pagination_options() + result = spec.access.user_access_policies(tenant_id, account_id, target_id, options=options) + maintainer_id = spec.maintainer_id(tenant_id, target_id) if spec.maintainer_id else None + _move_resource_maintainer_first(result.data, maintainer_id) + _hydrate_resource_user_account_names(result.data) + return _dump(result) + class UserAccessPolicyAssignmentApi(Resource): + @login_required + @console_ns.expect(console_ns.models[svc.ReplaceUserAccessPolicies.__name__]) + @console_ns.response(200, "Success", console_ns.models[svc.ReplaceUserAccessPoliciesResponse.__name__]) + def put(self, target_account_id, **path_params): + tenant_id, account_id = _current_ids() + payload = _payload(svc.ReplaceUserAccessPolicies) + return _dump( + spec.access.replace_user_access_policies( + tenant_id, + account_id, + resource_id(path_params), + str(target_account_id), + payload, + ) + ) -@console_ns.route("/workspaces/current/rbac/workspace/datasets/access-policy") -class RBACWorkspaceDatasetMatrixApi(Resource): - @login_required - @console_ns.response(200, "Success", console_ns.models[svc.WorkspaceAccessMatrix.__name__]) - def get(self): - tenant_id, account_id = _current_ids() - options = _pagination_options() - result = svc.RBACService.WorkspaceAccess.dataset_matrix(tenant_id, account_id, options=options) - _hydrate_access_matrix_account_names(result.items) - return _dump(result) + class RoleBindingsApi(Resource): + @login_required + @console_ns.response(200, "Success", console_ns.models[svc.RoleBindingsResponse.__name__]) + def get(self, policy_id, **path_params): + tenant_id, account_id = _current_ids() + return _dump( + spec.access.list_role_bindings(tenant_id, account_id, resource_id(path_params), str(policy_id)) + ) + class MemberBindingsApi(Resource): + @login_required + @console_ns.response(200, "Success", console_ns.models[svc.MemberBindingsResponse.__name__]) + def get(self, policy_id, **path_params): + tenant_id, account_id = _current_ids() + return _dump( + spec.access.list_member_bindings(tenant_id, account_id, resource_id(path_params), str(policy_id)) + ) -@console_ns.route("/workspaces/current/rbac/workspace/datasets/access-policies//role-bindings") -class RBACWorkspaceDatasetRoleBindingsApi(Resource): - @login_required - @console_ns.response(200, "Success", console_ns.models[svc.RoleBindingsResponse.__name__]) - def get(self, policy_id): - tenant_id, account_id = _current_ids() - return _dump(svc.RBACService.WorkspaceAccess.list_dataset_role_bindings(tenant_id, account_id, str(policy_id))) - - -@console_ns.route("/workspaces/current/rbac/workspace/datasets/access-policies//bindings") -class RBACWorkspaceDatasetBindingsApi(Resource): - @login_required - @console_ns.expect(console_ns.models[_ReplaceBindingsRequest.__name__]) - @console_ns.response(200, "Success", console_ns.models[svc.AccessMatrixItem.__name__]) - def put(self, policy_id): - tenant_id, account_id = _current_ids() - request = _payload(_ReplaceBindingsRequest) - return _dump( - svc.RBACService.WorkspaceAccess.replace_dataset_bindings( + @login_required + @console_ns.expect(console_ns.models[_DeleteMemberBindingsRequest.__name__]) + @console_ns.response(200, "Success", console_ns.models[svc.MemberBindingsResponse.__name__]) + def delete(self, policy_id, **path_params): + tenant_id, account_id = _current_ids() + body = _payload(_DeleteMemberBindingsRequest) + spec.access.delete_member_bindings( tenant_id, account_id, + resource_id(path_params), str(policy_id), - svc.ReplaceBindings(role_ids=list(request.role_ids), account_ids=list(request.account_ids)), + svc.DeleteMemberBindings(account_ids=body.account_ids), ) - ) + return {"result": "success"} + + class WorkspaceMatrixApi(Resource): + @login_required + @console_ns.response(200, "Success", console_ns.models[svc.WorkspaceAccessMatrix.__name__]) + def get(self): + tenant_id, account_id = _current_ids() + result = spec.workspace_matrix(tenant_id, account_id, _pagination_options()) + _hydrate_access_matrix_account_names(result.items) + return _dump(result) + + class WorkspaceRoleBindingsApi(Resource): + @login_required + @console_ns.response(200, "Success", console_ns.models[svc.RoleBindingsResponse.__name__]) + def get(self, policy_id): + tenant_id, account_id = _current_ids() + return _dump(spec.workspace_role_bindings(tenant_id, account_id, str(policy_id))) + + class WorkspaceBindingsApi(Resource): + @login_required + @console_ns.expect(console_ns.models[_ReplaceBindingsRequest.__name__]) + @console_ns.response(200, "Success", console_ns.models[svc.AccessMatrixItem.__name__]) + def put(self, policy_id): + tenant_id, account_id = _current_ids() + body = _payload(_ReplaceBindingsRequest) + return _dump( + spec.replace_workspace_bindings( + tenant_id, + account_id, + str(policy_id), + svc.ReplaceBindings(role_ids=list(body.role_ids), account_ids=list(body.account_ids)), + ) + ) + + class WorkspaceMemberBindingsApi(Resource): + @login_required + @console_ns.response(200, "Success", console_ns.models[svc.MemberBindingsResponse.__name__]) + def get(self, policy_id): + tenant_id, account_id = _current_ids() + return _dump(spec.workspace_member_bindings(tenant_id, account_id, str(policy_id))) + + prefix = spec.class_prefix + return _ResourceAccessApis( + catalog=register( + CatalogApi, + f"RBAC{prefix}CatalogApi", + f"/workspaces/current/rbac/role-permissions/catalog/{spec.resource_type.value}", + ), + matrix=register(MatrixApi, f"RBAC{prefix}MatrixApi", f"{resource_prefix}/access-policy"), + whitelist=register(WhitelistApi, f"RBAC{prefix}WhitelistApi", f"{resource_prefix}/whitelist"), + whitelist_config=register( + WhitelistConfigApi, f"RBAC{prefix}WhitelistConfigApi", f"{resource_prefix}/whitelist_config" + ), + user_access_policies=register( + UserAccessPoliciesApi, f"RBAC{prefix}UserAccessPoliciesApi", f"{resource_prefix}/user-access-policies" + ), + user_access_policy_assignment=register( + UserAccessPolicyAssignmentApi, + f"RBAC{prefix}UserAccessPolicyAssignmentApi", + f"{resource_prefix}/users//access-policies", + ), + role_bindings=register( + RoleBindingsApi, + f"RBAC{prefix}RoleBindingsApi", + f"{resource_prefix}/access-policies//role-bindings", + ), + member_bindings=register( + MemberBindingsApi, + f"RBAC{prefix}MemberBindingsApi", + f"{resource_prefix}/access-policies//member-bindings", + ), + workspace_matrix=register( + WorkspaceMatrixApi, f"RBACWorkspace{prefix}MatrixApi", f"{workspace_prefix}/access-policy" + ), + workspace_role_bindings=register( + WorkspaceRoleBindingsApi, + f"RBACWorkspace{prefix}RoleBindingsApi", + f"{workspace_prefix}/access-policies//role-bindings", + ), + workspace_bindings=register( + WorkspaceBindingsApi, + f"RBACWorkspace{prefix}BindingsApi", + f"{workspace_prefix}/access-policies//bindings", + ), + workspace_member_bindings=register( + WorkspaceMemberBindingsApi, + f"RBACWorkspace{prefix}MemberBindingsApi", + f"{workspace_prefix}/access-policies//member-bindings", + ), + ) -@console_ns.route("/workspaces/current/rbac/workspace/datasets/access-policies//member-bindings") -class RBACWorkspaceDatasetMemberBindingsApi(Resource): - @login_required - @console_ns.response(200, "Success", console_ns.models[svc.MemberBindingsResponse.__name__]) - def get(self, policy_id): - tenant_id, account_id = _current_ids() - return _dump( - svc.RBACService.WorkspaceAccess.list_dataset_member_bindings(tenant_id, account_id, str(policy_id)) - ) +_RESOURCE_ACCESS_APIS = {spec.resource_type: _build_resource_access_apis(spec) for spec in _RESOURCE_ACCESS_ROUTES} # --------------------------------------------------------------------------- diff --git a/api/controllers/console/workspace/skills.py b/api/controllers/console/workspace/skills.py index ce1dc6b51c7..05730ac72bf 100644 --- a/api/controllers/console/workspace/skills.py +++ b/api/controllers/console/workspace/skills.py @@ -10,13 +10,13 @@ from pydantic import BaseModel, ConfigDict, Field, ValidationError from sqlalchemy.orm import Session from controllers.common.fields import BinaryFileResponse +from controllers.common.rbac import RBACCheck, Workspace from controllers.common.schema import query_params_from_model, register_response_schema_models, register_schema_models from controllers.common.session import with_session from controllers.console import console_ns from controllers.console.flask_admission import console_account_admission from controllers.console.wraps import ( RBACPermission, - RBACResourceScope, edit_permission_required, ) from fields.base import ResponseModel @@ -298,9 +298,7 @@ class WorkspaceSkillsApi(Resource): @console_ns.doc(params=query_params_from_model(WorkspaceSkillsQuery)) @console_ns.response(200, "Workspace skills", console_ns.models[SkillListResponse.__name__]) @console_account_admission( - rbac_resource_scope=RBACResourceScope.WORKSPACE, - rbac_permission=RBACPermission.SKILL_VIEW, - rbac_resource_required=False, + rbac_checks=[RBACCheck(RBACPermission.SKILL_VIEW, Workspace())], ) @with_session(write=False) def get(self, session: Session, request_context: RequestContext): @@ -325,9 +323,7 @@ class WorkspaceSkillsApi(Resource): @console_ns.expect(console_ns.models[SkillCreatePayload.__name__]) @console_ns.response(201, "Skill created", console_ns.models[SkillDetailResponse.__name__]) @console_account_admission( - rbac_resource_scope=RBACResourceScope.WORKSPACE, - rbac_permission=RBACPermission.SKILL_EDIT, - rbac_resource_required=False, + rbac_checks=[RBACCheck(RBACPermission.SKILL_EDIT, Workspace())], ) @edit_permission_required @with_session @@ -391,9 +387,7 @@ class WorkspaceSkillImportApi(Resource): @console_ns.doc(description="Import a Skill zip package from multipart form field `file`.") @console_ns.response(201, "Skill imported", console_ns.models[SkillDetailResponse.__name__]) @console_account_admission( - rbac_resource_scope=RBACResourceScope.WORKSPACE, - rbac_permission=RBACPermission.SKILL_EDIT, - rbac_resource_required=False, + rbac_checks=[RBACCheck(RBACPermission.SKILL_EDIT, Workspace())], ) @edit_permission_required @with_session @@ -419,9 +413,7 @@ class WorkspaceSkillImportApi(Resource): class WorkspaceSkillApi(Resource): @console_ns.response(200, "Skill detail", console_ns.models[SkillDetailResponse.__name__]) @console_account_admission( - rbac_resource_scope=RBACResourceScope.WORKSPACE, - rbac_permission=RBACPermission.SKILL_VIEW, - rbac_resource_required=False, + rbac_checks=[RBACCheck(RBACPermission.SKILL_VIEW, Workspace())], ) @with_session(write=False) def get(self, session: Session, request_context: RequestContext, skill_id: str): @@ -436,9 +428,7 @@ class WorkspaceSkillApi(Resource): @console_ns.expect(console_ns.models[SkillMetadataPayload.__name__]) @console_ns.response(200, "Skill updated", console_ns.models[SkillResponse.__name__]) @console_account_admission( - rbac_resource_scope=RBACResourceScope.WORKSPACE, - rbac_permission=RBACPermission.SKILL_EDIT, - rbac_resource_required=False, + rbac_checks=[RBACCheck(RBACPermission.SKILL_EDIT, Workspace())], ) @edit_permission_required @with_session @@ -462,9 +452,7 @@ class WorkspaceSkillApi(Resource): @console_ns.expect(console_ns.models[SkillDeletePayload.__name__]) @console_ns.response(200, "Skill deleted", console_ns.models[SkillDeleteResponse.__name__]) @console_account_admission( - rbac_resource_scope=RBACResourceScope.WORKSPACE, - rbac_permission=RBACPermission.SKILL_DELETE, - rbac_resource_required=False, + rbac_checks=[RBACCheck(RBACPermission.SKILL_DELETE, Workspace())], ) @edit_permission_required @with_session @@ -487,9 +475,7 @@ class WorkspaceSkillApi(Resource): class WorkspaceSkillDuplicateApi(Resource): @console_ns.response(201, "Skill duplicated", console_ns.models[SkillDetailResponse.__name__]) @console_account_admission( - rbac_resource_scope=RBACResourceScope.WORKSPACE, - rbac_permission=RBACPermission.SKILL_EDIT, - rbac_resource_required=False, + rbac_checks=[RBACCheck(RBACPermission.SKILL_EDIT, Workspace())], ) @edit_permission_required @with_session @@ -509,9 +495,7 @@ class WorkspaceSkillDuplicateApi(Resource): class WorkspaceSkillExportApi(Resource): @console_ns.response(200, "Published Skill zip archive") @console_account_admission( - rbac_resource_scope=RBACResourceScope.WORKSPACE, - rbac_permission=RBACPermission.SKILL_VIEW, - rbac_resource_required=False, + rbac_checks=[RBACCheck(RBACPermission.SKILL_VIEW, Workspace())], ) @with_session(write=False) def get(self, session: Session, request_context: RequestContext, skill_id: str): @@ -696,9 +680,7 @@ class WorkspaceSkillPublishApi(Resource): @console_ns.expect(console_ns.models[SkillPublishPayload.__name__]) @console_ns.response(200, "Skill published", console_ns.models[SkillVersionResponse.__name__]) @console_account_admission( - rbac_resource_scope=RBACResourceScope.WORKSPACE, - rbac_permission=RBACPermission.SKILL_PUBLISH, - rbac_resource_required=False, + rbac_checks=[RBACCheck(RBACPermission.SKILL_PUBLISH, Workspace())], ) @edit_permission_required @with_session @@ -723,9 +705,7 @@ class WorkspaceSkillRestoreApi(Resource): @console_ns.expect(console_ns.models[SkillRestorePayload.__name__]) @console_ns.response(200, "Skill version restored to draft", console_ns.models[SkillDetailResponse.__name__]) @console_account_admission( - rbac_resource_scope=RBACResourceScope.WORKSPACE, - rbac_permission=RBACPermission.SKILL_PUBLISH, - rbac_resource_required=False, + rbac_checks=[RBACCheck(RBACPermission.SKILL_PUBLISH, Workspace())], ) @edit_permission_required @with_session diff --git a/api/controllers/console/workspace/snippets.py b/api/controllers/console/workspace/snippets.py index ba1b3d816b3..22769ba88a4 100644 --- a/api/controllers/console/workspace/snippets.py +++ b/api/controllers/console/workspace/snippets.py @@ -8,6 +8,7 @@ from sqlalchemy.orm import Session, sessionmaker from werkzeug.exceptions import NotFound from controllers.common.fields import TextFileResponse +from controllers.common.rbac import RBACCheck, Workspace from controllers.common.schema import ( query_params_from_model, register_response_schema_models, @@ -24,7 +25,6 @@ from controllers.console.snippets.payloads import ( ) from controllers.console.wraps import ( RBACPermission, - RBACResourceScope, account_initialization_required, edit_permission_required, model_validate, @@ -147,9 +147,7 @@ class CustomizedSnippetsApi(Resource): @login_required @account_initialization_required @edit_permission_required - @rbac_permission_required( - RBACResourceScope.WORKSPACE, RBACPermission.SNIPPETS_CREATE_AND_MODIFY, resource_required=False - ) + @rbac_permission_required(RBACCheck(RBACPermission.SNIPPETS_CREATE_AND_MODIFY, Workspace())) @with_current_user @with_current_tenant_id @model_validate(CreateSnippetPayload) @@ -211,9 +209,7 @@ class CustomizedSnippetDetailApi(Resource): @login_required @account_initialization_required @edit_permission_required - @rbac_permission_required( - RBACResourceScope.WORKSPACE, RBACPermission.SNIPPETS_CREATE_AND_MODIFY, resource_required=False - ) + @rbac_permission_required(RBACCheck(RBACPermission.SNIPPETS_CREATE_AND_MODIFY, Workspace())) @with_current_user @with_current_tenant_id @model_validate(UpdateSnippetPayload) @@ -258,7 +254,7 @@ class CustomizedSnippetDetailApi(Resource): @login_required @account_initialization_required @edit_permission_required - @rbac_permission_required(RBACResourceScope.WORKSPACE, RBACPermission.SNIPPETS_MANAGE, resource_required=False) + @rbac_permission_required(RBACCheck(RBACPermission.SNIPPETS_MANAGE, Workspace())) @with_current_user @with_current_tenant_id def delete(self, current_tenant_id: str, current_user: Account, snippet_id: str): @@ -296,9 +292,7 @@ class CustomizedSnippetExportApi(Resource): @login_required @account_initialization_required @edit_permission_required - @rbac_permission_required( - RBACResourceScope.WORKSPACE, RBACPermission.SNIPPETS_CREATE_AND_MODIFY, resource_required=False - ) + @rbac_permission_required(RBACCheck(RBACPermission.SNIPPETS_CREATE_AND_MODIFY, Workspace())) @with_current_tenant_id def get(self, current_tenant_id: str, snippet_id: str): """Export snippet as DSL.""" @@ -351,9 +345,7 @@ class CustomizedSnippetImportApi(Resource): @login_required @account_initialization_required @edit_permission_required - @rbac_permission_required( - RBACResourceScope.WORKSPACE, RBACPermission.SNIPPETS_CREATE_AND_MODIFY, resource_required=False - ) + @rbac_permission_required(RBACCheck(RBACPermission.SNIPPETS_CREATE_AND_MODIFY, Workspace())) @with_current_user @with_session @model_validate(SnippetImportPayload) @@ -390,9 +382,7 @@ class CustomizedSnippetImportConfirmApi(Resource): @login_required @account_initialization_required @edit_permission_required - @rbac_permission_required( - RBACResourceScope.WORKSPACE, RBACPermission.SNIPPETS_CREATE_AND_MODIFY, resource_required=False - ) + @rbac_permission_required(RBACCheck(RBACPermission.SNIPPETS_CREATE_AND_MODIFY, Workspace())) @with_current_user @with_session def post(self, session: Session, current_user: Account, import_id: str): @@ -420,9 +410,7 @@ class CustomizedSnippetCheckDependenciesApi(Resource): @login_required @account_initialization_required @edit_permission_required - @rbac_permission_required( - RBACResourceScope.WORKSPACE, RBACPermission.SNIPPETS_CREATE_AND_MODIFY, resource_required=False - ) + @rbac_permission_required(RBACCheck(RBACPermission.SNIPPETS_CREATE_AND_MODIFY, Workspace())) @with_current_tenant_id def get(self, current_tenant_id: str, snippet_id: str): """Check dependencies for a snippet.""" diff --git a/api/controllers/console/workspace/tool_providers.py b/api/controllers/console/workspace/tool_providers.py index cb61f1582b6..f37850a810d 100644 --- a/api/controllers/console/workspace/tool_providers.py +++ b/api/controllers/console/workspace/tool_providers.py @@ -20,6 +20,7 @@ from werkzeug.exceptions import Forbidden from configs import dify_config from controllers.common.fields import SimpleResultResponse +from controllers.common.rbac import RBACCheck, Workspace from controllers.common.schema import ( query_params_from_model, query_params_from_request, @@ -29,7 +30,6 @@ from controllers.common.schema import ( from controllers.console import console_ns from controllers.console.wraps import ( RBACPermission, - RBACResourceScope, account_initialization_required, enterprise_license_required, is_admin_or_owner_required, @@ -563,7 +563,7 @@ class ToolBuiltinProviderDeleteApi(Resource): @setup_required @login_required @is_admin_or_owner_required - @rbac_permission_required(RBACResourceScope.WORKSPACE, RBACPermission.CREDENTIAL_MANAGE, resource_required=False) + @rbac_permission_required(RBACCheck(RBACPermission.CREDENTIAL_MANAGE, Workspace())) @account_initialization_required @with_current_tenant_id @model_validate(BuiltinToolCredentialDeletePayload) @@ -590,7 +590,7 @@ class ToolBuiltinProviderAddApi(Resource): @setup_required @login_required @is_admin_or_owner_required - @rbac_permission_required(RBACResourceScope.WORKSPACE, RBACPermission.CREDENTIAL_CREATE, resource_required=False) + @rbac_permission_required(RBACCheck(RBACPermission.CREDENTIAL_CREATE, Workspace())) @account_initialization_required @with_current_user @with_current_tenant_id @@ -621,7 +621,7 @@ class ToolBuiltinProviderUpdateApi(Resource): @setup_required @login_required @is_admin_or_owner_required - @rbac_permission_required(RBACResourceScope.WORKSPACE, RBACPermission.CREDENTIAL_MANAGE, resource_required=False) + @rbac_permission_required(RBACCheck(RBACPermission.CREDENTIAL_MANAGE, Workspace())) @account_initialization_required @with_current_user @with_current_tenant_id @@ -689,7 +689,7 @@ class ToolApiProviderAddApi(Resource): @setup_required @login_required @is_admin_or_owner_required - @rbac_permission_required(RBACResourceScope.WORKSPACE, RBACPermission.TOOL_MANAGE, resource_required=False) + @rbac_permission_required(RBACCheck(RBACPermission.TOOL_MANAGE, Workspace())) @account_initialization_required @with_current_user @with_current_tenant_id @@ -769,7 +769,7 @@ class ToolApiProviderUpdateApi(Resource): @setup_required @login_required @is_admin_or_owner_required - @rbac_permission_required(RBACResourceScope.WORKSPACE, RBACPermission.TOOL_MANAGE, resource_required=False) + @rbac_permission_required(RBACCheck(RBACPermission.TOOL_MANAGE, Workspace())) @account_initialization_required @with_current_user @with_current_tenant_id @@ -800,7 +800,7 @@ class ToolApiProviderDeleteApi(Resource): @setup_required @login_required @is_admin_or_owner_required - @rbac_permission_required(RBACResourceScope.WORKSPACE, RBACPermission.TOOL_MANAGE, resource_required=False) + @rbac_permission_required(RBACCheck(RBACPermission.TOOL_MANAGE, Workspace())) @account_initialization_required @with_current_user @with_current_tenant_id @@ -908,7 +908,7 @@ class ToolWorkflowProviderCreateApi(Resource): @setup_required @login_required @is_admin_or_owner_required - @rbac_permission_required(RBACResourceScope.WORKSPACE, RBACPermission.TOOL_MANAGE, resource_required=False) + @rbac_permission_required(RBACCheck(RBACPermission.TOOL_MANAGE, Workspace())) @account_initialization_required @with_current_user @with_current_tenant_id @@ -938,7 +938,7 @@ class ToolWorkflowProviderUpdateApi(Resource): @setup_required @login_required @is_admin_or_owner_required - @rbac_permission_required(RBACResourceScope.WORKSPACE, RBACPermission.TOOL_MANAGE, resource_required=False) + @rbac_permission_required(RBACCheck(RBACPermission.TOOL_MANAGE, Workspace())) @account_initialization_required @with_current_user @with_current_tenant_id @@ -968,7 +968,7 @@ class ToolWorkflowProviderDeleteApi(Resource): @setup_required @login_required @is_admin_or_owner_required - @rbac_permission_required(RBACResourceScope.WORKSPACE, RBACPermission.TOOL_MANAGE, resource_required=False) + @rbac_permission_required(RBACCheck(RBACPermission.TOOL_MANAGE, Workspace())) @account_initialization_required @with_current_user @with_current_tenant_id @@ -1125,7 +1125,7 @@ class ToolPluginOAuthApi(Resource): @setup_required @login_required @is_admin_or_owner_required - @rbac_permission_required(RBACResourceScope.WORKSPACE, RBACPermission.CREDENTIAL_MANAGE, resource_required=False) + @rbac_permission_required(RBACCheck(RBACPermission.CREDENTIAL_MANAGE, Workspace())) @account_initialization_required @with_current_user @with_current_tenant_id @@ -1251,7 +1251,7 @@ class ToolBuiltinProviderSetDefaultApi(Resource): @setup_required @login_required @is_admin_or_owner_required - @rbac_permission_required(RBACResourceScope.WORKSPACE, RBACPermission.CREDENTIAL_USE, resource_required=False) + @rbac_permission_required(RBACCheck(RBACPermission.CREDENTIAL_USE, Workspace())) @account_initialization_required @with_current_tenant_id @model_validate(BuiltinProviderDefaultCredentialPayload) @@ -1273,7 +1273,7 @@ class ToolOAuthCustomClient(Resource): @setup_required @login_required @is_admin_or_owner_required - @rbac_permission_required(RBACResourceScope.WORKSPACE, RBACPermission.CREDENTIAL_MANAGE, resource_required=False) + @rbac_permission_required(RBACCheck(RBACPermission.CREDENTIAL_MANAGE, Workspace())) @account_initialization_required @with_current_tenant_id @model_validate(ToolOAuthCustomClientPayload) @@ -1297,7 +1297,7 @@ class ToolOAuthCustomClient(Resource): @setup_required @login_required @is_admin_or_owner_required - @rbac_permission_required(RBACResourceScope.WORKSPACE, RBACPermission.CREDENTIAL_MANAGE, resource_required=False) + @rbac_permission_required(RBACCheck(RBACPermission.CREDENTIAL_MANAGE, Workspace())) @account_initialization_required @with_current_tenant_id def get(self, current_tenant_id: str, provider: str): @@ -1309,7 +1309,7 @@ class ToolOAuthCustomClient(Resource): @setup_required @login_required @is_admin_or_owner_required - @rbac_permission_required(RBACResourceScope.WORKSPACE, RBACPermission.CREDENTIAL_MANAGE, resource_required=False) + @rbac_permission_required(RBACCheck(RBACPermission.CREDENTIAL_MANAGE, Workspace())) @account_initialization_required @with_current_tenant_id def delete(self, current_tenant_id: str, provider: str): @@ -1379,7 +1379,7 @@ class ToolProviderMCPApi(Resource): @setup_required @login_required @account_initialization_required - @rbac_permission_required(RBACResourceScope.WORKSPACE, RBACPermission.MCP_MANAGE, resource_required=False) + @rbac_permission_required(RBACCheck(RBACPermission.MCP_MANAGE, Workspace())) @with_current_user @with_current_tenant_id @model_validate(MCPProviderCreatePayload) @@ -1433,7 +1433,7 @@ class ToolProviderMCPApi(Resource): @setup_required @login_required @account_initialization_required - @rbac_permission_required(RBACResourceScope.WORKSPACE, RBACPermission.MCP_MANAGE, resource_required=False) + @rbac_permission_required(RBACCheck(RBACPermission.MCP_MANAGE, Workspace())) @with_current_tenant_id @model_validate(MCPProviderUpdatePayload) def put(self, req_data: MCPProviderUpdatePayload, current_tenant_id: str): @@ -1487,7 +1487,7 @@ class ToolProviderMCPApi(Resource): @setup_required @login_required @account_initialization_required - @rbac_permission_required(RBACResourceScope.WORKSPACE, RBACPermission.MCP_MANAGE, resource_required=False) + @rbac_permission_required(RBACCheck(RBACPermission.MCP_MANAGE, Workspace())) @with_current_tenant_id @model_validate(MCPProviderDeletePayload) def delete(self, req_data: MCPProviderDeletePayload, current_tenant_id: str): @@ -1505,7 +1505,7 @@ class ToolMCPAuthApi(Resource): @setup_required @login_required @account_initialization_required - @rbac_permission_required(RBACResourceScope.WORKSPACE, RBACPermission.MCP_MANAGE, resource_required=False) + @rbac_permission_required(RBACCheck(RBACPermission.MCP_MANAGE, Workspace())) @with_current_tenant_id @model_validate(MCPAuthPayload) def post(self, req_data: MCPAuthPayload, tenant_id: str): @@ -1617,7 +1617,7 @@ class ToolMCPUpdateApi(Resource): @setup_required @login_required @account_initialization_required - @rbac_permission_required(RBACResourceScope.WORKSPACE, RBACPermission.MCP_MANAGE, resource_required=False) + @rbac_permission_required(RBACCheck(RBACPermission.MCP_MANAGE, Workspace())) @with_current_tenant_id def get(self, tenant_id: str, provider_id: str): with sessionmaker(db.engine).begin() as session: diff --git a/api/controllers/console/workspace/trigger_providers.py b/api/controllers/console/workspace/trigger_providers.py index 2bc5b454696..8d15c3a99b4 100644 --- a/api/controllers/console/workspace/trigger_providers.py +++ b/api/controllers/console/workspace/trigger_providers.py @@ -10,6 +10,7 @@ from werkzeug.exceptions import BadRequest, Forbidden from configs import dify_config from controllers.common.errors import NotFoundError from controllers.common.fields import SimpleResultResponse +from controllers.common.rbac import RBACCheck, Workspace from controllers.common.schema import register_response_schema_models, register_schema_models from core.entities.provider_entities import ProviderConfig from core.plugin.entities.plugin_daemon import CredentialType @@ -35,7 +36,6 @@ from services.trigger.trigger_subscription_operator_service import TriggerSubscr from .. import console_ns from ..wraps import ( RBACPermission, - RBACResourceScope, account_initialization_required, edit_permission_required, is_admin_or_owner_required, @@ -199,7 +199,7 @@ class TriggerSubscriptionListApi(Resource): @setup_required @login_required @edit_permission_required - @rbac_permission_required(RBACResourceScope.WORKSPACE, RBACPermission.PLUGIN_PREFERENCES, resource_required=False) + @rbac_permission_required(RBACCheck(RBACPermission.PLUGIN_PREFERENCES, Workspace())) @account_initialization_required @with_current_user @with_current_tenant_id @@ -235,7 +235,7 @@ class TriggerSubscriptionBuilderCreateApi(Resource): @setup_required @login_required @edit_permission_required - @rbac_permission_required(RBACResourceScope.WORKSPACE, RBACPermission.PLUGIN_PREFERENCES, resource_required=False) + @rbac_permission_required(RBACCheck(RBACPermission.PLUGIN_PREFERENCES, Workspace())) @account_initialization_required @with_current_user @with_current_tenant_id @@ -271,7 +271,7 @@ class TriggerSubscriptionBuilderGetApi(Resource): @setup_required @login_required @edit_permission_required - @rbac_permission_required(RBACResourceScope.WORKSPACE, RBACPermission.PLUGIN_PREFERENCES, resource_required=False) + @rbac_permission_required(RBACCheck(RBACPermission.PLUGIN_PREFERENCES, Workspace())) @account_initialization_required @with_current_user @with_current_tenant_id @@ -299,7 +299,7 @@ class TriggerSubscriptionBuilderVerifyApi(Resource): @setup_required @login_required @edit_permission_required - @rbac_permission_required(RBACResourceScope.WORKSPACE, RBACPermission.CREDENTIAL_MANAGE, resource_required=False) + @rbac_permission_required(RBACCheck(RBACPermission.CREDENTIAL_MANAGE, Workspace())) @account_initialization_required @with_current_user @with_current_tenant_id @@ -344,7 +344,7 @@ class TriggerSubscriptionBuilderUpdateApi(Resource): @setup_required @login_required @edit_permission_required - @rbac_permission_required(RBACResourceScope.WORKSPACE, RBACPermission.CREDENTIAL_MANAGE, resource_required=False) + @rbac_permission_required(RBACCheck(RBACPermission.CREDENTIAL_MANAGE, Workspace())) @account_initialization_required @with_current_user @with_current_tenant_id @@ -389,7 +389,7 @@ class TriggerSubscriptionBuilderLogsApi(Resource): @setup_required @login_required @edit_permission_required - @rbac_permission_required(RBACResourceScope.WORKSPACE, RBACPermission.PLUGIN_PREFERENCES, resource_required=False) + @rbac_permission_required(RBACCheck(RBACPermission.PLUGIN_PREFERENCES, Workspace())) @account_initialization_required @with_current_user @with_current_tenant_id @@ -420,7 +420,7 @@ class TriggerSubscriptionBuilderBuildApi(Resource): @setup_required @login_required @edit_permission_required - @rbac_permission_required(RBACResourceScope.WORKSPACE, RBACPermission.PLUGIN_PREFERENCES, resource_required=False) + @rbac_permission_required(RBACCheck(RBACPermission.PLUGIN_PREFERENCES, Workspace())) @account_initialization_required @with_current_user @with_current_tenant_id @@ -464,7 +464,7 @@ class TriggerSubscriptionUpdateApi(Resource): @setup_required @login_required @edit_permission_required - @rbac_permission_required(RBACResourceScope.WORKSPACE, RBACPermission.PLUGIN_PREFERENCES, resource_required=False) + @rbac_permission_required(RBACCheck(RBACPermission.PLUGIN_PREFERENCES, Workspace())) @account_initialization_required @with_current_tenant_id @model_validate(TriggerSubscriptionBuilderUpdatePayload) @@ -524,7 +524,7 @@ class TriggerSubscriptionDeleteApi(Resource): @setup_required @login_required @is_admin_or_owner_required - @rbac_permission_required(RBACResourceScope.WORKSPACE, RBACPermission.PLUGIN_PREFERENCES, resource_required=False) + @rbac_permission_required(RBACCheck(RBACPermission.PLUGIN_PREFERENCES, Workspace())) @account_initialization_required @with_current_tenant_id def post(self, tenant_id: str, subscription_id: str): @@ -716,7 +716,7 @@ class TriggerOAuthClientManageApi(Resource): @setup_required @login_required @is_admin_or_owner_required - @rbac_permission_required(RBACResourceScope.WORKSPACE, RBACPermission.PLUGIN_PREFERENCES, resource_required=False) + @rbac_permission_required(RBACCheck(RBACPermission.PLUGIN_PREFERENCES, Workspace())) @account_initialization_required @with_current_tenant_id def get(self, tenant_id: str, provider: str): @@ -763,7 +763,7 @@ class TriggerOAuthClientManageApi(Resource): @setup_required @login_required @is_admin_or_owner_required - @rbac_permission_required(RBACResourceScope.WORKSPACE, RBACPermission.PLUGIN_PREFERENCES, resource_required=False) + @rbac_permission_required(RBACCheck(RBACPermission.PLUGIN_PREFERENCES, Workspace())) @account_initialization_required @with_current_tenant_id @model_validate(TriggerOAuthClientPayload) @@ -792,7 +792,7 @@ class TriggerOAuthClientManageApi(Resource): @setup_required @login_required @is_admin_or_owner_required - @rbac_permission_required(RBACResourceScope.WORKSPACE, RBACPermission.PLUGIN_PREFERENCES, resource_required=False) + @rbac_permission_required(RBACCheck(RBACPermission.PLUGIN_PREFERENCES, Workspace())) @account_initialization_required @with_current_tenant_id def delete(self, tenant_id: str, provider: str): @@ -826,7 +826,7 @@ class TriggerSubscriptionVerifyApi(Resource): @setup_required @login_required @edit_permission_required - @rbac_permission_required(RBACResourceScope.WORKSPACE, RBACPermission.PLUGIN_PREFERENCES, resource_required=False) + @rbac_permission_required(RBACCheck(RBACPermission.PLUGIN_PREFERENCES, Workspace())) @account_initialization_required @with_current_user @with_current_tenant_id diff --git a/api/controllers/console/wraps.py b/api/controllers/console/wraps.py index a56a6de5b14..749d3243750 100644 --- a/api/controllers/console/wraps.py +++ b/api/controllers/console/wraps.py @@ -11,11 +11,8 @@ from sqlalchemy import select from werkzeug.exceptions import Forbidden, UnprocessableEntity from configs import dify_config -from controllers.common.wraps import ( - RBACPermission, - RBACResourceScope, - rbac_permission_required, -) +from controllers.common.rbac import RBACPermission, RBACResourceScope +from controllers.common.wraps import rbac_permission_required from controllers.console.auth.error import AuthenticationFailedError, EmailCodeError from controllers.console.workspace.error import AccountNotInitializedError from enums import CloudPlan, DeploymentEdition diff --git a/api/controllers/openapi/app_dsl.py b/api/controllers/openapi/app_dsl.py index 5d036580654..0978c2c628f 100644 --- a/api/controllers/openapi/app_dsl.py +++ b/api/controllers/openapi/app_dsl.py @@ -6,12 +6,12 @@ from flask_restx import Resource from sqlalchemy.orm import Session from werkzeug.exceptions import Forbidden -from controllers.common.wraps import RBACPermission, RBACResourceScope +from controllers.common.rbac import PlainApp, RBACCheck, RBACPermission, Workspace from controllers.openapi import openapi_ns from controllers.openapi._contract import accepts, returns from controllers.openapi._models import AppDslExportQuery, AppDslExportResponse, AppDslImportPayload from controllers.openapi.auth.composition import auth_router -from controllers.openapi.auth.data import AuthData, RBACRequirement +from controllers.openapi.auth.data import AuthData from extensions.ext_database import db from libs.oauth_bearer import Scope, TokenType from models import Account, App @@ -40,11 +40,7 @@ class AppDslImportApi(Resource): scope=Scope.WORKSPACE_WRITE, allowed_token_types=frozenset({TokenType.OAUTH_ACCOUNT}), allowed_roles=frozenset({TenantAccountRole.EDITOR, TenantAccountRole.ADMIN, TenantAccountRole.OWNER}), - rbac=RBACRequirement( - resource_type=RBACResourceScope.APP, - scene=RBACPermission.APP_IMPORT_EXPORT_DSL, - resource_required=False, - ), + rbac=RBACCheck(RBACPermission.APP_IMPORT_EXPORT_DSL, Workspace()), ) @returns(200, Import, "Import completed") @returns(202, Import, "Import pending confirmation") @@ -100,11 +96,7 @@ class AppDslImportConfirmApi(Resource): scope=Scope.WORKSPACE_WRITE, allowed_token_types=frozenset({TokenType.OAUTH_ACCOUNT}), allowed_roles=frozenset({TenantAccountRole.EDITOR, TenantAccountRole.ADMIN, TenantAccountRole.OWNER}), - rbac=RBACRequirement( - resource_type=RBACResourceScope.APP, - scene=RBACPermission.APP_IMPORT_EXPORT_DSL, - resource_required=False, - ), + rbac=RBACCheck(RBACPermission.APP_IMPORT_EXPORT_DSL, Workspace()), ) @returns(200, Import, "Import confirmed") @returns(400, Import, "Import failed") @@ -144,7 +136,7 @@ class AppDslExportApi(Resource): scope=Scope.APPS_READ, allowed_token_types=frozenset({TokenType.OAUTH_ACCOUNT}), allowed_roles=frozenset({TenantAccountRole.EDITOR, TenantAccountRole.ADMIN, TenantAccountRole.OWNER}), - rbac=RBACRequirement(resource_type=RBACResourceScope.APP, scene=RBACPermission.APP_IMPORT_EXPORT_DSL), + rbac=RBACCheck(RBACPermission.APP_IMPORT_EXPORT_DSL, PlainApp()), ) @accepts(query=AppDslExportQuery) @returns(200, AppDslExportResponse, "Export successful") @@ -176,7 +168,7 @@ class AppDslCheckDependenciesApi(Resource): scope=Scope.APPS_READ, allowed_token_types=frozenset({TokenType.OAUTH_ACCOUNT}), allowed_roles=frozenset({TenantAccountRole.EDITOR, TenantAccountRole.ADMIN, TenantAccountRole.OWNER}), - rbac=RBACRequirement(resource_type=RBACResourceScope.APP, scene=RBACPermission.APP_IMPORT_EXPORT_DSL), + rbac=RBACCheck(RBACPermission.APP_IMPORT_EXPORT_DSL, PlainApp()), ) @returns(200, CheckDependenciesResult, "Dependencies checked") def get(self, app_id: str, *, auth_data: AuthData): diff --git a/api/controllers/openapi/app_run.py b/api/controllers/openapi/app_run.py index 631a750f0ee..e71d3e72bba 100644 --- a/api/controllers/openapi/app_run.py +++ b/api/controllers/openapi/app_run.py @@ -20,14 +20,14 @@ from werkzeug.exceptions import ( import services from controllers.common.fields import EventStreamResponse -from controllers.common.wraps import RBACPermission, RBACResourceScope +from controllers.common.rbac import PlainApp, RBACCheck, RBACPermission from controllers.console.app.wraps import with_session from controllers.openapi import openapi_ns from controllers.openapi._audit import emit_app_run from controllers.openapi._contract import accepts, returns from controllers.openapi._models import AppRunRequest, TaskStopResponse from controllers.openapi.auth.composition import auth_router -from controllers.openapi.auth.data import AuthData, RBACRequirement +from controllers.openapi.auth.data import AuthData from controllers.service_api.app.error import ( AppUnavailableError, CompletionRequestError, @@ -148,7 +148,7 @@ _DISPATCH: dict[AppMode, Callable[[App, Any, AppRunRequest, Session], Any]] = { class AppRunApi(Resource): @auth_router.guard( scope=Scope.APPS_RUN, - rbac=RBACRequirement(resource_type=RBACResourceScope.APP, scene=RBACPermission.APP_TEST_AND_RUN), + rbac=RBACCheck(RBACPermission.APP_TEST_AND_RUN, PlainApp()), ) @openapi_ns.response(200, "Run result (SSE stream)", openapi_ns.models[EventStreamResponse.__name__]) @accepts(body=AppRunRequest) @@ -184,7 +184,7 @@ class AppRunApi(Resource): class AppRunTaskStopApi(Resource): @auth_router.guard( scope=Scope.APPS_RUN, - rbac=RBACRequirement(resource_type=RBACResourceScope.APP, scene=RBACPermission.APP_TEST_AND_RUN), + rbac=RBACCheck(RBACPermission.APP_TEST_AND_RUN, PlainApp()), ) @returns(200, TaskStopResponse, description="Task stopped") def post(self, app_id: str, task_id: str, *, auth_data: AuthData): diff --git a/api/controllers/openapi/apps.py b/api/controllers/openapi/apps.py index f6dace6fd56..5e1abe27b98 100644 --- a/api/controllers/openapi/apps.py +++ b/api/controllers/openapi/apps.py @@ -12,8 +12,8 @@ from werkzeug.exceptions import Conflict, NotFound, UnprocessableEntity from configs import dify_config from controllers.common.app_access import AppAccessFilter, resolve_app_access_filter from controllers.common.fields import Parameters +from controllers.common.rbac import PlainApp, RBACCheck, RBACPermission from controllers.common.session import with_session -from controllers.common.wraps import RBACPermission, RBACResourceScope from controllers.openapi import openapi_ns from controllers.openapi._contract import accepts, returns from controllers.openapi._input_schema import EMPTY_INPUT_SCHEMA, build_input_schema, resolve_app_config @@ -27,7 +27,7 @@ from controllers.openapi._models import ( AppListRow, ) from controllers.openapi.auth.composition import auth_router -from controllers.openapi.auth.data import AuthData, CallerKind, RBACRequirement +from controllers.openapi.auth.data import AuthData, CallerKind from controllers.service_api.app.error import AppUnavailableError from core.app.app_config.common.parameters_mapping import get_parameters_from_feature_dict from libs.oauth_bearer import Scope, TokenType @@ -135,7 +135,7 @@ class AppDescribeApi(AppReadResource): @auth_router.guard( scope=Scope.APPS_READ, allowed_token_types=frozenset({TokenType.OAUTH_ACCOUNT}), - rbac=RBACRequirement(resource_type=RBACResourceScope.APP, scene=RBACPermission.APP_VIEW_LAYOUT), + rbac=RBACCheck(RBACPermission.APP_VIEW_LAYOUT, PlainApp()), ) @returns(200, AppDescribeResponse, description="App description") @accepts(query=AppDescribeQuery) diff --git a/api/controllers/openapi/auth/data.py b/api/controllers/openapi/auth/data.py index 79d139841aa..ab1d311fb06 100644 --- a/api/controllers/openapi/auth/data.py +++ b/api/controllers/openapi/auth/data.py @@ -6,7 +6,7 @@ from enum import StrEnum from pydantic import BaseModel, ConfigDict, Field from werkzeug.exceptions import InternalServerError -from core.rbac import RBACPermission, RBACResourceScope +from controllers.common.rbac import RBACCheck from libs.oauth_bearer import Scope, TokenType from models.account import Account, Tenant, TenantAccountRole from models.model import App, EndUser @@ -25,23 +25,15 @@ class ExternalIdentity(BaseModel): issuer: str | None = None -class RBACRequirement(BaseModel): - model_config = ConfigDict(frozen=True) - - resource_type: RBACResourceScope - scene: RBACPermission - resource_required: bool = True - - class RequestContext(BaseModel): - model_config = ConfigDict(frozen=True) + model_config = ConfigDict(frozen=True, arbitrary_types_allowed=True) token_type: TokenType scope: Scope | None = None path_params: dict[str, str] workspace_membership: bool = False allowed_roles: frozenset[TenantAccountRole] | None = None - rbac: RBACRequirement | None = None + rbac: RBACCheck | None = None class AuthData(BaseModel): @@ -58,7 +50,7 @@ class AuthData(BaseModel): path_params: dict[str, str] = Field(default_factory=dict) allowed_roles: frozenset[TenantAccountRole] | None = None - rbac: RBACRequirement | None = None + rbac: RBACCheck | None = None app: App | None = None tenant: Tenant | None = None diff --git a/api/controllers/openapi/auth/pipeline.py b/api/controllers/openapi/auth/pipeline.py index 9f14105a942..12c3c9dce6c 100644 --- a/api/controllers/openapi/auth/pipeline.py +++ b/api/controllers/openapi/auth/pipeline.py @@ -17,11 +17,11 @@ from flask_login import user_logged_in from werkzeug.exceptions import Forbidden, NotFound, Unauthorized from configs import dify_config +from controllers.common.rbac import RBACCheck from controllers.openapi._audit import emit_wrong_surface from controllers.openapi.auth.data import ( AuthData, ExternalIdentity, - RBACRequirement, RequestContext, ) from controllers.openapi.auth.flow import When @@ -61,7 +61,7 @@ class AuthPipeline: scope: Scope | None, workspace_membership: bool = False, allowed_roles: frozenset[TenantAccountRole] | None = None, - rbac: RBACRequirement | None = None, + rbac: RBACCheck | None = None, ) -> Any: req_ctx = RequestContext( token_type=identity.token_type, @@ -135,7 +135,7 @@ class PipelineRouter: require_valid_enterprise_license: bool = False, workspace_membership: bool = False, allowed_roles: frozenset[TenantAccountRole] | None = None, - rbac: RBACRequirement | None = None, + rbac: RBACCheck | None = None, ) -> Callable: return self._make_decorator( scope=scope, @@ -155,7 +155,7 @@ class PipelineRouter: edition: frozenset[DeploymentEdition] | None = None, require_valid_enterprise_license: bool = False, allowed_roles: frozenset[TenantAccountRole] | None = None, - rbac: RBACRequirement | None = None, + rbac: RBACCheck | None = None, ) -> Callable: return self._make_decorator( scope=scope, @@ -176,7 +176,7 @@ class PipelineRouter: require_valid_enterprise_license: bool, workspace_membership: bool, allowed_roles: frozenset[TenantAccountRole] | None, - rbac: RBACRequirement | None, + rbac: RBACCheck | None, ) -> Callable: def decorator(view: Callable) -> Callable: @wraps(view) @@ -210,7 +210,7 @@ class PipelineRouter: require_valid_enterprise_license: bool, workspace_membership: bool = False, allowed_roles: frozenset[TenantAccountRole] | None = None, - rbac: RBACRequirement | None = None, + rbac: RBACCheck | None = None, ) -> Any: # 404 not 403 — this edition doesn't expose the feature at all if edition is not None and dify_config.DEPLOYMENT_EDITION not in edition: diff --git a/api/controllers/openapi/auth/verify.py b/api/controllers/openapi/auth/verify.py index b6ef95e3ea3..7420841a8f3 100644 --- a/api/controllers/openapi/auth/verify.py +++ b/api/controllers/openapi/auth/verify.py @@ -4,7 +4,7 @@ from flask import request from werkzeug.exceptions import Forbidden, NotFound, UnprocessableEntity from configs import dify_config -from controllers.common.wraps import enforce_rbac_access +from controllers.common.rbac import enforce_rbac_checks from controllers.openapi.auth.data import AuthData, CallerKind from extensions.ext_database import db from libs.oauth_bearer import Scope, TokenType @@ -62,12 +62,10 @@ def check_rbac_permission(data: AuthData) -> None: return if data.account_id is None or data.tenant is None: raise Forbidden("rbac context missing") - enforce_rbac_access( + enforce_rbac_checks( tenant_id=str(data.tenant.id), account_id=str(data.account_id), - resource_type=req.resource_type, - scene=req.scene, - resource_required=req.resource_required, + checks=[req], path_args=dict(data.path_params), ) diff --git a/api/controllers/openapi/human_input_form.py b/api/controllers/openapi/human_input_form.py index 593887ffcfc..af3fcb8d85f 100644 --- a/api/controllers/openapi/human_input_form.py +++ b/api/controllers/openapi/human_input_form.py @@ -15,14 +15,14 @@ from flask_restx import Resource from werkzeug.exceptions import BadRequest from controllers.common.human_input import HumanInputFormSubmitPayload, stringify_form_default_values +from controllers.common.rbac import PlainApp, RBACCheck, RBACPermission from controllers.common.schema import register_schema_models -from controllers.common.wraps import RBACPermission, RBACResourceScope from controllers.openapi import openapi_ns from controllers.openapi._contract import accepts, returns from controllers.openapi._errors import HumanInputFormNotFound, RecipientSurfaceMismatch from controllers.openapi._models import FormSubmitResponse, HumanInputFormDefinitionResponse from controllers.openapi.auth.composition import auth_router -from controllers.openapi.auth.data import AuthData, CallerKind, RBACRequirement +from controllers.openapi.auth.data import AuthData, CallerKind from core.workflow.human_input_policy import ( HumanInputSurface, is_recipient_type_allowed_for_surface, @@ -65,7 +65,7 @@ class OpenApiWorkflowHumanInputFormApi(Resource): @openapi_ns.response(200, "Form definition", openapi_ns.models[HumanInputFormDefinitionResponse.__name__]) @auth_router.guard( scope=Scope.APPS_RUN, - rbac=RBACRequirement(resource_type=RBACResourceScope.APP, scene=RBACPermission.APP_TEST_AND_RUN), + rbac=RBACCheck(RBACPermission.APP_TEST_AND_RUN, PlainApp()), ) def get(self, app_id: str, form_token: str, *, auth_data: AuthData): app_model, _caller, _caller_kind = auth_data.require_app_context() @@ -84,7 +84,7 @@ class OpenApiWorkflowHumanInputFormApi(Resource): class OpenApiWorkflowHumanInputFormSubmitApi(Resource): @auth_router.guard( scope=Scope.APPS_RUN, - rbac=RBACRequirement(resource_type=RBACResourceScope.APP, scene=RBACPermission.APP_TEST_AND_RUN), + rbac=RBACCheck(RBACPermission.APP_TEST_AND_RUN, PlainApp()), ) @returns(200, FormSubmitResponse, description="Form submitted") @accepts(body=HumanInputFormSubmitPayload) diff --git a/api/controllers/openapi/workflow_events.py b/api/controllers/openapi/workflow_events.py index 1a2bef28772..5e54c301fba 100644 --- a/api/controllers/openapi/workflow_events.py +++ b/api/controllers/openapi/workflow_events.py @@ -18,11 +18,11 @@ from sqlalchemy.orm import sessionmaker from werkzeug.exceptions import NotFound, UnprocessableEntity from controllers.common.fields import EventStreamResponse +from controllers.common.rbac import PlainApp, RBACCheck, RBACPermission from controllers.common.schema import query_params_from_model -from controllers.common.wraps import RBACPermission, RBACResourceScope from controllers.openapi import openapi_ns from controllers.openapi.auth.composition import auth_router -from controllers.openapi.auth.data import AuthData, CallerKind, RBACRequirement +from controllers.openapi.auth.data import AuthData, CallerKind from core.app.apps.advanced_chat.app_generator import AdvancedChatAppGenerator from core.app.apps.base_app_generator import BaseAppGenerator from core.app.apps.common.workflow_response_converter import WorkflowResponseConverter @@ -49,7 +49,7 @@ class OpenApiWorkflowEventsApi(Resource): @openapi_ns.response(200, "SSE event stream", openapi_ns.models[EventStreamResponse.__name__]) @auth_router.guard( scope=Scope.APPS_RUN, - rbac=RBACRequirement(resource_type=RBACResourceScope.APP, scene=RBACPermission.APP_TEST_AND_RUN), + rbac=RBACCheck(RBACPermission.APP_TEST_AND_RUN, PlainApp()), ) def get(self, app_id: str, task_id: str, *, auth_data: AuthData): app_model, caller, caller_kind = auth_data.require_app_context() diff --git a/api/core/rbac/entities.py b/api/core/rbac/entities.py index 853b1703728..e35e12e01e5 100644 --- a/api/core/rbac/entities.py +++ b/api/core/rbac/entities.py @@ -5,11 +5,12 @@ class RBACResourceScope(StrEnum): """Resource scopes accepted by the ``rbac_permission_required`` decorator. ``WORKSPACE`` denotes a workspace-level check that carries no concrete - resource id; ``APP`` and ``DATASET`` are resource-scoped checks. + resource id; ``APP``, ``DATASET`` and ``AGENT`` are resource-scoped checks. """ APP = "app" DATASET = "dataset" + AGENT = "agent" WORKSPACE = "workspace" @@ -41,6 +42,19 @@ class RBACPermission(StrEnum): APP_DELETE = "app_delete" APP_ACCESS_CONFIG = "app_access_config" + AGENT_CREATE = "agent_create" + AGENT_PREVIEW = "agent_preview" + AGENT_EDIT = "agent_edit" + AGENT_TEST_AND_RUN = "agent_test_and_run" + AGENT_RELEASE_AND_VERSION = "agent_release_and_version" + AGENT_ACCESS_POINT_VIEW = "agent_access_point_view" + AGENT_ACCESS_POINT_MANAGE = "agent_access_point_manage" + AGENT_LOG_MANAGE = "agent_log_manage" + AGENT_MONITOR = "agent_monitor" + AGENT_ACCESS_CONFIG = "agent_access_config" + AGENT_IMPORT_EXPORT_DSL = "agent_import_export_dsl" + AGENT_DELETE = "agent_delete" + DATASET_PREVIEW = "dataset_preview" DATASET_READONLY = "dataset_readonly" DATASET_EDIT = "dataset_edit" @@ -61,7 +75,6 @@ class RBACPermission(StrEnum): WORKSPACE_ROLE_MANAGE = "workspace_role_manage" API_EXTENSION_MANAGE = "api_extension_manage" CUSTOMIZATION_MANAGE = "customization_manage" - AGENT_MANAGE = "agent_manage" SKILL_VIEW = "skill_view" SKILL_EDIT = "skill_edit" SKILL_PUBLISH = "skill_publish" @@ -83,3 +96,25 @@ class RBACPermission(StrEnum): TOOL_MANAGE = "tool_manage" MCP_MANAGE = "mcp_manage" + + @property + def scope(self) -> RBACResourceScope: + if self in _FUNCTION_SCOPED_RESOURCE_SCENES: + return RBACResourceScope.WORKSPACE + prefix = self.name.split("_", 1)[0] + return _SCENE_PREFIX_SCOPE.get(prefix, RBACResourceScope.WORKSPACE) + + +_SCENE_PREFIX_SCOPE: dict[str, RBACResourceScope] = { + "APP": RBACResourceScope.APP, + "DATASET": RBACResourceScope.DATASET, + "AGENT": RBACResourceScope.AGENT, +} + +_FUNCTION_SCOPED_RESOURCE_SCENES: frozenset[RBACPermission] = frozenset( + { + RBACPermission.APP_CREATE_AND_MANAGEMENT, + RBACPermission.DATASET_CREATE_AND_MANAGEMENT, + RBACPermission.AGENT_CREATE, + } +) diff --git a/api/extensions/ext_commands.py b/api/extensions/ext_commands.py index 92c47f630f1..290ade6992b 100644 --- a/api/extensions/ext_commands.py +++ b/api/extensions/ext_commands.py @@ -27,6 +27,7 @@ def init_app(app: DifyApp): import_migration_data, install_plugins, install_rag_pipeline_plugins, + migrate_agent_permissions_to_rbac, migrate_data_for_plugin, migrate_dataset_permissions_to_rbac, migrate_member_roles_to_rbac, @@ -60,6 +61,7 @@ def init_app(app: DifyApp): migrate_data_for_plugin, migrate_dataset_permissions_to_rbac, migrate_member_roles_to_rbac, + migrate_agent_permissions_to_rbac, backfill_plugin_auto_upgrade, extract_plugins, extract_unique_plugins, diff --git a/api/models/model.py b/api/models/model.py index ce9314ee03f..9883557099f 100644 --- a/api/models/model.py +++ b/api/models/model.py @@ -15,7 +15,7 @@ import sqlalchemy as sa from flask import request from flask_login import UserMixin # type: ignore[import-untyped] from sqlalchemy import BigInteger, Float, Index, PrimaryKeyConstraint, String, exists, func, select, text -from sqlalchemy.orm import Mapped, Session, mapped_column, validates +from sqlalchemy.orm import Mapped, Session, mapped_column, scoped_session, validates from configs import dify_config from constants import DEFAULT_FILE_NUMBER_LIMITS @@ -508,7 +508,9 @@ class App(Base): agent = self.agent_app_binding_with_session(session=session) return agent.id if agent else None - def agent_app_binding_with_session(self, *, session: Session, include_archived: bool = False) -> Agent | None: + def agent_app_binding_with_session( + self, *, session: Session | scoped_session, include_archived: bool = False + ) -> Agent | None: """For an Agent App (mode=agent), the Agent bound to it. A roster Agent is bound through ``Agent.app_id``; a workflow-only Agent diff --git a/api/openapi/markdown/console-openapi.md b/api/openapi/markdown/console-openapi.md index 2bf4c995c66..74a65d44382 100644 --- a/api/openapi/markdown/console-openapi.md +++ b/api/openapi/markdown/console-openapi.md @@ -2172,7 +2172,7 @@ Update annotation settings for an app | Code | Description | | ---- | ----------- | -| 204 | Annotations deleted successfully | +| 200 | Success | ### [GET] /apps/{app_id}/annotations Get annotations for an app with pagination @@ -2294,7 +2294,7 @@ Export all annotations for an app with CSV injection protection | Code | Description | | ---- | ----------- | -| 204 | Annotation deleted successfully | +| 200 | Success | ### [POST] /apps/{app_id}/annotations/{annotation_id} Update or delete an annotation @@ -3324,9 +3324,9 @@ Create a new tracing configuration for an application #### Responses -| Code | Description | Schema | -| ---- | ----------- | ------ | -| 200 | Success | **application/json**: [WorkflowTriggerResponse](#workflowtriggerresponse)
| +| Code | Description | +| ---- | ----------- | +| 200 | Success | ### [GET] /apps/{app_id}/triggers **Get app triggers list** @@ -4713,7 +4713,7 @@ Full value for one declared output of a published run. | Code | Description | | ---- | ----------- | -| 204 | Workflow deleted successfully | +| 200 | Success | ### [PATCH] /apps/{app_id}/workflows/{workflow_id} **Update workflow attributes** @@ -4861,9 +4861,9 @@ Restore a published workflow version into the draft workflow #### Responses -| Code | Description | Schema | -| ---- | ----------- | ------ | -| 200 | Success | **application/json**: [SimpleResultResponse](#simpleresultresponse)
| +| Code | Description | +| ---- | ----------- | +| 200 | Success | ### [POST] /auth/plugin/datasource/{provider_id}/custom-client #### Parameters @@ -5047,16 +5047,16 @@ Get compliance document download link ### [GET] /data-source/integrates #### Responses -| Code | Description | Schema | -| ---- | ----------- | ------ | -| 200 | Success | **application/json**: [DataSourceIntegrateListResponse](#datasourceintegratelistresponse)
| +| Code | Description | +| ---- | ----------- | +| 200 | Success | ### [PATCH] /data-source/integrates #### Responses -| Code | Description | Schema | -| ---- | ----------- | ------ | -| 200 | Success | **application/json**: [SimpleResultResponse](#simpleresultresponse)
| +| Code | Description | +| ---- | ----------- | +| 200 | Success | ### [GET] /data-source/integrates/{binding_id}/{action} #### Parameters @@ -5068,9 +5068,9 @@ Get compliance document download link #### Responses -| Code | Description | Schema | -| ---- | ----------- | ------ | -| 200 | Success | **application/json**: [DataSourceIntegrateListResponse](#datasourceintegratelistresponse)
| +| Code | Description | +| ---- | ----------- | +| 200 | Success | ### [PATCH] /data-source/integrates/{binding_id}/{action} #### Parameters @@ -5082,9 +5082,9 @@ Get compliance document download link #### Responses -| Code | Description | Schema | -| ---- | ----------- | ------ | -| 200 | Success | **application/json**: [SimpleResultResponse](#simpleresultresponse)
| +| Code | Description | +| ---- | ----------- | +| 200 | Success | ### [GET] /datasets Get list of datasets @@ -7869,17 +7869,11 @@ Update account-level Step-by-step Tour state | ---- | ---------- | ----------- | -------- | ------ | | pipeline_id | path | | Yes | string (uuid) | -#### Request Body - -| Required | Schema | -| -------- | ------ | -| Yes | **application/json**: [DraftWorkflowSyncPayload](#draftworkflowsyncpayload)
| - #### Responses -| Code | Description | Schema | -| ---- | ----------- | ------ | -| 200 | Success | **application/json**: [RagPipelineWorkflowSyncResponse](#ragpipelineworkflowsyncresponse)
| +| Code | Description | +| ---- | ----------- | +| 200 | Success | ### [POST] /rag/pipelines/{pipeline_id}/workflows/draft/datasource/nodes/{node_id}/run **Run rag pipeline datasource** @@ -8179,12 +8173,6 @@ Update account-level Step-by-step Tour state | pipeline_id | path | | Yes | string (uuid) | | variable_id | path | | Yes | string (uuid) | -#### Request Body - -| Required | Schema | -| -------- | ------ | -| Yes | **application/json**: [WorkflowDraftVariablePatchPayload](#workflowdraftvariablepatchpayload)
| - #### Responses | Code | Description | Schema | @@ -8359,12 +8347,6 @@ Update account-level Step-by-step Tour state | pipeline_id | path | | Yes | string (uuid) | | workflow_id | path | | Yes | string | -#### Request Body - -| Required | Schema | -| -------- | ------ | -| Yes | **application/json**: [WorkflowUpdatePayload](#workflowupdatepayload)
| - #### Responses | Code | Description | Schema | @@ -11159,16 +11141,16 @@ Returns permission flags that control workspace features like member invitations ### [GET] /workspaces/current/rbac/access-policies #### Responses -| Code | Description | Schema | -| ---- | ----------- | ------ | -| 200 | Success | **application/json**: [_AccessPolicyList](#_accesspolicylist)
| +| Code | Description | +| ---- | ----------- | +| 200 | Success | ### [POST] /workspaces/current/rbac/access-policies #### Responses -| Code | Description | Schema | -| ---- | ----------- | ------ | -| 201 | Policy created | **application/json**: [AccessPolicy](#accesspolicy)
| +| Code | Description | +| ---- | ----------- | +| 200 | Success | ### [DELETE] /workspaces/current/rbac/access-policies/{policy_id} #### Parameters @@ -11179,9 +11161,9 @@ Returns permission flags that control workspace features like member invitations #### Responses -| Code | Description | Schema | -| ---- | ----------- | ------ | -| 200 | Success | **application/json**: [AccessPolicy](#accesspolicy)
| +| Code | Description | +| ---- | ----------- | +| 200 | Success | ### [GET] /workspaces/current/rbac/access-policies/{policy_id} #### Parameters @@ -11192,9 +11174,9 @@ Returns permission flags that control workspace features like member invitations #### Responses -| Code | Description | Schema | -| ---- | ----------- | ------ | -| 200 | Success | **application/json**: [AccessPolicy](#accesspolicy)
| +| Code | Description | +| ---- | ----------- | +| 200 | Success | ### [PUT] /workspaces/current/rbac/access-policies/{policy_id} #### Parameters @@ -11205,9 +11187,9 @@ Returns permission flags that control workspace features like member invitations #### Responses -| Code | Description | Schema | -| ---- | ----------- | ------ | -| 200 | Success | **application/json**: [AccessPolicy](#accesspolicy)
| +| Code | Description | +| ---- | ----------- | +| 200 | Success | ### [POST] /workspaces/current/rbac/access-policies/{policy_id}/copy #### Parameters @@ -11218,9 +11200,9 @@ Returns permission flags that control workspace features like member invitations #### Responses -| Code | Description | Schema | -| ---- | ----------- | ------ | -| 201 | Policy copied | **application/json**: [AccessPolicy](#accesspolicy)
| +| Code | Description | +| ---- | ----------- | +| 200 | Success | ### [PUT] /workspaces/current/rbac/access-policy-bindings/{binding_id}/lock #### Parameters @@ -11231,9 +11213,9 @@ Returns permission flags that control workspace features like member invitations #### Responses -| Code | Description | Schema | -| ---- | ----------- | ------ | -| 200 | Success | **application/json**: [AccessPolicyBindingState](#accesspolicybindingstate)
| +| Code | Description | +| ---- | ----------- | +| 200 | Success | ### [PUT] /workspaces/current/rbac/access-policy-bindings/{binding_id}/unlock #### Parameters @@ -11244,9 +11226,153 @@ Returns permission flags that control workspace features like member invitations #### Responses +| Code | Description | +| ---- | ----------- | +| 200 | Success | + +### [DELETE] /workspaces/current/rbac/agents/{agent_id}/access-policies/{policy_id}/member-bindings +#### Parameters + +| Name | Located in | Description | Required | Schema | +| ---- | ---------- | ----------- | -------- | ------ | +| agent_id | path | | Yes | string (uuid) | +| policy_id | path | | Yes | string | + +#### Request Body + +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [_DeleteMemberBindingsRequest](#_deletememberbindingsrequest)
| + +#### Responses + | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Success | **application/json**: [AccessPolicyBindingState](#accesspolicybindingstate)
| +| 200 | Success | **application/json**: [MemberBindingsResponse](#memberbindingsresponse)
| + +### [GET] /workspaces/current/rbac/agents/{agent_id}/access-policies/{policy_id}/member-bindings +#### Parameters + +| Name | Located in | Description | Required | Schema | +| ---- | ---------- | ----------- | -------- | ------ | +| agent_id | path | | Yes | string (uuid) | +| policy_id | path | | Yes | string | + +#### Responses + +| Code | Description | Schema | +| ---- | ----------- | ------ | +| 200 | Success | **application/json**: [MemberBindingsResponse](#memberbindingsresponse)
| + +### [GET] /workspaces/current/rbac/agents/{agent_id}/access-policies/{policy_id}/role-bindings +#### Parameters + +| Name | Located in | Description | Required | Schema | +| ---- | ---------- | ----------- | -------- | ------ | +| agent_id | path | | Yes | string (uuid) | +| policy_id | path | | Yes | string (uuid) | + +#### Responses + +| Code | Description | Schema | +| ---- | ----------- | ------ | +| 200 | Success | **application/json**: [RoleBindingsResponse](#rolebindingsresponse)
| + +### [GET] /workspaces/current/rbac/agents/{agent_id}/access-policy +#### Parameters + +| Name | Located in | Description | Required | Schema | +| ---- | ---------- | ----------- | -------- | ------ | +| language | query | Localized policy label language | No | string,
**Available values:** "en", "ja", "zh" | +| agent_id | path | | Yes | string (uuid) | + +#### Responses + +| Code | Description | Schema | +| ---- | ----------- | ------ | +| 200 | Success | **application/json**: [AgentAccessMatrix](#agentaccessmatrix)
| + +### [GET] /workspaces/current/rbac/agents/{agent_id}/user-access-policies +#### Parameters + +| Name | Located in | Description | Required | Schema | +| ---- | ---------- | ----------- | -------- | ------ | +| language | query | Localized policy label language | No | string,
**Available values:** "en", "ja", "zh" | +| limit | query | | No | integer | +| page | query | | No | integer | +| reverse | query | | No | boolean | +| agent_id | path | | Yes | string (uuid) | + +#### Responses + +| Code | Description | Schema | +| ---- | ----------- | ------ | +| 200 | Success | **application/json**: [ResourceUserAccessPoliciesResponse](#resourceuseraccesspoliciesresponse)
| + +### [PUT] /workspaces/current/rbac/agents/{agent_id}/users/{target_account_id}/access-policies +#### Parameters + +| Name | Located in | Description | Required | Schema | +| ---- | ---------- | ----------- | -------- | ------ | +| agent_id | path | | Yes | string (uuid) | +| target_account_id | path | | Yes | string (uuid) | + +#### Request Body + +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [ReplaceUserAccessPolicies](#replaceuseraccesspolicies)
| + +#### Responses + +| Code | Description | Schema | +| ---- | ----------- | ------ | +| 200 | Success | **application/json**: [ReplaceUserAccessPoliciesResponse](#replaceuseraccesspoliciesresponse)
| + +### [GET] /workspaces/current/rbac/agents/{agent_id}/whitelist +#### Parameters + +| Name | Located in | Description | Required | Schema | +| ---- | ---------- | ----------- | -------- | ------ | +| agent_id | path | | Yes | string (uuid) | + +#### Responses + +| Code | Description | Schema | +| ---- | ----------- | ------ | +| 200 | Success | **application/json**: [ResourceWhitelist](#resourcewhitelist)
| + +### [PUT] /workspaces/current/rbac/agents/{agent_id}/whitelist +#### Parameters + +| Name | Located in | Description | Required | Schema | +| ---- | ---------- | ----------- | -------- | ------ | +| agent_id | path | | Yes | string (uuid) | + +#### Request Body + +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [_ResourceAccessScopeRequest](#_resourceaccessscoperequest)
| + +#### Responses + +| Code | Description | Schema | +| ---- | ----------- | ------ | +| 200 | Success | **application/json**: [ResourceWhitelist](#resourcewhitelist)
| + +### [GET] /workspaces/current/rbac/agents/{agent_id}/whitelist_config +#### Parameters + +| Name | Located in | Description | Required | Schema | +| ---- | ---------- | ----------- | -------- | ------ | +| agent_id | path | | Yes | string (uuid) | + +#### Responses + +| Code | Description | Schema | +| ---- | ----------- | ------ | +| 200 | Success | **application/json**: [ResourceWhitelistConfig](#resourcewhitelistconfig)
| ### [DELETE] /workspaces/current/rbac/apps/{app_id}/access-policies/{policy_id}/member-bindings #### Parameters @@ -11582,6 +11708,13 @@ Returns permission flags that control workspace features like member invitations | ---- | ----------- | ------ | | 200 | Success | **application/json**: [PermissionCatalogResponse](#permissioncatalogresponse)
| +### [GET] /workspaces/current/rbac/role-permissions/catalog/agent +#### Responses + +| Code | Description | Schema | +| ---- | ----------- | ------ | +| 200 | Success | **application/json**: [PermissionCatalogResponse](#permissioncatalogresponse)
| + ### [GET] /workspaces/current/rbac/role-permissions/catalog/app #### Responses @@ -11599,16 +11732,16 @@ Returns permission flags that control workspace features like member invitations ### [GET] /workspaces/current/rbac/roles #### Responses -| Code | Description | Schema | -| ---- | ----------- | ------ | -| 200 | Success | **application/json**: [_RBACRoleList](#_rbacrolelist)
| +| Code | Description | +| ---- | ----------- | +| 200 | Success | ### [POST] /workspaces/current/rbac/roles #### Responses -| Code | Description | Schema | -| ---- | ----------- | ------ | -| 201 | Role created | **application/json**: [RBACRole](#rbacrole)
| +| Code | Description | +| ---- | ----------- | +| 200 | Success | ### [DELETE] /workspaces/current/rbac/roles/{role_id} #### Parameters @@ -11619,9 +11752,9 @@ Returns permission flags that control workspace features like member invitations #### Responses -| Code | Description | Schema | -| ---- | ----------- | ------ | -| 200 | Success | **application/json**: [RBACRole](#rbacrole)
| +| Code | Description | +| ---- | ----------- | +| 200 | Success | ### [GET] /workspaces/current/rbac/roles/{role_id} #### Parameters @@ -11632,9 +11765,9 @@ Returns permission flags that control workspace features like member invitations #### Responses -| Code | Description | Schema | -| ---- | ----------- | ------ | -| 200 | Success | **application/json**: [RBACRole](#rbacrole)
| +| Code | Description | +| ---- | ----------- | +| 200 | Success | ### [PUT] /workspaces/current/rbac/roles/{role_id} #### Parameters @@ -11645,9 +11778,9 @@ Returns permission flags that control workspace features like member invitations #### Responses -| Code | Description | Schema | -| ---- | ----------- | ------ | -| 200 | Success | **application/json**: [RBACRole](#rbacrole)
| +| Code | Description | +| ---- | ----------- | +| 200 | Success | ### [POST] /workspaces/current/rbac/roles/{role_id}/copy #### Parameters @@ -11658,9 +11791,9 @@ Returns permission flags that control workspace features like member invitations #### Responses -| Code | Description | Schema | -| ---- | ----------- | ------ | -| 201 | Role copied | **application/json**: [RBACRole](#rbacrole)
| +| Code | Description | +| ---- | ----------- | +| 200 | Success | ### [GET] /workspaces/current/rbac/roles/{role_id}/members #### Parameters @@ -11675,6 +11808,58 @@ Returns permission flags that control workspace features like member invitations | ---- | ----------- | ------ | | 200 | Success | **application/json**: [_MembersInRoleList](#_membersinrolelist)
| +### [PUT] /workspaces/current/rbac/workspace/agents/access-policies/{policy_id}/bindings +#### Parameters + +| Name | Located in | Description | Required | Schema | +| ---- | ---------- | ----------- | -------- | ------ | +| policy_id | path | | Yes | string (uuid) | + +#### Request Body + +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [_ReplaceBindingsRequest](#_replacebindingsrequest)
| + +#### Responses + +| Code | Description | Schema | +| ---- | ----------- | ------ | +| 200 | Success | **application/json**: [AccessMatrixItem](#accessmatrixitem)
| + +### [GET] /workspaces/current/rbac/workspace/agents/access-policies/{policy_id}/member-bindings +#### Parameters + +| Name | Located in | Description | Required | Schema | +| ---- | ---------- | ----------- | -------- | ------ | +| policy_id | path | | Yes | string (uuid) | + +#### Responses + +| Code | Description | Schema | +| ---- | ----------- | ------ | +| 200 | Success | **application/json**: [MemberBindingsResponse](#memberbindingsresponse)
| + +### [GET] /workspaces/current/rbac/workspace/agents/access-policies/{policy_id}/role-bindings +#### Parameters + +| Name | Located in | Description | Required | Schema | +| ---- | ---------- | ----------- | -------- | ------ | +| policy_id | path | | Yes | string (uuid) | + +#### Responses + +| Code | Description | Schema | +| ---- | ----------- | ------ | +| 200 | Success | **application/json**: [RoleBindingsResponse](#rolebindingsresponse)
| + +### [GET] /workspaces/current/rbac/workspace/agents/access-policy +#### Responses + +| Code | Description | Schema | +| ---- | ----------- | ------ | +| 200 | Success | **application/json**: [WorkspaceAccessMatrix](#workspaceaccessmatrix)
| + ### [PUT] /workspaces/current/rbac/workspace/apps/access-policies/{policy_id}/bindings #### Parameters @@ -13401,6 +13586,13 @@ Model class for AI model. | chat_prompt_config | object | | No | | completion_prompt_config | object | | No | +#### AgentAccessMatrix + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| agent_id | string | | No | +| items | [ [AccessMatrixItem](#accessmatrixitem) ] | | No | + #### AgentApiAccessResponse | Name | Type | Description | Required | @@ -19598,6 +19790,7 @@ Model with provider entity. | Name | Type | Description | Required | | ---- | ---- | ----------- | -------- | +| agent | [ResourcePermissionSnapshot](#resourcepermissionsnapshot) | | No | | app | [ResourcePermissionSnapshot](#resourcepermissionsnapshot) | | No | | dataset | [ResourcePermissionSnapshot](#resourcepermissionsnapshot) | | No | | workspace | [WorkspacePermissionSnapshot](#workspacepermissionsnapshot) | | No | diff --git a/api/services/agent/roster_service.py b/api/services/agent/roster_service.py index a45ab42bf28..e781feb169e 100644 --- a/api/services/agent/roster_service.py +++ b/api/services/agent/roster_service.py @@ -44,6 +44,7 @@ from services.agent.workspace_service import AgentWorkspaceNotFoundError, AgentW from services.app_service import AppService, CreateAppParams from services.enterprise.enterprise_service import EnterpriseService from services.entities.agent_entities import RosterAgentCreatePayload, RosterAgentUpdatePayload +from services.rbac_agent_access_service import initialize_agent_rbac_access from services.system_feature_service import SystemFeatureService from tasks.collect_agent_resources_task import enqueue_agent_resource_collection @@ -314,11 +315,13 @@ class AgentRosterService: source=source, ) self._session.commit() - return agent except IntegrityError as exc: self._session.rollback() raise AgentNameConflictError() from exc + initialize_agent_rbac_access(tenant_id=tenant_id, agent_id=agent.id, creator_account_id=account_id) + return agent + def _create_roster_agent_in_transaction( self, *, diff --git a/api/services/app_dsl_service.py b/api/services/app_dsl_service.py index a67ae313813..87998b7a0a5 100644 --- a/api/services/app_dsl_service.py +++ b/api/services/app_dsl_service.py @@ -44,6 +44,7 @@ from graphon.nodes.question_classifier.entities import QuestionClassifierNodeDat from graphon.nodes.tool.entities import ToolNodeData from libs.datetime_utils import naive_utc_now from models import Account, App, AppMode +from models.agent import AgentScope from models.model import AppModelConfig, AppModelConfigDict, IconType, load_annotation_reply_config from models.workflow import Workflow from services.agent.dsl_service import AgentDslService, AgentPackage @@ -456,20 +457,27 @@ class AppDslService: raise NoPermissionError("You do not have permission to overwrite this app") return app - @staticmethod - def _ensure_agent_manage_permission(account: Account) -> None: - """Importing an Agent DSL creates a roster Agent, which requires ``agent.manage``.""" + def _ensure_agent_import_permission(self, account: Account, *, app: App | None) -> None: if not dify_config.RBAC_ENABLED: return if account.current_tenant_id is None: raise ValueError("Current tenant is not set") + binding = ( + app.agent_app_binding_with_session(session=self._session, include_archived=True) + if app is not None + else None + ) + if binding is not None and binding.scope == AgentScope.WORKFLOW_ONLY: + raise NoPermissionError("Agent DSL import permission is required to import an Agent App") allowed = RBACService.CheckAccess.check( account.current_tenant_id, account.id, - scene=RBACPermission.AGENT_MANAGE, + scene=RBACPermission.AGENT_IMPORT_EXPORT_DSL, + resource_type=RBACResourceScope.AGENT if binding is not None else None, + resource_id=str(binding.id) if binding is not None else None, ) if not allowed: - raise NoPermissionError("Agent management permission is required to import an Agent App") + raise NoPermissionError("Agent DSL import permission is required to import an Agent App") def _create_or_update_app( self, @@ -492,7 +500,7 @@ class AppDslService: raise ValueError("loss app mode") app_mode = AppMode(app_mode) if app_mode == AppMode.AGENT: - self._ensure_agent_manage_permission(account) + self._ensure_agent_import_permission(account, app=app) # Set icon type icon_type_value = icon_type or app_data.get("icon_type") diff --git a/api/services/app_service.py b/api/services/app_service.py index 30797c97ede..e90e962cf2b 100644 --- a/api/services/app_service.py +++ b/api/services/app_service.py @@ -1,6 +1,6 @@ import json import logging -from collections.abc import Sequence +from collections.abc import Callable, Sequence from dataclasses import dataclass from datetime import datetime from typing import Any, Literal, NotRequired, TypedDict, cast, override @@ -50,6 +50,7 @@ from services.billing_service import BillingService from services.enterprise import rbac_service as enterprise_rbac_service from services.enterprise.enterprise_service import EnterpriseService from services.openapi.visibility import apply_openapi_gate, is_openapi_visible +from services.rbac_agent_access_service import initialize_agent_rbac_access from services.system_feature_service import SystemFeatureService from services.tag_service import TagService from tasks.collect_agent_resources_task import enqueue_agent_resource_collection @@ -74,6 +75,38 @@ RECENT_APP_MODES: tuple[RecentAppMode, ...] = ( ) +@dataclass(frozen=True) +class _CreatedApp: + tenant_id: str + creator_account_id: str + app_id: str + backing_agent_id: str | None + + +def _initialize_created_app_access(created: _CreatedApp) -> None: + enterprise_rbac_service.try_sync_creator_access_policy_member_bindings( + created.tenant_id, + created.creator_account_id, + enterprise_rbac_service.RBACResourceType.APP, + created.app_id, + ) + + +def _initialize_created_agent_access(created: _CreatedApp) -> None: + if created.backing_agent_id is None: + raise ValueError(f"agent app {created.app_id} was created without a backing agent") + initialize_agent_rbac_access( + tenant_id=created.tenant_id, + agent_id=created.backing_agent_id, + creator_account_id=created.creator_account_id, + ) + + +_CREATED_APP_ACCESS_INITIALIZERS: dict[AppMode, Callable[[_CreatedApp], None]] = { + AppMode.AGENT: _initialize_created_agent_access, +} + + class AppListBaseParams(BaseModel): page: int = Field(default=1, ge=1) limit: int = Field(default=20, ge=1, le=100) @@ -670,12 +703,13 @@ class AppService: # Created in the same transaction so the App and its backing Agent persist # atomically; the Agent Soul (model/prompt/tools) is configured afterward # in the Composer. + backing_agent: Agent | None = None if app_mode == AppMode.AGENT: from services.agent.roster_service import AgentRosterService icon_type = AgentIconType(params.icon_type) if params.icon_type else None try: - AgentRosterService(session).create_backing_agent_for_app( + backing_agent = AgentRosterService(session).create_backing_agent_for_app( tenant_id=tenant_id, account_id=account.id, app_id=app.id, @@ -696,11 +730,14 @@ class AppService: session.commit() app_was_created.send(app, account=account, session=session) session.commit() - enterprise_rbac_service.try_sync_creator_access_policy_member_bindings( - tenant_id, - account.id, - enterprise_rbac_service.RBACResourceType.APP, - app.id, + initialize_access = _CREATED_APP_ACCESS_INITIALIZERS.get(app_mode, _initialize_created_app_access) + initialize_access( + _CreatedApp( + tenant_id=tenant_id, + creator_account_id=account.id, + app_id=app.id, + backing_agent_id=backing_agent.id if backing_agent else None, + ) ) if SystemFeatureService.is_webapp_auth_enabled(): diff --git a/api/services/enterprise/rbac_service.py b/api/services/enterprise/rbac_service.py index b511bfbb103..0edeb9485af 100644 --- a/api/services/enterprise/rbac_service.py +++ b/api/services/enterprise/rbac_service.py @@ -2,6 +2,7 @@ from __future__ import annotations import logging from collections.abc import Sequence +from dataclasses import dataclass from enum import StrEnum from typing import Any, TypeVar @@ -53,11 +54,27 @@ class MembersInRole(_RBACModel): account_name: str = "" +@dataclass(frozen=True) +class _ResourceAccessRoute: + segment: str + id_param: str + + class RBACResourceType(StrEnum): """Resource types understood by access policies.""" APP = "app" DATASET = "dataset" + AGENT = "agent" + + @property + def route(self) -> _ResourceAccessRoute: + routes = { + RBACResourceType.APP: _ResourceAccessRoute("apps", "app_id"), + RBACResourceType.DATASET: _ResourceAccessRoute("datasets", "dataset_id"), + RBACResourceType.AGENT: _ResourceAccessRoute("agents", "agent_id"), + } + return routes[self] class RBACRoleType(StrEnum): @@ -211,6 +228,11 @@ class DatasetAccessMatrix(_RBACModel): items: list[AccessMatrixItem] = Field(default_factory=list) +class AgentAccessMatrix(_RBACModel): + agent_id: str = Field(default="", validation_alias=AliasChoices("agent_id", "resource_id")) + items: list[AccessMatrixItem] = Field(default_factory=list) + + class WorkspaceAccessMatrix(_RBACModel): items: list[AccessMatrixItem] = Field(default_factory=list) pagination: Pagination | None = None @@ -281,6 +303,24 @@ class _LegacyResourceWhitelistConfig(_RBACModel): return value +class LegacyAgentRoleMigration(_RBACModel): + role_id: str + role_name: str = "" + added_keys: list[str] = Field(default_factory=list) + removed_keys: list[str] = Field(default_factory=list) + bound_policies: list[str] = Field(default_factory=list) + skipped: str = "" + + +class LegacyAgentMigrationReport(_RBACModel): + roles: list[LegacyAgentRoleMigration] = Field(default_factory=list) + role_templates: list[LegacyAgentRoleMigration] = Field(default_factory=list) + + +class ConfiguredAgentIDs(_RBACModel): + configured_agent_ids: list[str] = Field(default_factory=list) + + class ResourceWhitelistResources(_RBACModel): unrestricted: bool = False resource_ids: list[str] = Field(default_factory=list) @@ -339,6 +379,12 @@ class AppendDatasetWhitelistMembersBatchItem(_RBACModel): policy_id: str +class AppendAgentWhitelistMembersBatchItem(_RBACModel): + agent_id: str + account_ids: list[str] = Field(default_factory=list) + policy_id: str + + class MemberRolesResponse(_RBACModel): account_id: str roles: list[RBACRole] = Field(default_factory=list) @@ -369,6 +415,14 @@ class MyPermissionsResponse(_RBACModel): workspace: WorkspacePermissionSnapshot = Field(default_factory=WorkspacePermissionSnapshot) app: ResourcePermissionSnapshot = Field(default_factory=ResourcePermissionSnapshot) dataset: ResourcePermissionSnapshot = Field(default_factory=ResourcePermissionSnapshot) + agent: ResourcePermissionSnapshot = Field(default_factory=ResourcePermissionSnapshot) + + def resource_snapshot(self, resource_type: RBACResourceType) -> ResourcePermissionSnapshot: + return { + RBACResourceType.APP: self.app, + RBACResourceType.DATASET: self.dataset, + RBACResourceType.AGENT: self.agent, + }[resource_type] # Fallback permission snapshots for legacy Dify tenant roles when external RBAC is disabled. @@ -404,7 +458,9 @@ _LEGACY_WORKSPACE_OWNER_KEYS: list[str] = [ "snippets.management", "tool.manage", "mcp.manage", - "agent.manage", + "agent.create", + "agent.acl.preview", + "agent.acl.access_point_view", ] _LEGACY_WORKSPACE_ADMIN_KEYS: list[str] = [ @@ -436,7 +492,9 @@ _LEGACY_WORKSPACE_ADMIN_KEYS: list[str] = [ "snippets.management", "tool.manage", "mcp.manage", - "agent.manage", + "agent.create", + "agent.acl.preview", + "agent.acl.access_point_view", ] _LEGACY_WORKSPACE_EDITOR_KEYS: list[str] = [ @@ -455,7 +513,9 @@ _LEGACY_WORKSPACE_EDITOR_KEYS: list[str] = [ "dataset.external.connect", "snippets.create_and_modify", "tool.manage", - "agent.manage", + "agent.create", + "agent.acl.preview", + "agent.acl.access_point_view", ] _LEGACY_WORKSPACE_NORMAL_KEYS: list[str] = [ @@ -464,7 +524,8 @@ _LEGACY_WORKSPACE_NORMAL_KEYS: list[str] = [ "plugin.install", "credential.use", "app_library.access", - "agent.manage", + "agent.acl.preview", + "agent.acl.access_point_view", ] _LEGACY_WORKSPACE_DATASET_OPERATOR_KEYS: list[str] = [ @@ -472,7 +533,8 @@ _LEGACY_WORKSPACE_DATASET_OPERATOR_KEYS: list[str] = [ "plugin.install", "dataset.create_and_management", "dataset.external.connect", - "agent.manage", + "agent.acl.preview", + "agent.acl.access_point_view", ] _LEGACY_APP_OWNER_KEYS: list[str] = [ @@ -589,30 +651,54 @@ _LEGACY_DATASET_DATASET_OPERATOR_KEYS: list[str] = [ "dataset.acl.pipeline_release", ] +_LEGACY_AGENT_FULL_ACCESS_KEYS: list[str] = [ + "agent.acl.preview", + "agent.acl.edit", + "agent.acl.test_and_run", + "agent.acl.release_and_version", + "agent.acl.access_point_view", + "agent.acl.access_point_manage", + "agent.acl.log_manage", + "agent.acl.monitor", + "agent.acl.access_config", + "agent.acl.import_export_dsl", + "agent.acl.delete", +] + +_LEGACY_AGENT_PREVIEW_KEYS: list[str] = [ + "agent.acl.preview", + "agent.acl.access_point_view", +] + _LEGACY_MY_PERMISSIONS: dict[TenantAccountRole, dict[str, list[str]]] = { TenantAccountRole.OWNER: { "workspace": _LEGACY_WORKSPACE_OWNER_KEYS, "app": _LEGACY_APP_OWNER_KEYS, "dataset": _LEGACY_DATASET_OWNER_KEYS, + "agent": _LEGACY_AGENT_FULL_ACCESS_KEYS, }, TenantAccountRole.ADMIN: { "workspace": _LEGACY_WORKSPACE_ADMIN_KEYS, "app": _LEGACY_APP_ADMIN_KEYS, "dataset": _LEGACY_DATASET_ADMIN_KEYS, + "agent": _LEGACY_AGENT_FULL_ACCESS_KEYS, }, TenantAccountRole.EDITOR: { "workspace": _LEGACY_WORKSPACE_EDITOR_KEYS, "app": _LEGACY_APP_EDITOR_KEYS, "dataset": _LEGACY_DATASET_EDITOR_KEYS, + "agent": _LEGACY_AGENT_FULL_ACCESS_KEYS, }, TenantAccountRole.NORMAL: { "workspace": _LEGACY_WORKSPACE_NORMAL_KEYS, "app": _LEGACY_APP_NORMAL_KEYS, + "agent": _LEGACY_AGENT_PREVIEW_KEYS, }, TenantAccountRole.DATASET_OPERATOR: { "workspace": _LEGACY_WORKSPACE_DATASET_OPERATOR_KEYS, "app": _LEGACY_APP_DATASET_OPERATOR_KEYS, "dataset": _LEGACY_DATASET_DATASET_OPERATOR_KEYS, + "agent": _LEGACY_AGENT_PREVIEW_KEYS, }, } @@ -625,6 +711,7 @@ def _legacy_role_permission_keys(role: TenantAccountRole) -> list[str]: *permissions.get("workspace", []), *permissions.get("app", []), *permissions.get("dataset", []), + *permissions.get("agent", []), ] ) ) @@ -681,6 +768,7 @@ def _legacy_my_permissions(tenant_id: str, account_id: str | None, *, session: S workspace=WorkspacePermissionSnapshot(permission_keys=list(permissions.get("workspace", []))), app=ResourcePermissionSnapshot(default_permission_keys=list(permissions.get("app", []))), dataset=ResourcePermissionSnapshot(default_permission_keys=list(permissions.get("dataset", []))), + agent=ResourcePermissionSnapshot(default_permission_keys=list(permissions.get("agent", []))), ) @@ -693,10 +781,7 @@ def _legacy_resource_permission_keys_batch( session: Session, ) -> dict[str, list[str]]: snapshot = _legacy_my_permissions(tenant_id, account_id, session=session) - if resource_type == RBACResourceType.APP: - permission_keys = snapshot.app.default_permission_keys - else: - permission_keys = snapshot.dataset.default_permission_keys + permission_keys = snapshot.resource_snapshot(resource_type).default_permission_keys return {str(resource_id): list(permission_keys) for resource_id in resource_ids} @@ -830,13 +915,8 @@ def _inner_call( def _resource_id_params(resource_type: RBACResourceType | str, resource_id: str) -> dict[str, str]: - resource_type_value = resource_type.value if isinstance(resource_type, RBACResourceType) else str(resource_type) - resource_id = resource_id.strip() - if resource_type_value == RBACResourceType.APP.value: - return {"resource_type": resource_type_value, "app_id": resource_id} - if resource_type_value == RBACResourceType.DATASET.value: - return {"resource_type": resource_type_value, "dataset_id": resource_id} - raise ValueError(f"unsupported resource_type: {resource_type_value}") + resolved = resource_type if isinstance(resource_type, RBACResourceType) else RBACResourceType(resource_type) + return {"resource_type": resolved.value, resolved.route.id_param: resource_id.strip()} def try_sync_creator_access_policy_member_bindings( @@ -866,6 +946,333 @@ def try_sync_creator_access_policy_member_bindings( ) +class _ResourceAccessClient[MatrixT: _RBACModel]: + def __init__( + self, + route: _ResourceAccessRoute, + matrix_model: type[MatrixT], + *, + replace_user_policies_exclude_unset: bool = False, + ) -> None: + self._route = route + self._matrix_model = matrix_model + self._replace_user_policies_exclude_unset = replace_user_policies_exclude_unset + + def _path(self, suffix: str) -> str: + return f"{_INNER_PREFIX}/{self._route.segment}/{suffix}" + + def _params(self, resource_id: str, policy_id: str | None = None) -> dict[str, object]: + params: dict[str, object] = {self._route.id_param: resource_id} + if policy_id is not None: + params["policy_id"] = policy_id + return params + + def whitelist_resources(self, tenant_id: str, account_id: str | None) -> ResourceWhitelistResources: + data = _inner_call( + "GET", + self._path("whitelist/resources"), + tenant_id=tenant_id, + account_id=account_id, + ) + return ResourceWhitelistResources.model_validate(data or {}) + + def user_access_policies( + self, + tenant_id: str, + account_id: str | None, + resource_id: str, + *, + options: ListOption | None = None, + ) -> ResourceUserAccessPoliciesResponse: + params = (options or ListOption()).to_params({self._route.id_param: resource_id}) + data = _inner_call( + "GET", + self._path("user-access-policies"), + tenant_id=tenant_id, + account_id=account_id, + params=params, + ) + return ResourceUserAccessPoliciesResponse.model_validate(data or {}) + + def replace_user_access_policies( + self, + tenant_id: str, + account_id: str | None, + resource_id: str, + target_account_id: str | None, + payload: ReplaceUserAccessPolicies, + ) -> ReplaceUserAccessPoliciesResponse: + params = self._params(resource_id) + params["account_id"] = target_account_id + data = _inner_call( + "PUT", + self._path("user-access-policies"), + tenant_id=tenant_id, + account_id=account_id, + params=params, + json=payload.model_dump(mode="json", exclude_unset=self._replace_user_policies_exclude_unset), + ) + return ReplaceUserAccessPoliciesResponse.model_validate(data or {}) + + def whitelist(self, tenant_id: str, account_id: str | None, resource_id: str) -> ResourceWhitelist: + data = _inner_call( + "GET", + self._path("whitelist"), + tenant_id=tenant_id, + account_id=account_id, + params=self._params(resource_id), + ) + return ResourceWhitelist.model_validate(data or {}) + + def whitelist_config(self, tenant_id: str, account_id: str | None, resource_id: str) -> ResourceWhitelistConfig: + data = _inner_call( + "GET", + self._path("whitelist"), + tenant_id=tenant_id, + account_id=account_id, + params=self._params(resource_id), + ) + return ResourceWhitelistConfig.model_validate(data or {}) + + def legacy_whitelist_config( + self, tenant_id: str, account_id: str | None, resource_id: str + ) -> _LegacyResourceWhitelistConfig: + data = _inner_call( + "GET", + self._path("whitelist"), + tenant_id=tenant_id, + account_id=account_id, + params=self._params(resource_id), + ) + return _LegacyResourceWhitelistConfig.model_validate(data or {}) + + def replace_whitelist( + self, + tenant_id: str, + account_id: str | None, + resource_id: str, + payload: ReplaceMemberBindings, + ) -> ResourceWhitelist: + data = _inner_call( + "PUT", + self._path("whitelist"), + tenant_id=tenant_id, + account_id=account_id, + params=self._params(resource_id), + json=payload.model_dump(mode="json"), + ) + return ResourceWhitelist.model_validate(data or {}) + + def append_whitelist_members_batch( + self, + tenant_id: str, + account_id: str | None, + data: Sequence[_RBACModel], + ) -> None: + _inner_call( + "POST", + self._path("whitelist/members/batch"), + tenant_id=tenant_id, + account_id=account_id, + json={"data": [item.model_dump(mode="json") for item in data]}, + ) + + def matrix(self, tenant_id: str, account_id: str | None, resource_id: str) -> MatrixT: + data = _inner_call( + "GET", + self._path("access-policy"), + tenant_id=tenant_id, + account_id=account_id, + params=self._params(resource_id), + ) + return self._matrix_model.model_validate(data or {}) + + def list_role_bindings( + self, + tenant_id: str, + account_id: str | None, + resource_id: str, + policy_id: str, + ) -> RoleBindingsResponse: + data = _inner_call( + "GET", + self._path("access-policy/role-bindings"), + tenant_id=tenant_id, + account_id=account_id, + params=self._params(resource_id, policy_id), + ) + return RoleBindingsResponse.model_validate(data or {}) + + def replace_role_bindings( + self, + tenant_id: str, + account_id: str | None, + resource_id: str, + policy_id: str, + payload: ReplaceRoleBindings, + ) -> RoleBindingsResponse: + data = _inner_call( + "PUT", + self._path("access-policy/role-bindings"), + tenant_id=tenant_id, + account_id=account_id, + params=self._params(resource_id, policy_id), + json=payload.model_dump(mode="json"), + ) + return RoleBindingsResponse.model_validate(data or {}) + + def list_member_bindings( + self, + tenant_id: str, + account_id: str | None, + resource_id: str, + policy_id: str, + ) -> MemberBindingsResponse: + data = _inner_call( + "GET", + self._path("access-policy/member-bindings"), + tenant_id=tenant_id, + account_id=account_id, + params=self._params(resource_id, policy_id), + ) + return MemberBindingsResponse.model_validate(data or {}) + + def delete_member_bindings( + self, + tenant_id: str, + account_id: str | None, + resource_id: str, + policy_id: str, + payload: DeleteMemberBindings, + ) -> None: + _inner_call( + "DELETE", + self._path("access-policy/member-bindings"), + tenant_id=tenant_id, + account_id=account_id, + params=self._params(resource_id, policy_id), + json=payload.model_dump(mode="json"), + ) + + def replace_bindings( + self, + tenant_id: str, + account_id: str | None, + resource_id: str, + policy_id: str, + payload: ReplaceBindings, + ) -> AccessMatrixItem: + data = _inner_call( + "PUT", + self._path("access-policy/bindings"), + tenant_id=tenant_id, + account_id=account_id, + params=self._params(resource_id, policy_id), + json=payload.model_dump(mode="json"), + ) + return AccessMatrixItem.model_validate(data or {}) + + +class _WorkspaceAccessClient: + def __init__(self, route: _ResourceAccessRoute) -> None: + self._route = route + + def _path(self, suffix: str) -> str: + return f"{_INNER_PREFIX}/workspace/{self._route.segment}/{suffix}" + + def matrix( + self, + tenant_id: str, + account_id: str | None = None, + *, + options: ListOption | None = None, + ) -> WorkspaceAccessMatrix: + data = _inner_call( + "GET", + self._path("access-policy"), + tenant_id=tenant_id, + account_id=account_id, + params=(options or ListOption()).to_params() or None, + ) + return WorkspaceAccessMatrix.model_validate(data or {}) + + def list_role_bindings(self, tenant_id: str, account_id: str | None, policy_id: str) -> RoleBindingsResponse: + data = _inner_call( + "GET", + self._path("access-policy/role-bindings"), + tenant_id=tenant_id, + account_id=account_id, + params={"policy_id": policy_id}, + ) + return RoleBindingsResponse.model_validate(data or {}) + + def replace_role_bindings( + self, + tenant_id: str, + account_id: str | None, + policy_id: str, + payload: ReplaceRoleBindings, + ) -> RoleBindingsResponse: + data = _inner_call( + "PUT", + self._path("access-policy/role-bindings"), + tenant_id=tenant_id, + account_id=account_id, + params={"policy_id": policy_id}, + json=payload.model_dump(mode="json"), + ) + return RoleBindingsResponse.model_validate(data or {}) + + def list_member_bindings(self, tenant_id: str, account_id: str | None, policy_id: str) -> MemberBindingsResponse: + data = _inner_call( + "GET", + self._path("access-policy/member-bindings"), + tenant_id=tenant_id, + account_id=account_id, + params={"policy_id": policy_id}, + ) + return MemberBindingsResponse.model_validate(data or {}) + + def replace_bindings( + self, + tenant_id: str, + account_id: str | None, + policy_id: str, + payload: ReplaceBindings, + ) -> AccessMatrixItem: + data = _inner_call( + "PUT", + self._path("access-policy/bindings"), + tenant_id=tenant_id, + account_id=account_id, + params={"policy_id": policy_id}, + json=payload.model_dump(mode="json"), + ) + return AccessMatrixItem.model_validate(data or {}) + + +_APP_ACCESS = _ResourceAccessClient(RBACResourceType.APP.route, AppAccessMatrix) +_DATASET_ACCESS = _ResourceAccessClient( + RBACResourceType.DATASET.route, DatasetAccessMatrix, replace_user_policies_exclude_unset=True +) +_AGENT_ACCESS = _ResourceAccessClient(RBACResourceType.AGENT.route, AgentAccessMatrix) +_WORKSPACE_APP_ACCESS = _WorkspaceAccessClient(RBACResourceType.APP.route) +_WORKSPACE_DATASET_ACCESS = _WorkspaceAccessClient(RBACResourceType.DATASET.route) +_WORKSPACE_AGENT_ACCESS = _WorkspaceAccessClient(RBACResourceType.AGENT.route) + + +def _resource_permission_catalog( + resource_type: RBACResourceType, tenant_id: str, account_id: str | None +) -> PermissionCatalogResponse: + data = _inner_call( + "GET", + f"{_INNER_PREFIX}/role-permissions/catalog/{resource_type.value}", + tenant_id=tenant_id, + account_id=account_id, + ) + return PermissionCatalogResponse.model_validate(data or {}) + + class RBACService: """Single entry point grouping every inner RBAC call by feature area. @@ -890,23 +1297,15 @@ class RBACService: @staticmethod def app(tenant_id: str, account_id: str | None = None) -> PermissionCatalogResponse: - data = _inner_call( - "GET", - f"{_INNER_PREFIX}/role-permissions/catalog/app", - tenant_id=tenant_id, - account_id=account_id, - ) - return PermissionCatalogResponse.model_validate(data or {}) + return _resource_permission_catalog(RBACResourceType.APP, tenant_id, account_id) @staticmethod def dataset(tenant_id: str, account_id: str | None = None) -> PermissionCatalogResponse: - data = _inner_call( - "GET", - f"{_INNER_PREFIX}/role-permissions/catalog/dataset", - tenant_id=tenant_id, - account_id=account_id, - ) - return PermissionCatalogResponse.model_validate(data or {}) + return _resource_permission_catalog(RBACResourceType.DATASET, tenant_id, account_id) + + @staticmethod + def agent(tenant_id: str, account_id: str | None = None) -> PermissionCatalogResponse: + return _resource_permission_catalog(RBACResourceType.AGENT, tenant_id, account_id) # ------------------------------------------------------------------ # Role CRUD (Settings > Permissions). @@ -1200,13 +1599,7 @@ class RBACService: class AppAccess: @staticmethod def whitelist_resources(tenant_id: str, account_id: str | None) -> ResourceWhitelistResources: - data = _inner_call( - "GET", - f"{_INNER_PREFIX}/apps/whitelist/resources", - tenant_id=tenant_id, - account_id=account_id, - ) - return ResourceWhitelistResources.model_validate(data or {}) + return _APP_ACCESS.whitelist_resources(tenant_id, account_id) @staticmethod def user_access_policies( @@ -1216,15 +1609,7 @@ class RBACService: *, options: ListOption | None = None, ) -> ResourceUserAccessPoliciesResponse: - params = (options or ListOption()).to_params({"app_id": app_id}) - data = _inner_call( - "GET", - f"{_INNER_PREFIX}/apps/user-access-policies", - tenant_id=tenant_id, - account_id=account_id, - params=params, - ) - return ResourceUserAccessPoliciesResponse.model_validate(data or {}) + return _APP_ACCESS.user_access_policies(tenant_id, account_id, app_id, options=options) @staticmethod def replace_user_access_policies( @@ -1234,51 +1619,21 @@ class RBACService: target_account_id: str | None, payload: ReplaceUserAccessPolicies, ) -> ReplaceUserAccessPoliciesResponse: - request_data = payload.model_dump(mode="json") - data = _inner_call( - "PUT", - f"{_INNER_PREFIX}/apps/user-access-policies", - tenant_id=tenant_id, - account_id=account_id, - params={"app_id": app_id, "account_id": target_account_id}, - json=request_data, - ) - return ReplaceUserAccessPoliciesResponse.model_validate(data or {}) + return _APP_ACCESS.replace_user_access_policies(tenant_id, account_id, app_id, target_account_id, payload) @staticmethod def whitelist(tenant_id: str, account_id: str | None, app_id: str) -> ResourceWhitelist: - data = _inner_call( - "GET", - f"{_INNER_PREFIX}/apps/whitelist", - tenant_id=tenant_id, - account_id=account_id, - params={"app_id": app_id}, - ) - return ResourceWhitelist.model_validate(data or {}) + return _APP_ACCESS.whitelist(tenant_id, account_id, app_id) @staticmethod def whitelist_config(tenant_id: str, account_id: str | None, app_id: str) -> ResourceWhitelistConfig: - data = _inner_call( - "GET", - f"{_INNER_PREFIX}/apps/whitelist", - tenant_id=tenant_id, - account_id=account_id, - params={"app_id": app_id}, - ) - return ResourceWhitelistConfig.model_validate(data or {}) + return _APP_ACCESS.whitelist_config(tenant_id, account_id, app_id) @staticmethod def legacy_whitelist_config( tenant_id: str, account_id: str | None, app_id: str ) -> _LegacyResourceWhitelistConfig: - data = _inner_call( - "GET", - f"{_INNER_PREFIX}/apps/whitelist", - tenant_id=tenant_id, - account_id=account_id, - params={"app_id": app_id}, - ) - return _LegacyResourceWhitelistConfig.model_validate(data or {}) + return _APP_ACCESS.legacy_whitelist_config(tenant_id, account_id, app_id) @staticmethod def replace_whitelist( @@ -1287,15 +1642,7 @@ class RBACService: app_id: str, payload: ReplaceMemberBindings, ) -> ResourceWhitelist: - data = _inner_call( - "PUT", - f"{_INNER_PREFIX}/apps/whitelist", - tenant_id=tenant_id, - account_id=account_id, - params={"app_id": app_id}, - json=payload.model_dump(mode="json"), - ) - return ResourceWhitelist.model_validate(data or {}) + return _APP_ACCESS.replace_whitelist(tenant_id, account_id, app_id, payload) @staticmethod def append_whitelist_members_batch( @@ -1303,24 +1650,11 @@ class RBACService: account_id: str | None, data: Sequence[AppendAppWhitelistMembersBatchItem], ) -> None: - _inner_call( - "POST", - f"{_INNER_PREFIX}/apps/whitelist/members/batch", - tenant_id=tenant_id, - account_id=account_id, - json={"data": [item.model_dump(mode="json") for item in data]}, - ) + _APP_ACCESS.append_whitelist_members_batch(tenant_id, account_id, data) @staticmethod def matrix(tenant_id: str, account_id: str | None, app_id: str) -> AppAccessMatrix: - data = _inner_call( - "GET", - f"{_INNER_PREFIX}/apps/access-policy", - tenant_id=tenant_id, - account_id=account_id, - params={"app_id": app_id}, - ) - return AppAccessMatrix.model_validate(data or {}) + return _APP_ACCESS.matrix(tenant_id, account_id, app_id) @staticmethod def list_role_bindings( @@ -1329,14 +1663,7 @@ class RBACService: app_id: str, policy_id: str, ) -> RoleBindingsResponse: - data = _inner_call( - "GET", - f"{_INNER_PREFIX}/apps/access-policy/role-bindings", - tenant_id=tenant_id, - account_id=account_id, - params={"app_id": app_id, "policy_id": policy_id}, - ) - return RoleBindingsResponse.model_validate(data or {}) + return _APP_ACCESS.list_role_bindings(tenant_id, account_id, app_id, policy_id) @staticmethod def replace_role_bindings( @@ -1346,15 +1673,7 @@ class RBACService: policy_id: str, payload: ReplaceRoleBindings, ) -> RoleBindingsResponse: - data = _inner_call( - "PUT", - f"{_INNER_PREFIX}/apps/access-policy/role-bindings", - tenant_id=tenant_id, - account_id=account_id, - params={"app_id": app_id, "policy_id": policy_id}, - json=payload.model_dump(mode="json"), - ) - return RoleBindingsResponse.model_validate(data or {}) + return _APP_ACCESS.replace_role_bindings(tenant_id, account_id, app_id, policy_id, payload) @staticmethod def list_member_bindings( @@ -1363,14 +1682,7 @@ class RBACService: app_id: str, policy_id: str, ) -> MemberBindingsResponse: - data = _inner_call( - "GET", - f"{_INNER_PREFIX}/apps/access-policy/member-bindings", - tenant_id=tenant_id, - account_id=account_id, - params={"app_id": app_id, "policy_id": policy_id}, - ) - return MemberBindingsResponse.model_validate(data or {}) + return _APP_ACCESS.list_member_bindings(tenant_id, account_id, app_id, policy_id) @staticmethod def delete_member_bindings( @@ -1380,14 +1692,7 @@ class RBACService: policy_id: str, payload: DeleteMemberBindings, ) -> None: - _inner_call( - "DELETE", - f"{_INNER_PREFIX}/apps/access-policy/member-bindings", - tenant_id=tenant_id, - account_id=account_id, - params={"app_id": app_id, "policy_id": policy_id}, - json=payload.model_dump(mode="json"), - ) + _APP_ACCESS.delete_member_bindings(tenant_id, account_id, app_id, policy_id, payload) @staticmethod def replace_bindings( @@ -1397,15 +1702,7 @@ class RBACService: policy_id: str, payload: ReplaceBindings, ) -> AccessMatrixItem: - data = _inner_call( - "PUT", - f"{_INNER_PREFIX}/apps/access-policy/bindings", - tenant_id=tenant_id, - account_id=account_id, - params={"app_id": app_id, "policy_id": policy_id}, - json=payload.model_dump(mode="json"), - ) - return AccessMatrixItem.model_validate(data or {}) + return _APP_ACCESS.replace_bindings(tenant_id, account_id, app_id, policy_id, payload) # ------------------------------------------------------------------ # Per-dataset access (screenshot 1: Knowledge Base Access Config). @@ -1413,13 +1710,7 @@ class RBACService: class DatasetAccess: @staticmethod def whitelist_resources(tenant_id: str, account_id: str | None) -> ResourceWhitelistResources: - data = _inner_call( - "GET", - f"{_INNER_PREFIX}/datasets/whitelist/resources", - tenant_id=tenant_id, - account_id=account_id, - ) - return ResourceWhitelistResources.model_validate(data or {}) + return _DATASET_ACCESS.whitelist_resources(tenant_id, account_id) @staticmethod def user_access_policies( @@ -1429,15 +1720,7 @@ class RBACService: *, options: ListOption | None = None, ) -> ResourceUserAccessPoliciesResponse: - params = (options or ListOption()).to_params({"dataset_id": dataset_id}) - data = _inner_call( - "GET", - f"{_INNER_PREFIX}/datasets/user-access-policies", - tenant_id=tenant_id, - account_id=account_id, - params=params, - ) - return ResourceUserAccessPoliciesResponse.model_validate(data or {}) + return _DATASET_ACCESS.user_access_policies(tenant_id, account_id, dataset_id, options=options) @staticmethod def replace_user_access_policies( @@ -1447,50 +1730,23 @@ class RBACService: target_account_id: str | None, payload: ReplaceUserAccessPolicies, ) -> ReplaceUserAccessPoliciesResponse: - data = _inner_call( - "PUT", - f"{_INNER_PREFIX}/datasets/user-access-policies", - tenant_id=tenant_id, - account_id=account_id, - params={"dataset_id": dataset_id, "account_id": target_account_id}, - json=payload.model_dump(mode="json", exclude_unset=True), + return _DATASET_ACCESS.replace_user_access_policies( + tenant_id, account_id, dataset_id, target_account_id, payload ) - return ReplaceUserAccessPoliciesResponse.model_validate(data or {}) @staticmethod def whitelist(tenant_id: str, account_id: str | None, dataset_id: str) -> ResourceWhitelist: - data = _inner_call( - "GET", - f"{_INNER_PREFIX}/datasets/whitelist", - tenant_id=tenant_id, - account_id=account_id, - params={"dataset_id": dataset_id}, - ) - return ResourceWhitelist.model_validate(data or {}) + return _DATASET_ACCESS.whitelist(tenant_id, account_id, dataset_id) @staticmethod def whitelist_config(tenant_id: str, account_id: str | None, dataset_id: str) -> ResourceWhitelistConfig: - data = _inner_call( - "GET", - f"{_INNER_PREFIX}/datasets/whitelist", - tenant_id=tenant_id, - account_id=account_id, - params={"dataset_id": dataset_id}, - ) - return ResourceWhitelistConfig.model_validate(data or {}) + return _DATASET_ACCESS.whitelist_config(tenant_id, account_id, dataset_id) @staticmethod def legacy_whitelist_config( tenant_id: str, account_id: str | None, dataset_id: str ) -> _LegacyResourceWhitelistConfig: - data = _inner_call( - "GET", - f"{_INNER_PREFIX}/datasets/whitelist", - tenant_id=tenant_id, - account_id=account_id, - params={"dataset_id": dataset_id}, - ) - return _LegacyResourceWhitelistConfig.model_validate(data or {}) + return _DATASET_ACCESS.legacy_whitelist_config(tenant_id, account_id, dataset_id) @staticmethod def replace_whitelist( @@ -1499,15 +1755,7 @@ class RBACService: dataset_id: str, payload: ReplaceMemberBindings, ) -> ResourceWhitelist: - data = _inner_call( - "PUT", - f"{_INNER_PREFIX}/datasets/whitelist", - tenant_id=tenant_id, - account_id=account_id, - params={"dataset_id": dataset_id}, - json=payload.model_dump(mode="json"), - ) - return ResourceWhitelist.model_validate(data or {}) + return _DATASET_ACCESS.replace_whitelist(tenant_id, account_id, dataset_id, payload) @staticmethod def append_whitelist_members_batch( @@ -1515,24 +1763,11 @@ class RBACService: account_id: str | None, data: Sequence[AppendDatasetWhitelistMembersBatchItem], ) -> None: - _inner_call( - "POST", - f"{_INNER_PREFIX}/datasets/whitelist/members/batch", - tenant_id=tenant_id, - account_id=account_id, - json={"data": [item.model_dump(mode="json") for item in data]}, - ) + _DATASET_ACCESS.append_whitelist_members_batch(tenant_id, account_id, data) @staticmethod def matrix(tenant_id: str, account_id: str | None, dataset_id: str) -> DatasetAccessMatrix: - data = _inner_call( - "GET", - f"{_INNER_PREFIX}/datasets/access-policy", - tenant_id=tenant_id, - account_id=account_id, - params={"dataset_id": dataset_id}, - ) - return DatasetAccessMatrix.model_validate(data or {}) + return _DATASET_ACCESS.matrix(tenant_id, account_id, dataset_id) @staticmethod def list_role_bindings( @@ -1541,14 +1776,7 @@ class RBACService: dataset_id: str, policy_id: str, ) -> RoleBindingsResponse: - data = _inner_call( - "GET", - f"{_INNER_PREFIX}/datasets/access-policy/role-bindings", - tenant_id=tenant_id, - account_id=account_id, - params={"dataset_id": dataset_id, "policy_id": policy_id}, - ) - return RoleBindingsResponse.model_validate(data or {}) + return _DATASET_ACCESS.list_role_bindings(tenant_id, account_id, dataset_id, policy_id) @staticmethod def replace_role_bindings( @@ -1558,15 +1786,7 @@ class RBACService: policy_id: str, payload: ReplaceRoleBindings, ) -> RoleBindingsResponse: - data = _inner_call( - "PUT", - f"{_INNER_PREFIX}/datasets/access-policy/role-bindings", - tenant_id=tenant_id, - account_id=account_id, - params={"dataset_id": dataset_id, "policy_id": policy_id}, - json=payload.model_dump(mode="json"), - ) - return RoleBindingsResponse.model_validate(data or {}) + return _DATASET_ACCESS.replace_role_bindings(tenant_id, account_id, dataset_id, policy_id, payload) @staticmethod def list_member_bindings( @@ -1575,14 +1795,7 @@ class RBACService: dataset_id: str, policy_id: str, ) -> MemberBindingsResponse: - data = _inner_call( - "GET", - f"{_INNER_PREFIX}/datasets/access-policy/member-bindings", - tenant_id=tenant_id, - account_id=account_id, - params={"dataset_id": dataset_id, "policy_id": policy_id}, - ) - return MemberBindingsResponse.model_validate(data or {}) + return _DATASET_ACCESS.list_member_bindings(tenant_id, account_id, dataset_id, policy_id) @staticmethod def delete_member_bindings( @@ -1592,14 +1805,7 @@ class RBACService: policy_id: str, payload: DeleteMemberBindings, ) -> None: - _inner_call( - "DELETE", - f"{_INNER_PREFIX}/datasets/access-policy/member-bindings", - tenant_id=tenant_id, - account_id=account_id, - params={"dataset_id": dataset_id, "policy_id": policy_id}, - json=payload.model_dump(mode="json"), - ) + _DATASET_ACCESS.delete_member_bindings(tenant_id, account_id, dataset_id, policy_id, payload) @staticmethod def replace_bindings( @@ -1609,15 +1815,117 @@ class RBACService: policy_id: str, payload: ReplaceBindings, ) -> AccessMatrixItem: - data = _inner_call( - "PUT", - f"{_INNER_PREFIX}/datasets/access-policy/bindings", - tenant_id=tenant_id, - account_id=account_id, - params={"dataset_id": dataset_id, "policy_id": policy_id}, - json=payload.model_dump(mode="json"), + return _DATASET_ACCESS.replace_bindings(tenant_id, account_id, dataset_id, policy_id, payload) + + class AgentAccess: + @staticmethod + def whitelist_resources(tenant_id: str, account_id: str | None) -> ResourceWhitelistResources: + return _AGENT_ACCESS.whitelist_resources(tenant_id, account_id) + + @staticmethod + def user_access_policies( + tenant_id: str, + account_id: str | None, + agent_id: str, + *, + options: ListOption | None = None, + ) -> ResourceUserAccessPoliciesResponse: + return _AGENT_ACCESS.user_access_policies(tenant_id, account_id, agent_id, options=options) + + @staticmethod + def replace_user_access_policies( + tenant_id: str, + account_id: str | None, + agent_id: str, + target_account_id: str | None, + payload: ReplaceUserAccessPolicies, + ) -> ReplaceUserAccessPoliciesResponse: + return _AGENT_ACCESS.replace_user_access_policies( + tenant_id, account_id, agent_id, target_account_id, payload ) - return AccessMatrixItem.model_validate(data or {}) + + @staticmethod + def whitelist(tenant_id: str, account_id: str | None, agent_id: str) -> ResourceWhitelist: + return _AGENT_ACCESS.whitelist(tenant_id, account_id, agent_id) + + @staticmethod + def whitelist_config(tenant_id: str, account_id: str | None, agent_id: str) -> ResourceWhitelistConfig: + return _AGENT_ACCESS.whitelist_config(tenant_id, account_id, agent_id) + + @staticmethod + def legacy_whitelist_config( + tenant_id: str, account_id: str | None, agent_id: str + ) -> _LegacyResourceWhitelistConfig: + return _AGENT_ACCESS.legacy_whitelist_config(tenant_id, account_id, agent_id) + + @staticmethod + def replace_whitelist( + tenant_id: str, + account_id: str | None, + agent_id: str, + payload: ReplaceMemberBindings, + ) -> ResourceWhitelist: + return _AGENT_ACCESS.replace_whitelist(tenant_id, account_id, agent_id, payload) + + @staticmethod + def append_whitelist_members_batch( + tenant_id: str, + account_id: str | None, + data: Sequence[AppendAgentWhitelistMembersBatchItem], + ) -> None: + _AGENT_ACCESS.append_whitelist_members_batch(tenant_id, account_id, data) + + @staticmethod + def matrix(tenant_id: str, account_id: str | None, agent_id: str) -> AgentAccessMatrix: + return _AGENT_ACCESS.matrix(tenant_id, account_id, agent_id) + + @staticmethod + def list_role_bindings( + tenant_id: str, + account_id: str | None, + agent_id: str, + policy_id: str, + ) -> RoleBindingsResponse: + return _AGENT_ACCESS.list_role_bindings(tenant_id, account_id, agent_id, policy_id) + + @staticmethod + def replace_role_bindings( + tenant_id: str, + account_id: str | None, + agent_id: str, + policy_id: str, + payload: ReplaceRoleBindings, + ) -> RoleBindingsResponse: + return _AGENT_ACCESS.replace_role_bindings(tenant_id, account_id, agent_id, policy_id, payload) + + @staticmethod + def list_member_bindings( + tenant_id: str, + account_id: str | None, + agent_id: str, + policy_id: str, + ) -> MemberBindingsResponse: + return _AGENT_ACCESS.list_member_bindings(tenant_id, account_id, agent_id, policy_id) + + @staticmethod + def delete_member_bindings( + tenant_id: str, + account_id: str | None, + agent_id: str, + policy_id: str, + payload: DeleteMemberBindings, + ) -> None: + _AGENT_ACCESS.delete_member_bindings(tenant_id, account_id, agent_id, policy_id, payload) + + @staticmethod + def replace_bindings( + tenant_id: str, + account_id: str | None, + agent_id: str, + policy_id: str, + payload: ReplaceBindings, + ) -> AccessMatrixItem: + return _AGENT_ACCESS.replace_bindings(tenant_id, account_id, agent_id, policy_id, payload) # ------------------------------------------------------------------ # Workspace-level access (screenshot 2: Settings > Access Rules). @@ -1630,14 +1938,7 @@ class RBACService: *, options: ListOption | None = None, ) -> WorkspaceAccessMatrix: - data = _inner_call( - "GET", - f"{_INNER_PREFIX}/workspace/apps/access-policy", - tenant_id=tenant_id, - account_id=account_id, - params=(options or ListOption()).to_params() or None, - ) - return WorkspaceAccessMatrix.model_validate(data or {}) + return _WORKSPACE_APP_ACCESS.matrix(tenant_id, account_id, options=options) @staticmethod def dataset_matrix( @@ -1646,14 +1947,16 @@ class RBACService: *, options: ListOption | None = None, ) -> WorkspaceAccessMatrix: - data = _inner_call( - "GET", - f"{_INNER_PREFIX}/workspace/datasets/access-policy", - tenant_id=tenant_id, - account_id=account_id, - params=(options or ListOption()).to_params() or None, - ) - return WorkspaceAccessMatrix.model_validate(data or {}) + return _WORKSPACE_DATASET_ACCESS.matrix(tenant_id, account_id, options=options) + + @staticmethod + def agent_matrix( + tenant_id: str, + account_id: str | None = None, + *, + options: ListOption | None = None, + ) -> WorkspaceAccessMatrix: + return _WORKSPACE_AGENT_ACCESS.matrix(tenant_id, account_id, options=options) @staticmethod def list_app_role_bindings( @@ -1661,14 +1964,7 @@ class RBACService: account_id: str | None, policy_id: str, ) -> RoleBindingsResponse: - data = _inner_call( - "GET", - f"{_INNER_PREFIX}/workspace/apps/access-policy/role-bindings", - tenant_id=tenant_id, - account_id=account_id, - params={"policy_id": policy_id}, - ) - return RoleBindingsResponse.model_validate(data or {}) + return _WORKSPACE_APP_ACCESS.list_role_bindings(tenant_id, account_id, policy_id) @staticmethod def replace_app_role_bindings( @@ -1677,15 +1973,7 @@ class RBACService: policy_id: str, payload: ReplaceRoleBindings, ) -> RoleBindingsResponse: - data = _inner_call( - "PUT", - f"{_INNER_PREFIX}/workspace/apps/access-policy/role-bindings", - tenant_id=tenant_id, - account_id=account_id, - params={"policy_id": policy_id}, - json=payload.model_dump(mode="json"), - ) - return RoleBindingsResponse.model_validate(data or {}) + return _WORKSPACE_APP_ACCESS.replace_role_bindings(tenant_id, account_id, policy_id, payload) @staticmethod def list_app_member_bindings( @@ -1693,14 +1981,7 @@ class RBACService: account_id: str | None, policy_id: str, ) -> MemberBindingsResponse: - data = _inner_call( - "GET", - f"{_INNER_PREFIX}/workspace/apps/access-policy/member-bindings", - tenant_id=tenant_id, - account_id=account_id, - params={"policy_id": policy_id}, - ) - return MemberBindingsResponse.model_validate(data or {}) + return _WORKSPACE_APP_ACCESS.list_member_bindings(tenant_id, account_id, policy_id) @staticmethod def replace_app_bindings( @@ -1709,15 +1990,7 @@ class RBACService: policy_id: str, payload: ReplaceBindings, ) -> AccessMatrixItem: - data = _inner_call( - "PUT", - f"{_INNER_PREFIX}/workspace/apps/access-policy/bindings", - tenant_id=tenant_id, - account_id=account_id, - params={"policy_id": policy_id}, - json=payload.model_dump(mode="json"), - ) - return AccessMatrixItem.model_validate(data or {}) + return _WORKSPACE_APP_ACCESS.replace_bindings(tenant_id, account_id, policy_id, payload) @staticmethod def list_dataset_role_bindings( @@ -1725,14 +1998,7 @@ class RBACService: account_id: str | None, policy_id: str, ) -> RoleBindingsResponse: - data = _inner_call( - "GET", - f"{_INNER_PREFIX}/workspace/datasets/access-policy/role-bindings", - tenant_id=tenant_id, - account_id=account_id, - params={"policy_id": policy_id}, - ) - return RoleBindingsResponse.model_validate(data or {}) + return _WORKSPACE_DATASET_ACCESS.list_role_bindings(tenant_id, account_id, policy_id) @staticmethod def replace_dataset_role_bindings( @@ -1741,15 +2007,7 @@ class RBACService: policy_id: str, payload: ReplaceRoleBindings, ) -> RoleBindingsResponse: - data = _inner_call( - "PUT", - f"{_INNER_PREFIX}/workspace/datasets/access-policy/role-bindings", - tenant_id=tenant_id, - account_id=account_id, - params={"policy_id": policy_id}, - json=payload.model_dump(mode="json"), - ) - return RoleBindingsResponse.model_validate(data or {}) + return _WORKSPACE_DATASET_ACCESS.replace_role_bindings(tenant_id, account_id, policy_id, payload) @staticmethod def list_dataset_member_bindings( @@ -1757,14 +2015,7 @@ class RBACService: account_id: str | None, policy_id: str, ) -> MemberBindingsResponse: - data = _inner_call( - "GET", - f"{_INNER_PREFIX}/workspace/datasets/access-policy/member-bindings", - tenant_id=tenant_id, - account_id=account_id, - params={"policy_id": policy_id}, - ) - return MemberBindingsResponse.model_validate(data or {}) + return _WORKSPACE_DATASET_ACCESS.list_member_bindings(tenant_id, account_id, policy_id) @staticmethod def replace_dataset_bindings( @@ -1773,15 +2024,41 @@ class RBACService: policy_id: str, payload: ReplaceBindings, ) -> AccessMatrixItem: - data = _inner_call( - "PUT", - f"{_INNER_PREFIX}/workspace/datasets/access-policy/bindings", - tenant_id=tenant_id, - account_id=account_id, - params={"policy_id": policy_id}, - json=payload.model_dump(mode="json"), - ) - return AccessMatrixItem.model_validate(data or {}) + return _WORKSPACE_DATASET_ACCESS.replace_bindings(tenant_id, account_id, policy_id, payload) + + @staticmethod + def list_agent_role_bindings( + tenant_id: str, + account_id: str | None, + policy_id: str, + ) -> RoleBindingsResponse: + return _WORKSPACE_AGENT_ACCESS.list_role_bindings(tenant_id, account_id, policy_id) + + @staticmethod + def replace_agent_role_bindings( + tenant_id: str, + account_id: str | None, + policy_id: str, + payload: ReplaceRoleBindings, + ) -> RoleBindingsResponse: + return _WORKSPACE_AGENT_ACCESS.replace_role_bindings(tenant_id, account_id, policy_id, payload) + + @staticmethod + def list_agent_member_bindings( + tenant_id: str, + account_id: str | None, + policy_id: str, + ) -> MemberBindingsResponse: + return _WORKSPACE_AGENT_ACCESS.list_member_bindings(tenant_id, account_id, policy_id) + + @staticmethod + def replace_agent_bindings( + tenant_id: str, + account_id: str | None, + policy_id: str, + payload: ReplaceBindings, + ) -> AccessMatrixItem: + return _WORKSPACE_AGENT_ACCESS.replace_bindings(tenant_id, account_id, policy_id, payload) class MemberRoles: @staticmethod @@ -1888,6 +2165,27 @@ class RBACService: ) return data + class Migrations: + @staticmethod + def migrate_agent_manage_roles(tenant_id: str, *, apply: bool) -> LegacyAgentMigrationReport: + data = _inner_call( + "POST", + f"{_INNER_PREFIX}/migrations/agent-manage-roles", + tenant_id=tenant_id, + json={"apply": apply}, + ) + return LegacyAgentMigrationReport.model_validate(data or {}) + + @staticmethod + def list_configured_agent_ids(tenant_id: str, agent_ids: list[str]) -> list[str]: + data = _inner_call( + "POST", + f"{_INNER_PREFIX}/migrations/agent-access-state", + tenant_id=tenant_id, + json={"agent_ids": agent_ids}, + ) + return ConfiguredAgentIDs.model_validate(data or {}).configured_agent_ids + class CheckAccess: """Call the ``/inner/api/rbac/check-access`` endpoint.""" @@ -1979,6 +2277,7 @@ class RBACService: *, app_id: str | None = None, dataset_id: str | None = None, + agent_id: str | None = None, session: Session, ) -> MyPermissionsResponse: if not dify_config.RBAC_ENABLED: @@ -1994,6 +2293,7 @@ class RBACService: for k, v in { "app_id": app_id, "dataset_id": dataset_id, + "agent_id": agent_id, }.items() if v is not None } diff --git a/api/services/rbac_agent_access_service.py b/api/services/rbac_agent_access_service.py new file mode 100644 index 00000000000..4540c587c53 --- /dev/null +++ b/api/services/rbac_agent_access_service.py @@ -0,0 +1,35 @@ +import logging + +from configs import dify_config +from services.enterprise import rbac_service as enterprise_rbac_service +from tasks.initialize_created_app_rbac_access_task import initialize_created_app_rbac_access_task + +logger = logging.getLogger(__name__) + + +def initialize_agent_rbac_access(*, tenant_id: str, agent_id: str, creator_account_id: str) -> None: + if not dify_config.RBAC_ENABLED: + return + + try: + initialize_created_app_rbac_access_task.delay(tenant_id, creator_account_id, agent_id=agent_id) + enterprise_rbac_service.RBACService.AccessPolicies.sync_creator_access_policy_member_bindings( + tenant_id, + creator_account_id, + resource_type=enterprise_rbac_service.RBACResourceType.AGENT, + resource_id=agent_id, + ) + enterprise_rbac_service.RBACService.AgentAccess.replace_whitelist( + tenant_id, + creator_account_id, + agent_id, + enterprise_rbac_service.ReplaceMemberBindings(automatic_include_workspace_members=True), + ) + except Exception: + logger.warning( + "Failed to initialize RBAC access for agent: tenant_id=%s agent_id=%s account_id=%s", + tenant_id, + agent_id, + creator_account_id, + exc_info=True, + ) diff --git a/api/services/rbac_resource_service.py b/api/services/rbac_resource_service.py new file mode 100644 index 00000000000..7acc54abedb --- /dev/null +++ b/api/services/rbac_resource_service.py @@ -0,0 +1,40 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +from sqlalchemy import select + +from extensions.ext_database import db +from models.dataset import Dataset +from models.model import App + +if TYPE_CHECKING: + from models.agent import Agent + + +class RBACResourceService: + @staticmethod + def get_app_agent_binding(tenant_id: str, app_id: str) -> Agent | None: + app_model = db.session.scalar(select(App).where(App.id == app_id, App.tenant_id == tenant_id)) + if app_model is None: + return None + return app_model.agent_app_binding_with_session(session=db.session, include_archived=True) + + @staticmethod + def get_app_maintainer(tenant_id: str, app_id: str) -> str | None: + return db.session.scalar( + select(App.maintainer).where(App.id == app_id, App.tenant_id == tenant_id, App.status == "normal") + ) + + @staticmethod + def get_dataset_maintainer(tenant_id: str, dataset_id: str) -> str | None: + return db.session.scalar( + select(Dataset.maintainer).where(Dataset.id == dataset_id, Dataset.tenant_id == tenant_id) + ) + + @staticmethod + def get_dataset_id_by_pipeline(tenant_id: str, pipeline_id: str) -> str | None: + dataset_id = db.session.scalar( + select(Dataset.id).where(Dataset.pipeline_id == pipeline_id, Dataset.tenant_id == tenant_id) + ) + return None if dataset_id is None else str(dataset_id) diff --git a/api/tasks/initialize_created_app_rbac_access_task.py b/api/tasks/initialize_created_app_rbac_access_task.py index ad69f2374ac..1c3c87a483b 100644 --- a/api/tasks/initialize_created_app_rbac_access_task.py +++ b/api/tasks/initialize_created_app_rbac_access_task.py @@ -1,14 +1,15 @@ """Initialize default RBAC access for existing workspace members after app creation.""" import logging -from collections.abc import Iterator +from collections.abc import Callable, Iterator, Sequence +from dataclasses import dataclass from celery import shared_task from sqlalchemy import select from configs import dify_config from extensions.ext_database import db -from models import App, Dataset, TenantAccountJoin, TenantAccountRole +from models import Agent, App, Dataset, TenantAccountJoin, TenantAccountRole from services.account_service import TenantService from services.enterprise import rbac_service as enterprise_rbac_service @@ -30,43 +31,122 @@ def _owner_account_id(tenant_id: str) -> str | None: ) +@dataclass(frozen=True) +class _WhitelistResourceKind[ItemT]: + resource_type: enterprise_rbac_service.RBACResourceType + model: type[App] | type[Dataset] | type[Agent] + build_item: Callable[[str, str], ItemT] + append_members: Callable[[str, str | None, Sequence[ItemT]], None] + replace_user_access_policies: Callable[ + [str, str, str, enterprise_rbac_service.ReplaceUserAccessPolicies], + enterprise_rbac_service.ReplaceUserAccessPoliciesResponse, + ] + + def iter_id_batches(self, tenant_id: str, batch_size: int) -> Iterator[list[str]]: + last_id: str | None = None + while True: + stmt = ( + select(self.model.id) + .where(self.model.tenant_id == tenant_id) + .order_by(self.model.id.asc()) + .limit(batch_size) + ) + if last_id: + stmt = stmt.where(self.model.id > last_id) + ids = [str(resource_id) for resource_id in db.session().scalars(stmt).all()] + if not ids: + return + yield ids + last_id = ids[-1] + + def append_member(self, tenant_id: str, actor_account_id: str, resource_ids: list[str], member_account_id: str): + self.append_members( + tenant_id, + actor_account_id, + [self.build_item(resource_id, member_account_id) for resource_id in resource_ids], + ) + + +_WHITELIST_RESOURCE_KINDS = ( + _WhitelistResourceKind( + resource_type=enterprise_rbac_service.RBACResourceType.APP, + model=App, + build_item=lambda app_id, member: enterprise_rbac_service.AppendAppWhitelistMembersBatchItem( + app_id=app_id, account_ids=[member], policy_id=APP_RBAC_DEFAULT_ACCESS_POLICY_ID + ), + append_members=lambda tenant_id, account_id, data: ( + enterprise_rbac_service.RBACService.AppAccess.append_whitelist_members_batch( + tenant_id=tenant_id, account_id=account_id, data=data + ) + ), + replace_user_access_policies=lambda tenant_id, account_id, resource_id, payload: ( + enterprise_rbac_service.RBACService.AppAccess.replace_user_access_policies( + tenant_id=tenant_id, + account_id=account_id, + app_id=resource_id, + target_account_id=None, + payload=payload, + ) + ), + ), + _WhitelistResourceKind( + resource_type=enterprise_rbac_service.RBACResourceType.DATASET, + model=Dataset, + build_item=lambda dataset_id, member: enterprise_rbac_service.AppendDatasetWhitelistMembersBatchItem( + dataset_id=dataset_id, account_ids=[member], policy_id=APP_RBAC_DEFAULT_ACCESS_POLICY_ID + ), + append_members=lambda tenant_id, account_id, data: ( + enterprise_rbac_service.RBACService.DatasetAccess.append_whitelist_members_batch( + tenant_id=tenant_id, account_id=account_id, data=data + ) + ), + replace_user_access_policies=lambda tenant_id, account_id, resource_id, payload: ( + enterprise_rbac_service.RBACService.DatasetAccess.replace_user_access_policies( + tenant_id=tenant_id, + account_id=account_id, + dataset_id=resource_id, + target_account_id=None, + payload=payload, + ) + ), + ), + _WhitelistResourceKind( + resource_type=enterprise_rbac_service.RBACResourceType.AGENT, + model=Agent, + build_item=lambda agent_id, member: enterprise_rbac_service.AppendAgentWhitelistMembersBatchItem( + agent_id=agent_id, account_ids=[member], policy_id=APP_RBAC_DEFAULT_ACCESS_POLICY_ID + ), + append_members=lambda tenant_id, account_id, data: ( + enterprise_rbac_service.RBACService.AgentAccess.append_whitelist_members_batch( + tenant_id=tenant_id, account_id=account_id, data=data + ) + ), + replace_user_access_policies=lambda tenant_id, account_id, resource_id, payload: ( + enterprise_rbac_service.RBACService.AgentAccess.replace_user_access_policies( + tenant_id=tenant_id, + account_id=account_id, + agent_id=resource_id, + target_account_id=None, + payload=payload, + ) + ), + ), +) +_WHITELIST_RESOURCE_KIND_BY_TYPE = {kind.resource_type: kind for kind in _WHITELIST_RESOURCE_KINDS} + + def _iter_resource_config_batches( tenant_id: str, batch_size: int, ) -> Iterator[list[enterprise_rbac_service.ResourceWhitelistConfigResource]]: - last_app_id: str | None = None - while True: - stmt = select(App.id).where(App.tenant_id == tenant_id).order_by(App.id.asc()).limit(batch_size) - if last_app_id: - stmt = stmt.where(App.id > last_app_id) - app_ids = [str(app_id) for app_id in db.session().scalars(stmt).all()] - if not app_ids: - break - yield [ - enterprise_rbac_service.ResourceWhitelistConfigResource( - resource_type=enterprise_rbac_service.RBACResourceType.APP, - resource_id=app_id, - ) - for app_id in app_ids - ] - last_app_id = app_ids[-1] - - last_dataset_id: str | None = None - while True: - stmt = select(Dataset.id).where(Dataset.tenant_id == tenant_id).order_by(Dataset.id.asc()).limit(batch_size) - if last_dataset_id: - stmt = stmt.where(Dataset.id > last_dataset_id) - dataset_ids = [str(dataset_id) for dataset_id in db.session().scalars(stmt).all()] - if not dataset_ids: - break - yield [ - enterprise_rbac_service.ResourceWhitelistConfigResource( - resource_type=enterprise_rbac_service.RBACResourceType.DATASET, - resource_id=dataset_id, - ) - for dataset_id in dataset_ids - ] - last_dataset_id = dataset_ids[-1] + for kind in _WHITELIST_RESOURCE_KINDS: + for ids in kind.iter_id_batches(tenant_id, batch_size): + yield [ + enterprise_rbac_service.ResourceWhitelistConfigResource( + resource_type=kind.resource_type, resource_id=resource_id + ) + for resource_id in ids + ] def _chunks[T](items: list[T], chunk_size: int) -> Iterator[list[T]]: @@ -74,52 +154,64 @@ def _chunks[T](items: list[T], chunk_size: int) -> Iterator[list[T]]: yield items[index : index + chunk_size] +def _resolve_target_resource( + resource_ids: dict[enterprise_rbac_service.RBACResourceType, str | None], +) -> tuple[_WhitelistResourceKind, str]: + provided = [(resource_type, rid) for resource_type, rid in resource_ids.items() if rid is not None] + if len(provided) != 1: + given = ", ".join(sorted(resource_type.value for resource_type, _ in provided)) or "none" + raise ValueError(f"exactly one of app_id, dataset_id, agent_id must be given, got: {given}") + resource_type, resource_id = provided[0] + return _WHITELIST_RESOURCE_KIND_BY_TYPE[resource_type], resource_id + + @shared_task(queue=APP_RBAC_QUEUE, bind=True, max_retries=3, default_retry_delay=60) def initialize_created_app_rbac_access_task( - self, tenant_id: str, account_id: str, app_id: str | None = None, dataset_id: str | None = None + self, + tenant_id: str, + account_id: str, + app_id: str | None = None, + dataset_id: str | None = None, + agent_id: str | None = None, ) -> None: - """Grant the default app policy to current workspace members. + """Grant the default policy on one app, dataset or agent to current workspace members. - App scope is persisted synchronously before this task is queued. Replacing + The resource scope is persisted synchronously before this task is queued. Replacing member policies is idempotent, so retrying the whole synchronization is safe when the enterprise RBAC service is temporarily unavailable. """ if not dify_config.RBAC_ENABLED: return + kind, resource_id = _resolve_target_resource( + { + enterprise_rbac_service.RBACResourceType.APP: app_id, + enterprise_rbac_service.RBACResourceType.DATASET: dataset_id, + enterprise_rbac_service.RBACResourceType.AGENT: agent_id, + } + ) + try: for account_ids in TenantService.iter_member_account_id_batches( tenant_id, APP_RBAC_ACCOUNT_POLICY_BATCH_SIZE, session=db.session(), ): - if app_id is not None: - enterprise_rbac_service.RBACService.AppAccess.replace_user_access_policies( - tenant_id=tenant_id, - account_id=account_id, - app_id=app_id, - target_account_id=None, - payload=enterprise_rbac_service.ReplaceUserAccessPolicies( - access_policy_ids=[APP_RBAC_DEFAULT_ACCESS_POLICY_ID], - account_ids=account_ids, - ), - ) - elif dataset_id is not None: - enterprise_rbac_service.RBACService.DatasetAccess.replace_user_access_policies( - tenant_id=tenant_id, - account_id=account_id, - dataset_id=dataset_id, - target_account_id=None, - payload=enterprise_rbac_service.ReplaceUserAccessPolicies( - access_policy_ids=[APP_RBAC_DEFAULT_ACCESS_POLICY_ID], - account_ids=account_ids, - ), - ) + kind.replace_user_access_policies( + tenant_id, + account_id, + resource_id, + enterprise_rbac_service.ReplaceUserAccessPolicies( + access_policy_ids=[APP_RBAC_DEFAULT_ACCESS_POLICY_ID], + account_ids=account_ids, + ), + ) except Exception as exc: logger.exception( - "Failed to initialize app RBAC access; retrying: tenant_id=%s app_id=%s attempt=%s", + "Failed to initialize RBAC access; retrying: tenant_id=%s resource_type=%s resource_id=%s attempt=%s", tenant_id, - app_id, + kind.resource_type.value, + resource_id, self.request.retries + 1, ) raise self.retry(exc=exc) @@ -146,8 +238,9 @@ def sync_joined_workspace_member_rbac_access_task( ) return - app_ids: list[str] = [] - dataset_ids: list[str] = [] + auto_included: dict[enterprise_rbac_service.RBACResourceType, list[str]] = { + kind.resource_type: [] for kind in _WHITELIST_RESOURCE_KINDS + } for resources in _iter_resource_config_batches(tenant_id, APP_RBAC_RESOURCE_CONFIG_BATCH_SIZE): configs = enterprise_rbac_service.RBACService.ResourceWhitelistConfigs.batch_get( tenant_id=tenant_id, @@ -157,38 +250,14 @@ def sync_joined_workspace_member_rbac_access_task( for config in configs.data: if not config.automatic_include_workspace_members: continue - if config.resource_type == enterprise_rbac_service.RBACResourceType.APP: - app_ids.append(config.resource_id) - elif config.resource_type == enterprise_rbac_service.RBACResourceType.DATASET: - dataset_ids.append(config.resource_id) + ids = auto_included.get(config.resource_type) + if ids is not None: + ids.append(config.resource_id) - for app_id_batch in _chunks(app_ids, APP_RBAC_MEMBER_APPEND_BATCH_SIZE): - enterprise_rbac_service.RBACService.AppAccess.append_whitelist_members_batch( - tenant_id=tenant_id, - account_id=actor_account_id, - data=[ - enterprise_rbac_service.AppendAppWhitelistMembersBatchItem( - app_id=app_id, - account_ids=[member_account_id], - policy_id=APP_RBAC_DEFAULT_ACCESS_POLICY_ID, - ) - for app_id in app_id_batch - ], - ) - - for dataset_id_batch in _chunks(dataset_ids, APP_RBAC_MEMBER_APPEND_BATCH_SIZE): - enterprise_rbac_service.RBACService.DatasetAccess.append_whitelist_members_batch( - tenant_id=tenant_id, - account_id=actor_account_id, - data=[ - enterprise_rbac_service.AppendDatasetWhitelistMembersBatchItem( - dataset_id=dataset_id, - account_ids=[member_account_id], - policy_id=APP_RBAC_DEFAULT_ACCESS_POLICY_ID, - ) - for dataset_id in dataset_id_batch - ], - ) + for resource_type, resource_ids in auto_included.items(): + kind = _WHITELIST_RESOURCE_KIND_BY_TYPE[resource_type] + for id_batch in _chunks(resource_ids, APP_RBAC_MEMBER_APPEND_BATCH_SIZE): + kind.append_member(tenant_id, actor_account_id, id_batch, member_account_id) except Exception as exc: logger.exception( "Failed to sync joined member RBAC access; retrying: tenant_id=%s member=%s attempt=%s", diff --git a/api/tests/unit_tests/commands/test_rbac_agent_migration.py b/api/tests/unit_tests/commands/test_rbac_agent_migration.py new file mode 100644 index 00000000000..7a324e33034 --- /dev/null +++ b/api/tests/unit_tests/commands/test_rbac_agent_migration.py @@ -0,0 +1,193 @@ +import json +from collections.abc import Iterator +from contextlib import ExitStack +from dataclasses import dataclass, field +from unittest.mock import MagicMock, patch + +import pytest +from click.testing import CliRunner, Result + +from commands.rbac import migrate_agent_permissions_to_rbac +from services.enterprise.rbac_service import ( + LegacyAgentMigrationReport, + LegacyAgentRoleMigration, + _LegacyResourceWhitelistConfig, +) + +MODULE = "commands.rbac" + + +def _events(output: str) -> list[dict[str, object]]: + return [json.loads(line) for line in output.splitlines() if line.startswith("{")] + + +@pytest.fixture(autouse=True) +def _no_agents() -> Iterator[None]: + with patch(f"{MODULE}._iter_agent_row_batches", return_value=iter(())): + yield + + +def _roles() -> list[LegacyAgentRoleMigration]: + return [ + LegacyAgentRoleMigration( + role_id="r1", + role_name="ops", + added_keys=["agent.create"], + removed_keys=["agent.manage"], + bound_policies=["agent.full_access"], + ), + LegacyAgentRoleMigration(role_id="r2", role_name="stuck", skipped="policy row missing"), + ] + + +def test_apply_flag_writes_and_reports_applied() -> None: + with ( + patch(f"{MODULE}._iter_tenant_ids", return_value=iter(["t1", "t2"])), + patch( + f"{MODULE}.RBACService.Migrations.migrate_agent_manage_roles", + return_value=LegacyAgentMigrationReport(roles=_roles()[:1]), + ) as migrate, + ): + result = CliRunner().invoke(migrate_agent_permissions_to_rbac, ["--apply"]) + + assert result.exit_code == 0, result.output + assert migrate.call_count == 2 + assert all(call.kwargs == {"apply": True} for call in migrate.call_args_list) + events = _events(result.output) + assert [e["event"] for e in events] == ["agent_manage_role_migration_applied"] * 2 + assert {e["tenant_id"] for e in events} == {"t1", "t2"} + assert "changed" in result.output + + +def _whitelist_config( + scope: str | None = "all", + account_ids: list[str] | None = None, +) -> _LegacyResourceWhitelistConfig: + return _LegacyResourceWhitelistConfig(rbac_whitelist_scope=scope, account_ids=account_ids or []) + + +@dataclass +class _AgentPhaseMocks: + agent_whitelist_config: MagicMock + app_whitelist_config: MagicMock + replace_whitelist: MagicMock + replace_user_access_policies: MagicMock + sync_creator_bindings: MagicMock + owner_account_id: MagicMock + member_batches: MagicMock + write_order: MagicMock + + +@dataclass +class _AgentPhaseSetup: + agents: list[tuple[str, str | None, str | None]] + configured_agent_ids: list[str] = field(default_factory=list) + agent_configs: list[_LegacyResourceWhitelistConfig] | None = None + app_config: _LegacyResourceWhitelistConfig = field(default_factory=_whitelist_config) + workspace_members: list[str] = field(default_factory=lambda: ["m1", "m2", "m3"]) + owner_account_id: str = "owner-1" + + +def _run_agent_phase(args: list[str], setup: _AgentPhaseSetup) -> tuple[Result, _AgentPhaseMocks]: + def _member_batches(_tenant_id: str, batch_size: int) -> Iterator[list[str]]: + for start in range(0, len(setup.workspace_members), batch_size): + yield setup.workspace_members[start : start + batch_size] + + with ExitStack() as stack: + stack.enter_context(patch(f"{MODULE}._iter_tenant_ids", return_value=iter(["t1"]))) + stack.enter_context( + patch( + f"{MODULE}.RBACService.Migrations.migrate_agent_manage_roles", + return_value=LegacyAgentMigrationReport(), + ) + ) + stack.enter_context(patch(f"{MODULE}._iter_agent_row_batches", return_value=iter([setup.agents]))) + stack.enter_context( + patch( + f"{MODULE}.RBACService.Migrations.list_configured_agent_ids", + return_value=setup.configured_agent_ids, + ) + ) + agent_whitelist_config = stack.enter_context(patch(f"{MODULE}.RBACService.AgentAccess.legacy_whitelist_config")) + if setup.agent_configs is None: + agent_whitelist_config.return_value = _whitelist_config() + else: + agent_whitelist_config.side_effect = setup.agent_configs + mocks = _AgentPhaseMocks( + agent_whitelist_config=agent_whitelist_config, + app_whitelist_config=stack.enter_context( + patch(f"{MODULE}.RBACService.AppAccess.legacy_whitelist_config", return_value=setup.app_config) + ), + replace_whitelist=stack.enter_context(patch(f"{MODULE}.RBACService.AgentAccess.replace_whitelist")), + replace_user_access_policies=stack.enter_context( + patch(f"{MODULE}.RBACService.AgentAccess.replace_user_access_policies") + ), + sync_creator_bindings=stack.enter_context( + patch(f"{MODULE}.RBACService.AccessPolicies.sync_creator_access_policy_member_bindings") + ), + owner_account_id=stack.enter_context( + patch(f"{MODULE}._owner_account_id", return_value=setup.owner_account_id) + ), + member_batches=stack.enter_context( + patch(f"{MODULE}._workspace_member_account_id_batches", side_effect=_member_batches) + ), + write_order=MagicMock(), + ) + mocks.write_order.attach_mock(mocks.replace_user_access_policies, "seed_members") + mocks.write_order.attach_mock(mocks.sync_creator_bindings, "sync_creator") + mocks.write_order.attach_mock(mocks.replace_whitelist, "replace_whitelist") + result = CliRunner().invoke(migrate_agent_permissions_to_rbac, args) + return result, mocks + + +def _write_order(mocks: _AgentPhaseMocks) -> list[str]: + return [name for name, _, _ in mocks.write_order.mock_calls] + + +def test_agent_bootstrap_apply_writes_whitelist_member_batches_and_creator_sync() -> None: + result, mocks = _run_agent_phase( + ["--apply", "--member-batch-size", "2"], + _AgentPhaseSetup(agents=[("ag1", "c1", None)], workspace_members=["m1", "m2", "m3"]), + ) + + assert result.exit_code == 0, result.output + assert [e["event"] for e in _events(result.output)] == ["agent_access_bootstrap_applied"] + assert _events(result.output)[0]["dry_run"] is False + + mocks.replace_whitelist.assert_called_once() + assert mocks.replace_whitelist.call_args.kwargs["agent_id"] == "ag1" + assert mocks.replace_whitelist.call_args.kwargs["account_id"] == "c1" + assert mocks.replace_whitelist.call_args.kwargs["payload"].automatic_include_workspace_members is True + + mocks.member_batches.assert_called_once_with("t1", 2) + assert mocks.replace_user_access_policies.call_count == 2 + calls = mocks.replace_user_access_policies.call_args_list + assert [call.kwargs["payload"].account_ids for call in calls] == [["m1", "m2"], ["m3"]] + assert all(call.kwargs["payload"].access_policy_ids == ["default"] for call in calls) + assert all(call.kwargs["target_account_id"] is None for call in calls) + + mocks.sync_creator_bindings.assert_called_once() + assert mocks.sync_creator_bindings.call_args.kwargs["resource_id"] == "ag1" + assert mocks.sync_creator_bindings.call_args.kwargs["account_id"] == "c1" + assert "1 agent(s) changed, 0 already initialised" in result.output + + assert _write_order(mocks) == ["seed_members", "seed_members", "sync_creator", "replace_whitelist"] + + +def test_agent_bootstrap_is_idempotent_on_a_second_apply() -> None: + result, mocks = _run_agent_phase( + ["--apply"], + _AgentPhaseSetup( + agents=[("ag1", "c1", None), ("ag2", "c2", None)], + configured_agent_ids=["ag1", "ag2"], + ), + ) + + assert result.exit_code == 0, result.output + events = _events(result.output) + assert [e["event"] for e in events] == ["agent_access_bootstrap_skipped"] * 2 + assert {e["reason"] for e in events} == {"already_initialized"} + mocks.replace_whitelist.assert_not_called() + mocks.sync_creator_bindings.assert_not_called() + mocks.replace_user_access_policies.assert_not_called() + assert "0 agent(s) changed, 2 already initialised" in result.output diff --git a/api/tests/unit_tests/controllers/console/app/test_agent_app_sandbox.py b/api/tests/unit_tests/controllers/console/app/test_agent_app_sandbox.py index ae0f807a715..0ce728baef9 100644 --- a/api/tests/unit_tests/controllers/console/app/test_agent_app_sandbox.py +++ b/api/tests/unit_tests/controllers/console/app/test_agent_app_sandbox.py @@ -1,6 +1,6 @@ from __future__ import annotations -from inspect import getclosurevars, unwrap +from inspect import unwrap from types import FunctionType, SimpleNamespace import pytest @@ -11,6 +11,7 @@ from controllers.console import agent_app_sandbox as module from models.account import Account from models.model import App, AppMode, IconType from services.agent_app_sandbox_service import AgentSandboxDownload, AgentSandboxInfo, AgentSandboxInspectorError +from tests.unit_tests.controllers.rbac_introspection import rbac_checks class _AgentAppService: @@ -155,12 +156,10 @@ def _account() -> Account: ], ) def test_sandbox_resources_require_app_view_layout(method: FunctionType) -> None: - rbac_wrapper = unwrap(method, stop=lambda wrapper: "rbac_permission_required" in wrapper.__code__.co_qualname) - config = getclosurevars(rbac_wrapper).nonlocals + [check] = rbac_checks(method) - assert config["resource_type"] == module.RBACResourceScope.APP - assert config["scene"] == module.RBACPermission.APP_VIEW_LAYOUT - assert config["resource_required"] is True + assert isinstance(check.locator, module.PlainApp) + assert check.scene == module.RBACPermission.APP_VIEW_LAYOUT def test_handle_maps_sandbox_and_agent_backend_errors() -> None: diff --git a/api/tests/unit_tests/controllers/console/app/test_agent_manage_guard.py b/api/tests/unit_tests/controllers/console/app/test_agent_manage_guard.py deleted file mode 100644 index 707b11a3f97..00000000000 --- a/api/tests/unit_tests/controllers/console/app/test_agent_manage_guard.py +++ /dev/null @@ -1,186 +0,0 @@ -from unittest.mock import patch - -import pytest -from sqlalchemy.orm import Session, scoped_session, sessionmaker -from werkzeug.exceptions import Forbidden - -from controllers.console.app.error import AppNotFoundError -from controllers.console.app.wraps import agent_manage_required_for_agent_app -from core.rbac import RBACPermission, RBACResourceScope -from models import Account -from models.agent import Agent, AgentScope, AgentSource, AgentStatus -from models.model import App, AppMode -from tests.unit_tests.config_override import config_overrides_context - -TENANT_ID = "tenant-1" - - -def _guarded_view(): - calls: list[dict[str, object]] = [] - - @agent_manage_required_for_agent_app - def view(*args, **kwargs): - calls.append(kwargs) - return "ok" - - return view, calls - - -def _persist_app( - session: Session, - *, - scope: AgentScope | None = None, - status: AgentStatus = AgentStatus.ACTIVE, -) -> App: - app_model = App( - id="app-1", - tenant_id=TENANT_ID, - name="Managed App", - mode=AppMode.AGENT if scope is not None else AppMode.CHAT, - enable_site=True, - enable_api=False, - ) - session.add(app_model) - if scope is not None: - agent = Agent( - tenant_id=TENANT_ID, - name=f"{scope.value} agent", - scope=scope, - source=AgentSource.AGENT_APP if scope == AgentScope.ROSTER else AgentSource.WORKFLOW, - status=status, - app_id=app_model.id if scope == AgentScope.ROSTER else None, - backing_app_id=app_model.id if scope == AgentScope.WORKFLOW_ONLY else None, - ) - session.add(agent) - session.commit() - return app_model - - -def _patch_guard(account: Account, rbac_enabled: bool): - return ( - patch("controllers.console.app.wraps.current_account_with_tenant", return_value=(account, TENANT_ID)), - config_overrides_context(RBAC_ENABLED=rbac_enabled), - ) - - -class TestAgentManageRequiredForAgentApp: - @pytest.fixture(autouse=True) - def _bind_database( - self, - sqlite_session: Session, - sqlite_session_factory: sessionmaker[Session], - ): - self.sqlite_session = sqlite_session - self.account = Account(name="Guard User", email="guard@example.com") - self.account.id = "account-1" - session_proxy = scoped_session(sqlite_session_factory) - with patch("controllers.console.app.wraps.db.session", session_proxy): - yield - session_proxy.remove() - - def test_non_agent_app_passes_through_without_workspace_check(self): - view, calls = _guarded_view() - _persist_app(self.sqlite_session) - patches = _patch_guard(self.account, rbac_enabled=True) - - with patches[0], patches[1], patch("controllers.console.app.wraps.enforce_rbac_access") as gate: - assert view(app_id="app-1") == "ok" - - gate.assert_not_called() - assert calls == [{"app_id": "app-1"}] - - def test_roster_agent_app_requires_agent_manage_when_rbac_enabled(self): - view, _ = _guarded_view() - _persist_app(self.sqlite_session, scope=AgentScope.ROSTER) - patches = _patch_guard(self.account, rbac_enabled=True) - - with patches[0], patches[1], patch("controllers.console.app.wraps.enforce_rbac_access") as gate: - assert view(app_id="app-1") == "ok" - - gate.assert_called_once_with( - tenant_id=TENANT_ID, - account_id=self.account.id, - resource_type=RBACResourceScope.WORKSPACE, - scene=RBACPermission.AGENT_MANAGE, - resource_required=False, - ) - - def test_roster_agent_app_denied_without_agent_manage(self): - view, calls = _guarded_view() - _persist_app(self.sqlite_session, scope=AgentScope.ROSTER) - patches = _patch_guard(self.account, rbac_enabled=True) - - with ( - patches[0], - patches[1], - patch("controllers.console.app.wraps.enforce_rbac_access", side_effect=Forbidden()), - ): - with pytest.raises(Forbidden): - view(app_id="app-1") - - assert calls == [] - - def test_roster_agent_app_skips_workspace_check_when_rbac_disabled(self): - view, _ = _guarded_view() - _persist_app(self.sqlite_session, scope=AgentScope.ROSTER) - patches = _patch_guard(self.account, rbac_enabled=False) - - with patches[0], patches[1], patch("controllers.console.app.wraps.enforce_rbac_access") as gate: - assert view(app_id="app-1") == "ok" - - gate.assert_not_called() - - def test_hidden_backing_app_is_rejected_even_without_rbac(self): - """A workflow-only backing App is not part of the general app management plane.""" - view, calls = _guarded_view() - _persist_app(self.sqlite_session, scope=AgentScope.WORKFLOW_ONLY) - patches = _patch_guard(self.account, rbac_enabled=False) - - with patches[0], patches[1]: - with pytest.raises(AppNotFoundError): - view(app_id="app-1") - - assert calls == [] - - def test_hidden_backing_app_is_rejected_before_workspace_check(self): - view, calls = _guarded_view() - _persist_app(self.sqlite_session, scope=AgentScope.WORKFLOW_ONLY) - patches = _patch_guard(self.account, rbac_enabled=True) - - with patches[0], patches[1], patch("controllers.console.app.wraps.enforce_rbac_access") as gate: - with pytest.raises(AppNotFoundError): - view(app_id="app-1") - - gate.assert_not_called() - assert calls == [] - - def test_binding_lookup_covers_archived_agents(self): - """An Agent App stays gated after its roster Agent is archived.""" - view, _ = _guarded_view() - _persist_app(self.sqlite_session, scope=AgentScope.ROSTER, status=AgentStatus.ARCHIVED) - patches = _patch_guard(self.account, rbac_enabled=True) - - with patches[0], patches[1], patch("controllers.console.app.wraps.enforce_rbac_access") as gate: - view(app_id="app-1") - - gate.assert_called_once() - - def test_resource_id_path_alias_is_resolved(self): - view, _ = _guarded_view() - _persist_app(self.sqlite_session, scope=AgentScope.ROSTER) - patches = _patch_guard(self.account, rbac_enabled=True) - - with patches[0], patches[1], patch("controllers.console.app.wraps.enforce_rbac_access") as gate: - assert view(resource_id="app-1") == "ok" - - gate.assert_called_once() - - def test_unknown_app_passes_through_for_downstream_handling(self): - view, calls = _guarded_view() - patches = _patch_guard(self.account, rbac_enabled=True) - - with patches[0], patches[1], patch("controllers.console.app.wraps.enforce_rbac_access") as gate: - assert view(app_id="app-1") == "ok" - - gate.assert_not_called() - assert calls == [{"app_id": "app-1"}] diff --git a/api/tests/unit_tests/controllers/console/app/test_audio.py b/api/tests/unit_tests/controllers/console/app/test_audio.py index e9f01b00c48..1f8633923be 100644 --- a/api/tests/unit_tests/controllers/console/app/test_audio.py +++ b/api/tests/unit_tests/controllers/console/app/test_audio.py @@ -111,11 +111,11 @@ def test_agent_console_audio_api_uses_agent_draft( calls["asr"] = kwargs return {"text": "agent transcript"} - def enforce_rbac_access(**kwargs): + def enforce_rbac_checks(**kwargs): calls["rbac"] = kwargs monkeypatch.setattr(audio_module, "resolve_agent_runtime_app_model", resolve_agent_runtime_app_model) - monkeypatch.setattr(audio_module, "enforce_rbac_access", enforce_rbac_access) + monkeypatch.setattr(audio_module, "enforce_rbac_checks", enforce_rbac_checks) monkeypatch.setattr(AgentComposerService, "load_agent_soul_for_debug", load_agent_soul_for_debug) monkeypatch.setattr(AudioService, "transcript_agent_asr", transcript_agent_asr) @@ -138,13 +138,13 @@ def test_agent_console_audio_api_uses_agent_draft( assert response == {"text": "agent transcript"} assert calls["resolver"] == {"session": session, "tenant_id": "tenant-1", "agent_id": agent_id} - assert calls["rbac"] == { - "tenant_id": "tenant-1", - "account_id": "account-1", - "resource_type": audio_module.RBACResourceScope.APP, - "scene": audio_module.RBACPermission.APP_TEST_AND_RUN, - "path_args": {"app_id": "backing-app-1"}, - } + rbac_call = calls["rbac"] + assert rbac_call["tenant_id"] == "tenant-1" + assert rbac_call["account_id"] == "account-1" + assert rbac_call["path_args"] == {"app_id": "backing-app-1"} + (rbac_check,) = rbac_call["checks"] + assert rbac_check.scene is audio_module.RBACPermission.APP_TEST_AND_RUN + assert isinstance(rbac_check.locator, audio_module.PlainApp) assert calls["draft"] == { "tenant_id": "tenant-1", "agent_id": str(agent_id), @@ -216,7 +216,7 @@ def test_agent_console_audio_api_checks_rbac_with_backing_app_id( soul_loaded = True return AgentSoulConfig() - monkeypatch.setattr(audio_module, "enforce_rbac_access", deny_access) + monkeypatch.setattr(audio_module, "enforce_rbac_checks", deny_access) monkeypatch.setattr(AgentComposerService, "load_agent_soul_for_debug", load_agent_soul_for_debug) api = AgentChatMessageAudioApi() diff --git a/api/tests/unit_tests/controllers/console/app/test_mcp_server_response.py b/api/tests/unit_tests/controllers/console/app/test_mcp_server_response.py index 5e5762f9260..5db0cbe2a4c 100644 --- a/api/tests/unit_tests/controllers/console/app/test_mcp_server_response.py +++ b/api/tests/unit_tests/controllers/console/app/test_mcp_server_response.py @@ -315,19 +315,20 @@ class TestAppMCPServerRefreshController: "controllers.common.wraps.current_account_with_tenant", return_value=(current_user, "tenant-1"), ), + patch("controllers.common.rbac.locators.agent_binding", return_value=None), + patch("controllers.common.rbac.locators.PlainApp.owner_id", return_value=None), patch( - "controllers.common.wraps.enforce_rbac_access", + "controllers.common.rbac.checks.RBACService.CheckAccess.check", side_effect=PermissionCheckedError, - ) as enforce_rbac_access, + ) as check_access, pytest.raises(PermissionCheckedError), ): method(AppMCPServerRefreshController(), app_id="app-1") - enforce_rbac_access.assert_called_once_with( - tenant_id="tenant-1", - account_id="account-1", - resource_type=RBACResourceScope.APP, + check_access.assert_called_once_with( + "tenant-1", + "account-1", scene=RBACPermission.APP_EDIT, - resource_required=True, - path_args={"app_id": "app-1"}, + resource_type=RBACResourceScope.APP, + resource_id="app-1", ) diff --git a/api/tests/unit_tests/controllers/console/app/test_ops_trace_api.py b/api/tests/unit_tests/controllers/console/app/test_ops_trace_api.py index 03922de106d..25f94e9aa67 100644 --- a/api/tests/unit_tests/controllers/console/app/test_ops_trace_api.py +++ b/api/tests/unit_tests/controllers/console/app/test_ops_trace_api.py @@ -11,6 +11,7 @@ import pytest from flask import Flask from werkzeug.exceptions import Forbidden +from controllers.common.rbac import PlainApp from controllers.console import flask_admission from controllers.console.app import ops_trace as ops_trace_module from controllers.console.app.error import ( @@ -138,19 +139,19 @@ def test_trace_config_mutations_require_app_tracing_permission_when_rbac_is_enab lambda: AccountWithTenant(account=account, tenant_id=WORKSPACE_ID), ) denied = MagicMock(side_effect=Forbidden()) - monkeypatch.setattr(flask_admission, "enforce_rbac_access", denied) + monkeypatch.setattr(flask_admission, "enforce_rbac_checks", denied) with app.test_request_context(), pytest.raises(Forbidden): _admission_injector(method)(None, app_id=UUID(APP_ID)) - denied.assert_called_once_with( - tenant_id=WORKSPACE_ID, - account_id=ACCOUNT_ID, - resource_type=ops_trace_module.RBACResourceScope.APP, - scene=ops_trace_module.RBACPermission.APP_TRACING_CONFIG, - resource_required=True, - path_args={"app_id": UUID(APP_ID)}, - ) + denied.assert_called_once() + call = denied.call_args.kwargs + assert call["tenant_id"] == WORKSPACE_ID + assert call["account_id"] == ACCOUNT_ID + assert call["path_args"] == {"app_id": UUID(APP_ID)} + (check,) = call["checks"] + assert check.scene == ops_trace_module.RBACPermission.APP_TRACING_CONFIG + assert isinstance(check.locator, PlainApp) def test_trace_config_get_preserves_read_access_for_normal_member() -> None: diff --git a/api/tests/unit_tests/controllers/console/datasets/rag_pipeline/test_rag_pipeline.py b/api/tests/unit_tests/controllers/console/datasets/rag_pipeline/test_rag_pipeline.py index f3b0cb25953..5ef83ea902f 100644 --- a/api/tests/unit_tests/controllers/console/datasets/rag_pipeline/test_rag_pipeline.py +++ b/api/tests/unit_tests/controllers/console/datasets/rag_pipeline/test_rag_pipeline.py @@ -1,7 +1,7 @@ from __future__ import annotations from collections.abc import Iterator -from inspect import getclosurevars, unwrap +from inspect import unwrap from unittest.mock import ANY, PropertyMock, patch import pytest @@ -28,6 +28,7 @@ from services.entities.knowledge_entities.rag_pipeline_entities import PipelineT from services.errors.account import NoPermissionError from services.errors.rag_pipeline import RagPipelineResourceNotFoundError from tests.unit_tests.config_override import config_overrides_context +from tests.unit_tests.controllers.rbac_introspection import rbac_checks def _template_item() -> dict[str, object]: @@ -362,11 +363,8 @@ class TestCustomizedPipelineTemplateApi: class TestPublishCustomizedPipelineTemplateApi: def test_post_uses_pipeline_release_rbac_scene(self) -> None: - method = PublishCustomizedPipelineTemplateApi.post - while "scene" not in getclosurevars(method).nonlocals: - method = method.__wrapped__ - - assert getclosurevars(method).nonlocals["scene"] == module.RBACPermission.DATASET_PIPELINE_RELEASE + [check] = rbac_checks(PublishCustomizedPipelineTemplateApi.post) + assert check.scene == module.RBACPermission.DATASET_PIPELINE_RELEASE def test_post_validates_payload_and_returns_empty_204(self) -> None: api = PublishCustomizedPipelineTemplateApi() diff --git a/api/tests/unit_tests/controllers/console/datasets/rag_pipeline/test_rag_pipeline_draft_variable.py b/api/tests/unit_tests/controllers/console/datasets/rag_pipeline/test_rag_pipeline_draft_variable.py index b2afb14d252..581adaafe37 100644 --- a/api/tests/unit_tests/controllers/console/datasets/rag_pipeline/test_rag_pipeline_draft_variable.py +++ b/api/tests/unit_tests/controllers/console/datasets/rag_pipeline/test_rag_pipeline_draft_variable.py @@ -1,5 +1,5 @@ from collections.abc import Callable, Iterator -from inspect import getclosurevars, unwrap +from inspect import unwrap from types import SimpleNamespace from unittest.mock import MagicMock, patch @@ -9,6 +9,7 @@ from sqlalchemy import Engine from sqlalchemy.orm import Session, scoped_session, sessionmaker from controllers.common.errors import InvalidArgumentError, NotFoundError +from controllers.common.rbac import RBACResourceScope from controllers.console import console_ns from controllers.console.app.error import DraftWorkflowNotExist from controllers.console.datasets.rag_pipeline.rag_pipeline_draft_variable import ( @@ -21,7 +22,7 @@ from controllers.console.datasets.rag_pipeline.rag_pipeline_draft_variable impor RagPipelineVariableResetApi, WorkflowDraftVariablePatchPayload, ) -from controllers.console.wraps import RBACPermission, RBACResourceScope +from controllers.console.wraps import RBACPermission from core.workflow.llm_environment_variable import LLMEnvironmentVariable from core.workflow.variable_prefixes import SYSTEM_VARIABLE_NODE_ID from factories.variable_factory import build_segment @@ -30,6 +31,7 @@ from graphon.variables import IntegerVariable, StringVariable from models.account import Account, TenantAccountRole from models.dataset import Pipeline from models.workflow import Workflow, WorkflowDraftVariable, WorkflowType +from tests.unit_tests.controllers.rbac_introspection import rbac_checks @pytest.fixture @@ -102,12 +104,11 @@ def restx_config(app): def test_rag_draft_variable_routes_require_dataset_edit_permission() -> None: route = RagPipelineVariableApi.get legacy_gate = unwrap(route, stop=lambda decorator: "edit_permission_required" in decorator.__code__.co_qualname) - rbac_gate = unwrap(route, stop=lambda decorator: "scene" in getclosurevars(decorator).nonlocals) - assert "edit_permission_required" in legacy_gate.__code__.co_qualname - permissions = getclosurevars(rbac_gate).nonlocals - assert permissions["resource_type"] == RBACResourceScope.DATASET - assert permissions["scene"] == RBACPermission.DATASET_EDIT + + [check] = rbac_checks(route) + assert check.locator.scope == RBACResourceScope.DATASET + assert check.scene == RBACPermission.DATASET_EDIT class TestRagPipelineVariableCollectionApi: diff --git a/api/tests/unit_tests/controllers/console/tag/test_tags.py b/api/tests/unit_tests/controllers/console/tag/test_tags.py index 5d19e08a557..d40b84bd4dd 100644 --- a/api/tests/unit_tests/controllers/console/tag/test_tags.py +++ b/api/tests/unit_tests/controllers/console/tag/test_tags.py @@ -166,7 +166,7 @@ class TestTagListApi: app.test_request_context("/"), config_overrides_context(RBAC_ENABLED=True), patch.object(module, "current_account_with_tenant", return_value=(owner, "tenant-1")), - patch.object(module, "enforce_rbac_access") as enforce_rbac_access, + patch.object(module, "enforce_rbac_checks") as enforce_rbac_checks, ): unwrap(TagListApi().post)( TagListApi(), @@ -174,13 +174,13 @@ class TestTagListApi: request_context, ) - enforce_rbac_access.assert_called_once_with( - tenant_id="tenant-1", - account_id="user-1", - resource_type=module.RBACResourceScope.WORKSPACE, - scene=module.RBACPermission.SNIPPETS_CREATE_AND_MODIFY, - resource_required=False, - ) + enforce_rbac_checks.assert_called_once() + rbac_kwargs = enforce_rbac_checks.call_args.kwargs + assert rbac_kwargs["tenant_id"] == "tenant-1" + assert rbac_kwargs["account_id"] == "user-1" + (rbac_check,) = rbac_kwargs["checks"] + assert rbac_check.scene is module.RBACPermission.SNIPPETS_CREATE_AND_MODIFY + assert isinstance(rbac_check.locator, module.Workspace) def test_post_rejects_read_only_member(self, app: Flask, request_context: RequestContext) -> None: readonly = _account(TenantAccountRole.NORMAL) @@ -249,7 +249,7 @@ class TestTagUpdateDeleteApi: app.test_request_context("/"), config_overrides_context(RBAC_ENABLED=True), patch.object(module, "current_account_with_tenant", return_value=(owner, "tenant-1")), - patch.object(module, "enforce_rbac_access") as enforce_rbac_access, + patch.object(module, "enforce_rbac_checks") as enforce_rbac_checks, ): result, status = unwrap(TagUpdateDeleteApi().patch)( TagUpdateDeleteApi(), @@ -258,13 +258,13 @@ class TestTagUpdateDeleteApi: "tag-1", ) - enforce_rbac_access.assert_called_once_with( - tenant_id="tenant-1", - account_id="user-1", - resource_type=module.RBACResourceScope.WORKSPACE, - scene=module.RBACPermission.SNIPPETS_CREATE_AND_MODIFY, - resource_required=False, - ) + enforce_rbac_checks.assert_called_once() + rbac_kwargs = enforce_rbac_checks.call_args.kwargs + assert rbac_kwargs["tenant_id"] == "tenant-1" + assert rbac_kwargs["account_id"] == "user-1" + (rbac_check,) = rbac_kwargs["checks"] + assert rbac_check.scene is module.RBACPermission.SNIPPETS_CREATE_AND_MODIFY + assert isinstance(rbac_check.locator, module.Workspace) tags_service.update_tag.assert_called_once_with(request_context, "tag-1", UpdateTagInput(name="Updated")) assert status == 200 assert result["binding_count"] == "3" @@ -345,12 +345,12 @@ class TestTagUpdateDeleteApi: app.test_request_context("/"), config_overrides_context(RBAC_ENABLED=True), patch.object(module, "current_account_with_tenant", return_value=(owner, "tenant-1")), - patch.object(module, "enforce_rbac_access") as enforce_rbac_access, + patch.object(module, "enforce_rbac_checks") as enforce_rbac_checks, ): unwrap(TagUpdateDeleteApi().delete)(TagUpdateDeleteApi(), request_context, "tag-1") tags_service.get_tag_type.assert_called_once_with(request_context, "tag-1") - enforce_rbac_access.assert_called_once() + enforce_rbac_checks.assert_called_once() def test_delete_does_not_authorize_tag_outside_current_workspace( self, app: Flask, request_context: RequestContext, tags_service: MagicMock @@ -363,12 +363,12 @@ class TestTagUpdateDeleteApi: app.test_request_context("/"), config_overrides_context(RBAC_ENABLED=True), patch.object(module, "current_account_with_tenant", return_value=(owner, "tenant-1")), - patch.object(module, "enforce_rbac_access") as enforce_rbac_access, + patch.object(module, "enforce_rbac_checks") as enforce_rbac_checks, ): with pytest.raises(NotFound): unwrap(TagUpdateDeleteApi().delete)(TagUpdateDeleteApi(), request_context, "tag-1") - enforce_rbac_access.assert_not_called() + enforce_rbac_checks.assert_not_called() class TestTagBindings: diff --git a/api/tests/unit_tests/controllers/console/test_apikey.py b/api/tests/unit_tests/controllers/console/test_apikey.py index 8955ddf6bf7..d6ae0d4bd7f 100644 --- a/api/tests/unit_tests/controllers/console/test_apikey.py +++ b/api/tests/unit_tests/controllers/console/test_apikey.py @@ -57,9 +57,9 @@ def _make_account(role: TenantAccountRole) -> Account: return account -def _persist_app(session: Session, *, mode: AppMode = AppMode.CHAT) -> App: +def _persist_app(session: Session, *, mode: AppMode = AppMode.CHAT, app_id: str = "app-1") -> App: app = App( - id="app-1", + id=app_id, tenant_id="tenant-1", name="API key app", mode=mode, @@ -260,15 +260,23 @@ def test_api_key_lists_require_matching_rbac_permission(config_overrides: Callab cases = [ ( lambda: AppApiKeyListResource().get(resource_id=api_id), - [(RBACResourceScope.APP, RBACPermission.APP_RELEASE_AND_VERSION, True)], + { + "scene": RBACPermission.APP_RELEASE_AND_VERSION, + "resource_type": RBACResourceScope.APP, + "resource_id": str(api_id), + }, ), ( lambda: DatasetApiKeyApi().get(), - [(RBACResourceScope.DATASET, RBACPermission.DATASET_API_KEY_MANAGE, False)], + {"scene": RBACPermission.DATASET_API_KEY_MANAGE, "resource_type": None, "resource_id": None}, ), ( lambda: DatasetApiKeyListResource().get(resource_id=api_id), - [(RBACResourceScope.DATASET, RBACPermission.DATASET_API_KEY_MANAGE, True)], + { + "scene": RBACPermission.DATASET_API_KEY_MANAGE, + "resource_type": RBACResourceScope.DATASET, + "resource_id": str(api_id), + }, ), ] @@ -276,20 +284,25 @@ def test_api_key_lists_require_matching_rbac_permission(config_overrides: Callab app.test_request_context("/"), patch("controllers.console.wraps.current_account_with_tenant", return_value=(account, "tenant-1")), patch("controllers.common.wraps.current_account_with_tenant", return_value=(account, "tenant-1")), + patch("controllers.common.rbac.locators.agent_binding", return_value=None), + patch("controllers.common.rbac.locators.PlainApp.owner_id", return_value=None), + patch("controllers.common.rbac.locators.DatasetId.owner_id", return_value=None), patch.object(BaseApiKeyListResource, "_get_api_key_list") as get_api_key_list, ): - for invoke, expected_gates in cases: + for invoke, expected_kwargs in cases: with patch( - "controllers.common.wraps.enforce_rbac_access", - side_effect=[None] * (len(expected_gates) - 1) + [Forbidden()], - ) as enforce_rbac_access: + "controllers.common.rbac.checks.RBACService.CheckAccess.check", return_value=False + ) as check_access: with pytest.raises(Forbidden): invoke() - assert [ - (kwargs["resource_type"], kwargs["scene"], kwargs["resource_required"]) - for _, kwargs in enforce_rbac_access.call_args_list - ] == expected_gates + check_access.assert_called_once_with( + "tenant-1", + account.id, + scene=expected_kwargs["scene"], + resource_type=expected_kwargs["resource_type"], + resource_id=expected_kwargs["resource_id"], + ) get_api_key_list.assert_not_called() diff --git a/api/tests/unit_tests/controllers/console/test_oauth_custom_client_permissions.py b/api/tests/unit_tests/controllers/console/test_oauth_custom_client_permissions.py index 92b5ac14406..d53b4572526 100644 --- a/api/tests/unit_tests/controllers/console/test_oauth_custom_client_permissions.py +++ b/api/tests/unit_tests/controllers/console/test_oauth_custom_client_permissions.py @@ -1,28 +1,27 @@ -from inspect import getclosurevars, unwrap +from inspect import unwrap from types import FunctionType import pytest -from controllers.common.wraps import RBACPermission, RBACResourceScope +from controllers.common.rbac import RBACPermission, Workspace from controllers.console.datasets.rag_pipeline.datasource_auth import DatasourceAuthOauthCustomClient from controllers.console.workspace.tool_providers import ToolOAuthCustomClient +from tests.unit_tests.controllers.rbac_introspection import rbac_checks @pytest.mark.parametrize( - ("method", "legacy_gate", "resource_type"), + ("method", "legacy_gate"), [ - (ToolOAuthCustomClient.delete, "is_admin_or_owner_required", RBACResourceScope.WORKSPACE), - (DatasourceAuthOauthCustomClient.delete, "edit_permission_required", RBACResourceScope.DATASET), + (ToolOAuthCustomClient.delete, "is_admin_or_owner_required"), + (DatasourceAuthOauthCustomClient.delete, "edit_permission_required"), ], ) -def test_custom_oauth_client_delete_requires_management_permission( - method: FunctionType, legacy_gate: str, resource_type: RBACResourceScope -) -> None: +def test_custom_oauth_client_delete_requires_management_permission(method: FunctionType, legacy_gate: str) -> None: legacy_wrapper = unwrap(method, stop=lambda wrapper: legacy_gate in wrapper.__code__.co_qualname) assert legacy_gate in legacy_wrapper.__code__.co_qualname - rbac_wrapper = unwrap(method, stop=lambda wrapper: "rbac_permission_required" in wrapper.__code__.co_qualname) - rbac_config = getclosurevars(rbac_wrapper).nonlocals - assert rbac_config["resource_type"] == resource_type - assert rbac_config["scene"] == RBACPermission.CREDENTIAL_MANAGE - assert rbac_config["resource_required"] is False + # Both routes pass resource_required=False, so their bundle resolves to a Workspace + # locator regardless of the resource_type they declared (see _single_check). + [check] = rbac_checks(method) + assert check.scene == RBACPermission.CREDENTIAL_MANAGE + assert isinstance(check.locator, Workspace) diff --git a/api/tests/unit_tests/controllers/console/test_workspace_credential_mutation_permissions.py b/api/tests/unit_tests/controllers/console/test_workspace_credential_mutation_permissions.py index 9714a7cceb9..03cf9767036 100644 --- a/api/tests/unit_tests/controllers/console/test_workspace_credential_mutation_permissions.py +++ b/api/tests/unit_tests/controllers/console/test_workspace_credential_mutation_permissions.py @@ -1,9 +1,9 @@ -from inspect import getclosurevars, unwrap +from inspect import unwrap from types import FunctionType import pytest -from controllers.common.wraps import RBACPermission, RBACResourceScope +from controllers.common.rbac import AgentId, RBACPermission, Workspace from controllers.console.agent.composer import AgentComposerApi from controllers.console.agent.roster import AgentAppApi from controllers.console.datasets.data_source import DataSourceApi @@ -11,6 +11,7 @@ from controllers.console.datasets.rag_pipeline.datasource_auth import Datasource from controllers.console.workspace.model_providers import ModelProviderCredentialApi from controllers.console.workspace.models import ModelProviderModelCredentialApi from controllers.console.workspace.tool_providers import ToolBuiltinProviderAddApi, ToolOAuthCustomClient +from tests.unit_tests.controllers.rbac_introspection import rbac_checks @pytest.mark.parametrize( @@ -27,11 +28,9 @@ def test_workspace_credential_mutations_require_management_permission( legacy_wrapper = unwrap(method, stop=lambda wrapper: "is_admin_or_owner_required" in wrapper.__code__.co_qualname) assert "is_admin_or_owner_required" in legacy_wrapper.__code__.co_qualname - rbac_wrapper = unwrap(method, stop=lambda wrapper: "rbac_permission_required" in wrapper.__code__.co_qualname) - rbac_config = getclosurevars(rbac_wrapper).nonlocals - assert rbac_config["resource_type"] == RBACResourceScope.WORKSPACE - assert rbac_config["scene"] == permission - assert rbac_config["resource_required"] is False + [check] = rbac_checks(method) + assert check.scene == permission + assert isinstance(check.locator, Workspace) @pytest.mark.parametrize( @@ -49,11 +48,9 @@ def test_model_provider_credential_get_requires_admin_and_rbac( legacy_wrapper = unwrap(method, stop=lambda wrapper: "is_admin_or_owner_required" in wrapper.__code__.co_qualname) assert "is_admin_or_owner_required" in legacy_wrapper.__code__.co_qualname - rbac_wrapper = unwrap(method, stop=lambda wrapper: "rbac_permission_required" in wrapper.__code__.co_qualname) - rbac_config = getclosurevars(rbac_wrapper).nonlocals - assert rbac_config["resource_type"] == RBACResourceScope.WORKSPACE - assert rbac_config["scene"] == RBACPermission.CREDENTIAL_MANAGE - assert rbac_config["resource_required"] is False + [check] = rbac_checks(method) + assert check.scene == RBACPermission.CREDENTIAL_MANAGE + assert isinstance(check.locator, Workspace) def test_tool_oauth_custom_client_get_requires_admin_and_rbac() -> None: @@ -64,11 +61,9 @@ def test_tool_oauth_custom_client_get_requires_admin_and_rbac() -> None: legacy_wrapper = unwrap(method, stop=lambda wrapper: "is_admin_or_owner_required" in wrapper.__code__.co_qualname) assert "is_admin_or_owner_required" in legacy_wrapper.__code__.co_qualname - rbac_wrapper = unwrap(method, stop=lambda wrapper: "rbac_permission_required" in wrapper.__code__.co_qualname) - rbac_config = getclosurevars(rbac_wrapper).nonlocals - assert rbac_config["resource_type"] == RBACResourceScope.WORKSPACE - assert rbac_config["scene"] == RBACPermission.CREDENTIAL_MANAGE - assert rbac_config["resource_required"] is False + [check] = rbac_checks(method) + assert check.scene == RBACPermission.CREDENTIAL_MANAGE + assert isinstance(check.locator, Workspace) def test_datasource_auth_get_requires_edit_and_rbac() -> None: @@ -79,11 +74,9 @@ def test_datasource_auth_get_requires_edit_and_rbac() -> None: edit_wrapper = unwrap(method, stop=lambda wrapper: "edit_permission_required" in wrapper.__code__.co_qualname) assert "edit_permission_required" in edit_wrapper.__code__.co_qualname - rbac_wrapper = unwrap(method, stop=lambda wrapper: "rbac_permission_required" in wrapper.__code__.co_qualname) - rbac_config = getclosurevars(rbac_wrapper).nonlocals - assert rbac_config["resource_type"] == RBACResourceScope.DATASET - assert rbac_config["scene"] == RBACPermission.CREDENTIAL_MANAGE - assert rbac_config["resource_required"] is False + [check] = rbac_checks(method) + assert check.scene == RBACPermission.CREDENTIAL_MANAGE + assert isinstance(check.locator, Workspace) @pytest.mark.parametrize( @@ -94,10 +87,8 @@ def test_datasource_auth_get_requires_edit_and_rbac() -> None: ], ) def test_agent_app_get_requires_rbac(method: FunctionType) -> None: - """GET endpoints that return agent app details or composer state must enforce - the same RBAC gates as their sibling PUT/DELETE methods.""" - rbac_wrapper = unwrap(method, stop=lambda wrapper: "rbac_permission_required" in wrapper.__code__.co_qualname) - rbac_config = getclosurevars(rbac_wrapper).nonlocals - assert rbac_config["resource_type"] == RBACResourceScope.WORKSPACE - assert rbac_config["scene"] == RBACPermission.AGENT_MANAGE - assert rbac_config["resource_required"] is False + """GET endpoints that return agent app details or composer state must require + the agent preview scene on the agent resource, same as their sibling routes.""" + [check] = rbac_checks(method) + assert check.scene == RBACPermission.AGENT_PREVIEW + assert isinstance(check.locator, AgentId) diff --git a/api/tests/unit_tests/controllers/console/test_wraps.py b/api/tests/unit_tests/controllers/console/test_wraps.py index aa3d966bdf7..ee8926bd4d3 100644 --- a/api/tests/unit_tests/controllers/console/test_wraps.py +++ b/api/tests/unit_tests/controllers/console/test_wraps.py @@ -4,14 +4,14 @@ from typing import override from unittest.mock import MagicMock, patch import pytest -from flask import Flask, request +from flask import Flask from flask_login import LoginManager, UserMixin from pydantic import BaseModel from sqlalchemy import select from sqlalchemy.orm import Session from werkzeug.exceptions import HTTPException -from controllers.common.wraps import _extract_resource_id +from controllers.common.rbac import DatasetId, PlainApp, RBACCheck, Workspace from controllers.console import api as console_api from controllers.console import flask_admission from controllers.console import wraps as wraps_module @@ -41,10 +41,10 @@ from controllers.console.wraps import ( from enums import DeploymentEdition from libs.login import AccountWithTenant from machinery.context import RequestContext -from machinery.errors import ActiveWorkspaceRequiredError, AdmissionConfigurationError +from machinery.errors import ActiveWorkspaceRequiredError from models import Account, DifySetup from models.account import AccountStatus, TenantAccountRole -from models.dataset import Dataset, RateLimitLog +from models.dataset import RateLimitLog from services.entities.feature_entities import LicenseStatus from tests.unit_tests.config_override import config_overrides_context @@ -359,15 +359,13 @@ class TestCurrentContextInjection: ), patch("controllers.console.flask_admission.get_request_id", return_value="request-1"), patch("controllers.console.flask_admission.get_trace_id", return_value=None), - patch("controllers.console.flask_admission.enforce_rbac_access") as enforce_rbac_access, + patch("controllers.console.flask_admission.enforce_rbac_checks") as enforce_rbac_checks, ): class Handler: @flask_admission.console_account_admission( allowed_roles=frozenset({TenantAccountRole.ADMIN, TenantAccountRole.OWNER}), - rbac_resource_scope=RBACResourceScope.WORKSPACE, - rbac_permission=RBACPermission.CREDENTIAL_CREATE, - rbac_resource_required=False, + rbac_checks=[RBACCheck(RBACPermission.CREDENTIAL_CREATE, Workspace())], ) def post(self, request_context: RequestContext): return request_context @@ -378,18 +376,14 @@ class TestCurrentContextInjection: assert isinstance(result, RequestContext) assert result.active_workspace_id == "tenant-123" assert result.trace_id == "trace-1" - enforce_rbac_access.assert_called_once_with( - tenant_id="tenant-123", - account_id=current_user.id, - resource_type=RBACResourceScope.WORKSPACE, - scene=RBACPermission.CREDENTIAL_CREATE, - resource_required=False, - path_args={}, - ) - - def test_console_account_admission_rejects_incomplete_rbac_requirement(self): - with pytest.raises(AdmissionConfigurationError, match="configured together"): - flask_admission.console_account_admission(rbac_resource_scope=RBACResourceScope.WORKSPACE) + enforce_rbac_checks.assert_called_once() + call_kwargs = enforce_rbac_checks.call_args.kwargs + assert call_kwargs["tenant_id"] == "tenant-123" + assert call_kwargs["account_id"] == current_user.id + assert call_kwargs["path_args"] == {} + (check,) = call_kwargs["checks"] + assert check.scene is RBACPermission.CREDENTIAL_CREATE + assert isinstance(check.locator, Workspace) def test_console_account_admission_can_admit_uninitialized_accounts(self): current_user = make_account() @@ -501,67 +495,72 @@ class TestRbacPermissionRequired: def test_resource_scoped_check_uses_resource_id(self): current_user = make_account("account-1") - @rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_DELETE) + @rbac_permission_required(RBACCheck(RBACPermission.APP_DELETE, PlainApp())) def protected_view(**kwargs): return "ok" with ( - patch("controllers.common.wraps.current_account_with_tenant", return_value=(current_user, "tenant-1")), - patch("controllers.common.wraps._extract_resource_id", return_value="app-123") as mock_extract, - patch("controllers.common.wraps._is_resource_owned_by_current_user", return_value=False) as mock_owned, - patch("controllers.common.wraps.RBACService.CheckAccess.check", return_value=True) as mock_check, + Flask(__name__).test_request_context("/"), + patch( + "controllers.common.wraps.current_account_with_tenant", + return_value=(current_user, "tenant-1"), + ), + patch("controllers.common.rbac.locators.agent_binding", return_value=None) as mock_binding, + patch("controllers.common.rbac.locators.PlainApp.owner_id", return_value=None) as mock_owner, + patch("controllers.common.rbac.checks.RBACService.CheckAccess.check", return_value=True) as mock_check, ): assert protected_view(app_id="app-123") == "ok" - mock_extract.assert_called_once_with(RBACResourceScope.APP, "tenant-1", {"app_id": "app-123"}) - mock_owned.assert_called_once_with("tenant-1", "account-1", "app", "app-123") + mock_binding.assert_called_once_with("tenant-1", "app-123") + mock_owner.assert_called_once() mock_check.assert_called_once_with( "tenant-1", "account-1", - scene="app_delete", - resource_type="app", + scene=RBACPermission.APP_DELETE, + resource_type=RBACResourceScope.APP, resource_id="app-123", ) def test_workspace_scoped_check_skips_resource_id_extraction(self): current_user = make_account("account-2") - @rbac_permission_required( - RBACResourceScope.DATASET, RBACPermission.DATASET_CREATE_AND_MANAGEMENT, resource_required=False - ) + @rbac_permission_required(RBACCheck(RBACPermission.DATASET_CREATE_AND_MANAGEMENT, Workspace())) def protected_view(): return "ok" with ( - patch("controllers.common.wraps.current_account_with_tenant", return_value=(current_user, "tenant-2")), - patch("controllers.common.wraps._extract_resource_id") as mock_extract, - patch("controllers.common.wraps._is_resource_owned_by_current_user", return_value=False) as mock_owned, - patch("controllers.common.wraps.RBACService.CheckAccess.check", return_value=True) as mock_check, + Flask(__name__).test_request_context("/"), + patch( + "controllers.common.wraps.current_account_with_tenant", + return_value=(current_user, "tenant-2"), + ), + patch("controllers.common.rbac.locators.DatasetId.owner_id") as mock_owner, + patch("controllers.common.rbac.checks.RBACService.CheckAccess.check", return_value=True) as mock_check, ): assert protected_view() == "ok" - mock_extract.assert_not_called() - mock_owned.assert_not_called() + mock_owner.assert_not_called() mock_check.assert_called_once_with( "tenant-2", "account-2", - scene="dataset_create_and_management", - resource_type="dataset", + scene=RBACPermission.DATASET_CREATE_AND_MANAGEMENT, + resource_type=None, resource_id=None, ) def test_workspace_scene_omits_resource_type(self): current_user = make_account("account-3") - @rbac_permission_required( - RBACResourceScope.WORKSPACE, RBACPermission.WORKSPACE_ROLE_MANAGE, resource_required=False - ) + @rbac_permission_required(RBACCheck(RBACPermission.WORKSPACE_ROLE_MANAGE, Workspace())) def protected_view(): return "ok" with ( - patch("controllers.common.wraps.current_account_with_tenant", return_value=(current_user, "tenant-3")), - patch("controllers.common.wraps.RBACService.CheckAccess.check", return_value=True) as mock_check, + patch( + "controllers.common.wraps.current_account_with_tenant", + return_value=(current_user, "tenant-3"), + ), + patch("controllers.common.rbac.checks.RBACService.CheckAccess.check", return_value=True) as mock_check, ): assert protected_view() == "ok" @@ -576,137 +575,46 @@ class TestRbacPermissionRequired: def test_resource_owned_app_skips_rbac_check(self): current_user = make_account("account-4") - @rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_DELETE) + @rbac_permission_required(RBACCheck(RBACPermission.APP_DELETE, PlainApp())) def protected_view(**kwargs): return "ok" with ( - patch("controllers.common.wraps.current_account_with_tenant", return_value=(current_user, "tenant-4")), - patch("controllers.common.wraps._extract_resource_id", return_value="app-123"), - patch("controllers.common.wraps._is_resource_owned_by_current_user", return_value=True) as mock_owned, - patch("controllers.common.wraps.RBACService.CheckAccess.check") as mock_check, + Flask(__name__).test_request_context("/"), + patch( + "controllers.common.wraps.current_account_with_tenant", + return_value=(current_user, "tenant-4"), + ), + patch("controllers.common.rbac.locators.agent_binding", return_value=None), + patch("controllers.common.rbac.locators.PlainApp.owner_id", return_value="account-4") as mock_owner, + patch("controllers.common.rbac.checks.RBACService.CheckAccess.check") as mock_check, ): assert protected_view(app_id="app-123") == "ok" - mock_owned.assert_called_once_with("tenant-4", "account-4", "app", "app-123") + mock_owner.assert_called_once() mock_check.assert_not_called() def test_resource_owned_dataset_skips_rbac_check(self): current_user = make_account("account-5") - @rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_EDIT) + @rbac_permission_required(RBACCheck(RBACPermission.DATASET_EDIT, DatasetId())) def protected_view(**kwargs): return "ok" with ( - patch("controllers.common.wraps.current_account_with_tenant", return_value=(current_user, "tenant-5")), - patch("controllers.common.wraps._extract_resource_id", return_value="dataset-123"), - patch("controllers.common.wraps._is_resource_owned_by_current_user", return_value=True) as mock_owned, - patch("controllers.common.wraps.RBACService.CheckAccess.check") as mock_check, + Flask(__name__).test_request_context("/"), + patch( + "controllers.common.wraps.current_account_with_tenant", + return_value=(current_user, "tenant-5"), + ), + patch("controllers.common.rbac.locators.DatasetId.owner_id", return_value="account-5") as mock_owner, + patch("controllers.common.rbac.checks.RBACService.CheckAccess.check") as mock_check, ): assert protected_view(dataset_id="dataset-123") == "ok" - mock_owned.assert_called_once_with("tenant-5", "account-5", "dataset", "dataset-123") + mock_owner.assert_called_once() mock_check.assert_not_called() - def test_extract_resource_id_prefers_path_args(self): - app = Flask(__name__) - - with app.test_request_context("/"): - request.view_args = {"app_id": "view-app"} - - assert _extract_resource_id("app", "tenant-1", {"app_id": "path-app"}) == "path-app" - - def test_extract_resource_id_falls_back_to_request_view_args(self): - app = Flask(__name__) - - with app.test_request_context("/"): - request.view_args = {"app_id": "view-app"} - - assert _extract_resource_id("app", "tenant-1") == "view-app" - - def test_extract_resource_id_supports_legacy_route_aliases(self): - app = Flask(__name__) - - with app.test_request_context("/apps/app-1/api-keys"): - request.view_args = {"resource_id": "app-1"} - assert _extract_resource_id(RBACResourceScope.APP, "tenant-1") == "app-1" - - with app.test_request_context("/datasets/dataset-1/api-keys"): - request.view_args = {"resource_id": "dataset-1"} - assert _extract_resource_id(RBACResourceScope.DATASET, "tenant-1") == "dataset-1" - - def test_extract_resource_id_scopes_pipeline_resolution_to_the_calling_tenant(self, sqlite_session: Session): - app = Flask(__name__) - pipeline_id = "00000000-0000-0000-0000-000000000001" - current_tenant_id = "00000000-0000-0000-0000-000000000002" - foreign_dataset = Dataset( - id="00000000-0000-0000-0000-000000000003", - tenant_id="00000000-0000-0000-0000-000000000004", - name="Foreign decoy", - created_by="00000000-0000-0000-0000-000000000005", - pipeline_id=pipeline_id, - ) - current_dataset = Dataset( - id="00000000-0000-0000-0000-000000000006", - tenant_id=current_tenant_id, - name="Current tenant dataset", - created_by="00000000-0000-0000-0000-000000000007", - pipeline_id=pipeline_id, - ) - sqlite_session.add_all([foreign_dataset, current_dataset]) - - unscoped_dataset = sqlite_session.scalar(select(Dataset).where(Dataset.pipeline_id == pipeline_id)) - assert unscoped_dataset is foreign_dataset - - with ( - app.test_request_context("/rag/pipelines/pipeline-1"), - patch("controllers.common.wraps.db", SimpleNamespace(session=sqlite_session)), - ): - request.view_args = {"pipeline_id": pipeline_id} - assert _extract_resource_id(RBACResourceScope.DATASET, current_tenant_id) == current_dataset.id - - def test_extract_resource_id_resolves_agent_to_its_authz_app(self): - app = Flask(__name__) - - with ( - app.test_request_context("/agent/agent-1/chat-messages"), - patch("controllers.common.wraps.AgentRosterService") as mock_service, - ): - request.view_args = {"agent_id": "agent-1"} - mock_service.return_value.peek_authz_app_id.return_value = "parent-app-1" - - assert _extract_resource_id(RBACResourceScope.APP, "tenant-1") == "parent-app-1" - - def test_extract_resource_id_scopes_agent_resolution_to_the_calling_tenant(self): - """The tenant must reach the resolver, or an Agent id from any tenant resolves.""" - app = Flask(__name__) - - with ( - app.test_request_context("/agent/agent-1/chat-messages"), - patch("controllers.common.wraps.AgentRosterService") as mock_service, - ): - request.view_args = {"agent_id": "agent-1"} - mock_service.return_value.peek_authz_app_id.return_value = "parent-app-1" - - _extract_resource_id(RBACResourceScope.APP, "tenant-9") - - mock_service.return_value.peek_authz_app_id.assert_called_once_with( - tenant_id="tenant-9", agent_id="agent-1" - ) - - def test_extract_resource_id_keeps_agent_id_when_the_agent_does_not_resolve(self): - app = Flask(__name__) - - with ( - app.test_request_context("/agent/agent-1/chat-messages"), - patch("controllers.common.wraps.AgentRosterService") as mock_service, - ): - request.view_args = {"agent_id": "agent-1"} - mock_service.return_value.peek_authz_app_id.return_value = None - - assert _extract_resource_id(RBACResourceScope.APP, "tenant-1") == "agent-1" - def test_legacy_admin_decorator_noops_when_rbac_enabled(self): @is_admin_or_owner_required def protected_view(): diff --git a/api/tests/unit_tests/controllers/console/workspace/test_endpoint.py b/api/tests/unit_tests/controllers/console/workspace/test_endpoint.py index 99f9ff1af9a..eb328aaf8e8 100644 --- a/api/tests/unit_tests/controllers/console/workspace/test_endpoint.py +++ b/api/tests/unit_tests/controllers/console/workspace/test_endpoint.py @@ -6,6 +6,7 @@ from unittest.mock import patch import pytest from flask import Flask +from controllers.common.rbac import Workspace from controllers.console import console_ns from controllers.console.workspace.endpoint import ( DeprecatedEndpointCreateApi, @@ -24,10 +25,11 @@ from controllers.console.workspace.endpoint import ( EndpointUpdatePayload, LegacyEndpointUpdatePayload, ) -from controllers.console.wraps import RBACPermission, RBACResourceScope +from controllers.console.wraps import RBACPermission from core.entities.provider_entities import ProviderConfig, ProviderConfigType from core.plugin.entities.endpoint import EndpointEntityWithInstance, EndpointProviderDeclaration from core.plugin.impl.exc import PluginPermissionDeniedError +from tests.unit_tests.controllers.rbac_introspection import rbac_checks def _endpoint_entity() -> EndpointEntityWithInstance: @@ -65,13 +67,9 @@ def test_endpoint_lists_require_management_permission(method: FunctionType) -> N ) assert "is_admin_or_owner_required" in legacy_wrapper.__code__.co_qualname - rbac_wrapper = inspect.unwrap( - method, stop=lambda wrapper: "rbac_permission_required" in wrapper.__code__.co_qualname - ) - rbac_config = inspect.getclosurevars(rbac_wrapper).nonlocals - assert rbac_config["resource_type"] == RBACResourceScope.WORKSPACE - assert rbac_config["scene"] == RBACPermission.PLUGIN_MODEL_CONFIG - assert rbac_config["resource_required"] is False + [check] = rbac_checks(method) + assert check.scene == RBACPermission.PLUGIN_MODEL_CONFIG + assert isinstance(check.locator, Workspace) class TestEndpointCollectionApi: diff --git a/api/tests/unit_tests/controllers/console/workspace/test_rbac.py b/api/tests/unit_tests/controllers/console/workspace/test_rbac.py index 861cce48d24..15af9f1614d 100644 --- a/api/tests/unit_tests/controllers/console/workspace/test_rbac.py +++ b/api/tests/unit_tests/controllers/console/workspace/test_rbac.py @@ -85,6 +85,25 @@ class TestMyPermissions: assert response["app"]["default_permission_keys"] == ["app.acl.deploy"] mock_get.assert_called_once() + def test_forwards_agent_id_query_param(self, app): + permissions = rbac_mod.svc.MyPermissionsResponse( + agent=rbac_mod.svc.ResourcePermissionSnapshot( + default_permission_keys=["agent.acl.preview"], + ) + ) + with ( + app.test_request_context("/workspaces/current/rbac/my-permissions?agent_id=agent-1"), + patch("controllers.console.workspace.rbac._current_ids", return_value=("tenant-1", "acct-1")), + patch( + "controllers.console.workspace.rbac.svc.RBACService.MyPermissions.get", + return_value=permissions, + ) as mock_get, + ): + response = inspect.unwrap(rbac_mod.RBACMyPermissionsApi.get)(rbac_mod.RBACMyPermissionsApi()) + + assert mock_get.call_args.kwargs["agent_id"] == "agent-1" + assert response["agent"]["default_permission_keys"] == ["agent.acl.preview"] + class TestAccessMatrixAccountNames: def test_hydrates_missing_account_names(self): @@ -332,172 +351,6 @@ class TestPaginationMapping: assert options.reverse is True -class TestResourceAccessScopeBindings: - def test_app_whitelist_all_schedules_member_policy_sync(self, app): - with ( - app.test_request_context( - "/workspaces/current/rbac/apps/app-1/whitelist", - method="PUT", - json={"automatic_include_workspace_members": True}, - ), - patch("controllers.console.workspace.rbac._current_ids", return_value=("tenant-1", "acct-actor")), - patch( - "controllers.console.workspace.rbac.svc.RBACService.AppAccess.replace_whitelist", - return_value=rbac_mod.svc.ResourceWhitelist(automatic_include_workspace_members=True), - ), - patch("controllers.console.workspace.rbac.initialize_created_app_rbac_access_task") as mock_sync_task, - ): - inspect.unwrap(rbac_mod.RBACAppWhitelistApi.put)(rbac_mod.RBACAppWhitelistApi(), "app-1") - - mock_sync_task.delay.assert_called_once_with("tenant-1", "acct-actor", app_id="app-1") - - def test_dataset_whitelist_all_schedules_member_policy_sync(self, app): - # Widening a dataset to the whole workspace only records the scope; without granting the - # default policy to the current members nobody actually gains access. - with ( - app.test_request_context( - "/workspaces/current/rbac/datasets/dataset-1/whitelist", - method="PUT", - json={"automatic_include_workspace_members": True}, - ), - patch("controllers.console.workspace.rbac._current_ids", return_value=("tenant-1", "acct-actor")), - patch( - "controllers.console.workspace.rbac.svc.RBACService.DatasetAccess.replace_whitelist", - return_value=rbac_mod.svc.ResourceWhitelist(automatic_include_workspace_members=True), - ), - patch("controllers.console.workspace.rbac.initialize_created_app_rbac_access_task") as mock_sync_task, - ): - inspect.unwrap(rbac_mod.RBACDatasetWhitelistApi.put)(rbac_mod.RBACDatasetWhitelistApi(), "dataset-1") - - mock_sync_task.delay.assert_called_once_with("tenant-1", "acct-actor", dataset_id="dataset-1") - - def test_dataset_whitelist_specific_does_not_schedule_member_policy_sync(self, app): - with ( - app.test_request_context( - "/workspaces/current/rbac/datasets/dataset-1/whitelist", - method="PUT", - json={"automatic_include_workspace_members": False}, - ), - patch("controllers.console.workspace.rbac._current_ids", return_value=("tenant-1", "acct-actor")), - patch( - "controllers.console.workspace.rbac.svc.RBACService.DatasetAccess.replace_whitelist", - return_value=rbac_mod.svc.ResourceWhitelist(automatic_include_workspace_members=False), - ), - patch("controllers.console.workspace.rbac.initialize_created_app_rbac_access_task") as mock_sync_task, - ): - inspect.unwrap(rbac_mod.RBACDatasetWhitelistApi.put)(rbac_mod.RBACDatasetWhitelistApi(), "dataset-1") - - mock_sync_task.delay.assert_not_called() - - def test_app_whitelist_config_returns_switch_state_only(self, app): - result = rbac_mod.svc.ResourceWhitelistConfig(automatic_include_workspace_members=True) - with ( - app.test_request_context("/workspaces/current/rbac/apps/app-1/whitelist_config"), - patch("controllers.console.workspace.rbac._current_ids", return_value=("tenant-1", "acct-1")), - patch( - "controllers.console.workspace.rbac.svc.RBACService.AppAccess.whitelist_config", - return_value=result, - ) as mock_get, - ): - response = inspect.unwrap(rbac_mod.RBACAppWhitelistConfigApi.get)( - rbac_mod.RBACAppWhitelistConfigApi(), - "app-1", - ) - - assert response == {"automatic_include_workspace_members": True} - mock_get.assert_called_once_with("tenant-1", "acct-1", "app-1") - - def test_dataset_whitelist_config_returns_switch_state_only(self, app): - result = rbac_mod.svc.ResourceWhitelistConfig(automatic_include_workspace_members=False) - with ( - app.test_request_context("/workspaces/current/rbac/datasets/dataset-1/whitelist_config"), - patch("controllers.console.workspace.rbac._current_ids", return_value=("tenant-1", "acct-1")), - patch( - "controllers.console.workspace.rbac.svc.RBACService.DatasetAccess.whitelist_config", - return_value=result, - ) as mock_get, - ): - response = inspect.unwrap(rbac_mod.RBACDatasetWhitelistConfigApi.get)( - rbac_mod.RBACDatasetWhitelistConfigApi(), - "dataset-1", - ) - - assert response == {"automatic_include_workspace_members": False} - mock_get.assert_called_once_with("tenant-1", "acct-1", "dataset-1") - - def test_app_user_access_policy_assignment_forwards_ids(self, app): - with ( - app.test_request_context( - "/workspaces/current/rbac/apps/app-1/users/acct-target/access-policies", - method="PUT", - json={"access_policy_ids": ["policy-1", "policy-2"]}, - ), - patch("controllers.console.workspace.rbac._current_ids", return_value=("tenant-1", "acct-actor")), - patch( - "controllers.console.workspace.rbac.svc.RBACService.AppAccess.replace_user_access_policies" - ) as mock_replace, - patch("controllers.console.workspace.rbac._dump", return_value={}), - ): - inspect.unwrap(rbac_mod.RBACAppUserAccessPolicyAssignmentApi.put)( - rbac_mod.RBACAppUserAccessPolicyAssignmentApi(), - "app-1", - "acct-target", - ) - - tenant_id, actor_id, app_id, target_id, payload = mock_replace.call_args.args - assert (tenant_id, actor_id, app_id, target_id) == ( - "tenant-1", - "acct-actor", - "app-1", - "acct-target", - ) - assert payload.access_policy_ids == ["policy-1", "policy-2"] - - def test_app_member_bindings_delete_forwards_account_ids(self, app): - with ( - app.test_request_context( - "/workspaces/current/rbac/apps/app-1/access-policies/policy-1/member-bindings", - method="DELETE", - json={"account_ids": ["acct-2", "acct-3"]}, - ), - patch("controllers.console.workspace.rbac._current_ids", return_value=("tenant-1", "acct-actor")), - patch("controllers.console.workspace.rbac.svc.RBACService.AppAccess.delete_member_bindings") as mock_delete, - ): - response = inspect.unwrap(rbac_mod.RBACAppMemberBindingsApi.delete)( - rbac_mod.RBACAppMemberBindingsApi(), - "app-1", - "policy-1", - ) - - assert response == {"result": "success"} - tenant_id, actor_id, app_id, policy_id, payload = mock_delete.call_args.args - assert (tenant_id, actor_id, app_id, policy_id) == ("tenant-1", "acct-actor", "app-1", "policy-1") - assert payload.account_ids == ["acct-2", "acct-3"] - - def test_dataset_member_bindings_delete_forwards_account_ids(self, app): - with ( - app.test_request_context( - "/workspaces/current/rbac/datasets/dataset-1/access-policies/policy-1/member-bindings", - method="DELETE", - json={"account_ids": ["acct-2"]}, - ), - patch("controllers.console.workspace.rbac._current_ids", return_value=("tenant-1", "acct-actor")), - patch( - "controllers.console.workspace.rbac.svc.RBACService.DatasetAccess.delete_member_bindings" - ) as mock_delete, - ): - response = inspect.unwrap(rbac_mod.RBACDatasetMemberBindingsApi.delete)( - rbac_mod.RBACDatasetMemberBindingsApi(), - "dataset-1", - "policy-1", - ) - - assert response == {"result": "success"} - tenant_id, actor_id, dataset_id, policy_id, payload = mock_delete.call_args.args - assert (tenant_id, actor_id, dataset_id, policy_id) == ("tenant-1", "acct-actor", "dataset-1", "policy-1") - assert payload.account_ids == ["acct-2"] - - class TestPaginationForwarding: def test_access_policies_get_forwards_outer_pagination_params(self, app): with ( @@ -517,88 +370,6 @@ class TestPaginationForwarding: assert options.results_per_page == 25 assert options.reverse is False - def test_workspace_app_matrix_forwards_outer_pagination_params(self, app): - with ( - app.test_request_context("/workspaces/current/rbac/workspace/apps/access-policy?page=4&limit=10"), - patch("controllers.console.workspace.rbac._current_ids", return_value=("tenant-1", "acct-1")), - patch("controllers.console.workspace.rbac.svc.RBACService.WorkspaceAccess.app_matrix") as mock_list, - patch("controllers.console.workspace.rbac._dump", return_value={}), - ): - inspect.unwrap(rbac_mod.RBACWorkspaceAppMatrixApi.get)(rbac_mod.RBACWorkspaceAppMatrixApi()) - - _, kwargs = mock_list.call_args - options = kwargs["options"] - assert options.page_number == 4 - assert options.results_per_page == 10 - assert options.reverse is None - - def test_workspace_dataset_matrix_forwards_outer_pagination_params(self, app): - with ( - app.test_request_context( - "/workspaces/current/rbac/workspace/datasets/access-policy?page=5&limit=15&reverse=true" - ), - patch("controllers.console.workspace.rbac._current_ids", return_value=("tenant-1", "acct-1")), - patch("controllers.console.workspace.rbac.svc.RBACService.WorkspaceAccess.dataset_matrix") as mock_list, - patch("controllers.console.workspace.rbac._dump", return_value={}), - ): - inspect.unwrap(rbac_mod.RBACWorkspaceDatasetMatrixApi.get)(rbac_mod.RBACWorkspaceDatasetMatrixApi()) - - _, kwargs = mock_list.call_args - options = kwargs["options"] - assert options.page_number == 5 - assert options.results_per_page == 15 - assert options.reverse is True - - def test_app_user_access_policies_forwards_outer_pagination_params(self, app): - result = rbac_mod.svc.ResourceUserAccessPoliciesResponse() - with ( - app.test_request_context( - "/workspaces/current/rbac/apps/app-1/user-access-policies?page=2&limit=30&reverse=false" - ), - patch("controllers.console.workspace.rbac._current_ids", return_value=("tenant-1", "acct-1")), - patch( - "controllers.console.workspace.rbac.svc.RBACService.AppAccess.user_access_policies", - return_value=result, - ) as mock_list, - patch("controllers.console.workspace.rbac._dump", return_value={}), - ): - inspect.unwrap(rbac_mod.RBACAppUserAccessPoliciesApi.get)( - rbac_mod.RBACAppUserAccessPoliciesApi(), - "app-1", - ) - - args, kwargs = mock_list.call_args - assert args == ("tenant-1", "acct-1", "app-1") - options = kwargs["options"] - assert options.page_number == 2 - assert options.results_per_page == 30 - assert options.reverse is False - - def test_dataset_user_access_policies_forwards_outer_pagination_params(self, app): - result = rbac_mod.svc.ResourceUserAccessPoliciesResponse() - with ( - app.test_request_context( - "/workspaces/current/rbac/datasets/dataset-1/user-access-policies?page=3&limit=40&reverse=true" - ), - patch("controllers.console.workspace.rbac._current_ids", return_value=("tenant-1", "acct-1")), - patch( - "controllers.console.workspace.rbac.svc.RBACService.DatasetAccess.user_access_policies", - return_value=result, - ) as mock_list, - patch("controllers.console.workspace.rbac._dump", return_value={}), - ): - inspect.unwrap(rbac_mod.RBACDatasetUserAccessPoliciesApi.get)( - rbac_mod.RBACDatasetUserAccessPoliciesApi(), - "dataset-1", - ) - - args, kwargs = mock_list.call_args - assert args == ("tenant-1", "acct-1", "dataset-1") - options = kwargs["options"] - assert options.page_number == 3 - assert options.results_per_page == 40 - assert options.reverse is True - class TestAccessPolicyBindingLockUnlock: def test_lock_forwards_binding_id(self, app): @@ -653,7 +424,7 @@ class TestWorkspaceRbacGuards: "controllers.common.wraps.current_account_with_tenant", return_value=(_account(), "tenant-1"), ), - patch("controllers.common.wraps.RBACService.CheckAccess.check", return_value=False), + patch("controllers.common.rbac.checks.RBACService.CheckAccess.check", return_value=False), patch("controllers.console.workspace.rbac.svc.RBACService.Roles.create") as mock_create, ): with pytest.raises(Forbidden): @@ -672,7 +443,7 @@ class TestWorkspaceRbacGuards: "controllers.common.wraps.current_account_with_tenant", return_value=(_account(), "tenant-1"), ), - patch("controllers.common.wraps.RBACService.CheckAccess.check", return_value=False), + patch("controllers.common.rbac.checks.RBACService.CheckAccess.check", return_value=False), patch("controllers.console.workspace.rbac.svc.RBACService.AccessPolicies.create") as mock_create, ): with pytest.raises(Forbidden): diff --git a/api/tests/unit_tests/controllers/console/workspace/test_rbac_resource_access_routes.py b/api/tests/unit_tests/controllers/console/workspace/test_rbac_resource_access_routes.py new file mode 100644 index 00000000000..f5c593603b6 --- /dev/null +++ b/api/tests/unit_tests/controllers/console/workspace/test_rbac_resource_access_routes.py @@ -0,0 +1,167 @@ +"""Contract tests for the generated app / dataset / agent access-permission routes. + +``controllers.console.workspace.rbac`` builds the same twelve endpoints for every +resource kind from ``_RESOURCE_ACCESS_ROUTES``. These tests pin the two things the +generation must not get wrong: the registered URLs (with their class names and HTTP +methods) and which inner-API client each handler reaches for. +""" + +from __future__ import annotations + +import inspect +from collections.abc import Callable, Generator +from unittest.mock import MagicMock, patch + +import pytest +from flask import Flask + +from controllers.console import console_ns +from controllers.console.workspace import rbac as rbac_mod +from enums import DeploymentEdition +from services.enterprise import rbac_service as svc + +RESOURCE_ID = { + svc.RBACResourceType.APP: "app-1", + svc.RBACResourceType.DATASET: "dataset-1", + svc.RBACResourceType.AGENT: "agent-1", +} + +# The URL segment and path parameter of each resource kind, spelled out here instead of read +# back from the same enum the controller builds its URLs from, so a rename fails this test. +RESOURCE_URL_PARTS = { + svc.RBACResourceType.APP: ("apps", "app_id"), + svc.RBACResourceType.DATASET: ("datasets", "dataset_id"), + svc.RBACResourceType.AGENT: ("agents", "agent_id"), +} + + +def _segment(spec: rbac_mod._ResourceAccessRoutes) -> str: + return RESOURCE_URL_PARTS[spec.resource_type][0] + + +def _id_param(spec: rbac_mod._ResourceAccessRoutes) -> str: + return RESOURCE_URL_PARTS[spec.resource_type][1] + + +@pytest.fixture +def app() -> Flask: + flask_app = Flask(__name__) + flask_app.config["TESTING"] = True + return flask_app + + +@pytest.fixture(autouse=True) +def _rbac_config(config_overrides: Callable[..., None]) -> None: + config_overrides( + DEPLOYMENT_EDITION=DeploymentEdition.ENTERPRISE, + RBAC_ENABLED=True, + LOGIN_DISABLED=True, + ) + + +@pytest.fixture(params=rbac_mod._RESOURCE_ACCESS_ROUTES, ids=lambda spec: spec.resource_type.value) +def spec(request: pytest.FixtureRequest) -> rbac_mod._ResourceAccessRoutes: + return request.param + + +@pytest.fixture +def apis(spec: rbac_mod._ResourceAccessRoutes) -> rbac_mod._ResourceAccessApis: + return rbac_mod._RESOURCE_ACCESS_APIS[spec.resource_type] + + +@pytest.fixture +def resource_id(spec: rbac_mod._ResourceAccessRoutes) -> str: + return RESOURCE_ID[spec.resource_type] + + +@pytest.fixture(autouse=True) +def _patched_current_ids() -> Generator[None]: + with patch("controllers.console.workspace.rbac._current_ids", return_value=("tenant-1", "acct-actor")): + yield + + +def _expected_routes(spec: rbac_mod._ResourceAccessRoutes) -> dict[str, tuple[str, set[str]]]: + resource = f"/workspaces/current/rbac/{_segment(spec)}/" + workspace = f"/workspaces/current/rbac/workspace/{_segment(spec)}" + prefix = spec.class_prefix + return { + f"/workspaces/current/rbac/role-permissions/catalog/{spec.resource_type.value}": ( + f"RBAC{prefix}CatalogApi", + {"GET"}, + ), + f"{resource}/access-policy": (f"RBAC{prefix}MatrixApi", {"GET"}), + f"{resource}/whitelist": (f"RBAC{prefix}WhitelistApi", {"GET", "PUT"}), + f"{resource}/whitelist_config": (f"RBAC{prefix}WhitelistConfigApi", {"GET"}), + f"{resource}/user-access-policies": (f"RBAC{prefix}UserAccessPoliciesApi", {"GET"}), + f"{resource}/users//access-policies": ( + f"RBAC{prefix}UserAccessPolicyAssignmentApi", + {"PUT"}, + ), + f"{resource}/access-policies//role-bindings": (f"RBAC{prefix}RoleBindingsApi", {"GET"}), + f"{resource}/access-policies//member-bindings": ( + f"RBAC{prefix}MemberBindingsApi", + {"GET", "DELETE"}, + ), + f"{workspace}/access-policy": (f"RBACWorkspace{prefix}MatrixApi", {"GET"}), + f"{workspace}/access-policies//role-bindings": ( + f"RBACWorkspace{prefix}RoleBindingsApi", + {"GET"}, + ), + f"{workspace}/access-policies//bindings": (f"RBACWorkspace{prefix}BindingsApi", {"PUT"}), + f"{workspace}/access-policies//member-bindings": ( + f"RBACWorkspace{prefix}MemberBindingsApi", + {"GET"}, + ), + } + + +def _registered_resource_by_url() -> dict[str, type]: + registered: dict[str, type] = {} + for route in console_ns.resources: + for url in route.urls: + registered[url] = route.resource + return registered + + +def test_every_resource_kind_registers_the_same_twelve_routes(spec: rbac_mod._ResourceAccessRoutes) -> None: + registered = _registered_resource_by_url() + + for url, (class_name, methods) in _expected_routes(spec).items(): + assert url in registered, f"{url} is not registered" + resource = registered[url] + assert resource.__name__ == class_name + assert set(resource.methods or ()) == methods + + +@pytest.mark.parametrize("automatic_include_workspace_members", [True, False]) +def test_whitelist_put_queues_the_seed_task_only_when_auto_including( + app: Flask, + apis: rbac_mod._ResourceAccessApis, + spec: rbac_mod._ResourceAccessRoutes, + resource_id: str, + automatic_include_workspace_members: bool, +) -> None: + replace = MagicMock( + return_value=svc.ResourceWhitelist( + automatic_include_workspace_members=automatic_include_workspace_members, + ) + ) + with ( + app.test_request_context( + f"/{_segment(spec)}/{resource_id}/whitelist", + method="PUT", + json={"automatic_include_workspace_members": automatic_include_workspace_members}, + ), + patch.object(spec.access, "replace_whitelist", replace), + patch("controllers.console.workspace.rbac.initialize_created_app_rbac_access_task") as seed_task, + ): + inspect.unwrap(vars(apis.whitelist)["put"])(apis.whitelist(), **{_id_param(spec): resource_id}) + + tenant_id, actor_id, target_id, payload = replace.call_args.args + assert (tenant_id, actor_id, target_id) == ("tenant-1", "acct-actor", resource_id) + assert payload.automatic_include_workspace_members is automatic_include_workspace_members + + if automatic_include_workspace_members: + seed_task.delay.assert_called_once_with("tenant-1", "acct-actor", **{_id_param(spec): resource_id}) + else: + seed_task.delay.assert_not_called() diff --git a/api/tests/unit_tests/controllers/openapi/auth/test_composition.py b/api/tests/unit_tests/controllers/openapi/auth/test_composition.py index a6629caabcf..d61a38848f0 100644 --- a/api/tests/unit_tests/controllers/openapi/auth/test_composition.py +++ b/api/tests/unit_tests/controllers/openapi/auth/test_composition.py @@ -1,7 +1,8 @@ import uuid +from controllers.common.rbac import PlainApp, RBACCheck from controllers.openapi.auth.composition import account_pipeline, auth_router, external_sso_pipeline -from controllers.openapi.auth.data import RBACRequirement, RequestContext +from controllers.openapi.auth.data import RequestContext from controllers.openapi.auth.flow import When from controllers.openapi.auth.pipeline import AuthPipeline, PipelineRoute, PipelineRouter from controllers.openapi.auth.verify import ( @@ -12,7 +13,7 @@ from controllers.openapi.auth.verify import ( check_workspace_mismatch, check_workspace_role, ) -from core.rbac import RBACPermission, RBACResourceScope +from core.rbac import RBACPermission from enums import DeploymentEdition from libs.oauth_bearer import Scope, TokenType from models.account import TenantAccountRole @@ -206,7 +207,7 @@ def _selected_auth_steps_with_rbac(rbac): def test_account_pipeline_selects_rbac_step_when_required(): - rbac = RBACRequirement(resource_type=RBACResourceScope.APP, scene=RBACPermission.APP_VIEW_LAYOUT) + rbac = RBACCheck(RBACPermission.APP_VIEW_LAYOUT, PlainApp()) assert check_rbac_permission in _selected_auth_steps_with_rbac(rbac) diff --git a/api/tests/unit_tests/controllers/openapi/auth/test_conditions.py b/api/tests/unit_tests/controllers/openapi/auth/test_conditions.py index 2d67cc84883..59776892903 100644 --- a/api/tests/unit_tests/controllers/openapi/auth/test_conditions.py +++ b/api/tests/unit_tests/controllers/openapi/auth/test_conditions.py @@ -1,5 +1,6 @@ from unittest.mock import patch +from controllers.common.rbac import PlainApp, RBACCheck from controllers.openapi.auth.conditions import ( EDITION_CLOUD, EDITION_COMMUNITY, @@ -18,8 +19,8 @@ from controllers.openapi.auth.conditions import ( data_cond, request_cond, ) -from controllers.openapi.auth.data import AuthData, RBACRequirement, RequestContext -from core.rbac import RBACPermission, RBACResourceScope +from controllers.openapi.auth.data import AuthData, RequestContext +from core.rbac import RBACPermission from enums import DeploymentEdition from libs.oauth_bearer import Scope, TokenType from models.account import TenantAccountRole @@ -153,7 +154,7 @@ def test_webapp_run_scoped_false_when_scope_none(): def _rbac_req(): - return RBACRequirement(resource_type=RBACResourceScope.APP, scene=RBACPermission.APP_TEST_AND_RUN) + return RBACCheck(RBACPermission.APP_TEST_AND_RUN, PlainApp()) def test_has_rbac_true(): diff --git a/api/tests/unit_tests/controllers/openapi/auth/test_verify.py b/api/tests/unit_tests/controllers/openapi/auth/test_verify.py index cf0082bc5d6..89c38c59e4d 100644 --- a/api/tests/unit_tests/controllers/openapi/auth/test_verify.py +++ b/api/tests/unit_tests/controllers/openapi/auth/test_verify.py @@ -6,7 +6,8 @@ import pytest from flask import Flask from werkzeug.exceptions import Forbidden, NotFound -from controllers.openapi.auth.data import AuthData, RBACRequirement +from controllers.common.rbac import PlainApp, RBACCheck +from controllers.openapi.auth.data import AuthData from controllers.openapi.auth.verify import ( check_acl, check_app_access, @@ -18,7 +19,7 @@ from controllers.openapi.auth.verify import ( check_workspace_mismatch, check_workspace_role, ) -from core.rbac import RBACPermission, RBACResourceScope +from core.rbac import RBACPermission from libs.oauth_bearer import Scope, TokenType from models.account import Tenant, TenantAccountRole from models.model import App @@ -85,25 +86,25 @@ def test_check_app_access_raises_when_not_member(): # --- check_rbac_permission --- -_RBAC_REQ = RBACRequirement(resource_type=RBACResourceScope.APP, scene=RBACPermission.APP_VIEW_LAYOUT) +_RBAC_REQ = RBACCheck(RBACPermission.APP_VIEW_LAYOUT, PlainApp()) def test_check_rbac_noop_when_no_requirement(): - with patch("controllers.openapi.auth.verify.enforce_rbac_access") as mock_enforce: + with patch("controllers.openapi.auth.verify.enforce_rbac_checks") as mock_enforce: check_rbac_permission(_data(rbac=None, caller_kind="account")) mock_enforce.assert_not_called() def test_check_rbac_noop_when_rbac_disabled(config_overrides: Callable[..., None]): config_overrides(RBAC_ENABLED=False) - with patch("controllers.openapi.auth.verify.enforce_rbac_access") as mock_enforce: + with patch("controllers.openapi.auth.verify.enforce_rbac_checks") as mock_enforce: check_rbac_permission(_data(rbac=_RBAC_REQ, caller_kind="account")) mock_enforce.assert_not_called() def test_check_rbac_skips_end_user_caller(): with ( - patch("controllers.openapi.auth.verify.enforce_rbac_access") as mock_enforce, + patch("controllers.openapi.auth.verify.enforce_rbac_checks") as mock_enforce, ): check_rbac_permission(_data(rbac=_RBAC_REQ, caller_kind="end_user")) mock_enforce.assert_not_called() @@ -126,17 +127,18 @@ def test_check_rbac_enforces_for_account_caller(): path_params={"app_id": "app-1"}, ) with ( - patch("controllers.openapi.auth.verify.enforce_rbac_access") as mock_enforce, + patch("controllers.openapi.auth.verify.enforce_rbac_checks") as mock_enforce, ): check_rbac_permission(data) - mock_enforce.assert_called_once_with( - tenant_id="t1", - account_id=str(account_id), - resource_type=RBACResourceScope.APP, - scene=RBACPermission.APP_VIEW_LAYOUT, - resource_required=True, - path_args={"app_id": "app-1"}, - ) + mock_enforce.assert_called_once() + call_kwargs = mock_enforce.call_args.kwargs + assert call_kwargs["tenant_id"] == "t1" + assert call_kwargs["account_id"] == str(account_id) + assert call_kwargs["path_args"] == {"app_id": "app-1"} + (check,) = call_kwargs["checks"] + assert isinstance(check, RBACCheck) + assert check.scene is RBACPermission.APP_VIEW_LAYOUT + assert isinstance(check.locator, PlainApp) def test_check_acl_raises_when_app_or_mode_missing(): diff --git a/api/tests/unit_tests/controllers/rbac_introspection.py b/api/tests/unit_tests/controllers/rbac_introspection.py new file mode 100644 index 00000000000..6d88ba375a0 --- /dev/null +++ b/api/tests/unit_tests/controllers/rbac_introspection.py @@ -0,0 +1,26 @@ +from collections.abc import Callable + +from controllers.common.rbac import RBAC_CHECKS_ATTR, RBACCheck + + +def rbac_checks(view: Callable[..., object]) -> tuple[RBACCheck, ...]: + current = view + while RBAC_CHECKS_ATTR not in current.__dict__: + wrapped = current.__dict__.get("__wrapped__") + if wrapped is None: + raise AssertionError(f"no decorator layer of {view!r} carries an rbac_checks bundle") + current = wrapped + return current.__dict__[RBAC_CHECKS_ATTR] + + +def all_rbac_checks(view: Callable[..., object]) -> list[RBACCheck]: + found: list[RBACCheck] = [] + seen_bundle_ids: set[int] = set() + current: Callable[..., object] | None = view + while current is not None: + bundle = current.__dict__.get(RBAC_CHECKS_ATTR) + if bundle is not None and id(bundle) not in seen_bundle_ids: + seen_bundle_ids.add(id(bundle)) + found.extend(bundle) + current = current.__dict__.get("__wrapped__") + return found diff --git a/api/tests/unit_tests/controllers/test_rbac_route_resource_contract.py b/api/tests/unit_tests/controllers/test_rbac_route_resource_contract.py index 23cf107813d..0fd2a205b2f 100644 --- a/api/tests/unit_tests/controllers/test_rbac_route_resource_contract.py +++ b/api/tests/unit_tests/controllers/test_rbac_route_resource_contract.py @@ -1,102 +1,143 @@ -"""Guard against resource-scoped RBAC gates mounted on routes that carry no resource id. +"""Guard against resource-scoped RBAC gates mounted on routes their locator can't read. -``rbac_permission_required`` defaults to ``resource_required=True``, which makes -``_extract_resource_id`` raise ``ValueError`` when the matched path holds none of the -accepted identifiers. The request then fails with a 400 before the view ever runs, so the -endpoint is unreachable for every tenant with ``RBAC_ENABLED``. Creation endpoints and -other workspace-level actions must opt out with ``resource_required=False``. +Every RBAC gate is declared as one or more explicit ``RBACCheck(scene, Locator(...))`` +bundles, via ``rbac_permission_required(RBACCheck(...), ...)`` or +``console_account_admission(rbac_checks=[RBACCheck(...), ...])``. Each locator reads a +path parameter — its explicit ``param`` argument, or its class default (``PlainApp`` reads +``app_id``, ``DatasetId`` reads ``dataset_id``, etc.; ``Workspace`` reads none). A route +whose URL doesn't carry that parameter can never resolve the check. + +This check runs against the registered namespaces rather than the source text: both +decorators record their checks on the view function under ``RBAC_CHECKS_ATTR``, and reading +them back at runtime also covers resources built by a factory (the RBAC access-permission +endpoints in ``controllers/console/workspace/rbac.py``), which no AST scan can see. """ -import ast -from pathlib import Path +from __future__ import annotations -CONTROLLERS_DIR = Path(__file__).resolve().parents[3] / "controllers" +from collections.abc import Iterator -# Mirrors the lookup order in controllers/common/wraps.py::_extract_resource_id. -ACCEPTED_PATH_ARGS = { - "APP": ("app_id", "agent_id", "resource_id"), - "DATASET": ("dataset_id", "pipeline_id", "resource_id"), -} +import controllers.console # noqa: F401 -- importing the package registers every console route +import controllers.openapi # noqa: F401 -- same for the user-scoped OpenAPI routes +from controllers.common.rbac import RBAC_CHECKS_ATTR, RBACCheck +from controllers.console import console_ns +from controllers.openapi import openapi_ns + +NAMESPACES = (console_ns, openapi_ns) + +HTTP_METHODS = ("delete", "get", "head", "options", "patch", "post", "put") # Known violations tracked separately: DatasetDocumentSegmentBatchImportApi binds one class # to both the dataset-scoped import route and the job-scoped status route, so every method # on it is reachable at a path carrying only a job id. Its permission points are genuinely -# per-dataset, so it needs the route split rather than resource_required=False. Remove -# these entries with that fix. +# per-dataset, so it needs the route split rather than a Workspace() locator. Remove these +# entries with that fix. KNOWN_VIOLATIONS = { - ("console/datasets/datasets_segments.py", "DatasetDocumentSegmentBatchImportApi", "post"), - ("console/datasets/datasets_segments.py", "DatasetDocumentSegmentBatchImportApi", "get"), + ("DatasetDocumentSegmentBatchImportApi", "post"), + ("DatasetDocumentSegmentBatchImportApi", "get"), } - -def _decorator_name(node: ast.Call) -> str: - func = node.func - parts = [] - while isinstance(func, ast.Attribute): - parts.append(func.attr) - func = func.value - if isinstance(func, ast.Name): - parts.append(func.id) - return ".".join(reversed(parts)) +# The generated access-permission endpoints, spelled out here so this guard fails loudly if +# the factory stops registering them (or renames a URL) rather than silently checking less. +RBAC_ACCESS_RESOURCE_PARTS = (("apps", "app_id"), ("datasets", "dataset_id"), ("agents", "agent_id")) -def _attribute_name(node: ast.expr | None) -> str | None: - return node.attr if isinstance(node, ast.Attribute) else None +def _rbac_access_urls() -> set[str]: + urls: set[str] = set() + for segment, id_param in RBAC_ACCESS_RESOURCE_PARTS: + resource = f"/workspaces/current/rbac/{segment}/" + workspace = f"/workspaces/current/rbac/workspace/{segment}" + urls.update( + { + f"{resource}/access-policy", + f"{resource}/whitelist", + f"{resource}/whitelist_config", + f"{resource}/user-access-policies", + f"{resource}/users//access-policies", + f"{resource}/access-policies//role-bindings", + f"{resource}/access-policies//member-bindings", + f"{workspace}/access-policy", + f"{workspace}/access-policies//role-bindings", + f"{workspace}/access-policies//bindings", + f"{workspace}/access-policies//member-bindings", + } + ) + return urls -def _route_paths(class_node: ast.ClassDef) -> list[str]: - paths: list[str] = [] - for decorator in class_node.decorator_list: - if isinstance(decorator, ast.Call) and _decorator_name(decorator).endswith(".route"): - paths.extend( - arg.value for arg in decorator.args if isinstance(arg, ast.Constant) and isinstance(arg.value, str) - ) - return paths +def _class_attribute(resource: type, name: str) -> object | None: + """Look up ``name`` on ``resource`` through the MRO without ``getattr``.""" + for klass in resource.__mro__: + attributes = vars(klass) + if name in attributes: + return attributes[name] + return None + + +def _declared_checks(view: object) -> list[RBACCheck]: + """Collect the RBAC checks a decorator recorded anywhere in this view's wrapper chain. + + ``functools.wraps`` copies the recorded tuple onto every outer wrapper, so the same + bundle shows up at several levels; identity tells the copies apart from a second gate. + """ + checks: list[RBACCheck] = [] + seen: set[int] = set() + current = view + while current is not None: + attributes = vars(current) + recorded = attributes.get(RBAC_CHECKS_ATTR) + if recorded and id(recorded) not in seen: + seen.add(id(recorded)) + checks.extend(recorded) + current = attributes.get("__wrapped__") + return checks def _path_args(route: str) -> set[str]: - args = set() - args.update(segment.split(">")[0].split(":")[-1] for segment in route.split("<")[1:]) - return args + return {segment.split(">")[0].split(":")[-1] for segment in route.split("<")[1:]} -def _resource_scoped_gates(method: ast.FunctionDef | ast.AsyncFunctionDef) -> list[str]: - scopes = [] - for decorator in method.decorator_list: - if not (isinstance(decorator, ast.Call) and _decorator_name(decorator).endswith("rbac_permission_required")): - continue - keywords = {keyword.arg: keyword.value for keyword in decorator.keywords} - resource_required = keywords.get("resource_required") - if isinstance(resource_required, ast.Constant) and resource_required.value is False: - continue - scope = _attribute_name(decorator.args[0] if decorator.args else keywords.get("resource_type")) - if scope in ACCEPTED_PATH_ARGS: - scopes.append(scope) - return scopes +def _registered_methods() -> Iterator[tuple[type, str, object, tuple[str, ...]]]: + """Yield ``(resource class, method name, view function, urls)`` for every routed method.""" + for namespace in NAMESPACES: + for route in namespace.resources: + for method_name in HTTP_METHODS: + view = _class_attribute(route.resource, method_name) + if view is None: + continue + yield route.resource, method_name, view, tuple(route.urls) def test_resource_scoped_rbac_gates_have_a_resource_id_in_the_route() -> None: violations = [] - for path in sorted(CONTROLLERS_DIR.rglob("*.py")): - tree = ast.parse(path.read_text(encoding="utf-8")) - for class_node in (node for node in ast.walk(tree) if isinstance(node, ast.ClassDef)): - routes = _route_paths(class_node) - if not routes: - continue - methods = (node for node in class_node.body if isinstance(node, ast.FunctionDef | ast.AsyncFunctionDef)) - for method in methods: - for scope in _resource_scoped_gates(method): - accepted = set(ACCEPTED_PATH_ARGS[scope]) - unscoped = [route for route in routes if not _path_args(route) & accepted] - if not unscoped: - continue - key = (path.relative_to(CONTROLLERS_DIR).as_posix(), class_node.name, method.name) - if key in KNOWN_VIOLATIONS: - continue - violations.append(f"{key[0]}::{key[1]}.{key[2]} scope={scope} routes={unscoped}") + for resource, method_name, view, urls in _registered_methods(): + if (resource.__name__, method_name) in KNOWN_VIOLATIONS: + continue - assert not violations, ( - "resource-scoped rbac_permission_required on routes without a resource id; " - "pass resource_required=False for workspace-level actions:\n" + "\n".join(violations) + for check in _declared_checks(view): + # Only ``_ParamLocator`` subclasses store a path parameter; ``Workspace`` reads none. + required_param = vars(check.locator).get("param") + if not isinstance(required_param, str): + continue + + unscoped = [url for url in urls if required_param not in _path_args(url)] + if unscoped: + violations.append( + f"{resource.__name__}.{method_name} " + f"locator={type(check.locator).__name__}({required_param!r}) " + f"scene={check.scene} routes={unscoped}" + ) + + assert not violations, "RBACCheck locator on a route that doesn't carry its required path parameter:\n" + "\n".join( + violations ) + + +def test_guard_reaches_the_generated_rbac_access_endpoints() -> None: + """The factory-built endpoints are the reason this guard is runtime-based; prove it sees them.""" + scanned_urls = {url for _, _, _, urls in _registered_methods() for url in urls} + + missing = sorted(_rbac_access_urls() - scanned_urls) + + assert not missing, "generated RBAC access routes are not reachable by this guard:\n" + "\n".join(missing) diff --git a/api/tests/unit_tests/services/enterprise/test_rbac_agent_access.py b/api/tests/unit_tests/services/enterprise/test_rbac_agent_access.py new file mode 100644 index 00000000000..28fabd201eb --- /dev/null +++ b/api/tests/unit_tests/services/enterprise/test_rbac_agent_access.py @@ -0,0 +1,278 @@ +"""Unit tests for the agent-flavoured RBAC inner-API client. + +`RBACService.AgentAccess`, the agent methods on `RBACService.WorkspaceAccess` and +`RBACService.Catalog.agent` are all built from one generic resource-access client. +These tests monkeypatch `_inner_call` and assert the HTTP method, the exact endpoint, +the query/body keys and the returned model for every operation. +""" + +from __future__ import annotations + +from collections.abc import Callable, Iterator +from types import SimpleNamespace +from unittest.mock import MagicMock, patch + +import pytest + +from services.enterprise import rbac_service as svc + +MODULE = "services.enterprise.rbac_service" + +TENANT = "tenant-1" +ACTOR = "acct-1" +AGENT = "agent-1" +POLICY = "policy-1" + + +@pytest.fixture +def inner_call() -> Iterator[MagicMock]: + with patch(f"{MODULE}._inner_call") as call: + call.return_value = {"automatic_include_workspace_members": True} + yield call + + +def _last(call: MagicMock) -> SimpleNamespace: + call.assert_called_once() + args, kwargs = call.call_args + return SimpleNamespace( + method=args[0], + endpoint=args[1], + tenant_id=kwargs.get("tenant_id"), + account_id=kwargs.get("account_id"), + json=kwargs.get("json"), + params=kwargs.get("params"), + ) + + +_AGENT_CASES: list[tuple[str, str, str, Callable[[], object]]] = [ + ( + "whitelist_resources", + "GET", + "/rbac/agents/whitelist/resources", + lambda: svc.RBACService.AgentAccess.whitelist_resources(TENANT, ACTOR), + ), + ( + "user_access_policies", + "GET", + "/rbac/agents/user-access-policies", + lambda: svc.RBACService.AgentAccess.user_access_policies(TENANT, ACTOR, agent_id=AGENT), + ), + ( + "replace_user_access_policies", + "PUT", + "/rbac/agents/user-access-policies", + lambda: svc.RBACService.AgentAccess.replace_user_access_policies( + TENANT, + ACTOR, + agent_id=AGENT, + target_account_id="member-1", + payload=svc.ReplaceUserAccessPolicies(access_policy_ids=[POLICY], account_ids=["member-1"]), + ), + ), + ( + "whitelist", + "GET", + "/rbac/agents/whitelist", + lambda: svc.RBACService.AgentAccess.whitelist(TENANT, ACTOR, agent_id=AGENT), + ), + ( + "whitelist_config", + "GET", + "/rbac/agents/whitelist", + lambda: svc.RBACService.AgentAccess.whitelist_config(TENANT, ACTOR, agent_id=AGENT), + ), + ( + "legacy_whitelist_config", + "GET", + "/rbac/agents/whitelist", + lambda: svc.RBACService.AgentAccess.legacy_whitelist_config(TENANT, ACTOR, agent_id=AGENT), + ), + ( + "replace_whitelist", + "PUT", + "/rbac/agents/whitelist", + lambda: svc.RBACService.AgentAccess.replace_whitelist( + TENANT, + ACTOR, + agent_id=AGENT, + payload=svc.ReplaceMemberBindings(automatic_include_workspace_members=True), + ), + ), + ( + "append_whitelist_members_batch", + "POST", + "/rbac/agents/whitelist/members/batch", + lambda: svc.RBACService.AgentAccess.append_whitelist_members_batch( + tenant_id=TENANT, + account_id=ACTOR, + data=[svc.AppendAgentWhitelistMembersBatchItem(agent_id=AGENT, account_ids=["member-1"], policy_id=POLICY)], + ), + ), + ( + "matrix", + "GET", + "/rbac/agents/access-policy", + lambda: svc.RBACService.AgentAccess.matrix(TENANT, ACTOR, agent_id=AGENT), + ), + ( + "list_role_bindings", + "GET", + "/rbac/agents/access-policy/role-bindings", + lambda: svc.RBACService.AgentAccess.list_role_bindings(TENANT, ACTOR, agent_id=AGENT, policy_id=POLICY), + ), + ( + "replace_role_bindings", + "PUT", + "/rbac/agents/access-policy/role-bindings", + lambda: svc.RBACService.AgentAccess.replace_role_bindings( + TENANT, ACTOR, agent_id=AGENT, policy_id=POLICY, payload=svc.ReplaceRoleBindings(role_ids=["role-1"]) + ), + ), + ( + "list_member_bindings", + "GET", + "/rbac/agents/access-policy/member-bindings", + lambda: svc.RBACService.AgentAccess.list_member_bindings(TENANT, ACTOR, agent_id=AGENT, policy_id=POLICY), + ), + ( + "delete_member_bindings", + "DELETE", + "/rbac/agents/access-policy/member-bindings", + lambda: svc.RBACService.AgentAccess.delete_member_bindings( + TENANT, ACTOR, agent_id=AGENT, policy_id=POLICY, payload=svc.DeleteMemberBindings(account_ids=["member-1"]) + ), + ), + ( + "replace_bindings", + "PUT", + "/rbac/agents/access-policy/bindings", + lambda: svc.RBACService.AgentAccess.replace_bindings( + TENANT, + ACTOR, + agent_id=AGENT, + policy_id=POLICY, + payload=svc.ReplaceBindings(role_ids=["role-1"], account_ids=["member-1"]), + ), + ), + ( + "workspace.agent_matrix", + "GET", + "/rbac/workspace/agents/access-policy", + lambda: svc.RBACService.WorkspaceAccess.agent_matrix(TENANT, ACTOR), + ), + ( + "workspace.list_agent_role_bindings", + "GET", + "/rbac/workspace/agents/access-policy/role-bindings", + lambda: svc.RBACService.WorkspaceAccess.list_agent_role_bindings(TENANT, ACTOR, POLICY), + ), + ( + "workspace.replace_agent_role_bindings", + "PUT", + "/rbac/workspace/agents/access-policy/role-bindings", + lambda: svc.RBACService.WorkspaceAccess.replace_agent_role_bindings( + TENANT, ACTOR, POLICY, svc.ReplaceRoleBindings(role_ids=["role-1"]) + ), + ), + ( + "workspace.list_agent_member_bindings", + "GET", + "/rbac/workspace/agents/access-policy/member-bindings", + lambda: svc.RBACService.WorkspaceAccess.list_agent_member_bindings(TENANT, ACTOR, POLICY), + ), + ( + "workspace.replace_agent_bindings", + "PUT", + "/rbac/workspace/agents/access-policy/bindings", + lambda: svc.RBACService.WorkspaceAccess.replace_agent_bindings( + TENANT, ACTOR, POLICY, svc.ReplaceBindings(role_ids=["role-1"], account_ids=["member-1"]) + ), + ), + ( + "catalog.agent", + "GET", + "/rbac/role-permissions/catalog/agent", + lambda: svc.RBACService.Catalog.agent(TENANT, account_id=ACTOR), + ), +] + + +@pytest.mark.parametrize( + ("method", "endpoint", "invoke"), + [(case[1], case[2], case[3]) for case in _AGENT_CASES], + ids=[case[0] for case in _AGENT_CASES], +) +def test_agent_operations_hit_the_agent_route( + inner_call: MagicMock, method: str, endpoint: str, invoke: Callable[[], object] +) -> None: + invoke() + + call = _last(inner_call) + assert call.method == method + assert call.endpoint == endpoint + assert "/apps/" not in call.endpoint + assert "/datasets/" not in call.endpoint + assert call.tenant_id == TENANT + assert call.account_id == ACTOR + params = call.params or {} + assert "app_id" not in params + assert "dataset_id" not in params + if AGENT in params.values(): + assert params.get("agent_id") == AGENT + + +_PARITY_CASES: list[tuple[str, Callable[[], object], Callable[[], object]]] = [ + ( + "whitelist_resources", + lambda: svc.RBACService.AppAccess.whitelist_resources(TENANT, ACTOR), + lambda: svc.RBACService.AgentAccess.whitelist_resources(TENANT, ACTOR), + ), + ( + "whitelist", + lambda: svc.RBACService.AppAccess.whitelist(TENANT, ACTOR, "res-1"), + lambda: svc.RBACService.AgentAccess.whitelist(TENANT, ACTOR, agent_id="res-1"), + ), + ( + "user_access_policies", + lambda: svc.RBACService.AppAccess.user_access_policies(TENANT, ACTOR, "res-1"), + lambda: svc.RBACService.AgentAccess.user_access_policies(TENANT, ACTOR, agent_id="res-1"), + ), + ( + "matrix", + lambda: svc.RBACService.AppAccess.matrix(TENANT, ACTOR, "res-1"), + lambda: svc.RBACService.AgentAccess.matrix(TENANT, ACTOR, agent_id="res-1"), + ), + ( + "list_role_bindings", + lambda: svc.RBACService.AppAccess.list_role_bindings(TENANT, ACTOR, "res-1", POLICY), + lambda: svc.RBACService.AgentAccess.list_role_bindings(TENANT, ACTOR, agent_id="res-1", policy_id=POLICY), + ), + ( + "list_member_bindings", + lambda: svc.RBACService.AppAccess.list_member_bindings(TENANT, ACTOR, "res-1", POLICY), + lambda: svc.RBACService.AgentAccess.list_member_bindings(TENANT, ACTOR, agent_id="res-1", policy_id=POLICY), + ), +] + + +@pytest.mark.parametrize( + ("app_call", "agent_call"), + [(case[1], case[2]) for case in _PARITY_CASES], + ids=[case[0] for case in _PARITY_CASES], +) +def test_same_path_and_param_shape_modulo_segment( + inner_call: MagicMock, + app_call: Callable[[], object], + agent_call: Callable[[], object], +) -> None: + app_call() + app = _last(inner_call) + inner_call.reset_mock() + agent_call() + agent = _last(inner_call) + + assert app.endpoint.replace("/apps/", "/agents/") == agent.endpoint + assert app.method == agent.method + + app_params = {("agent_id" if k == "app_id" else k): v for k, v in (app.params or {}).items()} + assert app_params == (agent.params or {}) diff --git a/api/tests/unit_tests/services/enterprise/test_rbac_service.py b/api/tests/unit_tests/services/enterprise/test_rbac_service.py index 078dbd2c38b..1d76cc79ff2 100644 --- a/api/tests/unit_tests/services/enterprise/test_rbac_service.py +++ b/api/tests/unit_tests/services/enterprise/test_rbac_service.py @@ -748,37 +748,42 @@ class TestMyPermissions: assert out.workspace.permission_keys == ["workspace.member.manage"] @pytest.mark.parametrize( - ("role", "workspace_keys", "app_keys", "dataset_keys"), + ("role", "workspace_keys", "app_keys", "dataset_keys", "agent_keys"), [ ( "owner", svc._LEGACY_WORKSPACE_OWNER_KEYS, svc._LEGACY_APP_OWNER_KEYS, svc._LEGACY_DATASET_OWNER_KEYS, + svc._LEGACY_AGENT_FULL_ACCESS_KEYS, ), ( "admin", svc._LEGACY_WORKSPACE_ADMIN_KEYS, svc._LEGACY_APP_ADMIN_KEYS, svc._LEGACY_DATASET_ADMIN_KEYS, + svc._LEGACY_AGENT_FULL_ACCESS_KEYS, ), ( "editor", svc._LEGACY_WORKSPACE_EDITOR_KEYS, svc._LEGACY_APP_EDITOR_KEYS, svc._LEGACY_DATASET_EDITOR_KEYS, + svc._LEGACY_AGENT_FULL_ACCESS_KEYS, ), ( "normal", svc._LEGACY_WORKSPACE_NORMAL_KEYS, svc._LEGACY_APP_NORMAL_KEYS, [], + svc._LEGACY_AGENT_PREVIEW_KEYS, ), ( "dataset_operator", svc._LEGACY_WORKSPACE_DATASET_OPERATOR_KEYS, svc._LEGACY_APP_DATASET_OPERATOR_KEYS, svc._LEGACY_DATASET_DATASET_OPERATOR_KEYS, + svc._LEGACY_AGENT_PREVIEW_KEYS, ), ], ) @@ -789,6 +794,7 @@ class TestMyPermissions: workspace_keys: list[str], app_keys: list[str], dataset_keys: list[str], + agent_keys: list[str], sqlite_session: Session, config_overrides, ): @@ -804,8 +810,10 @@ class TestMyPermissions: assert len(out.workspace.permission_keys) == len(set(out.workspace.permission_keys)) assert out.app.default_permission_keys == app_keys assert out.dataset.default_permission_keys == dataset_keys + assert out.agent.default_permission_keys == agent_keys assert out.app.overrides == [] assert out.dataset.overrides == [] + assert out.agent.overrides == [] if role == "owner": assert "snippets.management" in out.workspace.permission_keys assert "app.acl.preview" in out.workspace.permission_keys @@ -859,6 +867,7 @@ class TestMyPermissions: assert out.workspace.permission_keys == [] assert out.app.default_permission_keys == [] assert out.dataset.default_permission_keys == [] + assert out.agent.default_permission_keys == [] def test_get_with_single_resource_filters(self, mock_send: MagicMock, sqlite_session: Session): mock_send.return_value = { @@ -878,6 +887,27 @@ class TestMyPermissions: assert call.params == {"app_id": "app-1"} assert out.app.overrides[0].resource_id == "app-1" + def test_get_forwards_agent_id_and_parses_agent_snapshot(self, mock_send: MagicMock, sqlite_session: Session): + mock_send.return_value = { + "workspace": {"permission_keys": []}, + "app": {"default_permission_keys": [], "overrides": []}, + "dataset": {"default_permission_keys": [], "overrides": []}, + "agent": { + "default_permission_keys": ["agent.acl.preview"], + "overrides": [{"resource_id": "agent-1", "permission_keys": ["agent.acl.edit"]}], + }, + } + + out = svc.RBACService.MyPermissions.get("tenant-1", "acct-1", agent_id="agent-1", session=sqlite_session) + + call = _call_args(mock_send) + assert call.method == "GET" + assert call.endpoint == "/rbac/my-permissions" + assert call.params == {"agent_id": "agent-1"} + assert out.agent.default_permission_keys == ["agent.acl.preview"] + assert out.agent.overrides[0].resource_id == "agent-1" + assert out.agent.overrides[0].permission_keys == ["agent.acl.edit"] + @pytest.mark.parametrize("sqlite_session", [(TenantAccountJoin,)], indirect=True) class TestMemberRoles: @@ -924,6 +954,7 @@ class TestMemberRoles: *svc._LEGACY_WORKSPACE_EDITOR_KEYS, *svc._LEGACY_APP_EDITOR_KEYS, *svc._LEGACY_DATASET_EDITOR_KEYS, + *svc._LEGACY_AGENT_FULL_ACCESS_KEYS, ] ) ) @@ -1126,9 +1157,8 @@ class TestListOption: } -class TestLegacyAgentManageKey: - def test_legacy_agent_manage_key_membership(self): - # Preserve Agent access for every legacy role while external RBAC is disabled. +class TestLegacyAgentKeys: + def test_legacy_workspace_keys_no_longer_carry_agent_manage(self): for keys in ( svc._LEGACY_WORKSPACE_OWNER_KEYS, svc._LEGACY_WORKSPACE_ADMIN_KEYS, @@ -1136,4 +1166,5 @@ class TestLegacyAgentManageKey: svc._LEGACY_WORKSPACE_NORMAL_KEYS, svc._LEGACY_WORKSPACE_DATASET_OPERATOR_KEYS, ): - assert "agent.manage" in keys + assert "agent.manage" not in keys + assert {"agent.acl.preview", "agent.acl.access_point_view"} <= set(keys) diff --git a/api/tests/unit_tests/services/test_app_dsl_service.py b/api/tests/unit_tests/services/test_app_dsl_service.py index 178041d2e48..af4c7c1ee7d 100644 --- a/api/tests/unit_tests/services/test_app_dsl_service.py +++ b/api/tests/unit_tests/services/test_app_dsl_service.py @@ -542,38 +542,25 @@ def test_export_dsl_loads_model_config_and_annotation_reply_with_request_session load_annotation_reply_config.assert_called_once_with(service_session, app_id) -def test_ensure_agent_manage_permission_noops_when_rbac_disabled( - monkeypatch: pytest.MonkeyPatch, config_overrides: Callable[..., None] -) -> None: - config_overrides(RBAC_ENABLED=False) - check = Mock() - monkeypatch.setattr("services.app_dsl_service.RBACService.CheckAccess.check", check) - - AppDslService._ensure_agent_manage_permission(_account()) - - check.assert_not_called() - - -def test_ensure_agent_manage_permission_allows_agent_manager( - monkeypatch: pytest.MonkeyPatch, config_overrides: Callable[..., None] +def test_agent_import_of_new_agent_checks_function_scope( + monkeypatch: pytest.MonkeyPatch, + unbound_session: Session, + config_overrides: Callable[..., None], ) -> None: config_overrides(RBAC_ENABLED=True) check = Mock(return_value=True) monkeypatch.setattr("services.app_dsl_service.RBACService.CheckAccess.check", check) + account = _account() - AppDslService._ensure_agent_manage_permission(_account()) + AppDslService(session=unbound_session)._ensure_agent_import_permission(account, app=None) - check.assert_called_once_with("tenant-1", "account-1", scene=RBACPermission.AGENT_MANAGE) - - -def test_ensure_agent_manage_permission_rejects_without_agent_manage( - monkeypatch: pytest.MonkeyPatch, config_overrides: Callable[..., None] -) -> None: - config_overrides(RBAC_ENABLED=True) - monkeypatch.setattr("services.app_dsl_service.RBACService.CheckAccess.check", Mock(return_value=False)) - - with pytest.raises(NoPermissionError): - AppDslService._ensure_agent_manage_permission(_account()) + check.assert_called_once_with( + account.current_tenant_id, + account.id, + scene=RBACPermission.AGENT_IMPORT_EXPORT_DSL, + resource_type=None, + resource_id=None, + ) def test_create_or_update_app_gates_agent_mode_before_creation( diff --git a/api/tests/unit_tests/services/test_app_service.py b/api/tests/unit_tests/services/test_app_service.py index a2dadd3a04c..796439acc72 100644 --- a/api/tests/unit_tests/services/test_app_service.py +++ b/api/tests/unit_tests/services/test_app_service.py @@ -27,6 +27,7 @@ from models.model import App, AppMode, AppModelConfig, IconType from models.workflow import Workflow, WorkflowType from services.agent.errors import AgentAccessNotReadyError, AgentNameConflictError from services.app_service import AppListParams, AppService, CreateAppParams +from services.enterprise import rbac_service as enterprise_rbac_service def _persist_account(session: Session) -> Account: @@ -222,6 +223,63 @@ class TestCreateAppTransactionBoundary: ) +class TestCreateAppRBACAccessInitialization: + """`create_app` bootstraps RBAC access per app mode: agents get the agent flavour.""" + + @staticmethod + def _create(session: Session, account: Account, mode: AppMode) -> App: + with ( + patch("services.app_service.app_was_created.send"), + patch( + "services.app_service.SystemFeatureService.is_webapp_auth_enabled", + return_value=False, + ), + ): + return AppService().create_app( + account.current_tenant_id, + CreateAppParams(name=f"RBAC {mode.value}", mode=mode.value), + account, + session=session, + ) + + def test_agent_app_initializes_agent_access_and_skips_the_app_creator_sync( + self, sqlite_session: Session, config_overrides: Callable[..., None] + ) -> None: + config_overrides(DEPLOYMENT_EDITION=DeploymentEdition.COMMUNITY, RBAC_ENABLED=True) + account = _persist_account(sqlite_session) + + with ( + patch( + "services.rbac_agent_access_service.enterprise_rbac_service.RBACService.AgentAccess.replace_whitelist" + ) as replace_whitelist, + patch("services.rbac_agent_access_service.initialize_created_app_rbac_access_task.delay") as seed_task, + patch( + "services.rbac_agent_access_service.enterprise_rbac_service.RBACService.AccessPolicies" + ".sync_creator_access_policy_member_bindings" + ) as creator_sync, + patch( + "services.app_service.enterprise_rbac_service.try_sync_creator_access_policy_member_bindings" + ) as app_creator_sync, + ): + app = self._create(sqlite_session, account, AppMode.AGENT) + + agent = sqlite_session.scalars(select(Agent).where(Agent.app_id == app.id)).one() + replace_whitelist.assert_called_once_with( + account.current_tenant_id, + account.id, + agent.id, + enterprise_rbac_service.ReplaceMemberBindings(automatic_include_workspace_members=True), + ) + seed_task.assert_called_once_with(account.current_tenant_id, account.id, agent_id=agent.id) + creator_sync.assert_called_once_with( + account.current_tenant_id, + account.id, + resource_type=enterprise_rbac_service.RBACResourceType.AGENT, + resource_id=agent.id, + ) + app_creator_sync.assert_not_called() + + @pytest.mark.parametrize( "update_status", [AppService.update_app_site_status, AppService.update_app_api_status], diff --git a/api/tests/unit_tests/services/test_rbac_agent_access_service.py b/api/tests/unit_tests/services/test_rbac_agent_access_service.py new file mode 100644 index 00000000000..92cd40fa5ff --- /dev/null +++ b/api/tests/unit_tests/services/test_rbac_agent_access_service.py @@ -0,0 +1,56 @@ +"""Unit tests for the RBAC bootstrap applied to a newly created agent.""" + +from __future__ import annotations + +from collections.abc import Callable, Iterator +from unittest.mock import MagicMock, patch + +import pytest + +from services import rbac_agent_access_service +from services.enterprise import rbac_service as enterprise_rbac_service + +MODULE = "services.rbac_agent_access_service" + +TENANT = "tenant-1" +AGENT = "agent-1" +CREATOR = "account-1" + + +@pytest.fixture +def rbac_calls(config_overrides: Callable[..., None]) -> Iterator[MagicMock]: + config_overrides(RBAC_ENABLED=True) + recorder = MagicMock() + with ( + patch(f"{MODULE}.enterprise_rbac_service.RBACService.AgentAccess.replace_whitelist") as replace_whitelist, + patch(f"{MODULE}.initialize_created_app_rbac_access_task.delay") as seed_task, + patch( + f"{MODULE}.enterprise_rbac_service.RBACService.AccessPolicies.sync_creator_access_policy_member_bindings" + ) as creator_sync, + ): + recorder.attach_mock(replace_whitelist, "replace_whitelist") + recorder.attach_mock(seed_task, "seed_task") + recorder.attach_mock(creator_sync, "creator_sync") + yield recorder + + +def test_initialize_agent_rbac_access_seeds_scope_members_and_creator_policy( + rbac_calls: MagicMock, +) -> None: + rbac_agent_access_service.initialize_agent_rbac_access(tenant_id=TENANT, agent_id=AGENT, creator_account_id=CREATOR) + + rbac_calls.replace_whitelist.assert_called_once_with( + TENANT, + CREATOR, + AGENT, + enterprise_rbac_service.ReplaceMemberBindings(automatic_include_workspace_members=True), + ) + rbac_calls.seed_task.assert_called_once_with(TENANT, CREATOR, agent_id=AGENT) + rbac_calls.creator_sync.assert_called_once_with( + TENANT, + CREATOR, + resource_type=enterprise_rbac_service.RBACResourceType.AGENT, + resource_id=AGENT, + ) + + assert [call[0] for call in rbac_calls.mock_calls] == ["seed_task", "creator_sync", "replace_whitelist"] diff --git a/api/tests/unit_tests/tasks/test_initialize_created_app_rbac_access_task.py b/api/tests/unit_tests/tasks/test_initialize_created_app_rbac_access_task.py index be96c1d6285..b9d96fc01db 100644 --- a/api/tests/unit_tests/tasks/test_initialize_created_app_rbac_access_task.py +++ b/api/tests/unit_tests/tasks/test_initialize_created_app_rbac_access_task.py @@ -51,6 +51,49 @@ def test_initialize_created_app_rbac_access_task_batches_workspace_members(monke assert call.kwargs["payload"].access_policy_ids == [task_module.APP_RBAC_DEFAULT_ACCESS_POLICY_ID] +@pytest.mark.parametrize( + ("id_kwarg", "resource_id", "access_class"), + [ + ("app_id", "app-1", "AppAccess"), + ("dataset_id", "dataset-1", "DatasetAccess"), + ("agent_id", "agent-1", "AgentAccess"), + ], +) +def test_initialize_created_app_rbac_access_task_targets_the_resource_that_was_passed( + monkeypatch: pytest.MonkeyPatch, id_kwarg: str, resource_id: str, access_class: str +): + import tasks.initialize_created_app_rbac_access_task as task_module + + apply_config_overrides(monkeypatch, RBAC_ENABLED=True) + monkeypatch.setattr( + task_module.TenantService, + "iter_member_account_id_batches", + lambda tenant_id, batch_size, session: iter([["acct-1"]]), + ) + rbac_service = task_module.enterprise_rbac_service.RBACService + access_clients = { + "AppAccess": rbac_service.AppAccess, + "DatasetAccess": rbac_service.DatasetAccess, + "AgentAccess": rbac_service.AgentAccess, + } + replace_calls = {} + for name, client in access_clients.items(): + replace_calls[name] = MagicMock() + monkeypatch.setattr(client, "replace_user_access_policies", replace_calls[name]) + + initialize_created_app_rbac_access_task.run("tenant-1", "actor-1", **{id_kwarg: resource_id}) + + for name, mock in replace_calls.items(): + if name != access_class: + mock.assert_not_called() + + called = replace_calls[access_class] + called.assert_called_once() + assert called.call_args.kwargs[id_kwarg] == resource_id + assert called.call_args.kwargs["target_account_id"] is None + assert called.call_args.kwargs["payload"].account_ids == ["acct-1"] + + def test_initialize_created_app_rbac_access_task_retries_on_failure(monkeypatch: pytest.MonkeyPatch): import tasks.initialize_created_app_rbac_access_task as task_module from tasks.initialize_created_app_rbac_access_task import initialize_created_app_rbac_access_task @@ -87,6 +130,8 @@ def test_sync_joined_workspace_member_rbac_access_task_appends_auto_included_res rbac.ResourceWhitelistConfigResource(resource_type=rbac.RBACResourceType.APP, resource_id="app-1"), rbac.ResourceWhitelistConfigResource(resource_type=rbac.RBACResourceType.DATASET, resource_id="dataset-1"), rbac.ResourceWhitelistConfigResource(resource_type=rbac.RBACResourceType.APP, resource_id="app-2"), + rbac.ResourceWhitelistConfigResource(resource_type=rbac.RBACResourceType.AGENT, resource_id="agent-1"), + rbac.ResourceWhitelistConfigResource(resource_type=rbac.RBACResourceType.AGENT, resource_id="agent-2"), ] configs = rbac.ResourceWhitelistConfigsResponse( data=[ @@ -105,17 +150,29 @@ def test_sync_joined_workspace_member_rbac_access_task_appends_auto_included_res resource_id="app-2", automatic_include_workspace_members=False, ), + rbac.ResourceWhitelistConfigItem( + resource_type=rbac.RBACResourceType.AGENT, + resource_id="agent-1", + automatic_include_workspace_members=True, + ), + rbac.ResourceWhitelistConfigItem( + resource_type=rbac.RBACResourceType.AGENT, + resource_id="agent-2", + automatic_include_workspace_members=False, + ), ] ) batch_get = MagicMock(return_value=configs) app_append = MagicMock() dataset_append = MagicMock() + agent_append = MagicMock() apply_config_overrides(monkeypatch, RBAC_ENABLED=True) monkeypatch.setattr(task_module, "_iter_resource_config_batches", lambda tenant_id, batch_size: iter([resources])) monkeypatch.setattr(rbac.RBACService.ResourceWhitelistConfigs, "batch_get", batch_get) monkeypatch.setattr(rbac.RBACService.AppAccess, "append_whitelist_members_batch", app_append) monkeypatch.setattr(rbac.RBACService.DatasetAccess, "append_whitelist_members_batch", dataset_append) + monkeypatch.setattr(rbac.RBACService.AgentAccess, "append_whitelist_members_batch", agent_append) sync_joined_workspace_member_rbac_access_task.run("tenant-1", "member-1", "actor-1") @@ -141,3 +198,11 @@ def test_sync_joined_workspace_member_rbac_access_task_appends_auto_included_res assert dataset_call["data"][0].dataset_id == "dataset-1" assert dataset_call["data"][0].account_ids == ["member-1"] assert dataset_call["data"][0].policy_id == task_module.APP_RBAC_DEFAULT_ACCESS_POLICY_ID + + agent_append.assert_called_once() + agent_call = agent_append.call_args.kwargs + assert agent_call["tenant_id"] == "tenant-1" + assert agent_call["account_id"] == "actor-1" + assert [item.agent_id for item in agent_call["data"]] == ["agent-1"] + assert agent_call["data"][0].account_ids == ["member-1"] + assert agent_call["data"][0].policy_id == task_module.APP_RBAC_DEFAULT_ACCESS_POLICY_ID diff --git a/packages/contracts/generated/api/console/apps/orpc.gen.ts b/packages/contracts/generated/api/console/apps/orpc.gen.ts index f9dd6e0c582..d2f3910cd36 100644 --- a/packages/contracts/generated/api/console/apps/orpc.gen.ts +++ b/packages/contracts/generated/api/console/apps/orpc.gen.ts @@ -1358,7 +1358,6 @@ export const delete3 = oc method: 'DELETE', operationId: 'deleteAppsByAppIdAnnotationsByAnnotationId', path: '/apps/{app_id}/annotations/{annotation_id}', - successStatus: 204, tags: ['console'], }) .input(z.object({ params: zDeleteAppsByAppIdAnnotationsByAnnotationIdPath })) @@ -1396,7 +1395,6 @@ export const delete4 = oc method: 'DELETE', operationId: 'deleteAppsByAppIdAnnotations', path: '/apps/{app_id}/annotations', - successStatus: 204, tags: ['console'], }) .input(z.object({ params: zDeleteAppsByAppIdAnnotationsPath })) @@ -4334,7 +4332,6 @@ export const delete14 = oc method: 'DELETE', operationId: 'deleteAppsByAppIdWorkflowsByWorkflowId', path: '/apps/{app_id}/workflows/{workflow_id}', - successStatus: 204, summary: 'Delete workflow', tags: ['console'], }) diff --git a/packages/contracts/generated/api/console/apps/types.gen.ts b/packages/contracts/generated/api/console/apps/types.gen.ts index 1098bb2809e..5899611953a 100644 --- a/packages/contracts/generated/api/console/apps/types.gen.ts +++ b/packages/contracts/generated/api/console/apps/types.gen.ts @@ -729,18 +729,6 @@ export type ParserEnable = { trigger_id: string } -export type WorkflowTriggerResponse = { - created_at?: string | null - icon: string - id: string - node_id: string - provider_name: string - status: string - title: string - trigger_type: string - updated_at?: string | null -} - export type WorkflowTriggerListResponse = { data: Array } @@ -1667,6 +1655,18 @@ export type TextToSpeechVoiceResponse = { value: string } +export type WorkflowTriggerResponse = { + created_at?: string | null + icon: string + id: string + node_id: string + provider_name: string + status: string + title: string + trigger_type: string + updated_at?: string | null +} + export type WorkflowAppLogPartialResponse = { created_at?: number | null created_by_account?: SimpleAccountResponse | null @@ -3870,7 +3870,9 @@ export type DeleteAppsByAppIdAnnotationsData = { } export type DeleteAppsByAppIdAnnotationsResponses = { - 204: void + 200: { + [key: string]: unknown + } } export type DeleteAppsByAppIdAnnotationsResponse = @@ -4011,7 +4013,9 @@ export type DeleteAppsByAppIdAnnotationsByAnnotationIdData = { } export type DeleteAppsByAppIdAnnotationsByAnnotationIdResponses = { - 204: void + 200: { + [key: string]: unknown + } } export type DeleteAppsByAppIdAnnotationsByAnnotationIdResponse = @@ -5095,7 +5099,9 @@ export type PostAppsByAppIdTriggerEnableData = { } export type PostAppsByAppIdTriggerEnableResponses = { - 200: WorkflowTriggerResponse + 200: { + [key: string]: unknown + } } export type PostAppsByAppIdTriggerEnableResponse = @@ -6503,7 +6509,9 @@ export type DeleteAppsByAppIdWorkflowsByWorkflowIdData = { } export type DeleteAppsByAppIdWorkflowsByWorkflowIdResponses = { - 204: void + 200: { + [key: string]: unknown + } } export type DeleteAppsByAppIdWorkflowsByWorkflowIdResponse = diff --git a/packages/contracts/generated/api/console/apps/zod.gen.ts b/packages/contracts/generated/api/console/apps/zod.gen.ts index ef20a64815b..3f556df7381 100644 --- a/packages/contracts/generated/api/console/apps/zod.gen.ts +++ b/packages/contracts/generated/api/console/apps/zod.gen.ts @@ -481,28 +481,6 @@ export const zParserEnable = z.object({ trigger_id: z.string(), }) -/** - * WorkflowTriggerResponse - */ -export const zWorkflowTriggerResponse = z.object({ - created_at: z.iso.datetime().nullish(), - icon: z.string(), - id: z.string(), - node_id: z.string(), - provider_name: z.string(), - status: z.string(), - title: z.string(), - trigger_type: z.string(), - updated_at: z.iso.datetime().nullish(), -}) - -/** - * WorkflowTriggerListResponse - */ -export const zWorkflowTriggerListResponse = z.object({ - data: z.array(zWorkflowTriggerResponse), -}) - /** * WorkflowAgentSandboxDownloadPayload */ @@ -1479,6 +1457,28 @@ export const zTextToSpeechVoiceResponse = z.object({ */ export const zTextToSpeechVoiceListResponse = z.array(zTextToSpeechVoiceResponse) +/** + * WorkflowTriggerResponse + */ +export const zWorkflowTriggerResponse = z.object({ + created_at: z.iso.datetime().nullish(), + icon: z.string(), + id: z.string(), + node_id: z.string(), + provider_name: z.string(), + status: z.string(), + title: z.string(), + trigger_type: z.string(), + updated_at: z.iso.datetime().nullish(), +}) + +/** + * WorkflowTriggerListResponse + */ +export const zWorkflowTriggerListResponse = z.object({ + data: z.array(zWorkflowTriggerResponse), +}) + /** * SimpleAccountResponse */ @@ -4723,9 +4723,9 @@ export const zDeleteAppsByAppIdAnnotationsPath = z.object({ }) /** - * Annotations deleted successfully + * Success */ -export const zDeleteAppsByAppIdAnnotationsResponse = z.void() +export const zDeleteAppsByAppIdAnnotationsResponse = z.record(z.string(), z.unknown()) export const zGetAppsByAppIdAnnotationsPath = z.object({ app_id: z.uuid(), @@ -4797,9 +4797,9 @@ export const zDeleteAppsByAppIdAnnotationsByAnnotationIdPath = z.object({ }) /** - * Annotation deleted successfully + * Success */ -export const zDeleteAppsByAppIdAnnotationsByAnnotationIdResponse = z.void() +export const zDeleteAppsByAppIdAnnotationsByAnnotationIdResponse = z.record(z.string(), z.unknown()) export const zPostAppsByAppIdAnnotationsByAnnotationIdBody = zUpdateAnnotationPayload @@ -5421,7 +5421,7 @@ export const zPostAppsByAppIdTriggerEnablePath = z.object({ /** * Success */ -export const zPostAppsByAppIdTriggerEnableResponse = zWorkflowTriggerResponse +export const zPostAppsByAppIdTriggerEnableResponse = z.record(z.string(), z.unknown()) export const zGetAppsByAppIdTriggersPath = z.object({ app_id: z.uuid(), @@ -6314,9 +6314,9 @@ export const zDeleteAppsByAppIdWorkflowsByWorkflowIdPath = z.object({ }) /** - * Workflow deleted successfully + * Success */ -export const zDeleteAppsByAppIdWorkflowsByWorkflowIdResponse = z.void() +export const zDeleteAppsByAppIdWorkflowsByWorkflowIdResponse = z.record(z.string(), z.unknown()) export const zPatchAppsByAppIdWorkflowsByWorkflowIdBody = zWorkflowUpdatePayload diff --git a/packages/contracts/generated/api/console/auth/types.gen.ts b/packages/contracts/generated/api/console/auth/types.gen.ts index 7b9404d5bfb..3f83a36fb2a 100644 --- a/packages/contracts/generated/api/console/auth/types.gen.ts +++ b/packages/contracts/generated/api/console/auth/types.gen.ts @@ -205,7 +205,9 @@ export type DeleteAuthPluginDatasourceByProviderIdCustomClientData = { } export type DeleteAuthPluginDatasourceByProviderIdCustomClientResponses = { - 200: SimpleResultResponse + 200: { + [key: string]: unknown + } } export type DeleteAuthPluginDatasourceByProviderIdCustomClientResponse = diff --git a/packages/contracts/generated/api/console/auth/zod.gen.ts b/packages/contracts/generated/api/console/auth/zod.gen.ts index b52a8f4f63a..cedba982045 100644 --- a/packages/contracts/generated/api/console/auth/zod.gen.ts +++ b/packages/contracts/generated/api/console/auth/zod.gen.ts @@ -223,7 +223,10 @@ export const zDeleteAuthPluginDatasourceByProviderIdCustomClientPath = z.object( /** * Success */ -export const zDeleteAuthPluginDatasourceByProviderIdCustomClientResponse = zSimpleResultResponse +export const zDeleteAuthPluginDatasourceByProviderIdCustomClientResponse = z.record( + z.string(), + z.unknown(), +) export const zPostAuthPluginDatasourceByProviderIdCustomClientBody = zDatasourceCustomClientPayload diff --git a/packages/contracts/generated/api/console/data-source/types.gen.ts b/packages/contracts/generated/api/console/data-source/types.gen.ts index 1c074190df0..1c1051369c4 100644 --- a/packages/contracts/generated/api/console/data-source/types.gen.ts +++ b/packages/contracts/generated/api/console/data-source/types.gen.ts @@ -4,46 +4,6 @@ export type ClientOptions = { baseUrl: `${string}://${string}/console/api` | (string & {}) } -export type DataSourceIntegrateListResponse = { - data: Array -} - -export type SimpleResultResponse = { - result: string -} - -export type DataSourceIntegrateResponse = { - created_at: number | null - disabled: boolean | null - id: string | null - is_bound: boolean - link: string - provider: string - source_info: DataSourceIntegrateWorkspaceResponse | null -} - -export type DataSourceIntegrateWorkspaceResponse = { - pages: Array - total: number - workspace_icon: string | null - workspace_id: string | null - workspace_name: string | null -} - -export type DataSourceIntegratePageResponse = { - page_icon: DataSourceIntegrateIconResponse | null - page_id: string - page_name: string - parent_id: string - type: string -} - -export type DataSourceIntegrateIconResponse = { - emoji?: string | null - type?: string | null - url?: string | null -} - export type GetDataSourceIntegratesData = { body?: never path?: never @@ -52,7 +12,9 @@ export type GetDataSourceIntegratesData = { } export type GetDataSourceIntegratesResponses = { - 200: DataSourceIntegrateListResponse + 200: { + [key: string]: unknown + } } export type GetDataSourceIntegratesResponse = @@ -66,7 +28,9 @@ export type PatchDataSourceIntegratesData = { } export type PatchDataSourceIntegratesResponses = { - 200: SimpleResultResponse + 200: { + [key: string]: unknown + } } export type PatchDataSourceIntegratesResponse = @@ -83,7 +47,9 @@ export type GetDataSourceIntegratesByBindingIdByActionData = { } export type GetDataSourceIntegratesByBindingIdByActionResponses = { - 200: DataSourceIntegrateListResponse + 200: { + [key: string]: unknown + } } export type GetDataSourceIntegratesByBindingIdByActionResponse = @@ -100,7 +66,9 @@ export type PatchDataSourceIntegratesByBindingIdByActionData = { } export type PatchDataSourceIntegratesByBindingIdByActionResponses = { - 200: SimpleResultResponse + 200: { + [key: string]: unknown + } } export type PatchDataSourceIntegratesByBindingIdByActionResponse = diff --git a/packages/contracts/generated/api/console/data-source/zod.gen.ts b/packages/contracts/generated/api/console/data-source/zod.gen.ts index e2774d10f42..8236a06d2e4 100644 --- a/packages/contracts/generated/api/console/data-source/zod.gen.ts +++ b/packages/contracts/generated/api/console/data-source/zod.gen.ts @@ -3,72 +3,14 @@ import * as z from 'zod' /** - * SimpleResultResponse + * Success */ -export const zSimpleResultResponse = z.object({ - result: z.string(), -}) - -/** - * DataSourceIntegrateIconResponse - */ -export const zDataSourceIntegrateIconResponse = z.object({ - emoji: z.string().nullish(), - type: z.string().nullish(), - url: z.string().nullish(), -}) - -/** - * DataSourceIntegratePageResponse - */ -export const zDataSourceIntegratePageResponse = z.object({ - page_icon: zDataSourceIntegrateIconResponse.nullable(), - page_id: z.string(), - page_name: z.string(), - parent_id: z.string(), - type: z.string(), -}) - -/** - * DataSourceIntegrateWorkspaceResponse - */ -export const zDataSourceIntegrateWorkspaceResponse = z.object({ - pages: z.array(zDataSourceIntegratePageResponse), - total: z.int(), - workspace_icon: z.string().nullable(), - workspace_id: z.string().nullable(), - workspace_name: z.string().nullable(), -}) - -/** - * DataSourceIntegrateResponse - */ -export const zDataSourceIntegrateResponse = z.object({ - created_at: z.int().nullable(), - disabled: z.boolean().nullable(), - id: z.string().nullable(), - is_bound: z.boolean(), - link: z.string(), - provider: z.string(), - source_info: zDataSourceIntegrateWorkspaceResponse.nullable(), -}) - -/** - * DataSourceIntegrateListResponse - */ -export const zDataSourceIntegrateListResponse = z.object({ - data: z.array(zDataSourceIntegrateResponse), -}) +export const zGetDataSourceIntegratesResponse = z.record(z.string(), z.unknown()) /** * Success */ -export const zGetDataSourceIntegratesResponse = zDataSourceIntegrateListResponse - -/** - * Success - */ -export const zPatchDataSourceIntegratesResponse = zSimpleResultResponse +export const zPatchDataSourceIntegratesResponse = z.record(z.string(), z.unknown()) export const zGetDataSourceIntegratesByBindingIdByActionPath = z.object({ action: z.string(), @@ -78,7 +20,7 @@ export const zGetDataSourceIntegratesByBindingIdByActionPath = z.object({ /** * Success */ -export const zGetDataSourceIntegratesByBindingIdByActionResponse = zDataSourceIntegrateListResponse +export const zGetDataSourceIntegratesByBindingIdByActionResponse = z.record(z.string(), z.unknown()) export const zPatchDataSourceIntegratesByBindingIdByActionPath = z.object({ action: z.string(), @@ -88,4 +30,7 @@ export const zPatchDataSourceIntegratesByBindingIdByActionPath = z.object({ /** * Success */ -export const zPatchDataSourceIntegratesByBindingIdByActionResponse = zSimpleResultResponse +export const zPatchDataSourceIntegratesByBindingIdByActionResponse = z.record( + z.string(), + z.unknown(), +) diff --git a/packages/contracts/generated/api/console/rag/orpc.gen.ts b/packages/contracts/generated/api/console/rag/orpc.gen.ts index 848cfd1d15b..3609fc31511 100644 --- a/packages/contracts/generated/api/console/rag/orpc.gen.ts +++ b/packages/contracts/generated/api/console/rag/orpc.gen.ts @@ -73,10 +73,8 @@ import { zPatchRagPipelineCustomizedTemplatesByTemplateIdBody, zPatchRagPipelineCustomizedTemplatesByTemplateIdPath, zPatchRagPipelineCustomizedTemplatesByTemplateIdResponse, - zPatchRagPipelinesByPipelineIdWorkflowsByWorkflowIdBody, zPatchRagPipelinesByPipelineIdWorkflowsByWorkflowIdPath, zPatchRagPipelinesByPipelineIdWorkflowsByWorkflowIdResponse, - zPatchRagPipelinesByPipelineIdWorkflowsDraftVariablesByVariableIdBody, zPatchRagPipelinesByPipelineIdWorkflowsDraftVariablesByVariableIdPath, zPatchRagPipelinesByPipelineIdWorkflowsDraftVariablesByVariableIdResponse, zPostRagPipelineCustomizedTemplatesByTemplateIdPath, @@ -91,7 +89,6 @@ import { zPostRagPipelinesByPipelineIdWorkflowRunsTasksByTaskIdStopResponse, zPostRagPipelinesByPipelineIdWorkflowsByWorkflowIdRestorePath, zPostRagPipelinesByPipelineIdWorkflowsByWorkflowIdRestoreResponse, - zPostRagPipelinesByPipelineIdWorkflowsDraftBody, zPostRagPipelinesByPipelineIdWorkflowsDraftDatasourceNodesByNodeIdRunBody, zPostRagPipelinesByPipelineIdWorkflowsDraftDatasourceNodesByNodeIdRunPath, zPostRagPipelinesByPipelineIdWorkflowsDraftDatasourceNodesByNodeIdRunResponse, @@ -927,10 +924,7 @@ export const patch2 = oc tags: ['console'], }) .input( - z.object({ - body: zPatchRagPipelinesByPipelineIdWorkflowsDraftVariablesByVariableIdBody, - params: zPatchRagPipelinesByPipelineIdWorkflowsDraftVariablesByVariableIdPath, - }), + z.object({ params: zPatchRagPipelinesByPipelineIdWorkflowsDraftVariablesByVariableIdPath }), ) .output(zPatchRagPipelinesByPipelineIdWorkflowsDraftVariablesByVariableIdResponse) @@ -1006,12 +1000,7 @@ export const post15 = oc summary: 'Sync draft workflow', tags: ['console'], }) - .input( - z.object({ - body: zPostRagPipelinesByPipelineIdWorkflowsDraftBody, - params: zPostRagPipelinesByPipelineIdWorkflowsDraftPath, - }), - ) + .input(z.object({ params: zPostRagPipelinesByPipelineIdWorkflowsDraftPath })) .output(zPostRagPipelinesByPipelineIdWorkflowsDraftResponse) export const draft = { @@ -1255,12 +1244,7 @@ export const patch3 = oc summary: 'Update workflow attributes', tags: ['console'], }) - .input( - z.object({ - body: zPatchRagPipelinesByPipelineIdWorkflowsByWorkflowIdBody, - params: zPatchRagPipelinesByPipelineIdWorkflowsByWorkflowIdPath, - }), - ) + .input(z.object({ params: zPatchRagPipelinesByPipelineIdWorkflowsByWorkflowIdPath })) .output(zPatchRagPipelinesByPipelineIdWorkflowsByWorkflowIdResponse) export const byWorkflowId = { diff --git a/packages/contracts/generated/api/console/rag/types.gen.ts b/packages/contracts/generated/api/console/rag/types.gen.ts index dedbd4ec55e..c5ea18e56b1 100644 --- a/packages/contracts/generated/api/console/rag/types.gen.ts +++ b/packages/contracts/generated/api/console/rag/types.gen.ts @@ -178,31 +178,6 @@ export type WorkflowResponse = { version_number?: number | null } -export type DraftWorkflowSyncPayload = { - conversation_variables?: Array<{ - [key: string]: unknown - }> | null - environment_variables?: Array<{ - [key: string]: unknown - }> | null - features?: { - [key: string]: unknown - } | null - graph: { - [key: string]: unknown - } - hash?: string | null - rag_pipeline_variables?: Array<{ - [key: string]: unknown - }> | null -} - -export type RagPipelineWorkflowSyncResponse = { - hash: string - result: string - updated_at: number -} - export type DatasourceNodeRunPayload = { credential_id?: string | null datasource_type: string @@ -311,11 +286,6 @@ export type WorkflowDraftVariable = { visible?: boolean } -export type WorkflowDraftVariablePatchPayload = { - name?: string | null - value?: unknown | null -} - export type RagPipelineWorkflowPublishResponse = { created_at: number result: string @@ -343,9 +313,10 @@ export type PublishedWorkflowRunPayload = { start_node_id: string } -export type WorkflowUpdatePayload = { - marked_comment?: string | null - marked_name?: string | null +export type RagPipelineWorkflowSyncResponse = { + hash: string + result: string + updated_at: number } export type ImportStatus = 'completed' | 'completed-with-warnings' | 'failed' | 'pending' @@ -963,7 +934,7 @@ export type GetRagPipelinesByPipelineIdWorkflowsDraftResponse = GetRagPipelinesByPipelineIdWorkflowsDraftResponses[keyof GetRagPipelinesByPipelineIdWorkflowsDraftResponses] export type PostRagPipelinesByPipelineIdWorkflowsDraftData = { - body: DraftWorkflowSyncPayload + body?: never path: { pipeline_id: string } @@ -972,7 +943,9 @@ export type PostRagPipelinesByPipelineIdWorkflowsDraftData = { } export type PostRagPipelinesByPipelineIdWorkflowsDraftResponses = { - 200: RagPipelineWorkflowSyncResponse + 200: { + [key: string]: unknown + } } export type PostRagPipelinesByPipelineIdWorkflowsDraftResponse = @@ -1267,7 +1240,7 @@ export type GetRagPipelinesByPipelineIdWorkflowsDraftVariablesByVariableIdRespon GetRagPipelinesByPipelineIdWorkflowsDraftVariablesByVariableIdResponses[keyof GetRagPipelinesByPipelineIdWorkflowsDraftVariablesByVariableIdResponses] export type PatchRagPipelinesByPipelineIdWorkflowsDraftVariablesByVariableIdData = { - body: WorkflowDraftVariablePatchPayload + body?: never path: { pipeline_id: string variable_id: string @@ -1440,7 +1413,7 @@ export type DeleteRagPipelinesByPipelineIdWorkflowsByWorkflowIdResponse = DeleteRagPipelinesByPipelineIdWorkflowsByWorkflowIdResponses[keyof DeleteRagPipelinesByPipelineIdWorkflowsByWorkflowIdResponses] export type PatchRagPipelinesByPipelineIdWorkflowsByWorkflowIdData = { - body: WorkflowUpdatePayload + body?: never path: { pipeline_id: string workflow_id: string diff --git a/packages/contracts/generated/api/console/rag/zod.gen.ts b/packages/contracts/generated/api/console/rag/zod.gen.ts index 7ff07aabc10..36e185707f8 100644 --- a/packages/contracts/generated/api/console/rag/zod.gen.ts +++ b/packages/contracts/generated/api/console/rag/zod.gen.ts @@ -76,27 +76,6 @@ export const zDefaultBlockConfigsResponse = z.array(z.record(z.string(), z.unkno */ export const zDefaultBlockConfigResponse = z.record(z.string(), z.unknown()) -/** - * DraftWorkflowSyncPayload - */ -export const zDraftWorkflowSyncPayload = z.object({ - conversation_variables: z.array(z.record(z.string(), z.unknown())).nullish(), - environment_variables: z.array(z.record(z.string(), z.unknown())).nullish(), - features: z.record(z.string(), z.unknown()).nullish(), - graph: z.record(z.string(), z.unknown()), - hash: z.string().nullish(), - rag_pipeline_variables: z.array(z.record(z.string(), z.unknown())).nullish(), -}) - -/** - * RagPipelineWorkflowSyncResponse - */ -export const zRagPipelineWorkflowSyncResponse = z.object({ - hash: z.string(), - result: z.string(), - updated_at: z.int(), -}) - /** * DatasourceNodeRunPayload */ @@ -174,14 +153,6 @@ export const zWorkflowDraftVariableList = z.object({ items: z.array(zWorkflowDraftVariable).optional(), }) -/** - * WorkflowDraftVariablePatchPayload - */ -export const zWorkflowDraftVariablePatchPayload = z.object({ - name: z.string().nullish(), - value: z.unknown().nullish(), -}) - /** * RagPipelineWorkflowPublishResponse */ @@ -213,11 +184,12 @@ export const zPublishedWorkflowRunPayload = z.object({ }) /** - * WorkflowUpdatePayload + * RagPipelineWorkflowSyncResponse */ -export const zWorkflowUpdatePayload = z.object({ - marked_comment: z.string().max(100).nullish(), - marked_name: z.string().max(20).nullish(), +export const zRagPipelineWorkflowSyncResponse = z.object({ + hash: z.string(), + result: z.string(), + updated_at: z.int(), }) /** @@ -906,8 +878,6 @@ export const zGetRagPipelinesByPipelineIdWorkflowsDraftPath = z.object({ */ export const zGetRagPipelinesByPipelineIdWorkflowsDraftResponse = zWorkflowResponse -export const zPostRagPipelinesByPipelineIdWorkflowsDraftBody = zDraftWorkflowSyncPayload - export const zPostRagPipelinesByPipelineIdWorkflowsDraftPath = z.object({ pipeline_id: z.uuid(), }) @@ -915,7 +885,7 @@ export const zPostRagPipelinesByPipelineIdWorkflowsDraftPath = z.object({ /** * Success */ -export const zPostRagPipelinesByPipelineIdWorkflowsDraftResponse = zRagPipelineWorkflowSyncResponse +export const zPostRagPipelinesByPipelineIdWorkflowsDraftResponse = z.record(z.string(), z.unknown()) export const zPostRagPipelinesByPipelineIdWorkflowsDraftDatasourceNodesByNodeIdRunBody = zDatasourceNodeRunPayload @@ -1121,9 +1091,6 @@ export const zGetRagPipelinesByPipelineIdWorkflowsDraftVariablesByVariableIdPath export const zGetRagPipelinesByPipelineIdWorkflowsDraftVariablesByVariableIdResponse = zWorkflowDraftVariable -export const zPatchRagPipelinesByPipelineIdWorkflowsDraftVariablesByVariableIdBody = - zWorkflowDraftVariablePatchPayload - export const zPatchRagPipelinesByPipelineIdWorkflowsDraftVariablesByVariableIdPath = z.object({ pipeline_id: z.uuid(), variable_id: z.uuid(), @@ -1242,8 +1209,6 @@ export const zDeleteRagPipelinesByPipelineIdWorkflowsByWorkflowIdPath = z.object */ export const zDeleteRagPipelinesByPipelineIdWorkflowsByWorkflowIdResponse = z.void() -export const zPatchRagPipelinesByPipelineIdWorkflowsByWorkflowIdBody = zWorkflowUpdatePayload - export const zPatchRagPipelinesByPipelineIdWorkflowsByWorkflowIdPath = z.object({ pipeline_id: z.uuid(), workflow_id: z.string(), diff --git a/packages/contracts/generated/api/console/workspaces/orpc.gen.ts b/packages/contracts/generated/api/console/workspaces/orpc.gen.ts index 69a7e3a9d64..0ff9cd3c520 100644 --- a/packages/contracts/generated/api/console/workspaces/orpc.gen.ts +++ b/packages/contracts/generated/api/console/workspaces/orpc.gen.ts @@ -20,6 +20,9 @@ import { zDeleteWorkspacesCurrentModelProvidersByProviderModelsResponse, zDeleteWorkspacesCurrentRbacAccessPoliciesByPolicyIdPath, zDeleteWorkspacesCurrentRbacAccessPoliciesByPolicyIdResponse, + zDeleteWorkspacesCurrentRbacAgentsByAgentIdAccessPoliciesByPolicyIdMemberBindingsBody, + zDeleteWorkspacesCurrentRbacAgentsByAgentIdAccessPoliciesByPolicyIdMemberBindingsPath, + zDeleteWorkspacesCurrentRbacAgentsByAgentIdAccessPoliciesByPolicyIdMemberBindingsResponse, zDeleteWorkspacesCurrentRbacAppsByAppIdAccessPoliciesByPolicyIdMemberBindingsBody, zDeleteWorkspacesCurrentRbacAppsByAppIdAccessPoliciesByPolicyIdMemberBindingsPath, zDeleteWorkspacesCurrentRbacAppsByAppIdAccessPoliciesByPolicyIdMemberBindingsResponse, @@ -113,6 +116,20 @@ import { zGetWorkspacesCurrentRbacAccessPoliciesByPolicyIdPath, zGetWorkspacesCurrentRbacAccessPoliciesByPolicyIdResponse, zGetWorkspacesCurrentRbacAccessPoliciesResponse, + zGetWorkspacesCurrentRbacAgentsByAgentIdAccessPoliciesByPolicyIdMemberBindingsPath, + zGetWorkspacesCurrentRbacAgentsByAgentIdAccessPoliciesByPolicyIdMemberBindingsResponse, + zGetWorkspacesCurrentRbacAgentsByAgentIdAccessPoliciesByPolicyIdRoleBindingsPath, + zGetWorkspacesCurrentRbacAgentsByAgentIdAccessPoliciesByPolicyIdRoleBindingsResponse, + zGetWorkspacesCurrentRbacAgentsByAgentIdAccessPolicyPath, + zGetWorkspacesCurrentRbacAgentsByAgentIdAccessPolicyQuery, + zGetWorkspacesCurrentRbacAgentsByAgentIdAccessPolicyResponse, + zGetWorkspacesCurrentRbacAgentsByAgentIdUserAccessPoliciesPath, + zGetWorkspacesCurrentRbacAgentsByAgentIdUserAccessPoliciesQuery, + zGetWorkspacesCurrentRbacAgentsByAgentIdUserAccessPoliciesResponse, + zGetWorkspacesCurrentRbacAgentsByAgentIdWhitelistConfigPath, + zGetWorkspacesCurrentRbacAgentsByAgentIdWhitelistConfigResponse, + zGetWorkspacesCurrentRbacAgentsByAgentIdWhitelistPath, + zGetWorkspacesCurrentRbacAgentsByAgentIdWhitelistResponse, zGetWorkspacesCurrentRbacAppsByAppIdAccessPoliciesByPolicyIdMemberBindingsPath, zGetWorkspacesCurrentRbacAppsByAppIdAccessPoliciesByPolicyIdMemberBindingsResponse, zGetWorkspacesCurrentRbacAppsByAppIdAccessPoliciesByPolicyIdRoleBindingsPath, @@ -144,6 +161,7 @@ import { zGetWorkspacesCurrentRbacMembersByMemberIdRbacRolesPath, zGetWorkspacesCurrentRbacMembersByMemberIdRbacRolesResponse, zGetWorkspacesCurrentRbacMyPermissionsResponse, + zGetWorkspacesCurrentRbacRolePermissionsCatalogAgentResponse, zGetWorkspacesCurrentRbacRolePermissionsCatalogAppResponse, zGetWorkspacesCurrentRbacRolePermissionsCatalogDatasetResponse, zGetWorkspacesCurrentRbacRolePermissionsCatalogResponse, @@ -152,6 +170,11 @@ import { zGetWorkspacesCurrentRbacRolesByRoleIdPath, zGetWorkspacesCurrentRbacRolesByRoleIdResponse, zGetWorkspacesCurrentRbacRolesResponse, + zGetWorkspacesCurrentRbacWorkspaceAgentsAccessPoliciesByPolicyIdMemberBindingsPath, + zGetWorkspacesCurrentRbacWorkspaceAgentsAccessPoliciesByPolicyIdMemberBindingsResponse, + zGetWorkspacesCurrentRbacWorkspaceAgentsAccessPoliciesByPolicyIdRoleBindingsPath, + zGetWorkspacesCurrentRbacWorkspaceAgentsAccessPoliciesByPolicyIdRoleBindingsResponse, + zGetWorkspacesCurrentRbacWorkspaceAgentsAccessPolicyResponse, zGetWorkspacesCurrentRbacWorkspaceAppsAccessPoliciesByPolicyIdMemberBindingsPath, zGetWorkspacesCurrentRbacWorkspaceAppsAccessPoliciesByPolicyIdMemberBindingsResponse, zGetWorkspacesCurrentRbacWorkspaceAppsAccessPoliciesByPolicyIdRoleBindingsPath, @@ -463,6 +486,12 @@ import { zPutWorkspacesCurrentRbacAccessPolicyBindingsByBindingIdLockResponse, zPutWorkspacesCurrentRbacAccessPolicyBindingsByBindingIdUnlockPath, zPutWorkspacesCurrentRbacAccessPolicyBindingsByBindingIdUnlockResponse, + zPutWorkspacesCurrentRbacAgentsByAgentIdUsersByTargetAccountIdAccessPoliciesBody, + zPutWorkspacesCurrentRbacAgentsByAgentIdUsersByTargetAccountIdAccessPoliciesPath, + zPutWorkspacesCurrentRbacAgentsByAgentIdUsersByTargetAccountIdAccessPoliciesResponse, + zPutWorkspacesCurrentRbacAgentsByAgentIdWhitelistBody, + zPutWorkspacesCurrentRbacAgentsByAgentIdWhitelistPath, + zPutWorkspacesCurrentRbacAgentsByAgentIdWhitelistResponse, zPutWorkspacesCurrentRbacAppsByAppIdUsersByTargetAccountIdAccessPoliciesBody, zPutWorkspacesCurrentRbacAppsByAppIdUsersByTargetAccountIdAccessPoliciesPath, zPutWorkspacesCurrentRbacAppsByAppIdUsersByTargetAccountIdAccessPoliciesResponse, @@ -480,6 +509,9 @@ import { zPutWorkspacesCurrentRbacMembersByMemberIdRbacRolesResponse, zPutWorkspacesCurrentRbacRolesByRoleIdPath, zPutWorkspacesCurrentRbacRolesByRoleIdResponse, + zPutWorkspacesCurrentRbacWorkspaceAgentsAccessPoliciesByPolicyIdBindingsBody, + zPutWorkspacesCurrentRbacWorkspaceAgentsAccessPoliciesByPolicyIdBindingsPath, + zPutWorkspacesCurrentRbacWorkspaceAgentsAccessPoliciesByPolicyIdBindingsResponse, zPutWorkspacesCurrentRbacWorkspaceAppsAccessPoliciesByPolicyIdBindingsBody, zPutWorkspacesCurrentRbacWorkspaceAppsAccessPoliciesByPolicyIdBindingsPath, zPutWorkspacesCurrentRbacWorkspaceAppsAccessPoliciesByPolicyIdBindingsResponse, @@ -2277,7 +2309,6 @@ export const post44 = oc method: 'POST', operationId: 'postWorkspacesCurrentRbacAccessPoliciesByPolicyIdCopy', path: '/workspaces/current/rbac/access-policies/{policy_id}/copy', - successStatus: 201, tags: ['console'], }) .input(z.object({ params: zPostWorkspacesCurrentRbacAccessPoliciesByPolicyIdCopyPath })) @@ -2343,7 +2374,6 @@ export const post45 = oc method: 'POST', operationId: 'postWorkspacesCurrentRbacAccessPolicies', path: '/workspaces/current/rbac/access-policies', - successStatus: 201, tags: ['console'], }) .output(zPostWorkspacesCurrentRbacAccessPoliciesResponse) @@ -2397,32 +2427,32 @@ export const delete10 = oc .route({ inputStructure: 'detailed', method: 'DELETE', - operationId: 'deleteWorkspacesCurrentRbacAppsByAppIdAccessPoliciesByPolicyIdMemberBindings', - path: '/workspaces/current/rbac/apps/{app_id}/access-policies/{policy_id}/member-bindings', + operationId: 'deleteWorkspacesCurrentRbacAgentsByAgentIdAccessPoliciesByPolicyIdMemberBindings', + path: '/workspaces/current/rbac/agents/{agent_id}/access-policies/{policy_id}/member-bindings', tags: ['console'], }) .input( z.object({ - body: zDeleteWorkspacesCurrentRbacAppsByAppIdAccessPoliciesByPolicyIdMemberBindingsBody, - params: zDeleteWorkspacesCurrentRbacAppsByAppIdAccessPoliciesByPolicyIdMemberBindingsPath, + body: zDeleteWorkspacesCurrentRbacAgentsByAgentIdAccessPoliciesByPolicyIdMemberBindingsBody, + params: zDeleteWorkspacesCurrentRbacAgentsByAgentIdAccessPoliciesByPolicyIdMemberBindingsPath, }), ) - .output(zDeleteWorkspacesCurrentRbacAppsByAppIdAccessPoliciesByPolicyIdMemberBindingsResponse) + .output(zDeleteWorkspacesCurrentRbacAgentsByAgentIdAccessPoliciesByPolicyIdMemberBindingsResponse) export const get39 = oc .route({ inputStructure: 'detailed', method: 'GET', - operationId: 'getWorkspacesCurrentRbacAppsByAppIdAccessPoliciesByPolicyIdMemberBindings', - path: '/workspaces/current/rbac/apps/{app_id}/access-policies/{policy_id}/member-bindings', + operationId: 'getWorkspacesCurrentRbacAgentsByAgentIdAccessPoliciesByPolicyIdMemberBindings', + path: '/workspaces/current/rbac/agents/{agent_id}/access-policies/{policy_id}/member-bindings', tags: ['console'], }) .input( z.object({ - params: zGetWorkspacesCurrentRbacAppsByAppIdAccessPoliciesByPolicyIdMemberBindingsPath, + params: zGetWorkspacesCurrentRbacAgentsByAgentIdAccessPoliciesByPolicyIdMemberBindingsPath, }), ) - .output(zGetWorkspacesCurrentRbacAppsByAppIdAccessPoliciesByPolicyIdMemberBindingsResponse) + .output(zGetWorkspacesCurrentRbacAgentsByAgentIdAccessPoliciesByPolicyIdMemberBindingsResponse) export const memberBindings = { delete: delete10, @@ -2433,16 +2463,16 @@ export const get40 = oc .route({ inputStructure: 'detailed', method: 'GET', - operationId: 'getWorkspacesCurrentRbacAppsByAppIdAccessPoliciesByPolicyIdRoleBindings', - path: '/workspaces/current/rbac/apps/{app_id}/access-policies/{policy_id}/role-bindings', + operationId: 'getWorkspacesCurrentRbacAgentsByAgentIdAccessPoliciesByPolicyIdRoleBindings', + path: '/workspaces/current/rbac/agents/{agent_id}/access-policies/{policy_id}/role-bindings', tags: ['console'], }) .input( z.object({ - params: zGetWorkspacesCurrentRbacAppsByAppIdAccessPoliciesByPolicyIdRoleBindingsPath, + params: zGetWorkspacesCurrentRbacAgentsByAgentIdAccessPoliciesByPolicyIdRoleBindingsPath, }), ) - .output(zGetWorkspacesCurrentRbacAppsByAppIdAccessPoliciesByPolicyIdRoleBindingsResponse) + .output(zGetWorkspacesCurrentRbacAgentsByAgentIdAccessPoliciesByPolicyIdRoleBindingsResponse) export const roleBindings = { get: get40, @@ -2461,17 +2491,17 @@ export const get41 = oc .route({ inputStructure: 'detailed', method: 'GET', - operationId: 'getWorkspacesCurrentRbacAppsByAppIdAccessPolicy', - path: '/workspaces/current/rbac/apps/{app_id}/access-policy', + operationId: 'getWorkspacesCurrentRbacAgentsByAgentIdAccessPolicy', + path: '/workspaces/current/rbac/agents/{agent_id}/access-policy', tags: ['console'], }) .input( z.object({ - params: zGetWorkspacesCurrentRbacAppsByAppIdAccessPolicyPath, - query: zGetWorkspacesCurrentRbacAppsByAppIdAccessPolicyQuery.optional(), + params: zGetWorkspacesCurrentRbacAgentsByAgentIdAccessPolicyPath, + query: zGetWorkspacesCurrentRbacAgentsByAgentIdAccessPolicyQuery.optional(), }), ) - .output(zGetWorkspacesCurrentRbacAppsByAppIdAccessPolicyResponse) + .output(zGetWorkspacesCurrentRbacAgentsByAgentIdAccessPolicyResponse) export const accessPolicy = { get: get41, @@ -2481,17 +2511,17 @@ export const get42 = oc .route({ inputStructure: 'detailed', method: 'GET', - operationId: 'getWorkspacesCurrentRbacAppsByAppIdUserAccessPolicies', - path: '/workspaces/current/rbac/apps/{app_id}/user-access-policies', + operationId: 'getWorkspacesCurrentRbacAgentsByAgentIdUserAccessPolicies', + path: '/workspaces/current/rbac/agents/{agent_id}/user-access-policies', tags: ['console'], }) .input( z.object({ - params: zGetWorkspacesCurrentRbacAppsByAppIdUserAccessPoliciesPath, - query: zGetWorkspacesCurrentRbacAppsByAppIdUserAccessPoliciesQuery.optional(), + params: zGetWorkspacesCurrentRbacAgentsByAgentIdUserAccessPoliciesPath, + query: zGetWorkspacesCurrentRbacAgentsByAgentIdUserAccessPoliciesQuery.optional(), }), ) - .output(zGetWorkspacesCurrentRbacAppsByAppIdUserAccessPoliciesResponse) + .output(zGetWorkspacesCurrentRbacAgentsByAgentIdUserAccessPoliciesResponse) export const userAccessPolicies = { get: get42, @@ -2501,17 +2531,17 @@ export const put8 = oc .route({ inputStructure: 'detailed', method: 'PUT', - operationId: 'putWorkspacesCurrentRbacAppsByAppIdUsersByTargetAccountIdAccessPolicies', - path: '/workspaces/current/rbac/apps/{app_id}/users/{target_account_id}/access-policies', + operationId: 'putWorkspacesCurrentRbacAgentsByAgentIdUsersByTargetAccountIdAccessPolicies', + path: '/workspaces/current/rbac/agents/{agent_id}/users/{target_account_id}/access-policies', tags: ['console'], }) .input( z.object({ - body: zPutWorkspacesCurrentRbacAppsByAppIdUsersByTargetAccountIdAccessPoliciesBody, - params: zPutWorkspacesCurrentRbacAppsByAppIdUsersByTargetAccountIdAccessPoliciesPath, + body: zPutWorkspacesCurrentRbacAgentsByAgentIdUsersByTargetAccountIdAccessPoliciesBody, + params: zPutWorkspacesCurrentRbacAgentsByAgentIdUsersByTargetAccountIdAccessPoliciesPath, }), ) - .output(zPutWorkspacesCurrentRbacAppsByAppIdUsersByTargetAccountIdAccessPoliciesResponse) + .output(zPutWorkspacesCurrentRbacAgentsByAgentIdUsersByTargetAccountIdAccessPoliciesResponse) export const accessPolicies3 = { put: put8, @@ -2526,6 +2556,198 @@ export const users = { } export const get43 = oc + .route({ + inputStructure: 'detailed', + method: 'GET', + operationId: 'getWorkspacesCurrentRbacAgentsByAgentIdWhitelist', + path: '/workspaces/current/rbac/agents/{agent_id}/whitelist', + tags: ['console'], + }) + .input(z.object({ params: zGetWorkspacesCurrentRbacAgentsByAgentIdWhitelistPath })) + .output(zGetWorkspacesCurrentRbacAgentsByAgentIdWhitelistResponse) + +export const put9 = oc + .route({ + inputStructure: 'detailed', + method: 'PUT', + operationId: 'putWorkspacesCurrentRbacAgentsByAgentIdWhitelist', + path: '/workspaces/current/rbac/agents/{agent_id}/whitelist', + tags: ['console'], + }) + .input( + z.object({ + body: zPutWorkspacesCurrentRbacAgentsByAgentIdWhitelistBody, + params: zPutWorkspacesCurrentRbacAgentsByAgentIdWhitelistPath, + }), + ) + .output(zPutWorkspacesCurrentRbacAgentsByAgentIdWhitelistResponse) + +export const whitelist = { + get: get43, + put: put9, +} + +export const get44 = oc + .route({ + inputStructure: 'detailed', + method: 'GET', + operationId: 'getWorkspacesCurrentRbacAgentsByAgentIdWhitelistConfig', + path: '/workspaces/current/rbac/agents/{agent_id}/whitelist_config', + tags: ['console'], + }) + .input(z.object({ params: zGetWorkspacesCurrentRbacAgentsByAgentIdWhitelistConfigPath })) + .output(zGetWorkspacesCurrentRbacAgentsByAgentIdWhitelistConfigResponse) + +export const whitelistConfig = { + get: get44, +} + +export const byAgentId2 = { + accessPolicies: accessPolicies2, + accessPolicy, + userAccessPolicies, + users, + whitelist, + whitelistConfig, +} + +export const agents2 = { + byAgentId: byAgentId2, +} + +export const delete11 = oc + .route({ + inputStructure: 'detailed', + method: 'DELETE', + operationId: 'deleteWorkspacesCurrentRbacAppsByAppIdAccessPoliciesByPolicyIdMemberBindings', + path: '/workspaces/current/rbac/apps/{app_id}/access-policies/{policy_id}/member-bindings', + tags: ['console'], + }) + .input( + z.object({ + body: zDeleteWorkspacesCurrentRbacAppsByAppIdAccessPoliciesByPolicyIdMemberBindingsBody, + params: zDeleteWorkspacesCurrentRbacAppsByAppIdAccessPoliciesByPolicyIdMemberBindingsPath, + }), + ) + .output(zDeleteWorkspacesCurrentRbacAppsByAppIdAccessPoliciesByPolicyIdMemberBindingsResponse) + +export const get45 = oc + .route({ + inputStructure: 'detailed', + method: 'GET', + operationId: 'getWorkspacesCurrentRbacAppsByAppIdAccessPoliciesByPolicyIdMemberBindings', + path: '/workspaces/current/rbac/apps/{app_id}/access-policies/{policy_id}/member-bindings', + tags: ['console'], + }) + .input( + z.object({ + params: zGetWorkspacesCurrentRbacAppsByAppIdAccessPoliciesByPolicyIdMemberBindingsPath, + }), + ) + .output(zGetWorkspacesCurrentRbacAppsByAppIdAccessPoliciesByPolicyIdMemberBindingsResponse) + +export const memberBindings2 = { + delete: delete11, + get: get45, +} + +export const get46 = oc + .route({ + inputStructure: 'detailed', + method: 'GET', + operationId: 'getWorkspacesCurrentRbacAppsByAppIdAccessPoliciesByPolicyIdRoleBindings', + path: '/workspaces/current/rbac/apps/{app_id}/access-policies/{policy_id}/role-bindings', + tags: ['console'], + }) + .input( + z.object({ + params: zGetWorkspacesCurrentRbacAppsByAppIdAccessPoliciesByPolicyIdRoleBindingsPath, + }), + ) + .output(zGetWorkspacesCurrentRbacAppsByAppIdAccessPoliciesByPolicyIdRoleBindingsResponse) + +export const roleBindings2 = { + get: get46, +} + +export const byPolicyId3 = { + memberBindings: memberBindings2, + roleBindings: roleBindings2, +} + +export const accessPolicies4 = { + byPolicyId: byPolicyId3, +} + +export const get47 = oc + .route({ + inputStructure: 'detailed', + method: 'GET', + operationId: 'getWorkspacesCurrentRbacAppsByAppIdAccessPolicy', + path: '/workspaces/current/rbac/apps/{app_id}/access-policy', + tags: ['console'], + }) + .input( + z.object({ + params: zGetWorkspacesCurrentRbacAppsByAppIdAccessPolicyPath, + query: zGetWorkspacesCurrentRbacAppsByAppIdAccessPolicyQuery.optional(), + }), + ) + .output(zGetWorkspacesCurrentRbacAppsByAppIdAccessPolicyResponse) + +export const accessPolicy2 = { + get: get47, +} + +export const get48 = oc + .route({ + inputStructure: 'detailed', + method: 'GET', + operationId: 'getWorkspacesCurrentRbacAppsByAppIdUserAccessPolicies', + path: '/workspaces/current/rbac/apps/{app_id}/user-access-policies', + tags: ['console'], + }) + .input( + z.object({ + params: zGetWorkspacesCurrentRbacAppsByAppIdUserAccessPoliciesPath, + query: zGetWorkspacesCurrentRbacAppsByAppIdUserAccessPoliciesQuery.optional(), + }), + ) + .output(zGetWorkspacesCurrentRbacAppsByAppIdUserAccessPoliciesResponse) + +export const userAccessPolicies2 = { + get: get48, +} + +export const put10 = oc + .route({ + inputStructure: 'detailed', + method: 'PUT', + operationId: 'putWorkspacesCurrentRbacAppsByAppIdUsersByTargetAccountIdAccessPolicies', + path: '/workspaces/current/rbac/apps/{app_id}/users/{target_account_id}/access-policies', + tags: ['console'], + }) + .input( + z.object({ + body: zPutWorkspacesCurrentRbacAppsByAppIdUsersByTargetAccountIdAccessPoliciesBody, + params: zPutWorkspacesCurrentRbacAppsByAppIdUsersByTargetAccountIdAccessPoliciesPath, + }), + ) + .output(zPutWorkspacesCurrentRbacAppsByAppIdUsersByTargetAccountIdAccessPoliciesResponse) + +export const accessPolicies5 = { + put: put10, +} + +export const byTargetAccountId2 = { + accessPolicies: accessPolicies5, +} + +export const users2 = { + byTargetAccountId: byTargetAccountId2, +} + +export const get49 = oc .route({ inputStructure: 'detailed', method: 'GET', @@ -2536,7 +2758,7 @@ export const get43 = oc .input(z.object({ params: zGetWorkspacesCurrentRbacAppsByAppIdWhitelistPath })) .output(zGetWorkspacesCurrentRbacAppsByAppIdWhitelistResponse) -export const put9 = oc +export const put11 = oc .route({ inputStructure: 'detailed', method: 'PUT', @@ -2552,12 +2774,12 @@ export const put9 = oc ) .output(zPutWorkspacesCurrentRbacAppsByAppIdWhitelistResponse) -export const whitelist = { - get: get43, - put: put9, +export const whitelist2 = { + get: get49, + put: put11, } -export const get44 = oc +export const get50 = oc .route({ inputStructure: 'detailed', method: 'GET', @@ -2568,24 +2790,24 @@ export const get44 = oc .input(z.object({ params: zGetWorkspacesCurrentRbacAppsByAppIdWhitelistConfigPath })) .output(zGetWorkspacesCurrentRbacAppsByAppIdWhitelistConfigResponse) -export const whitelistConfig = { - get: get44, +export const whitelistConfig2 = { + get: get50, } export const byAppId = { - accessPolicies: accessPolicies2, - accessPolicy, - userAccessPolicies, - users, - whitelist, - whitelistConfig, + accessPolicies: accessPolicies4, + accessPolicy: accessPolicy2, + userAccessPolicies: userAccessPolicies2, + users: users2, + whitelist: whitelist2, + whitelistConfig: whitelistConfig2, } export const apps = { byAppId, } -export const delete11 = oc +export const delete12 = oc .route({ inputStructure: 'detailed', method: 'DELETE', @@ -2605,7 +2827,7 @@ export const delete11 = oc zDeleteWorkspacesCurrentRbacDatasetsByDatasetIdAccessPoliciesByPolicyIdMemberBindingsResponse, ) -export const get45 = oc +export const get51 = oc .route({ inputStructure: 'detailed', method: 'GET', @@ -2624,12 +2846,12 @@ export const get45 = oc zGetWorkspacesCurrentRbacDatasetsByDatasetIdAccessPoliciesByPolicyIdMemberBindingsResponse, ) -export const memberBindings2 = { - delete: delete11, - get: get45, +export const memberBindings3 = { + delete: delete12, + get: get51, } -export const get46 = oc +export const get52 = oc .route({ inputStructure: 'detailed', method: 'GET', @@ -2644,20 +2866,20 @@ export const get46 = oc ) .output(zGetWorkspacesCurrentRbacDatasetsByDatasetIdAccessPoliciesByPolicyIdRoleBindingsResponse) -export const roleBindings2 = { - get: get46, +export const roleBindings3 = { + get: get52, } -export const byPolicyId3 = { - memberBindings: memberBindings2, - roleBindings: roleBindings2, +export const byPolicyId4 = { + memberBindings: memberBindings3, + roleBindings: roleBindings3, } -export const accessPolicies4 = { - byPolicyId: byPolicyId3, +export const accessPolicies6 = { + byPolicyId: byPolicyId4, } -export const get47 = oc +export const get53 = oc .route({ inputStructure: 'detailed', method: 'GET', @@ -2673,11 +2895,11 @@ export const get47 = oc ) .output(zGetWorkspacesCurrentRbacDatasetsByDatasetIdAccessPolicyResponse) -export const accessPolicy2 = { - get: get47, +export const accessPolicy3 = { + get: get53, } -export const get48 = oc +export const get54 = oc .route({ inputStructure: 'detailed', method: 'GET', @@ -2693,11 +2915,11 @@ export const get48 = oc ) .output(zGetWorkspacesCurrentRbacDatasetsByDatasetIdUserAccessPoliciesResponse) -export const userAccessPolicies2 = { - get: get48, +export const userAccessPolicies3 = { + get: get54, } -export const put10 = oc +export const put12 = oc .route({ inputStructure: 'detailed', method: 'PUT', @@ -2713,19 +2935,19 @@ export const put10 = oc ) .output(zPutWorkspacesCurrentRbacDatasetsByDatasetIdUsersByTargetAccountIdAccessPoliciesResponse) -export const accessPolicies5 = { - put: put10, +export const accessPolicies7 = { + put: put12, } -export const byTargetAccountId2 = { - accessPolicies: accessPolicies5, +export const byTargetAccountId3 = { + accessPolicies: accessPolicies7, } -export const users2 = { - byTargetAccountId: byTargetAccountId2, +export const users3 = { + byTargetAccountId: byTargetAccountId3, } -export const get49 = oc +export const get55 = oc .route({ inputStructure: 'detailed', method: 'GET', @@ -2736,7 +2958,7 @@ export const get49 = oc .input(z.object({ params: zGetWorkspacesCurrentRbacDatasetsByDatasetIdWhitelistPath })) .output(zGetWorkspacesCurrentRbacDatasetsByDatasetIdWhitelistResponse) -export const put11 = oc +export const put13 = oc .route({ inputStructure: 'detailed', method: 'PUT', @@ -2752,12 +2974,12 @@ export const put11 = oc ) .output(zPutWorkspacesCurrentRbacDatasetsByDatasetIdWhitelistResponse) -export const whitelist2 = { - get: get49, - put: put11, +export const whitelist3 = { + get: get55, + put: put13, } -export const get50 = oc +export const get56 = oc .route({ inputStructure: 'detailed', method: 'GET', @@ -2768,24 +2990,24 @@ export const get50 = oc .input(z.object({ params: zGetWorkspacesCurrentRbacDatasetsByDatasetIdWhitelistConfigPath })) .output(zGetWorkspacesCurrentRbacDatasetsByDatasetIdWhitelistConfigResponse) -export const whitelistConfig2 = { - get: get50, +export const whitelistConfig3 = { + get: get56, } export const byDatasetId = { - accessPolicies: accessPolicies4, - accessPolicy: accessPolicy2, - userAccessPolicies: userAccessPolicies2, - users: users2, - whitelist: whitelist2, - whitelistConfig: whitelistConfig2, + accessPolicies: accessPolicies6, + accessPolicy: accessPolicy3, + userAccessPolicies: userAccessPolicies3, + users: users3, + whitelist: whitelist3, + whitelistConfig: whitelistConfig3, } export const datasets = { byDatasetId, } -export const get51 = oc +export const get57 = oc .route({ inputStructure: 'detailed', method: 'GET', @@ -2796,7 +3018,7 @@ export const get51 = oc .input(z.object({ params: zGetWorkspacesCurrentRbacMembersByMemberIdRbacRolesPath })) .output(zGetWorkspacesCurrentRbacMembersByMemberIdRbacRolesResponse) -export const put12 = oc +export const put14 = oc .route({ inputStructure: 'detailed', method: 'PUT', @@ -2813,8 +3035,8 @@ export const put12 = oc .output(zPutWorkspacesCurrentRbacMembersByMemberIdRbacRolesResponse) export const rbacRoles = { - get: get51, - put: put12, + get: get57, + put: put14, } export const byMemberId2 = { @@ -2825,7 +3047,7 @@ export const members2 = { byMemberId: byMemberId2, } -export const get52 = oc +export const get58 = oc .route({ inputStructure: 'detailed', method: 'GET', @@ -2836,10 +3058,24 @@ export const get52 = oc .output(zGetWorkspacesCurrentRbacMyPermissionsResponse) export const myPermissions = { - get: get52, + get: get58, } -export const get53 = oc +export const get59 = oc + .route({ + inputStructure: 'detailed', + method: 'GET', + operationId: 'getWorkspacesCurrentRbacRolePermissionsCatalogAgent', + path: '/workspaces/current/rbac/role-permissions/catalog/agent', + tags: ['console'], + }) + .output(zGetWorkspacesCurrentRbacRolePermissionsCatalogAgentResponse) + +export const agent = { + get: get59, +} + +export const get60 = oc .route({ inputStructure: 'detailed', method: 'GET', @@ -2850,10 +3086,10 @@ export const get53 = oc .output(zGetWorkspacesCurrentRbacRolePermissionsCatalogAppResponse) export const app = { - get: get53, + get: get60, } -export const get54 = oc +export const get61 = oc .route({ inputStructure: 'detailed', method: 'GET', @@ -2864,10 +3100,10 @@ export const get54 = oc .output(zGetWorkspacesCurrentRbacRolePermissionsCatalogDatasetResponse) export const dataset = { - get: get54, + get: get61, } -export const get55 = oc +export const get62 = oc .route({ inputStructure: 'detailed', method: 'GET', @@ -2878,7 +3114,8 @@ export const get55 = oc .output(zGetWorkspacesCurrentRbacRolePermissionsCatalogResponse) export const catalog = { - get: get55, + get: get62, + agent, app, dataset, } @@ -2893,7 +3130,6 @@ export const post46 = oc method: 'POST', operationId: 'postWorkspacesCurrentRbacRolesByRoleIdCopy', path: '/workspaces/current/rbac/roles/{role_id}/copy', - successStatus: 201, tags: ['console'], }) .input(z.object({ params: zPostWorkspacesCurrentRbacRolesByRoleIdCopyPath })) @@ -2903,7 +3139,7 @@ export const copy2 = { post: post46, } -export const get56 = oc +export const get63 = oc .route({ inputStructure: 'detailed', method: 'GET', @@ -2915,10 +3151,10 @@ export const get56 = oc .output(zGetWorkspacesCurrentRbacRolesByRoleIdMembersResponse) export const members3 = { - get: get56, + get: get63, } -export const delete12 = oc +export const delete13 = oc .route({ inputStructure: 'detailed', method: 'DELETE', @@ -2929,7 +3165,7 @@ export const delete12 = oc .input(z.object({ params: zDeleteWorkspacesCurrentRbacRolesByRoleIdPath })) .output(zDeleteWorkspacesCurrentRbacRolesByRoleIdResponse) -export const get57 = oc +export const get64 = oc .route({ inputStructure: 'detailed', method: 'GET', @@ -2940,7 +3176,7 @@ export const get57 = oc .input(z.object({ params: zGetWorkspacesCurrentRbacRolesByRoleIdPath })) .output(zGetWorkspacesCurrentRbacRolesByRoleIdResponse) -export const put13 = oc +export const put15 = oc .route({ inputStructure: 'detailed', method: 'PUT', @@ -2952,14 +3188,14 @@ export const put13 = oc .output(zPutWorkspacesCurrentRbacRolesByRoleIdResponse) export const byRoleId = { - delete: delete12, - get: get57, - put: put13, + delete: delete13, + get: get64, + put: put15, copy: copy2, members: members3, } -export const get58 = oc +export const get65 = oc .route({ inputStructure: 'detailed', method: 'GET', @@ -2975,18 +3211,104 @@ export const post47 = oc method: 'POST', operationId: 'postWorkspacesCurrentRbacRoles', path: '/workspaces/current/rbac/roles', - successStatus: 201, tags: ['console'], }) .output(zPostWorkspacesCurrentRbacRolesResponse) export const roles = { - get: get58, + get: get65, post: post47, byRoleId, } -export const put14 = oc +export const put16 = oc + .route({ + inputStructure: 'detailed', + method: 'PUT', + operationId: 'putWorkspacesCurrentRbacWorkspaceAgentsAccessPoliciesByPolicyIdBindings', + path: '/workspaces/current/rbac/workspace/agents/access-policies/{policy_id}/bindings', + tags: ['console'], + }) + .input( + z.object({ + body: zPutWorkspacesCurrentRbacWorkspaceAgentsAccessPoliciesByPolicyIdBindingsBody, + params: zPutWorkspacesCurrentRbacWorkspaceAgentsAccessPoliciesByPolicyIdBindingsPath, + }), + ) + .output(zPutWorkspacesCurrentRbacWorkspaceAgentsAccessPoliciesByPolicyIdBindingsResponse) + +export const bindings = { + put: put16, +} + +export const get66 = oc + .route({ + inputStructure: 'detailed', + method: 'GET', + operationId: 'getWorkspacesCurrentRbacWorkspaceAgentsAccessPoliciesByPolicyIdMemberBindings', + path: '/workspaces/current/rbac/workspace/agents/access-policies/{policy_id}/member-bindings', + tags: ['console'], + }) + .input( + z.object({ + params: zGetWorkspacesCurrentRbacWorkspaceAgentsAccessPoliciesByPolicyIdMemberBindingsPath, + }), + ) + .output(zGetWorkspacesCurrentRbacWorkspaceAgentsAccessPoliciesByPolicyIdMemberBindingsResponse) + +export const memberBindings4 = { + get: get66, +} + +export const get67 = oc + .route({ + inputStructure: 'detailed', + method: 'GET', + operationId: 'getWorkspacesCurrentRbacWorkspaceAgentsAccessPoliciesByPolicyIdRoleBindings', + path: '/workspaces/current/rbac/workspace/agents/access-policies/{policy_id}/role-bindings', + tags: ['console'], + }) + .input( + z.object({ + params: zGetWorkspacesCurrentRbacWorkspaceAgentsAccessPoliciesByPolicyIdRoleBindingsPath, + }), + ) + .output(zGetWorkspacesCurrentRbacWorkspaceAgentsAccessPoliciesByPolicyIdRoleBindingsResponse) + +export const roleBindings4 = { + get: get67, +} + +export const byPolicyId5 = { + bindings, + memberBindings: memberBindings4, + roleBindings: roleBindings4, +} + +export const accessPolicies8 = { + byPolicyId: byPolicyId5, +} + +export const get68 = oc + .route({ + inputStructure: 'detailed', + method: 'GET', + operationId: 'getWorkspacesCurrentRbacWorkspaceAgentsAccessPolicy', + path: '/workspaces/current/rbac/workspace/agents/access-policy', + tags: ['console'], + }) + .output(zGetWorkspacesCurrentRbacWorkspaceAgentsAccessPolicyResponse) + +export const accessPolicy4 = { + get: get68, +} + +export const agents3 = { + accessPolicies: accessPolicies8, + accessPolicy: accessPolicy4, +} + +export const put17 = oc .route({ inputStructure: 'detailed', method: 'PUT', @@ -3002,11 +3324,11 @@ export const put14 = oc ) .output(zPutWorkspacesCurrentRbacWorkspaceAppsAccessPoliciesByPolicyIdBindingsResponse) -export const bindings = { - put: put14, +export const bindings2 = { + put: put17, } -export const get59 = oc +export const get69 = oc .route({ inputStructure: 'detailed', method: 'GET', @@ -3021,11 +3343,11 @@ export const get59 = oc ) .output(zGetWorkspacesCurrentRbacWorkspaceAppsAccessPoliciesByPolicyIdMemberBindingsResponse) -export const memberBindings3 = { - get: get59, +export const memberBindings5 = { + get: get69, } -export const get60 = oc +export const get70 = oc .route({ inputStructure: 'detailed', method: 'GET', @@ -3040,21 +3362,21 @@ export const get60 = oc ) .output(zGetWorkspacesCurrentRbacWorkspaceAppsAccessPoliciesByPolicyIdRoleBindingsResponse) -export const roleBindings3 = { - get: get60, +export const roleBindings5 = { + get: get70, } -export const byPolicyId4 = { - bindings, - memberBindings: memberBindings3, - roleBindings: roleBindings3, +export const byPolicyId6 = { + bindings: bindings2, + memberBindings: memberBindings5, + roleBindings: roleBindings5, } -export const accessPolicies6 = { - byPolicyId: byPolicyId4, +export const accessPolicies9 = { + byPolicyId: byPolicyId6, } -export const get61 = oc +export const get71 = oc .route({ inputStructure: 'detailed', method: 'GET', @@ -3064,16 +3386,16 @@ export const get61 = oc }) .output(zGetWorkspacesCurrentRbacWorkspaceAppsAccessPolicyResponse) -export const accessPolicy3 = { - get: get61, +export const accessPolicy5 = { + get: get71, } export const apps2 = { - accessPolicies: accessPolicies6, - accessPolicy: accessPolicy3, + accessPolicies: accessPolicies9, + accessPolicy: accessPolicy5, } -export const put15 = oc +export const put18 = oc .route({ inputStructure: 'detailed', method: 'PUT', @@ -3089,11 +3411,11 @@ export const put15 = oc ) .output(zPutWorkspacesCurrentRbacWorkspaceDatasetsAccessPoliciesByPolicyIdBindingsResponse) -export const bindings2 = { - put: put15, +export const bindings3 = { + put: put18, } -export const get62 = oc +export const get72 = oc .route({ inputStructure: 'detailed', method: 'GET', @@ -3108,11 +3430,11 @@ export const get62 = oc ) .output(zGetWorkspacesCurrentRbacWorkspaceDatasetsAccessPoliciesByPolicyIdMemberBindingsResponse) -export const memberBindings4 = { - get: get62, +export const memberBindings6 = { + get: get72, } -export const get63 = oc +export const get73 = oc .route({ inputStructure: 'detailed', method: 'GET', @@ -3127,21 +3449,21 @@ export const get63 = oc ) .output(zGetWorkspacesCurrentRbacWorkspaceDatasetsAccessPoliciesByPolicyIdRoleBindingsResponse) -export const roleBindings4 = { - get: get63, +export const roleBindings6 = { + get: get73, } -export const byPolicyId5 = { - bindings: bindings2, - memberBindings: memberBindings4, - roleBindings: roleBindings4, +export const byPolicyId7 = { + bindings: bindings3, + memberBindings: memberBindings6, + roleBindings: roleBindings6, } -export const accessPolicies7 = { - byPolicyId: byPolicyId5, +export const accessPolicies10 = { + byPolicyId: byPolicyId7, } -export const get64 = oc +export const get74 = oc .route({ inputStructure: 'detailed', method: 'GET', @@ -3151,16 +3473,17 @@ export const get64 = oc }) .output(zGetWorkspacesCurrentRbacWorkspaceDatasetsAccessPolicyResponse) -export const accessPolicy4 = { - get: get64, +export const accessPolicy6 = { + get: get74, } export const datasets2 = { - accessPolicies: accessPolicies7, - accessPolicy: accessPolicy4, + accessPolicies: accessPolicies10, + accessPolicy: accessPolicy6, } export const workspace = { + agents: agents3, apps: apps2, datasets: datasets2, } @@ -3168,6 +3491,7 @@ export const workspace = { export const rbac = { accessPolicies, accessPolicyBindings, + agents: agents2, apps, datasets, members: members2, @@ -3216,7 +3540,7 @@ export const import_ = { post: post49, } -export const get65 = oc +export const get75 = oc .route({ inputStructure: 'detailed', method: 'GET', @@ -3227,7 +3551,7 @@ export const get65 = oc .output(zGetWorkspacesCurrentSkillsTagsResponse) export const tags = { - get: get65, + get: get75, } export const post50 = oc @@ -3270,7 +3594,7 @@ export const duplicate = { post: post51, } -export const get66 = oc +export const get76 = oc .route({ inputStructure: 'detailed', method: 'GET', @@ -3282,7 +3606,7 @@ export const get66 = oc .output(zGetWorkspacesCurrentSkillsBySkillIdExportResponse) export const export2 = { - get: get66, + get: get76, } export const post52 = oc @@ -3305,7 +3629,7 @@ export const check = { post: post52, } -export const get67 = oc +export const get77 = oc .route({ inputStructure: 'detailed', method: 'GET', @@ -3322,10 +3646,10 @@ export const get67 = oc .output(zGetWorkspacesCurrentSkillsBySkillIdFilesContentResponse) export const content = { - get: get67, + get: get77, } -export const get68 = oc +export const get78 = oc .route({ inputStructure: 'detailed', method: 'GET', @@ -3342,7 +3666,7 @@ export const get68 = oc .output(zGetWorkspacesCurrentSkillsBySkillIdFilesPreviewResponse) export const preview = { - get: get68, + get: get78, } export const patch5 = oc @@ -3361,7 +3685,7 @@ export const patch5 = oc ) .output(zPatchWorkspacesCurrentSkillsBySkillIdFilesResponse) -export const put16 = oc +export const put19 = oc .route({ inputStructure: 'detailed', method: 'PUT', @@ -3379,7 +3703,7 @@ export const put16 = oc export const files2 = { patch: patch5, - put: put16, + put: put19, check, content, preview, @@ -3405,7 +3729,7 @@ export const publish = { post: post53, } -export const get69 = oc +export const get79 = oc .route({ inputStructure: 'detailed', method: 'GET', @@ -3417,7 +3741,7 @@ export const get69 = oc .output(zGetWorkspacesCurrentSkillsBySkillIdReferencesResponse) export const references = { - get: get69, + get: get79, } export const post54 = oc @@ -3440,7 +3764,7 @@ export const restore = { post: post54, } -export const delete13 = oc +export const delete14 = oc .route({ inputStructure: 'detailed', method: 'DELETE', @@ -3451,7 +3775,7 @@ export const delete13 = oc .input(z.object({ params: zDeleteWorkspacesCurrentSkillsBySkillIdVersionsByVersionIdPath })) .output(zDeleteWorkspacesCurrentSkillsBySkillIdVersionsByVersionIdResponse) -export const get70 = oc +export const get80 = oc .route({ inputStructure: 'detailed', method: 'GET', @@ -3479,12 +3803,12 @@ export const patch6 = oc .output(zPatchWorkspacesCurrentSkillsBySkillIdVersionsByVersionIdResponse) export const byVersionId = { - delete: delete13, - get: get70, + delete: delete14, + get: get80, patch: patch6, } -export const get71 = oc +export const get81 = oc .route({ inputStructure: 'detailed', method: 'GET', @@ -3496,11 +3820,11 @@ export const get71 = oc .output(zGetWorkspacesCurrentSkillsBySkillIdVersionsResponse) export const versions = { - get: get71, + get: get81, byVersionId, } -export const delete14 = oc +export const delete15 = oc .route({ inputStructure: 'detailed', method: 'DELETE', @@ -3516,7 +3840,7 @@ export const delete14 = oc ) .output(zDeleteWorkspacesCurrentSkillsBySkillIdResponse) -export const get72 = oc +export const get82 = oc .route({ inputStructure: 'detailed', method: 'GET', @@ -3544,8 +3868,8 @@ export const patch7 = oc .output(zPatchWorkspacesCurrentSkillsBySkillIdResponse) export const bySkillId = { - delete: delete14, - get: get72, + delete: delete15, + get: get82, patch: patch7, assist, duplicate, @@ -3557,7 +3881,7 @@ export const bySkillId = { versions, } -export const get73 = oc +export const get83 = oc .route({ inputStructure: 'detailed', method: 'GET', @@ -3581,7 +3905,7 @@ export const post55 = oc .output(zPostWorkspacesCurrentSkillsResponse) export const skills2 = { - get: get73, + get: get83, post: post55, files, import: import_, @@ -3589,7 +3913,7 @@ export const skills2 = { bySkillId, } -export const get74 = oc +export const get84 = oc .route({ inputStructure: 'detailed', method: 'GET', @@ -3600,10 +3924,10 @@ export const get74 = oc .output(zGetWorkspacesCurrentSummaryResponse) export const summary2 = { - get: get74, + get: get84, } -export const get75 = oc +export const get85 = oc .route({ inputStructure: 'detailed', method: 'GET', @@ -3614,7 +3938,7 @@ export const get75 = oc .output(zGetWorkspacesCurrentToolLabelsResponse) export const toolLabels = { - get: get75, + get: get85, } export const post56 = oc @@ -3643,11 +3967,11 @@ export const post57 = oc .input(z.object({ body: zPostWorkspacesCurrentToolProviderApiDeleteBody })) .output(zPostWorkspacesCurrentToolProviderApiDeleteResponse) -export const delete15 = { +export const delete16 = { post: post57, } -export const get76 = oc +export const get86 = oc .route({ inputStructure: 'detailed', method: 'GET', @@ -3658,11 +3982,11 @@ export const get76 = oc .input(z.object({ query: zGetWorkspacesCurrentToolProviderApiGetQuery })) .output(zGetWorkspacesCurrentToolProviderApiGetResponse) -export const get77 = { - get: get76, +export const get87 = { + get: get86, } -export const get78 = oc +export const get88 = oc .route({ inputStructure: 'detailed', method: 'GET', @@ -3674,7 +3998,7 @@ export const get78 = oc .output(zGetWorkspacesCurrentToolProviderApiRemoteResponse) export const remote = { - get: get78, + get: get88, } export const post58 = oc @@ -3711,7 +4035,7 @@ export const test = { pre, } -export const get79 = oc +export const get89 = oc .route({ inputStructure: 'detailed', method: 'GET', @@ -3723,7 +4047,7 @@ export const get79 = oc .output(zGetWorkspacesCurrentToolProviderApiToolsResponse) export const tools = { - get: get79, + get: get89, } export const post60 = oc @@ -3743,8 +4067,8 @@ export const update2 = { export const api = { add, - delete: delete15, - get: get77, + delete: delete16, + get: get87, remote, schema, test, @@ -3772,7 +4096,7 @@ export const add2 = { post: post61, } -export const get80 = oc +export const get90 = oc .route({ inputStructure: 'detailed', method: 'GET', @@ -3789,10 +4113,10 @@ export const get80 = oc .output(zGetWorkspacesCurrentToolProviderBuiltinByProviderCredentialInfoResponse) export const info = { - get: get80, + get: get90, } -export const get81 = oc +export const get91 = oc .route({ inputStructure: 'detailed', method: 'GET', @@ -3812,7 +4136,7 @@ export const get81 = oc ) export const byCredentialType = { - get: get81, + get: get91, } export const schema2 = { @@ -3824,7 +4148,7 @@ export const credential = { schema: schema2, } -export const get82 = oc +export const get92 = oc .route({ inputStructure: 'detailed', method: 'GET', @@ -3841,7 +4165,7 @@ export const get82 = oc .output(zGetWorkspacesCurrentToolProviderBuiltinByProviderCredentialsResponse) export const credentials3 = { - get: get82, + get: get92, } export const post62 = oc @@ -3880,11 +4204,11 @@ export const post63 = oc ) .output(zPostWorkspacesCurrentToolProviderBuiltinByProviderDeleteResponse) -export const delete16 = { +export const delete17 = { post: post63, } -export const get83 = oc +export const get93 = oc .route({ inputStructure: 'detailed', method: 'GET', @@ -3896,10 +4220,10 @@ export const get83 = oc .output(zGetWorkspacesCurrentToolProviderBuiltinByProviderIconResponse) export const icon2 = { - get: get83, + get: get93, } -export const get84 = oc +export const get94 = oc .route({ inputStructure: 'detailed', method: 'GET', @@ -3911,10 +4235,10 @@ export const get84 = oc .output(zGetWorkspacesCurrentToolProviderBuiltinByProviderInfoResponse) export const info2 = { - get: get84, + get: get94, } -export const get85 = oc +export const get95 = oc .route({ inputStructure: 'detailed', method: 'GET', @@ -3928,10 +4252,10 @@ export const get85 = oc .output(zGetWorkspacesCurrentToolProviderBuiltinByProviderOauthClientSchemaResponse) export const clientSchema = { - get: get85, + get: get95, } -export const delete17 = oc +export const delete18 = oc .route({ inputStructure: 'detailed', method: 'DELETE', @@ -3946,7 +4270,7 @@ export const delete17 = oc ) .output(zDeleteWorkspacesCurrentToolProviderBuiltinByProviderOauthCustomClientResponse) -export const get86 = oc +export const get96 = oc .route({ inputStructure: 'detailed', method: 'GET', @@ -3976,8 +4300,8 @@ export const post64 = oc .output(zPostWorkspacesCurrentToolProviderBuiltinByProviderOauthCustomClientResponse) export const customClient = { - delete: delete17, - get: get86, + delete: delete18, + get: get96, post: post64, } @@ -3986,7 +4310,7 @@ export const oauth = { customClient, } -export const get87 = oc +export const get97 = oc .route({ inputStructure: 'detailed', method: 'GET', @@ -3998,7 +4322,7 @@ export const get87 = oc .output(zGetWorkspacesCurrentToolProviderBuiltinByProviderToolsResponse) export const tools2 = { - get: get87, + get: get97, } export const post65 = oc @@ -4026,7 +4350,7 @@ export const byProvider2 = { credential, credentials: credentials3, defaultCredential, - delete: delete16, + delete: delete17, icon: icon2, info: info2, oauth, @@ -4053,7 +4377,7 @@ export const auth = { post: post66, } -export const get88 = oc +export const get98 = oc .route({ inputStructure: 'detailed', method: 'GET', @@ -4065,14 +4389,14 @@ export const get88 = oc .output(zGetWorkspacesCurrentToolProviderMcpToolsByProviderIdResponse) export const byProviderId = { - get: get88, + get: get98, } export const tools3 = { byProviderId, } -export const get89 = oc +export const get99 = oc .route({ inputStructure: 'detailed', method: 'GET', @@ -4084,14 +4408,14 @@ export const get89 = oc .output(zGetWorkspacesCurrentToolProviderMcpUpdateByProviderIdResponse) export const byProviderId2 = { - get: get89, + get: get99, } export const update4 = { byProviderId: byProviderId2, } -export const delete18 = oc +export const delete19 = oc .route({ inputStructure: 'detailed', method: 'DELETE', @@ -4113,7 +4437,7 @@ export const post67 = oc .input(z.object({ body: zPostWorkspacesCurrentToolProviderMcpBody })) .output(zPostWorkspacesCurrentToolProviderMcpResponse) -export const put17 = oc +export const put20 = oc .route({ inputStructure: 'detailed', method: 'PUT', @@ -4125,9 +4449,9 @@ export const put17 = oc .output(zPutWorkspacesCurrentToolProviderMcpResponse) export const mcp = { - delete: delete18, + delete: delete19, post: post67, - put: put17, + put: put20, auth, tools: tools3, update: update4, @@ -4159,11 +4483,11 @@ export const post69 = oc .input(z.object({ body: zPostWorkspacesCurrentToolProviderWorkflowDeleteBody })) .output(zPostWorkspacesCurrentToolProviderWorkflowDeleteResponse) -export const delete19 = { +export const delete20 = { post: post69, } -export const get90 = oc +export const get100 = oc .route({ inputStructure: 'detailed', method: 'GET', @@ -4174,11 +4498,11 @@ export const get90 = oc .input(z.object({ query: zGetWorkspacesCurrentToolProviderWorkflowGetQuery.optional() })) .output(zGetWorkspacesCurrentToolProviderWorkflowGetResponse) -export const get91 = { - get: get90, +export const get101 = { + get: get100, } -export const get92 = oc +export const get102 = oc .route({ inputStructure: 'detailed', method: 'GET', @@ -4190,7 +4514,7 @@ export const get92 = oc .output(zGetWorkspacesCurrentToolProviderWorkflowToolsResponse) export const tools4 = { - get: get92, + get: get102, } export const post70 = oc @@ -4210,8 +4534,8 @@ export const update5 = { export const workflow = { create: create2, - delete: delete19, - get: get91, + delete: delete20, + get: get101, tools: tools4, update: update5, } @@ -4223,7 +4547,7 @@ export const toolProvider = { workflow, } -export const get93 = oc +export const get103 = oc .route({ inputStructure: 'detailed', method: 'GET', @@ -4235,10 +4559,10 @@ export const get93 = oc .output(zGetWorkspacesCurrentToolProvidersResponse) export const toolProviders = { - get: get93, + get: get103, } -export const get94 = oc +export const get104 = oc .route({ inputStructure: 'detailed', method: 'GET', @@ -4249,10 +4573,10 @@ export const get94 = oc .output(zGetWorkspacesCurrentToolsApiResponse) export const api2 = { - get: get94, + get: get104, } -export const get95 = oc +export const get105 = oc .route({ inputStructure: 'detailed', method: 'GET', @@ -4263,10 +4587,10 @@ export const get95 = oc .output(zGetWorkspacesCurrentToolsBuiltinResponse) export const builtin2 = { - get: get95, + get: get105, } -export const get96 = oc +export const get106 = oc .route({ inputStructure: 'detailed', method: 'GET', @@ -4277,10 +4601,10 @@ export const get96 = oc .output(zGetWorkspacesCurrentToolsMcpResponse) export const mcp2 = { - get: get96, + get: get106, } -export const get97 = oc +export const get107 = oc .route({ inputStructure: 'detailed', method: 'GET', @@ -4291,7 +4615,7 @@ export const get97 = oc .output(zGetWorkspacesCurrentToolsWorkflowResponse) export const workflow2 = { - get: get97, + get: get107, } export const tools5 = { @@ -4301,7 +4625,7 @@ export const tools5 = { workflow: workflow2, } -export const get98 = oc +export const get108 = oc .route({ inputStructure: 'detailed', method: 'GET', @@ -4313,13 +4637,13 @@ export const get98 = oc .output(zGetWorkspacesCurrentTriggerProviderByProviderIconResponse) export const icon3 = { - get: get98, + get: get108, } /** * Get info for a trigger provider */ -export const get99 = oc +export const get109 = oc .route({ inputStructure: 'detailed', method: 'GET', @@ -4332,13 +4656,13 @@ export const get99 = oc .output(zGetWorkspacesCurrentTriggerProviderByProviderInfoResponse) export const info3 = { - get: get99, + get: get109, } /** * Remove custom OAuth client configuration */ -export const delete20 = oc +export const delete21 = oc .route({ inputStructure: 'detailed', method: 'DELETE', @@ -4353,7 +4677,7 @@ export const delete20 = oc /** * Get OAuth client configuration for a provider */ -export const get100 = oc +export const get110 = oc .route({ inputStructure: 'detailed', method: 'GET', @@ -4386,8 +4710,8 @@ export const post71 = oc .output(zPostWorkspacesCurrentTriggerProviderByProviderOauthClientResponse) export const client = { - delete: delete20, - get: get100, + delete: delete21, + get: get110, post: post71, } @@ -4454,7 +4778,7 @@ export const create3 = { /** * Get the request logs for a subscription instance for a trigger provider */ -export const get101 = oc +export const get111 = oc .route({ inputStructure: 'detailed', method: 'GET', @@ -4475,7 +4799,7 @@ export const get101 = oc ) export const bySubscriptionBuilderId2 = { - get: get101, + get: get111, } export const logs = { @@ -4549,7 +4873,7 @@ export const verifyAndUpdate = { /** * Get a subscription instance for a trigger provider */ -export const get102 = oc +export const get112 = oc .route({ inputStructure: 'detailed', method: 'GET', @@ -4570,7 +4894,7 @@ export const get102 = oc ) export const bySubscriptionBuilderId5 = { - get: get102, + get: get112, } export const builder = { @@ -4585,7 +4909,7 @@ export const builder = { /** * List all trigger subscriptions for the current tenant's provider */ -export const get103 = oc +export const get113 = oc .route({ inputStructure: 'detailed', method: 'GET', @@ -4598,13 +4922,13 @@ export const get103 = oc .output(zGetWorkspacesCurrentTriggerProviderByProviderSubscriptionsListResponse) export const list4 = { - get: get103, + get: get113, } /** * Initiate OAuth authorization flow for a trigger provider */ -export const get104 = oc +export const get114 = oc .route({ inputStructure: 'detailed', method: 'GET', @@ -4621,7 +4945,7 @@ export const get104 = oc .output(zGetWorkspacesCurrentTriggerProviderByProviderSubscriptionsOauthAuthorizeResponse) export const authorize = { - get: get104, + get: get114, } export const oauth3 = { @@ -4693,7 +5017,7 @@ export const post77 = oc ) .output(zPostWorkspacesCurrentTriggerProviderBySubscriptionIdSubscriptionsDeleteResponse) -export const delete21 = { +export const delete22 = { post: post77, } @@ -4722,7 +5046,7 @@ export const update7 = { } export const subscriptions2 = { - delete: delete21, + delete: delete22, update: update7, } @@ -4738,7 +5062,7 @@ export const triggerProvider = { /** * List all trigger providers for the current tenant */ -export const get105 = oc +export const get115 = oc .route({ inputStructure: 'detailed', method: 'GET', @@ -4750,7 +5074,7 @@ export const get105 = oc .output(zGetWorkspacesCurrentTriggersResponse) export const triggers = { - get: get105, + get: get115, } export const current = { @@ -4797,7 +5121,7 @@ export const webappLogo = { upload: upload3, } -export const get106 = oc +export const get116 = oc .route({ inputStructure: 'detailed', method: 'GET', @@ -4819,7 +5143,7 @@ export const post80 = oc .output(zPostWorkspacesCustomConfigResponse) export const customConfig = { - get: get106, + get: get116, post: post80, webappLogo, } @@ -4854,7 +5178,7 @@ export const switch3 = { post: post82, } -export const get107 = oc +export const get117 = oc .route({ inputStructure: 'detailed', method: 'GET', @@ -4866,7 +5190,7 @@ export const get107 = oc .output(zGetWorkspacesByTenantIdModelProvidersByProviderByIconTypeByLangResponse) export const byLang = { - get: get107, + get: get117, } export const byIconType = { @@ -4885,7 +5209,7 @@ export const byTenantId = { modelProviders: modelProviders2, } -export const get108 = oc +export const get118 = oc .route({ inputStructure: 'detailed', method: 'GET', @@ -4896,7 +5220,7 @@ export const get108 = oc .output(zGetWorkspacesResponse) export const workspaces = { - get: get108, + get: get118, current, customConfig, info: info4, diff --git a/packages/contracts/generated/api/console/workspaces/types.gen.ts b/packages/contracts/generated/api/console/workspaces/types.gen.ts index 58b995b4c08..4b3373e7bf1 100644 --- a/packages/contracts/generated/api/console/workspaces/types.gen.ts +++ b/packages/contracts/generated/api/console/workspaces/types.gen.ts @@ -527,30 +527,6 @@ export type PluginCategoryListResponse = { plugins: Array } -export type AccessPolicyList = { - data?: Array - pagination?: Pagination | null -} - -export type AccessPolicy = { - category?: string - created_at?: number - description?: string - id: string - is_builtin?: boolean - name: string - permission_keys?: Array - policy_key?: string - resource_type: string - tenant_id?: string - updated_at?: number -} - -export type AccessPolicyBindingState = { - binding_id: string - is_locked?: boolean -} - export type DeleteMemberBindingsRequest = { account_ids?: Array } @@ -563,8 +539,8 @@ export type RoleBindingsResponse = { data?: Array } -export type AppAccessMatrix = { - app_id?: string +export type AgentAccessMatrix = { + agent_id?: string items?: Array } @@ -594,6 +570,11 @@ export type ResourceWhitelistConfig = { automatic_include_workspace_members: boolean } +export type AppAccessMatrix = { + app_id?: string + items?: Array +} + export type DatasetAccessMatrix = { dataset_id?: string items?: Array @@ -609,6 +590,7 @@ export type ReplaceMemberRolesRequest = { } export type MyPermissionsResponse = { + agent?: ResourcePermissionSnapshot app?: ResourcePermissionSnapshot dataset?: ResourcePermissionSnapshot workspace?: WorkspacePermissionSnapshot @@ -618,23 +600,6 @@ export type PermissionCatalogResponse = { groups?: Array } -export type RbacRoleList = { - data?: Array - pagination?: Pagination | null -} - -export type RbacRole = { - category?: string - description?: string - id: string - is_builtin?: boolean - name: string - permission_keys?: Array - role_tag?: string - tenant_id?: string | null - type: string -} - export type MembersInRoleList = { data?: Array pagination?: Pagination | null @@ -1690,13 +1655,6 @@ export type PluginCategoryInstalledPluginResponse = { version: string } -export type Pagination = { - current_page?: number - per_page?: number - total_count?: number - total_pages?: number -} - export type AccessPolicyMemberBinding = { access_policy_id: string account_id: string @@ -1725,6 +1683,39 @@ export type ResourceUserAccessPolicies = { roles?: Array } +export type Pagination = { + current_page?: number + per_page?: number + total_count?: number + total_pages?: number +} + +export type AccessPolicy = { + category?: string + created_at?: number + description?: string + id: string + is_builtin?: boolean + name: string + permission_keys?: Array + policy_key?: string + resource_type: string + tenant_id?: string + updated_at?: number +} + +export type RbacRole = { + category?: string + description?: string + id: string + is_builtin?: boolean + name: string + permission_keys?: Array + role_tag?: string + tenant_id?: string | null + type: string +} + export type ResourcePermissionSnapshot = { default_permission_keys?: Array overrides?: Array @@ -4372,7 +4363,9 @@ export type GetWorkspacesCurrentRbacAccessPoliciesData = { } export type GetWorkspacesCurrentRbacAccessPoliciesResponses = { - 200: AccessPolicyList + 200: { + [key: string]: unknown + } } export type GetWorkspacesCurrentRbacAccessPoliciesResponse = @@ -4386,7 +4379,9 @@ export type PostWorkspacesCurrentRbacAccessPoliciesData = { } export type PostWorkspacesCurrentRbacAccessPoliciesResponses = { - 201: AccessPolicy + 200: { + [key: string]: unknown + } } export type PostWorkspacesCurrentRbacAccessPoliciesResponse = @@ -4402,7 +4397,9 @@ export type DeleteWorkspacesCurrentRbacAccessPoliciesByPolicyIdData = { } export type DeleteWorkspacesCurrentRbacAccessPoliciesByPolicyIdResponses = { - 200: AccessPolicy + 200: { + [key: string]: unknown + } } export type DeleteWorkspacesCurrentRbacAccessPoliciesByPolicyIdResponse = @@ -4418,7 +4415,9 @@ export type GetWorkspacesCurrentRbacAccessPoliciesByPolicyIdData = { } export type GetWorkspacesCurrentRbacAccessPoliciesByPolicyIdResponses = { - 200: AccessPolicy + 200: { + [key: string]: unknown + } } export type GetWorkspacesCurrentRbacAccessPoliciesByPolicyIdResponse = @@ -4434,7 +4433,9 @@ export type PutWorkspacesCurrentRbacAccessPoliciesByPolicyIdData = { } export type PutWorkspacesCurrentRbacAccessPoliciesByPolicyIdResponses = { - 200: AccessPolicy + 200: { + [key: string]: unknown + } } export type PutWorkspacesCurrentRbacAccessPoliciesByPolicyIdResponse = @@ -4450,7 +4451,9 @@ export type PostWorkspacesCurrentRbacAccessPoliciesByPolicyIdCopyData = { } export type PostWorkspacesCurrentRbacAccessPoliciesByPolicyIdCopyResponses = { - 201: AccessPolicy + 200: { + [key: string]: unknown + } } export type PostWorkspacesCurrentRbacAccessPoliciesByPolicyIdCopyResponse = @@ -4466,7 +4469,9 @@ export type PutWorkspacesCurrentRbacAccessPolicyBindingsByBindingIdLockData = { } export type PutWorkspacesCurrentRbacAccessPolicyBindingsByBindingIdLockResponses = { - 200: AccessPolicyBindingState + 200: { + [key: string]: unknown + } } export type PutWorkspacesCurrentRbacAccessPolicyBindingsByBindingIdLockResponse = @@ -4482,12 +4487,171 @@ export type PutWorkspacesCurrentRbacAccessPolicyBindingsByBindingIdUnlockData = } export type PutWorkspacesCurrentRbacAccessPolicyBindingsByBindingIdUnlockResponses = { - 200: AccessPolicyBindingState + 200: { + [key: string]: unknown + } } export type PutWorkspacesCurrentRbacAccessPolicyBindingsByBindingIdUnlockResponse = PutWorkspacesCurrentRbacAccessPolicyBindingsByBindingIdUnlockResponses[keyof PutWorkspacesCurrentRbacAccessPolicyBindingsByBindingIdUnlockResponses] +export type DeleteWorkspacesCurrentRbacAgentsByAgentIdAccessPoliciesByPolicyIdMemberBindingsData = { + body: DeleteMemberBindingsRequest + path: { + agent_id: string + policy_id: string + } + query?: never + url: '/workspaces/current/rbac/agents/{agent_id}/access-policies/{policy_id}/member-bindings' +} + +export type DeleteWorkspacesCurrentRbacAgentsByAgentIdAccessPoliciesByPolicyIdMemberBindingsResponses = + { + 200: MemberBindingsResponse + } + +export type DeleteWorkspacesCurrentRbacAgentsByAgentIdAccessPoliciesByPolicyIdMemberBindingsResponse = + DeleteWorkspacesCurrentRbacAgentsByAgentIdAccessPoliciesByPolicyIdMemberBindingsResponses[keyof DeleteWorkspacesCurrentRbacAgentsByAgentIdAccessPoliciesByPolicyIdMemberBindingsResponses] + +export type GetWorkspacesCurrentRbacAgentsByAgentIdAccessPoliciesByPolicyIdMemberBindingsData = { + body?: never + path: { + agent_id: string + policy_id: string + } + query?: never + url: '/workspaces/current/rbac/agents/{agent_id}/access-policies/{policy_id}/member-bindings' +} + +export type GetWorkspacesCurrentRbacAgentsByAgentIdAccessPoliciesByPolicyIdMemberBindingsResponses = + { + 200: MemberBindingsResponse + } + +export type GetWorkspacesCurrentRbacAgentsByAgentIdAccessPoliciesByPolicyIdMemberBindingsResponse = + GetWorkspacesCurrentRbacAgentsByAgentIdAccessPoliciesByPolicyIdMemberBindingsResponses[keyof GetWorkspacesCurrentRbacAgentsByAgentIdAccessPoliciesByPolicyIdMemberBindingsResponses] + +export type GetWorkspacesCurrentRbacAgentsByAgentIdAccessPoliciesByPolicyIdRoleBindingsData = { + body?: never + path: { + agent_id: string + policy_id: string + } + query?: never + url: '/workspaces/current/rbac/agents/{agent_id}/access-policies/{policy_id}/role-bindings' +} + +export type GetWorkspacesCurrentRbacAgentsByAgentIdAccessPoliciesByPolicyIdRoleBindingsResponses = { + 200: RoleBindingsResponse +} + +export type GetWorkspacesCurrentRbacAgentsByAgentIdAccessPoliciesByPolicyIdRoleBindingsResponse = + GetWorkspacesCurrentRbacAgentsByAgentIdAccessPoliciesByPolicyIdRoleBindingsResponses[keyof GetWorkspacesCurrentRbacAgentsByAgentIdAccessPoliciesByPolicyIdRoleBindingsResponses] + +export type GetWorkspacesCurrentRbacAgentsByAgentIdAccessPolicyData = { + body?: never + path: { + agent_id: string + } + query?: { + language?: 'en' | 'ja' | 'zh' + } + url: '/workspaces/current/rbac/agents/{agent_id}/access-policy' +} + +export type GetWorkspacesCurrentRbacAgentsByAgentIdAccessPolicyResponses = { + 200: AgentAccessMatrix +} + +export type GetWorkspacesCurrentRbacAgentsByAgentIdAccessPolicyResponse = + GetWorkspacesCurrentRbacAgentsByAgentIdAccessPolicyResponses[keyof GetWorkspacesCurrentRbacAgentsByAgentIdAccessPolicyResponses] + +export type GetWorkspacesCurrentRbacAgentsByAgentIdUserAccessPoliciesData = { + body?: never + path: { + agent_id: string + } + query?: { + language?: 'en' | 'ja' | 'zh' + limit?: number + page?: number + reverse?: boolean + } + url: '/workspaces/current/rbac/agents/{agent_id}/user-access-policies' +} + +export type GetWorkspacesCurrentRbacAgentsByAgentIdUserAccessPoliciesResponses = { + 200: ResourceUserAccessPoliciesResponse +} + +export type GetWorkspacesCurrentRbacAgentsByAgentIdUserAccessPoliciesResponse = + GetWorkspacesCurrentRbacAgentsByAgentIdUserAccessPoliciesResponses[keyof GetWorkspacesCurrentRbacAgentsByAgentIdUserAccessPoliciesResponses] + +export type PutWorkspacesCurrentRbacAgentsByAgentIdUsersByTargetAccountIdAccessPoliciesData = { + body: ReplaceUserAccessPolicies + path: { + agent_id: string + target_account_id: string + } + query?: never + url: '/workspaces/current/rbac/agents/{agent_id}/users/{target_account_id}/access-policies' +} + +export type PutWorkspacesCurrentRbacAgentsByAgentIdUsersByTargetAccountIdAccessPoliciesResponses = { + 200: ReplaceUserAccessPoliciesResponse +} + +export type PutWorkspacesCurrentRbacAgentsByAgentIdUsersByTargetAccountIdAccessPoliciesResponse = + PutWorkspacesCurrentRbacAgentsByAgentIdUsersByTargetAccountIdAccessPoliciesResponses[keyof PutWorkspacesCurrentRbacAgentsByAgentIdUsersByTargetAccountIdAccessPoliciesResponses] + +export type GetWorkspacesCurrentRbacAgentsByAgentIdWhitelistData = { + body?: never + path: { + agent_id: string + } + query?: never + url: '/workspaces/current/rbac/agents/{agent_id}/whitelist' +} + +export type GetWorkspacesCurrentRbacAgentsByAgentIdWhitelistResponses = { + 200: ResourceWhitelist +} + +export type GetWorkspacesCurrentRbacAgentsByAgentIdWhitelistResponse = + GetWorkspacesCurrentRbacAgentsByAgentIdWhitelistResponses[keyof GetWorkspacesCurrentRbacAgentsByAgentIdWhitelistResponses] + +export type PutWorkspacesCurrentRbacAgentsByAgentIdWhitelistData = { + body: ResourceAccessScopeRequest + path: { + agent_id: string + } + query?: never + url: '/workspaces/current/rbac/agents/{agent_id}/whitelist' +} + +export type PutWorkspacesCurrentRbacAgentsByAgentIdWhitelistResponses = { + 200: ResourceWhitelist +} + +export type PutWorkspacesCurrentRbacAgentsByAgentIdWhitelistResponse = + PutWorkspacesCurrentRbacAgentsByAgentIdWhitelistResponses[keyof PutWorkspacesCurrentRbacAgentsByAgentIdWhitelistResponses] + +export type GetWorkspacesCurrentRbacAgentsByAgentIdWhitelistConfigData = { + body?: never + path: { + agent_id: string + } + query?: never + url: '/workspaces/current/rbac/agents/{agent_id}/whitelist_config' +} + +export type GetWorkspacesCurrentRbacAgentsByAgentIdWhitelistConfigResponses = { + 200: ResourceWhitelistConfig +} + +export type GetWorkspacesCurrentRbacAgentsByAgentIdWhitelistConfigResponse = + GetWorkspacesCurrentRbacAgentsByAgentIdWhitelistConfigResponses[keyof GetWorkspacesCurrentRbacAgentsByAgentIdWhitelistConfigResponses] + export type DeleteWorkspacesCurrentRbacAppsByAppIdAccessPoliciesByPolicyIdMemberBindingsData = { body: DeleteMemberBindingsRequest path: { @@ -4865,6 +5029,20 @@ export type GetWorkspacesCurrentRbacRolePermissionsCatalogResponses = { export type GetWorkspacesCurrentRbacRolePermissionsCatalogResponse = GetWorkspacesCurrentRbacRolePermissionsCatalogResponses[keyof GetWorkspacesCurrentRbacRolePermissionsCatalogResponses] +export type GetWorkspacesCurrentRbacRolePermissionsCatalogAgentData = { + body?: never + path?: never + query?: never + url: '/workspaces/current/rbac/role-permissions/catalog/agent' +} + +export type GetWorkspacesCurrentRbacRolePermissionsCatalogAgentResponses = { + 200: PermissionCatalogResponse +} + +export type GetWorkspacesCurrentRbacRolePermissionsCatalogAgentResponse = + GetWorkspacesCurrentRbacRolePermissionsCatalogAgentResponses[keyof GetWorkspacesCurrentRbacRolePermissionsCatalogAgentResponses] + export type GetWorkspacesCurrentRbacRolePermissionsCatalogAppData = { body?: never path?: never @@ -4901,7 +5079,9 @@ export type GetWorkspacesCurrentRbacRolesData = { } export type GetWorkspacesCurrentRbacRolesResponses = { - 200: RbacRoleList + 200: { + [key: string]: unknown + } } export type GetWorkspacesCurrentRbacRolesResponse = @@ -4915,7 +5095,9 @@ export type PostWorkspacesCurrentRbacRolesData = { } export type PostWorkspacesCurrentRbacRolesResponses = { - 201: RbacRole + 200: { + [key: string]: unknown + } } export type PostWorkspacesCurrentRbacRolesResponse = @@ -4931,7 +5113,9 @@ export type DeleteWorkspacesCurrentRbacRolesByRoleIdData = { } export type DeleteWorkspacesCurrentRbacRolesByRoleIdResponses = { - 200: RbacRole + 200: { + [key: string]: unknown + } } export type DeleteWorkspacesCurrentRbacRolesByRoleIdResponse = @@ -4947,7 +5131,9 @@ export type GetWorkspacesCurrentRbacRolesByRoleIdData = { } export type GetWorkspacesCurrentRbacRolesByRoleIdResponses = { - 200: RbacRole + 200: { + [key: string]: unknown + } } export type GetWorkspacesCurrentRbacRolesByRoleIdResponse = @@ -4963,7 +5149,9 @@ export type PutWorkspacesCurrentRbacRolesByRoleIdData = { } export type PutWorkspacesCurrentRbacRolesByRoleIdResponses = { - 200: RbacRole + 200: { + [key: string]: unknown + } } export type PutWorkspacesCurrentRbacRolesByRoleIdResponse = @@ -4979,7 +5167,9 @@ export type PostWorkspacesCurrentRbacRolesByRoleIdCopyData = { } export type PostWorkspacesCurrentRbacRolesByRoleIdCopyResponses = { - 201: RbacRole + 200: { + [key: string]: unknown + } } export type PostWorkspacesCurrentRbacRolesByRoleIdCopyResponse = @@ -5001,6 +5191,69 @@ export type GetWorkspacesCurrentRbacRolesByRoleIdMembersResponses = { export type GetWorkspacesCurrentRbacRolesByRoleIdMembersResponse = GetWorkspacesCurrentRbacRolesByRoleIdMembersResponses[keyof GetWorkspacesCurrentRbacRolesByRoleIdMembersResponses] +export type PutWorkspacesCurrentRbacWorkspaceAgentsAccessPoliciesByPolicyIdBindingsData = { + body: ReplaceBindingsRequest + path: { + policy_id: string + } + query?: never + url: '/workspaces/current/rbac/workspace/agents/access-policies/{policy_id}/bindings' +} + +export type PutWorkspacesCurrentRbacWorkspaceAgentsAccessPoliciesByPolicyIdBindingsResponses = { + 200: AccessMatrixItem +} + +export type PutWorkspacesCurrentRbacWorkspaceAgentsAccessPoliciesByPolicyIdBindingsResponse = + PutWorkspacesCurrentRbacWorkspaceAgentsAccessPoliciesByPolicyIdBindingsResponses[keyof PutWorkspacesCurrentRbacWorkspaceAgentsAccessPoliciesByPolicyIdBindingsResponses] + +export type GetWorkspacesCurrentRbacWorkspaceAgentsAccessPoliciesByPolicyIdMemberBindingsData = { + body?: never + path: { + policy_id: string + } + query?: never + url: '/workspaces/current/rbac/workspace/agents/access-policies/{policy_id}/member-bindings' +} + +export type GetWorkspacesCurrentRbacWorkspaceAgentsAccessPoliciesByPolicyIdMemberBindingsResponses = + { + 200: MemberBindingsResponse + } + +export type GetWorkspacesCurrentRbacWorkspaceAgentsAccessPoliciesByPolicyIdMemberBindingsResponse = + GetWorkspacesCurrentRbacWorkspaceAgentsAccessPoliciesByPolicyIdMemberBindingsResponses[keyof GetWorkspacesCurrentRbacWorkspaceAgentsAccessPoliciesByPolicyIdMemberBindingsResponses] + +export type GetWorkspacesCurrentRbacWorkspaceAgentsAccessPoliciesByPolicyIdRoleBindingsData = { + body?: never + path: { + policy_id: string + } + query?: never + url: '/workspaces/current/rbac/workspace/agents/access-policies/{policy_id}/role-bindings' +} + +export type GetWorkspacesCurrentRbacWorkspaceAgentsAccessPoliciesByPolicyIdRoleBindingsResponses = { + 200: RoleBindingsResponse +} + +export type GetWorkspacesCurrentRbacWorkspaceAgentsAccessPoliciesByPolicyIdRoleBindingsResponse = + GetWorkspacesCurrentRbacWorkspaceAgentsAccessPoliciesByPolicyIdRoleBindingsResponses[keyof GetWorkspacesCurrentRbacWorkspaceAgentsAccessPoliciesByPolicyIdRoleBindingsResponses] + +export type GetWorkspacesCurrentRbacWorkspaceAgentsAccessPolicyData = { + body?: never + path?: never + query?: never + url: '/workspaces/current/rbac/workspace/agents/access-policy' +} + +export type GetWorkspacesCurrentRbacWorkspaceAgentsAccessPolicyResponses = { + 200: WorkspaceAccessMatrix +} + +export type GetWorkspacesCurrentRbacWorkspaceAgentsAccessPolicyResponse = + GetWorkspacesCurrentRbacWorkspaceAgentsAccessPolicyResponses[keyof GetWorkspacesCurrentRbacWorkspaceAgentsAccessPolicyResponses] + export type PutWorkspacesCurrentRbacWorkspaceAppsAccessPoliciesByPolicyIdBindingsData = { body: ReplaceBindingsRequest path: { diff --git a/packages/contracts/generated/api/console/workspaces/zod.gen.ts b/packages/contracts/generated/api/console/workspaces/zod.gen.ts index fa7e8ee27c4..efec8abd342 100644 --- a/packages/contracts/generated/api/console/workspaces/zod.gen.ts +++ b/packages/contracts/generated/api/console/workspaces/zod.gen.ts @@ -373,31 +373,6 @@ export const zParserGithubUpload = z.object({ version: z.string(), }) -/** - * AccessPolicy - */ -export const zAccessPolicy = z.object({ - category: z.string().optional().default(''), - created_at: z.int().optional().default(0), - description: z.string().optional().default(''), - id: z.string(), - is_builtin: z.boolean().optional().default(false), - name: z.string(), - permission_keys: z.array(z.string()).optional(), - policy_key: z.string().optional().default(''), - resource_type: z.string(), - tenant_id: z.string().optional().default(''), - updated_at: z.int().optional().default(0), -}) - -/** - * AccessPolicyBindingState - */ -export const zAccessPolicyBindingState = z.object({ - binding_id: z.string(), - is_locked: z.boolean().optional().default(false), -}) - /** * _DeleteMemberBindingsRequest */ @@ -413,13 +388,6 @@ export const zReplaceUserAccessPolicies = z.object({ account_ids: z.array(z.string()).optional(), }) -/** - * ReplaceUserAccessPoliciesResponse - */ -export const zReplaceUserAccessPoliciesResponse = z.object({ - access_policies: z.array(zAccessPolicy).optional(), -}) - /** * ResourceWhitelist */ @@ -448,29 +416,6 @@ export const zReplaceMemberRolesRequest = z.object({ role_ids: z.array(z.string()).optional().default([]), }) -/** - * RBACRole - */ -export const zRbacRole = z.object({ - category: z.string().optional().default(''), - description: z.string().optional().default(''), - id: z.string(), - is_builtin: z.boolean().optional().default(false), - name: z.string(), - permission_keys: z.array(z.string()).optional(), - role_tag: z.string().optional().default(''), - tenant_id: z.string().nullish(), - type: z.string(), -}) - -/** - * MemberRolesResponse - */ -export const zMemberRolesResponse = z.object({ - account_id: z.string(), - roles: z.array(zRbacRole).optional(), -}) - /** * _ReplaceBindingsRequest */ @@ -1256,32 +1201,6 @@ export const zPluginPermissionResponse = z.object({ install_permission: zTenantPluginInstallPermission, }) -/** - * Pagination - */ -export const zPagination = z.object({ - current_page: z.int().optional().default(0), - per_page: z.int().optional().default(0), - total_count: z.int().optional().default(0), - total_pages: z.int().optional().default(0), -}) - -/** - * _AccessPolicyList - */ -export const zAccessPolicyList = z.object({ - data: z.array(zAccessPolicy).optional(), - pagination: zPagination.nullish(), -}) - -/** - * _RBACRoleList - */ -export const zRbacRoleList = z.object({ - data: z.array(zRbacRole).optional(), - pagination: zPagination.nullish(), -}) - /** * AccessPolicyMemberBinding */ @@ -1324,6 +1243,63 @@ export const zRoleBindingsResponse = z.object({ data: z.array(zAccessPolicyRoleBinding).optional(), }) +/** + * Pagination + */ +export const zPagination = z.object({ + current_page: z.int().optional().default(0), + per_page: z.int().optional().default(0), + total_count: z.int().optional().default(0), + total_pages: z.int().optional().default(0), +}) + +/** + * AccessPolicy + */ +export const zAccessPolicy = z.object({ + category: z.string().optional().default(''), + created_at: z.int().optional().default(0), + description: z.string().optional().default(''), + id: z.string(), + is_builtin: z.boolean().optional().default(false), + name: z.string(), + permission_keys: z.array(z.string()).optional(), + policy_key: z.string().optional().default(''), + resource_type: z.string(), + tenant_id: z.string().optional().default(''), + updated_at: z.int().optional().default(0), +}) + +/** + * ReplaceUserAccessPoliciesResponse + */ +export const zReplaceUserAccessPoliciesResponse = z.object({ + access_policies: z.array(zAccessPolicy).optional(), +}) + +/** + * RBACRole + */ +export const zRbacRole = z.object({ + category: z.string().optional().default(''), + description: z.string().optional().default(''), + id: z.string(), + is_builtin: z.boolean().optional().default(false), + name: z.string(), + permission_keys: z.array(z.string()).optional(), + role_tag: z.string().optional().default(''), + tenant_id: z.string().nullish(), + type: z.string(), +}) + +/** + * MemberRolesResponse + */ +export const zMemberRolesResponse = z.object({ + account_id: z.string(), + roles: z.array(zRbacRole).optional(), +}) + /** * WorkspacePermissionSnapshot */ @@ -1379,6 +1355,14 @@ export const zAccessMatrixItem = z.object({ roles: z.array(zAccessPolicyRole).optional(), }) +/** + * AgentAccessMatrix + */ +export const zAgentAccessMatrix = z.object({ + agent_id: z.string().optional().default(''), + items: z.array(zAccessMatrixItem).optional(), +}) + /** * AppAccessMatrix */ @@ -2553,6 +2537,7 @@ export const zResourcePermissionSnapshot = z.object({ * MyPermissionsResponse */ export const zMyPermissionsResponse = z.object({ + agent: zResourcePermissionSnapshot.optional(), app: zResourcePermissionSnapshot.optional(), dataset: zResourcePermissionSnapshot.optional(), workspace: zWorkspacePermissionSnapshot.optional(), @@ -4843,12 +4828,12 @@ export const zGetWorkspacesCurrentPluginByCategoryListResponse = zPluginCategory /** * Success */ -export const zGetWorkspacesCurrentRbacAccessPoliciesResponse = zAccessPolicyList +export const zGetWorkspacesCurrentRbacAccessPoliciesResponse = z.record(z.string(), z.unknown()) /** - * Policy created + * Success */ -export const zPostWorkspacesCurrentRbacAccessPoliciesResponse = zAccessPolicy +export const zPostWorkspacesCurrentRbacAccessPoliciesResponse = z.record(z.string(), z.unknown()) export const zDeleteWorkspacesCurrentRbacAccessPoliciesByPolicyIdPath = z.object({ policy_id: z.uuid(), @@ -4857,7 +4842,10 @@ export const zDeleteWorkspacesCurrentRbacAccessPoliciesByPolicyIdPath = z.object /** * Success */ -export const zDeleteWorkspacesCurrentRbacAccessPoliciesByPolicyIdResponse = zAccessPolicy +export const zDeleteWorkspacesCurrentRbacAccessPoliciesByPolicyIdResponse = z.record( + z.string(), + z.unknown(), +) export const zGetWorkspacesCurrentRbacAccessPoliciesByPolicyIdPath = z.object({ policy_id: z.uuid(), @@ -4866,7 +4854,10 @@ export const zGetWorkspacesCurrentRbacAccessPoliciesByPolicyIdPath = z.object({ /** * Success */ -export const zGetWorkspacesCurrentRbacAccessPoliciesByPolicyIdResponse = zAccessPolicy +export const zGetWorkspacesCurrentRbacAccessPoliciesByPolicyIdResponse = z.record( + z.string(), + z.unknown(), +) export const zPutWorkspacesCurrentRbacAccessPoliciesByPolicyIdPath = z.object({ policy_id: z.uuid(), @@ -4875,16 +4866,22 @@ export const zPutWorkspacesCurrentRbacAccessPoliciesByPolicyIdPath = z.object({ /** * Success */ -export const zPutWorkspacesCurrentRbacAccessPoliciesByPolicyIdResponse = zAccessPolicy +export const zPutWorkspacesCurrentRbacAccessPoliciesByPolicyIdResponse = z.record( + z.string(), + z.unknown(), +) export const zPostWorkspacesCurrentRbacAccessPoliciesByPolicyIdCopyPath = z.object({ policy_id: z.uuid(), }) /** - * Policy copied + * Success */ -export const zPostWorkspacesCurrentRbacAccessPoliciesByPolicyIdCopyResponse = zAccessPolicy +export const zPostWorkspacesCurrentRbacAccessPoliciesByPolicyIdCopyResponse = z.record( + z.string(), + z.unknown(), +) export const zPutWorkspacesCurrentRbacAccessPolicyBindingsByBindingIdLockPath = z.object({ binding_id: z.uuid(), @@ -4893,8 +4890,10 @@ export const zPutWorkspacesCurrentRbacAccessPolicyBindingsByBindingIdLockPath = /** * Success */ -export const zPutWorkspacesCurrentRbacAccessPolicyBindingsByBindingIdLockResponse = - zAccessPolicyBindingState +export const zPutWorkspacesCurrentRbacAccessPolicyBindingsByBindingIdLockResponse = z.record( + z.string(), + z.unknown(), +) export const zPutWorkspacesCurrentRbacAccessPolicyBindingsByBindingIdUnlockPath = z.object({ binding_id: z.uuid(), @@ -4903,8 +4902,124 @@ export const zPutWorkspacesCurrentRbacAccessPolicyBindingsByBindingIdUnlockPath /** * Success */ -export const zPutWorkspacesCurrentRbacAccessPolicyBindingsByBindingIdUnlockResponse = - zAccessPolicyBindingState +export const zPutWorkspacesCurrentRbacAccessPolicyBindingsByBindingIdUnlockResponse = z.record( + z.string(), + z.unknown(), +) + +export const zDeleteWorkspacesCurrentRbacAgentsByAgentIdAccessPoliciesByPolicyIdMemberBindingsBody = + zDeleteMemberBindingsRequest + +export const zDeleteWorkspacesCurrentRbacAgentsByAgentIdAccessPoliciesByPolicyIdMemberBindingsPath = + z.object({ + agent_id: z.uuid(), + policy_id: z.string(), + }) + +/** + * Success + */ +export const zDeleteWorkspacesCurrentRbacAgentsByAgentIdAccessPoliciesByPolicyIdMemberBindingsResponse = + zMemberBindingsResponse + +export const zGetWorkspacesCurrentRbacAgentsByAgentIdAccessPoliciesByPolicyIdMemberBindingsPath = + z.object({ + agent_id: z.uuid(), + policy_id: z.string(), + }) + +/** + * Success + */ +export const zGetWorkspacesCurrentRbacAgentsByAgentIdAccessPoliciesByPolicyIdMemberBindingsResponse = + zMemberBindingsResponse + +export const zGetWorkspacesCurrentRbacAgentsByAgentIdAccessPoliciesByPolicyIdRoleBindingsPath = + z.object({ + agent_id: z.uuid(), + policy_id: z.uuid(), + }) + +/** + * Success + */ +export const zGetWorkspacesCurrentRbacAgentsByAgentIdAccessPoliciesByPolicyIdRoleBindingsResponse = + zRoleBindingsResponse + +export const zGetWorkspacesCurrentRbacAgentsByAgentIdAccessPolicyPath = z.object({ + agent_id: z.uuid(), +}) + +export const zGetWorkspacesCurrentRbacAgentsByAgentIdAccessPolicyQuery = z.object({ + language: z.enum(['en', 'ja', 'zh']).optional(), +}) + +/** + * Success + */ +export const zGetWorkspacesCurrentRbacAgentsByAgentIdAccessPolicyResponse = zAgentAccessMatrix + +export const zGetWorkspacesCurrentRbacAgentsByAgentIdUserAccessPoliciesPath = z.object({ + agent_id: z.uuid(), +}) + +export const zGetWorkspacesCurrentRbacAgentsByAgentIdUserAccessPoliciesQuery = z.object({ + language: z.enum(['en', 'ja', 'zh']).optional(), + limit: z.int().gte(1).lte(99999).optional(), + page: z.int().gte(1).optional(), + reverse: z.boolean().optional(), +}) + +/** + * Success + */ +export const zGetWorkspacesCurrentRbacAgentsByAgentIdUserAccessPoliciesResponse = + zResourceUserAccessPoliciesResponse + +export const zPutWorkspacesCurrentRbacAgentsByAgentIdUsersByTargetAccountIdAccessPoliciesBody = + zReplaceUserAccessPolicies + +export const zPutWorkspacesCurrentRbacAgentsByAgentIdUsersByTargetAccountIdAccessPoliciesPath = + z.object({ + agent_id: z.uuid(), + target_account_id: z.uuid(), + }) + +/** + * Success + */ +export const zPutWorkspacesCurrentRbacAgentsByAgentIdUsersByTargetAccountIdAccessPoliciesResponse = + zReplaceUserAccessPoliciesResponse + +export const zGetWorkspacesCurrentRbacAgentsByAgentIdWhitelistPath = z.object({ + agent_id: z.uuid(), +}) + +/** + * Success + */ +export const zGetWorkspacesCurrentRbacAgentsByAgentIdWhitelistResponse = zResourceWhitelist + +export const zPutWorkspacesCurrentRbacAgentsByAgentIdWhitelistBody = zResourceAccessScopeRequest + +export const zPutWorkspacesCurrentRbacAgentsByAgentIdWhitelistPath = z.object({ + agent_id: z.uuid(), +}) + +/** + * Success + */ +export const zPutWorkspacesCurrentRbacAgentsByAgentIdWhitelistResponse = zResourceWhitelist + +export const zGetWorkspacesCurrentRbacAgentsByAgentIdWhitelistConfigPath = z.object({ + agent_id: z.uuid(), +}) + +/** + * Success + */ +export const zGetWorkspacesCurrentRbacAgentsByAgentIdWhitelistConfigResponse = + zResourceWhitelistConfig export const zDeleteWorkspacesCurrentRbacAppsByAppIdAccessPoliciesByPolicyIdMemberBindingsBody = zDeleteMemberBindingsRequest @@ -5163,6 +5278,12 @@ export const zGetWorkspacesCurrentRbacMyPermissionsResponse = zMyPermissionsResp */ export const zGetWorkspacesCurrentRbacRolePermissionsCatalogResponse = zPermissionCatalogResponse +/** + * Success + */ +export const zGetWorkspacesCurrentRbacRolePermissionsCatalogAgentResponse = + zPermissionCatalogResponse + /** * Success */ @@ -5177,12 +5298,12 @@ export const zGetWorkspacesCurrentRbacRolePermissionsCatalogDatasetResponse = /** * Success */ -export const zGetWorkspacesCurrentRbacRolesResponse = zRbacRoleList +export const zGetWorkspacesCurrentRbacRolesResponse = z.record(z.string(), z.unknown()) /** - * Role created + * Success */ -export const zPostWorkspacesCurrentRbacRolesResponse = zRbacRole +export const zPostWorkspacesCurrentRbacRolesResponse = z.record(z.string(), z.unknown()) export const zDeleteWorkspacesCurrentRbacRolesByRoleIdPath = z.object({ role_id: z.uuid(), @@ -5191,7 +5312,7 @@ export const zDeleteWorkspacesCurrentRbacRolesByRoleIdPath = z.object({ /** * Success */ -export const zDeleteWorkspacesCurrentRbacRolesByRoleIdResponse = zRbacRole +export const zDeleteWorkspacesCurrentRbacRolesByRoleIdResponse = z.record(z.string(), z.unknown()) export const zGetWorkspacesCurrentRbacRolesByRoleIdPath = z.object({ role_id: z.uuid(), @@ -5200,7 +5321,7 @@ export const zGetWorkspacesCurrentRbacRolesByRoleIdPath = z.object({ /** * Success */ -export const zGetWorkspacesCurrentRbacRolesByRoleIdResponse = zRbacRole +export const zGetWorkspacesCurrentRbacRolesByRoleIdResponse = z.record(z.string(), z.unknown()) export const zPutWorkspacesCurrentRbacRolesByRoleIdPath = z.object({ role_id: z.uuid(), @@ -5209,16 +5330,16 @@ export const zPutWorkspacesCurrentRbacRolesByRoleIdPath = z.object({ /** * Success */ -export const zPutWorkspacesCurrentRbacRolesByRoleIdResponse = zRbacRole +export const zPutWorkspacesCurrentRbacRolesByRoleIdResponse = z.record(z.string(), z.unknown()) export const zPostWorkspacesCurrentRbacRolesByRoleIdCopyPath = z.object({ role_id: z.uuid(), }) /** - * Role copied + * Success */ -export const zPostWorkspacesCurrentRbacRolesByRoleIdCopyResponse = zRbacRole +export const zPostWorkspacesCurrentRbacRolesByRoleIdCopyResponse = z.record(z.string(), z.unknown()) export const zGetWorkspacesCurrentRbacRolesByRoleIdMembersPath = z.object({ role_id: z.uuid(), @@ -5229,6 +5350,47 @@ export const zGetWorkspacesCurrentRbacRolesByRoleIdMembersPath = z.object({ */ export const zGetWorkspacesCurrentRbacRolesByRoleIdMembersResponse = zMembersInRoleList +export const zPutWorkspacesCurrentRbacWorkspaceAgentsAccessPoliciesByPolicyIdBindingsBody = + zReplaceBindingsRequest + +export const zPutWorkspacesCurrentRbacWorkspaceAgentsAccessPoliciesByPolicyIdBindingsPath = + z.object({ + policy_id: z.uuid(), + }) + +/** + * Success + */ +export const zPutWorkspacesCurrentRbacWorkspaceAgentsAccessPoliciesByPolicyIdBindingsResponse = + zAccessMatrixItem + +export const zGetWorkspacesCurrentRbacWorkspaceAgentsAccessPoliciesByPolicyIdMemberBindingsPath = + z.object({ + policy_id: z.uuid(), + }) + +/** + * Success + */ +export const zGetWorkspacesCurrentRbacWorkspaceAgentsAccessPoliciesByPolicyIdMemberBindingsResponse = + zMemberBindingsResponse + +export const zGetWorkspacesCurrentRbacWorkspaceAgentsAccessPoliciesByPolicyIdRoleBindingsPath = + z.object({ + policy_id: z.uuid(), + }) + +/** + * Success + */ +export const zGetWorkspacesCurrentRbacWorkspaceAgentsAccessPoliciesByPolicyIdRoleBindingsResponse = + zRoleBindingsResponse + +/** + * Success + */ +export const zGetWorkspacesCurrentRbacWorkspaceAgentsAccessPolicyResponse = zWorkspaceAccessMatrix + export const zPutWorkspacesCurrentRbacWorkspaceAppsAccessPoliciesByPolicyIdBindingsBody = zReplaceBindingsRequest diff --git a/web/app/(commonLayout)/__tests__/hydration-boundary.spec.tsx b/web/app/(commonLayout)/__tests__/hydration-boundary.spec.tsx index fb9ce499b03..2efd15cc830 100644 --- a/web/app/(commonLayout)/__tests__/hydration-boundary.spec.tsx +++ b/web/app/(commonLayout)/__tests__/hydration-boundary.spec.tsx @@ -107,7 +107,7 @@ describe('CommonLayoutHydrationBoundary', () => { credits: 200, }) mocks.permissionQueryFn.mockResolvedValue({ - workspace: { permission_keys: ['agent.manage'] }, + workspace: { permission_keys: ['agent.acl.preview'] }, app: { default_permission_keys: [], overrides: [] }, dataset: { default_permission_keys: [], overrides: [] }, }) diff --git a/web/app/(commonLayout)/agents/__tests__/agents-access-guard.spec.tsx b/web/app/(commonLayout)/agents/__tests__/agents-access-guard.spec.tsx index 14feed6124e..b7c4d8507d5 100644 --- a/web/app/(commonLayout)/agents/__tests__/agents-access-guard.spec.tsx +++ b/web/app/(commonLayout)/agents/__tests__/agents-access-guard.spec.tsx @@ -25,7 +25,7 @@ vi.mock('@/context/permission-state', async () => { vi.mock('@/features/agent-v2/permissions', () => { return { useCanManageAgents: () => - mockConsoleStateReader().workspacePermissionKeys.includes('agent.manage'), + mockConsoleStateReader().workspacePermissionKeys.includes('agent.acl.preview'), } }) @@ -41,7 +41,7 @@ type ConsoleStateFixture = { const baseContext: ConsoleStateFixture = { isLoadingCurrentWorkspace: false, isLoadingWorkspacePermissionKeys: false, - workspacePermissionKeys: ['agent.manage'], + workspacePermissionKeys: ['agent.acl.preview'], currentWorkspace: { id: 'workspace-1', }, @@ -88,7 +88,7 @@ describe('AgentsAccessGuard', () => { expect(mockReplace).not.toHaveBeenCalled() }) - it('redirects to /apps without agent.manage', async () => { + it('redirects to /apps without agent.acl.preview', async () => { setConsoleState({ workspacePermissionKeys: ['dataset.create_and_management'] }) render( @@ -103,7 +103,7 @@ describe('AgentsAccessGuard', () => { }) }) - it('renders children with agent.manage', () => { + it('renders children with agent.acl.preview', () => { render(
agents
diff --git a/web/app/components/main-nav/__tests__/index.spec.tsx b/web/app/components/main-nav/__tests__/index.spec.tsx index e5c22d4edbb..648b20861aa 100644 --- a/web/app/components/main-nav/__tests__/index.spec.tsx +++ b/web/app/components/main-nav/__tests__/index.spec.tsx @@ -464,7 +464,7 @@ const ownerWorkspacePermissionKeys = [ 'dataset.external.connect', 'tool.manage', 'mcp.manage', - 'agent.manage', + 'agent.acl.preview', 'skill.view', ] @@ -750,10 +750,12 @@ describe('MainNav', () => { ) }) - it('hides the roster entry when the user lacks agent.manage', () => { + it('hides the roster entry when the user lacks agent.acl.preview', () => { mockConsoleState.current = { ...consoleState, - workspacePermissionKeys: ownerWorkspacePermissionKeys.filter((key) => key !== 'agent.manage'), + workspacePermissionKeys: ownerWorkspacePermissionKeys.filter( + (key) => key !== 'agent.acl.preview', + ), } renderMainNav() @@ -761,7 +763,7 @@ describe('MainNav', () => { expect(screen.queryByRole('link', { name: /Agents/ })).not.toBeInTheDocument() }) - it('shows the roster entry when the user has agent.manage', () => { + it('shows the roster entry when the user has agent.acl.preview', () => { renderMainNav() expect(screen.getByRole('link', { name: /Agents/ })).toBeInTheDocument() @@ -898,7 +900,7 @@ describe('MainNav', () => { isCurrentWorkspaceEditor: false, isCurrentWorkspaceManager: false, isCurrentWorkspaceOwner: false, - workspacePermissionKeys: ['app_library.access', 'tool.manage', 'agent.manage'], + workspacePermissionKeys: ['app_library.access', 'tool.manage', 'agent.acl.preview'], } renderMainNav({ branding: { enabled: false } }) diff --git a/web/app/components/workflow/block-selector/__tests__/agent-selector.spec.tsx b/web/app/components/workflow/block-selector/__tests__/agent-selector.spec.tsx index 3b264940f17..4a0ed57107c 100644 --- a/web/app/components/workflow/block-selector/__tests__/agent-selector.spec.tsx +++ b/web/app/components/workflow/block-selector/__tests__/agent-selector.spec.tsx @@ -63,7 +63,7 @@ describe('AgentSelectorContent', () => { mocks.agents = [] }) - it('offers the Agent Console link with agent.manage', async () => { + it('offers the Agent Console link with agent.acl.preview', async () => { await renderSelector({ onStartFromScratch: vi.fn() }) const listbox = screen.getByRole('listbox') @@ -99,7 +99,7 @@ describe('AgentSelectorContent', () => { expect(listbox.querySelector('.overflow-y-auto')).not.toBeInTheDocument() }) - it('hides the Agent Console link without agent.manage', async () => { + it('hides the Agent Console link without agent.acl.preview', async () => { mocks.canManageAgents = false await renderSelector() @@ -107,7 +107,7 @@ describe('AgentSelectorContent', () => { expect(screen.queryByText(manageInConsoleLabel)).not.toBeInTheDocument() }) - it('keeps start from scratch without agent.manage', async () => { + it('keeps start from scratch without agent.acl.preview', async () => { mocks.canManageAgents = false await renderSelector({ onStartFromScratch: vi.fn() }) diff --git a/web/features/agent-v2/__tests__/permissions.spec.tsx b/web/features/agent-v2/__tests__/permissions.spec.tsx index f816cb6395c..e9d57067d5e 100644 --- a/web/features/agent-v2/__tests__/permissions.spec.tsx +++ b/web/features/agent-v2/__tests__/permissions.spec.tsx @@ -8,13 +8,16 @@ function PermissionProbe() { describe('useCanManageAgents', () => { it.each([ - [['agent.manage'], 'true'], + [['agent.acl.preview'], 'true'], [['dataset.create_and_management'], 'false'], - ])('resolves agent.manage from the current permission snapshot', (permissionKeys, expected) => { - const { wrapper } = createConsoleQueryWrapper({ workspacePermissionKeys: permissionKeys }) + ])( + 'resolves agent.acl.preview from the current permission snapshot', + (permissionKeys, expected) => { + const { wrapper } = createConsoleQueryWrapper({ workspacePermissionKeys: permissionKeys }) - render(, { wrapper }) + render(, { wrapper }) - expect(screen.getByText(expected)).toBeInTheDocument() - }) + expect(screen.getByText(expected)).toBeInTheDocument() + }, + ) }) diff --git a/web/features/agent-v2/permissions.ts b/web/features/agent-v2/permissions.ts index 7108f395015..322fd259f54 100644 --- a/web/features/agent-v2/permissions.ts +++ b/web/features/agent-v2/permissions.ts @@ -4,10 +4,10 @@ import { useAtomValue } from 'jotai' import { workspacePermissionKeysAtom } from '@/context/permission-state' import { hasPermission } from '@/utils/permission' -const AGENT_MANAGE_PERMISSION_KEY = 'agent.manage' +const AGENT_PREVIEW_PERMISSION_KEY = 'agent.acl.preview' export const useCanManageAgents = () => { const workspacePermissionKeys = useAtomValue(workspacePermissionKeysAtom) - return hasPermission(workspacePermissionKeys, AGENT_MANAGE_PERMISSION_KEY) + return hasPermission(workspacePermissionKeys, AGENT_PREVIEW_PERMISSION_KEY) } diff --git a/web/i18n/ar-TN/permission-keys.json b/web/i18n/ar-TN/permission-keys.json index 6877172382b..cfe89ac7394 100644 --- a/web/i18n/ar-TN/permission-keys.json +++ b/web/i18n/ar-TN/permission-keys.json @@ -1,5 +1,4 @@ { - "agent.manage": "إدارة Agents", "api_extension.manage": "إدارة إعدادات امتداد API", "app.access_config": "تكوين أذونات الوصول إلى التطبيق", "app.acl.access_config": "عرض أذونات الوصول وإدارتها", diff --git a/web/i18n/de-DE/permission-keys.json b/web/i18n/de-DE/permission-keys.json index ea97ede3c54..b2945cf4d7b 100644 --- a/web/i18n/de-DE/permission-keys.json +++ b/web/i18n/de-DE/permission-keys.json @@ -1,5 +1,4 @@ { - "agent.manage": "Agents verwalten", "api_extension.manage": "API-Erweiterungskonfiguration verwalten", "app.access_config": "App-Zugriffsberechtigungen konfigurieren", "app.acl.access_config": "Zugriffsberechtigungen anzeigen und verwalten", diff --git a/web/i18n/en-US/permission-keys.json b/web/i18n/en-US/permission-keys.json index a54789b2afd..f1a31c8bd7f 100644 --- a/web/i18n/en-US/permission-keys.json +++ b/web/i18n/en-US/permission-keys.json @@ -1,5 +1,16 @@ { - "agent.manage": "Manage Agents", + "agent.acl.access_config": "View and manage access permissions", + "agent.acl.access_point_manage": "Manage access points", + "agent.acl.access_point_view": "View access points", + "agent.acl.delete": "Delete agent", + "agent.acl.edit": "Edit agent information and configuration", + "agent.acl.import_export_dsl": "Import and export agent DSL", + "agent.acl.log_manage": "View and manage logs", + "agent.acl.monitor": "View agent monitoring", + "agent.acl.preview": "View agent basic information", + "agent.acl.release_and_version": "Publish and manage agent versions", + "agent.acl.test_and_run": "Test and run agent", + "agent.create": "Create agents", "api_extension.manage": "Manage API extension configuration", "app.access_config": "Configure app access permissions", "app.acl.access_config": "View and manage access permissions", diff --git a/web/i18n/en-US/permission.json b/web/i18n/en-US/permission.json index c1df7921340..82f7caa37e5 100644 --- a/web/i18n/en-US/permission.json +++ b/web/i18n/en-US/permission.json @@ -34,6 +34,7 @@ "accessRule.updated": "Access rule updated successfully", "accessRule.workspaceOwner": "Workspace Owner", "common.duplicateAction": "Duplicate", + "group.agent": "Agents", "group.app": "Applications", "group.app_acl": "App access permissions", "group.billing": "Billing", diff --git a/web/i18n/es-ES/permission-keys.json b/web/i18n/es-ES/permission-keys.json index 3a460e330da..c170a2f9ebe 100644 --- a/web/i18n/es-ES/permission-keys.json +++ b/web/i18n/es-ES/permission-keys.json @@ -1,5 +1,4 @@ { - "agent.manage": "Gestionar Agents", "api_extension.manage": "Gestionar la configuración de la extensión de API", "app.access_config": "Configurar los permisos de acceso de la app", "app.acl.access_config": "Ver y gestionar los permisos de acceso", diff --git a/web/i18n/fa-IR/permission-keys.json b/web/i18n/fa-IR/permission-keys.json index c6e56e6f32b..33d5898d6a7 100644 --- a/web/i18n/fa-IR/permission-keys.json +++ b/web/i18n/fa-IR/permission-keys.json @@ -1,5 +1,4 @@ { - "agent.manage": "مدیریت Agents", "api_extension.manage": "مدیریت پیکربندی افزونه API", "app.access_config": "پیکربندی مجوزهای دسترسی برنامه", "app.acl.access_config": "مشاهده و مدیریت مجوزهای دسترسی", diff --git a/web/i18n/fr-FR/permission-keys.json b/web/i18n/fr-FR/permission-keys.json index 5ad714f0111..ce183ff8adb 100644 --- a/web/i18n/fr-FR/permission-keys.json +++ b/web/i18n/fr-FR/permission-keys.json @@ -1,5 +1,4 @@ { - "agent.manage": "Gérer Agents", "api_extension.manage": "Gérer la configuration de l'extension API", "app.access_config": "Configurer les autorisations d'accès à l'application", "app.acl.access_config": "Afficher et gérer les autorisations d'accès", diff --git a/web/i18n/hi-IN/permission-keys.json b/web/i18n/hi-IN/permission-keys.json index fdf90ceb2fa..0d4c01259f4 100644 --- a/web/i18n/hi-IN/permission-keys.json +++ b/web/i18n/hi-IN/permission-keys.json @@ -1,5 +1,4 @@ { - "agent.manage": "Agents प्रबंधित करें", "api_extension.manage": "API एक्सटेंशन कॉन्फ़िगरेशन प्रबंधित करें", "app.access_config": "ऐप एक्सेस अनुमतियाँ कॉन्फ़िगर करें", "app.acl.access_config": "एक्सेस अनुमतियाँ देखें और प्रबंधित करें", diff --git a/web/i18n/id-ID/permission-keys.json b/web/i18n/id-ID/permission-keys.json index 85cf08e1f32..a365f445f49 100644 --- a/web/i18n/id-ID/permission-keys.json +++ b/web/i18n/id-ID/permission-keys.json @@ -1,5 +1,4 @@ { - "agent.manage": "Kelola Agents", "api_extension.manage": "Kelola konfigurasi ekstensi API", "app.access_config": "Konfigurasikan izin akses aplikasi", "app.acl.access_config": "Lihat dan kelola izin akses", diff --git a/web/i18n/it-IT/permission-keys.json b/web/i18n/it-IT/permission-keys.json index 341dd582413..6fc91ba36ba 100644 --- a/web/i18n/it-IT/permission-keys.json +++ b/web/i18n/it-IT/permission-keys.json @@ -1,5 +1,4 @@ { - "agent.manage": "Gestisci Agents", "api_extension.manage": "Gestisci la configurazione delle estensioni API", "app.access_config": "Configura i permessi di accesso all'app", "app.acl.access_config": "Visualizza e gestisci i permessi di accesso", diff --git a/web/i18n/ja-JP/permission-keys.json b/web/i18n/ja-JP/permission-keys.json index bd02a9a1c5b..ad60729a752 100644 --- a/web/i18n/ja-JP/permission-keys.json +++ b/web/i18n/ja-JP/permission-keys.json @@ -1,5 +1,4 @@ { - "agent.manage": "Agentsを管理", "api_extension.manage": "API拡張設定を管理", "app.access_config": "アプリアクセス権限を設定", "app.acl.access_config": "アクセス権限の表示と管理", diff --git a/web/i18n/ko-KR/permission-keys.json b/web/i18n/ko-KR/permission-keys.json index 6da7b9a9b62..15abb52f085 100644 --- a/web/i18n/ko-KR/permission-keys.json +++ b/web/i18n/ko-KR/permission-keys.json @@ -1,5 +1,4 @@ { - "agent.manage": "Agents 관리", "api_extension.manage": "API 확장 구성 관리", "app.access_config": "앱 접근 권한 구성", "app.acl.access_config": "접근 권한 보기 및 관리", diff --git a/web/i18n/lo-LA/permission-keys.json b/web/i18n/lo-LA/permission-keys.json index c2ce516b901..bfa9f1e93c6 100644 --- a/web/i18n/lo-LA/permission-keys.json +++ b/web/i18n/lo-LA/permission-keys.json @@ -1,5 +1,4 @@ { - "agent.manage": "ຈັດການຕົວແທນ (Agents)", "api_extension.manage": "ຈັດການການຕັ້ງຄ່າ API extension", "app.access_config": "ຕັ້ງຄ່າສິດການເຂົ້າເຖິງແອັບ", "app.acl.access_config": "ເບິ່ງ ແລະ ຈັດການສິດການເຂົ້າເຖິງ", diff --git a/web/i18n/nl-NL/permission-keys.json b/web/i18n/nl-NL/permission-keys.json index 5852e170200..1595e732fda 100644 --- a/web/i18n/nl-NL/permission-keys.json +++ b/web/i18n/nl-NL/permission-keys.json @@ -1,5 +1,4 @@ { - "agent.manage": "Agents beheren", "api_extension.manage": "API-extensieconfiguratie beheren", "app.access_config": "Toegangsrechten voor app configureren", "app.acl.access_config": "Toegangsrechten bekijken en beheren", diff --git a/web/i18n/pl-PL/permission-keys.json b/web/i18n/pl-PL/permission-keys.json index 703c97c38d7..b97ba5ca37d 100644 --- a/web/i18n/pl-PL/permission-keys.json +++ b/web/i18n/pl-PL/permission-keys.json @@ -1,5 +1,4 @@ { - "agent.manage": "Zarządzaj Agents", "api_extension.manage": "Zarządzaj konfiguracją rozszerzenia API", "app.access_config": "Konfiguruj uprawnienia dostępu do aplikacji", "app.acl.access_config": "Wyświetlaj uprawnienia dostępu i zarządzaj nimi", diff --git a/web/i18n/pt-BR/permission-keys.json b/web/i18n/pt-BR/permission-keys.json index 8399bbee918..71a796da289 100644 --- a/web/i18n/pt-BR/permission-keys.json +++ b/web/i18n/pt-BR/permission-keys.json @@ -1,5 +1,4 @@ { - "agent.manage": "Gerenciar Agents", "api_extension.manage": "Gerenciar configuração de extensão de API", "app.access_config": "Configurar permissões de acesso ao aplicativo", "app.acl.access_config": "Visualizar e gerenciar permissões de acesso", diff --git a/web/i18n/ro-RO/permission-keys.json b/web/i18n/ro-RO/permission-keys.json index 24ca726fe5b..bf5e080ba32 100644 --- a/web/i18n/ro-RO/permission-keys.json +++ b/web/i18n/ro-RO/permission-keys.json @@ -1,5 +1,4 @@ { - "agent.manage": "Gestionează Agents", "api_extension.manage": "Gestionează configurația extensiei API", "app.access_config": "Configurează permisiunile de acces ale aplicației", "app.acl.access_config": "Vizualizează și gestionează permisiunile de acces", diff --git a/web/i18n/ru-RU/permission-keys.json b/web/i18n/ru-RU/permission-keys.json index 2fe0215f9e0..6dcddb362bb 100644 --- a/web/i18n/ru-RU/permission-keys.json +++ b/web/i18n/ru-RU/permission-keys.json @@ -1,5 +1,4 @@ { - "agent.manage": "Управление Agents", "api_extension.manage": "Управление конфигурацией API-расширений", "app.access_config": "Настройка прав доступа к приложению", "app.acl.access_config": "Просмотр и управление правами доступа", diff --git a/web/i18n/sl-SI/permission-keys.json b/web/i18n/sl-SI/permission-keys.json index 27c234daf13..b65a6cf0a54 100644 --- a/web/i18n/sl-SI/permission-keys.json +++ b/web/i18n/sl-SI/permission-keys.json @@ -1,5 +1,4 @@ { - "agent.manage": "Upravljanje Agents", "api_extension.manage": "Upravljanje konfiguracije razširitve API", "app.access_config": "Konfiguracija dovoljenj za dostop do aplikacije", "app.acl.access_config": "Ogled in upravljanje dovoljenj za dostop", diff --git a/web/i18n/th-TH/permission-keys.json b/web/i18n/th-TH/permission-keys.json index 51ed8a1f8c3..3e3d914ea5c 100644 --- a/web/i18n/th-TH/permission-keys.json +++ b/web/i18n/th-TH/permission-keys.json @@ -1,5 +1,4 @@ { - "agent.manage": "จัดการ Agents", "api_extension.manage": "จัดการการกําหนดค่าส่วนขยาย API", "app.access_config": "กําหนดค่าสิทธิ์การเข้าถึงแอป", "app.acl.access_config": "ดูและจัดการสิทธิ์การเข้าถึง", diff --git a/web/i18n/tr-TR/permission-keys.json b/web/i18n/tr-TR/permission-keys.json index e4fcff7601f..ea6f9d9cc95 100644 --- a/web/i18n/tr-TR/permission-keys.json +++ b/web/i18n/tr-TR/permission-keys.json @@ -1,5 +1,4 @@ { - "agent.manage": "Agents'ı yönet", "api_extension.manage": "API uzantısı yapılandırmasını yönet", "app.access_config": "Uygulama erişim izinlerini yapılandır", "app.acl.access_config": "Erişim izinlerini görüntüle ve yönet", diff --git a/web/i18n/uk-UA/permission-keys.json b/web/i18n/uk-UA/permission-keys.json index c616bc6859e..a9d0d9fe4e8 100644 --- a/web/i18n/uk-UA/permission-keys.json +++ b/web/i18n/uk-UA/permission-keys.json @@ -1,5 +1,4 @@ { - "agent.manage": "Керування Agents", "api_extension.manage": "Керування конфігурацією розширення API", "app.access_config": "Налаштування дозволів доступу до застосунку", "app.acl.access_config": "Переглядати дозволи доступу та керувати ними", diff --git a/web/i18n/vi-VN/permission-keys.json b/web/i18n/vi-VN/permission-keys.json index 336e212f2e0..21777984f4a 100644 --- a/web/i18n/vi-VN/permission-keys.json +++ b/web/i18n/vi-VN/permission-keys.json @@ -1,5 +1,4 @@ { - "agent.manage": "Quản lý Agents", "api_extension.manage": "Quản lý cấu hình phần mở rộng API", "app.access_config": "Cấu hình quyền truy cập ứng dụng", "app.acl.access_config": "Xem và quản lý quyền truy cập", diff --git a/web/i18n/zh-Hans/permission-keys.json b/web/i18n/zh-Hans/permission-keys.json index a9c453ca182..5e338f847b3 100644 --- a/web/i18n/zh-Hans/permission-keys.json +++ b/web/i18n/zh-Hans/permission-keys.json @@ -1,5 +1,4 @@ { - "agent.manage": "管理 Agents", "api_extension.manage": "管理API扩展", "app.access_config": "配置应用访问权限", "app.acl.access_config": "查看与管理访问权限", diff --git a/web/i18n/zh-Hant/permission-keys.json b/web/i18n/zh-Hant/permission-keys.json index 88ce2c705c1..0114a588efa 100644 --- a/web/i18n/zh-Hant/permission-keys.json +++ b/web/i18n/zh-Hant/permission-keys.json @@ -1,5 +1,4 @@ { - "agent.manage": "管理 Agents", "api_extension.manage": "管理API擴充配置", "app.access_config": "配置應用訪問權限", "app.acl.access_config": "檢視與管理存取權限",