feat(rbac): agent RBAC/ACL scenes and resource-locator check bundles (#41768)

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
This commit is contained in:
Xiyuan Chen 2026-09-07 04:39:43 +00:00 committed by GitHub
parent 60e983c493
commit 86134ea99e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
152 changed files with 5360 additions and 3405 deletions

View File

@ -23,6 +23,7 @@ from .plugin import (
transform_datasource_credentials, transform_datasource_credentials,
) )
from .rbac import ( from .rbac import (
migrate_agent_permissions_to_rbac,
migrate_dataset_permissions_to_rbac, migrate_dataset_permissions_to_rbac,
migrate_member_roles_to_rbac, migrate_member_roles_to_rbac,
migrate_only_me_resource_whitelist_scopes_to_automatic_include, migrate_only_me_resource_whitelist_scopes_to_automatic_include,
@ -81,6 +82,7 @@ __all__ = [
"install_plugins", "install_plugins",
"install_rag_pipeline_plugins", "install_rag_pipeline_plugins",
"legacy_model_types", "legacy_model_types",
"migrate_agent_permissions_to_rbac",
"migrate_annotation_vector_database", "migrate_annotation_vector_database",
"migrate_data_for_plugin", "migrate_data_for_plugin",
"migrate_dataset_permissions_to_rbac", "migrate_dataset_permissions_to_rbac",

View File

@ -3,6 +3,8 @@ from __future__ import annotations
import json import json
from collections.abc import Iterator from collections.abc import Iterator
from concurrent.futures import ThreadPoolExecutor, as_completed from concurrent.futures import ThreadPoolExecutor, as_completed
from dataclasses import dataclass
from enum import StrEnum
import click import click
from sqlalchemy import select from sqlalchemy import select
@ -11,12 +13,34 @@ from sqlalchemy.orm import Session
from configs import dify_config from configs import dify_config
from core.db.session_factory import session_factory from core.db.session_factory import session_factory
from core.rbac import RBACResourceWhitelistScope from core.rbac import RBACResourceWhitelistScope
from models import App, Dataset, DatasetPermission, DatasetPermissionEnum, TenantAccountJoin, TenantAccountRole from models import (
from services.enterprise.rbac_service import ListOption, RBACService, ReplaceMemberBindings, ReplaceUserAccessPolicies 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_DEFAULT_ACCESS_POLICY_ID = "default"
_RBAC_RESOURCE_ACCESS_POLICY_BATCH_SIZE = 500 _RBAC_RESOURCE_ACCESS_POLICY_BATCH_SIZE = 500
_AGENT_MIGRATION_STATE_LABEL = {
False: "would change",
True: "changed",
}
_LEGACY_ROLE_TO_BUILTIN_TAG = { _LEGACY_ROLE_TO_BUILTIN_TAG = {
TenantAccountRole.OWNER.value: "owner", TenantAccountRole.OWNER.value: "owner",
TenantAccountRole.ADMIN.value: "admin", 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] 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( def _iter_tenant_member_batches(
tenant_id: str | None, tenant_id: str | None,
*, *,
@ -1005,3 +1111,302 @@ def migrate_dataset_permissions_to_rbac(
fg="green", 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"))

View File

@ -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",
]

View File

@ -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()

View File

@ -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)

View File

@ -1,181 +1,32 @@
from collections.abc import Callable from collections.abc import Callable
from functools import wraps from functools import wraps
from sqlalchemy import select
from werkzeug.exceptions import Forbidden, NotFound
from configs import dify_config from configs import dify_config
from core.rbac import RBACPermission, RBACResourceScope from controllers.common.rbac import RBAC_CHECKS_ATTR, RBACCheck, enforce_rbac_checks
from extensions.ext_database import db
from libs.login import current_account_with_tenant 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( def rbac_permission_required[**P, R](*checks: RBACCheck) -> Callable[[Callable[P, R]], Callable[P, R]]:
*, if not checks:
tenant_id: str, raise ValueError("rbac_permission_required requires at least one RBACCheck")
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 decorator(view: Callable[P, R]) -> Callable[P, R]: def decorator(view: Callable[P, R]) -> Callable[P, R]:
@wraps(view) @wraps(view, updated=())
def decorated(*args: P.args, **kwargs: P.kwargs) -> R: def decorated(*args: P.args, **kwargs: P.kwargs) -> R:
if not dify_config.RBAC_ENABLED: if not dify_config.RBAC_ENABLED:
return view(*args, **kwargs) return view(*args, **kwargs)
current_user, current_tenant_id = current_account_with_tenant() current_user, current_tenant_id = current_account_with_tenant()
enforce_rbac_access( enforce_rbac_checks(
tenant_id=current_tenant_id, tenant_id=current_tenant_id,
account_id=current_user.id, account_id=current_user.id,
resource_type=resource_type, checks=checks,
scene=scene,
resource_required=resource_required,
path_args=kwargs, path_args=kwargs,
) )
return view(*args, **kwargs) return view(*args, **kwargs)
setattr(decorated, RBAC_CHECKS_ATTR, checks)
return decorated return decorated
return decorator 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}")

View File

@ -4,13 +4,13 @@ from flask_restx import Resource
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from werkzeug.exceptions import NotFound 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.schema import query_params_from_model, register_response_schema_models, register_schema_models
from controllers.common.session import with_session from controllers.common.session import with_session
from controllers.console import console_ns 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 ( from controllers.console.wraps import (
RBACPermission, RBACPermission,
RBACResourceScope,
account_initialization_required, account_initialization_required,
edit_permission_required, edit_permission_required,
model_validate, model_validate,
@ -94,7 +94,7 @@ class WorkflowAgentComposerApi(Resource):
@login_required @login_required
@account_initialization_required @account_initialization_required
@edit_permission_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_user_id
@with_current_tenant_id @with_current_tenant_id
@with_session @with_session
@ -134,7 +134,7 @@ class WorkflowAgentComposerCopyFromRosterApi(Resource):
@login_required @login_required
@account_initialization_required @account_initialization_required
@edit_permission_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_user_id
@with_current_tenant_id @with_current_tenant_id
@with_session @with_session
@ -246,8 +246,8 @@ class WorkflowAgentComposerSaveToRosterApi(Resource):
@login_required @login_required
@account_initialization_required @account_initialization_required
@edit_permission_required @edit_permission_required
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_EDIT) @rbac_permission_required(RBACCheck(RBACPermission.APP_EDIT, PlainApp()))
@rbac_permission_required(RBACResourceScope.WORKSPACE, RBACPermission.AGENT_MANAGE, resource_required=False) @rbac_permission_required(RBACCheck(RBACPermission.AGENT_CREATE, Workspace()))
@with_current_user_id @with_current_user_id
@with_current_tenant_id @with_current_tenant_id
@with_session @with_session
@ -323,9 +323,7 @@ class SnippetAgentComposerApi(Resource):
@login_required @login_required
@account_initialization_required @account_initialization_required
@edit_permission_required @edit_permission_required
@rbac_permission_required( @rbac_permission_required(RBACCheck(RBACPermission.SNIPPETS_CREATE_AND_MODIFY, Workspace()))
RBACResourceScope.WORKSPACE, RBACPermission.SNIPPETS_CREATE_AND_MODIFY, resource_required=False
)
@with_current_user_id @with_current_user_id
@with_current_tenant_id @with_current_tenant_id
@with_session @with_session
@ -362,9 +360,7 @@ class SnippetAgentComposerCopyFromRosterApi(Resource):
@login_required @login_required
@account_initialization_required @account_initialization_required
@edit_permission_required @edit_permission_required
@rbac_permission_required( @rbac_permission_required(RBACCheck(RBACPermission.SNIPPETS_CREATE_AND_MODIFY, Workspace()))
RBACResourceScope.WORKSPACE, RBACPermission.SNIPPETS_CREATE_AND_MODIFY, resource_required=False
)
@with_current_user_id @with_current_user_id
@with_current_tenant_id @with_current_tenant_id
@with_session @with_session
@ -476,10 +472,8 @@ class SnippetAgentComposerSaveToRosterApi(Resource):
@login_required @login_required
@account_initialization_required @account_initialization_required
@edit_permission_required @edit_permission_required
@rbac_permission_required( @rbac_permission_required(RBACCheck(RBACPermission.SNIPPETS_CREATE_AND_MODIFY, Workspace()))
RBACResourceScope.WORKSPACE, RBACPermission.SNIPPETS_CREATE_AND_MODIFY, resource_required=False @rbac_permission_required(RBACCheck(RBACPermission.AGENT_CREATE, Workspace()))
)
@rbac_permission_required(RBACResourceScope.WORKSPACE, RBACPermission.AGENT_MANAGE, resource_required=False)
@with_current_user_id @with_current_user_id
@with_current_tenant_id @with_current_tenant_id
@with_session @with_session
@ -512,7 +506,7 @@ class AgentComposerApi(Resource):
@setup_required @setup_required
@login_required @login_required
@account_initialization_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_current_tenant_id
@with_session @with_session
def get(self, session: Session, tenant_id: str, agent_id: UUID): def get(self, session: Session, tenant_id: str, agent_id: UUID):
@ -527,7 +521,7 @@ class AgentComposerApi(Resource):
@login_required @login_required
@account_initialization_required @account_initialization_required
@edit_permission_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_user_id
@with_current_tenant_id @with_current_tenant_id
@with_session @with_session
@ -554,6 +548,7 @@ class AgentComposerValidateApi(Resource):
@setup_required @setup_required
@login_required @login_required
@account_initialization_required @account_initialization_required
@rbac_permission_required(RBACCheck(RBACPermission.AGENT_PREVIEW, AgentId()))
@with_current_tenant_id @with_current_tenant_id
@with_session @with_session
@model_validate(ComposerSavePayload) @model_validate(ComposerSavePayload)
@ -575,6 +570,7 @@ class AgentComposerCandidatesApi(Resource):
@setup_required @setup_required
@login_required @login_required
@account_initialization_required @account_initialization_required
@rbac_permission_required(RBACCheck(RBACPermission.AGENT_PREVIEW, AgentId()))
@with_current_user_id @with_current_user_id
@with_current_tenant_id @with_current_tenant_id
@with_session(write=False) @with_session(write=False)

View File

@ -7,6 +7,7 @@ from pydantic import AliasChoices, BaseModel, Field, field_validator
from sqlalchemy import func, or_, select from sqlalchemy import func, or_, select
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from controllers.common.rbac import AgentId, RBACCheck, Workspace
from controllers.common.schema import ( from controllers.common.schema import (
query_params_from_model, query_params_from_model,
query_params_from_request, query_params_from_request,
@ -32,10 +33,8 @@ from controllers.console.app.app import (
from controllers.console.app.app import ( from controllers.console.app.app import (
UpdateAppPayload as GenericUpdateAppPayload, UpdateAppPayload as GenericUpdateAppPayload,
) )
from controllers.console.app.wraps import agent_manage_required_for_agent_app
from controllers.console.wraps import ( from controllers.console.wraps import (
RBACPermission, RBACPermission,
RBACResourceScope,
account_initialization_required, account_initialization_required,
edit_permission_required, edit_permission_required,
enterprise_license_required, enterprise_license_required,
@ -608,7 +607,7 @@ class AgentAppListApi(Resource):
@setup_required @setup_required
@login_required @login_required
@account_initialization_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_user
@with_current_tenant_id @with_current_tenant_id
@with_session @with_session
@ -668,7 +667,7 @@ class AgentAppListApi(Resource):
@login_required @login_required
@account_initialization_required @account_initialization_required
@edit_permission_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_user
@with_current_tenant_id @with_current_tenant_id
@with_session @with_session
@ -694,7 +693,7 @@ class AgentAppApi(Resource):
@setup_required @setup_required
@login_required @login_required
@account_initialization_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 @enterprise_license_required
@with_current_user @with_current_user
@with_current_tenant_id @with_current_tenant_id
@ -711,7 +710,7 @@ class AgentAppApi(Resource):
@login_required @login_required
@account_initialization_required @account_initialization_required
@edit_permission_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_user
@with_current_tenant_id @with_current_tenant_id
@with_session @with_session
@ -744,7 +743,7 @@ class AgentAppApi(Resource):
@login_required @login_required
@account_initialization_required @account_initialization_required
@edit_permission_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_current_tenant_id
@with_session @with_session
def delete(self, session: Session, tenant_id: str, agent_id: UUID): def delete(self, session: Session, tenant_id: str, agent_id: UUID):
@ -765,6 +764,7 @@ class AgentDebugConversationRefreshApi(Resource):
@login_required @login_required
@account_initialization_required @account_initialization_required
@edit_permission_required @edit_permission_required
@rbac_permission_required(RBACCheck(RBACPermission.AGENT_TEST_AND_RUN, AgentId()))
@with_current_user @with_current_user
@with_current_tenant_id @with_current_tenant_id
@with_session @with_session
@ -790,7 +790,7 @@ class AgentPublishApi(Resource):
@login_required @login_required
@account_initialization_required @account_initialization_required
@edit_permission_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_user
@with_current_tenant_id @with_current_tenant_id
@with_session @with_session
@ -820,7 +820,7 @@ class AgentBuildDraftCheckoutApi(Resource):
@login_required @login_required
@account_initialization_required @account_initialization_required
@edit_permission_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_user
@with_current_tenant_id @with_current_tenant_id
@with_session(write=False) @with_session(write=False)
@ -850,6 +850,7 @@ class AgentBuildDraftApi(Resource):
@login_required @login_required
@account_initialization_required @account_initialization_required
@edit_permission_required @edit_permission_required
@rbac_permission_required(RBACCheck(RBACPermission.AGENT_PREVIEW, AgentId()))
@with_current_user @with_current_user
@with_current_tenant_id @with_current_tenant_id
@with_session(write=False) @with_session(write=False)
@ -867,6 +868,7 @@ class AgentBuildDraftApi(Resource):
@login_required @login_required
@account_initialization_required @account_initialization_required
@edit_permission_required @edit_permission_required
@rbac_permission_required(RBACCheck(RBACPermission.AGENT_EDIT, AgentId()))
@with_current_user @with_current_user
@with_current_tenant_id @with_current_tenant_id
@with_session @with_session
@ -892,6 +894,7 @@ class AgentBuildDraftApi(Resource):
@login_required @login_required
@account_initialization_required @account_initialization_required
@edit_permission_required @edit_permission_required
@rbac_permission_required(RBACCheck(RBACPermission.AGENT_EDIT, AgentId()))
@with_current_user @with_current_user
@with_current_tenant_id @with_current_tenant_id
@with_session(write=False) @with_session(write=False)
@ -911,7 +914,7 @@ class AgentBuildDraftApplyApi(Resource):
@login_required @login_required
@account_initialization_required @account_initialization_required
@edit_permission_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_user
@with_current_tenant_id @with_current_tenant_id
@with_session(write=False) @with_session(write=False)
@ -934,7 +937,8 @@ class AgentAppCopyApi(Resource):
@login_required @login_required
@account_initialization_required @account_initialization_required
@edit_permission_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_user
@with_current_tenant_id @with_current_tenant_id
@with_session @with_session
@ -967,7 +971,7 @@ class AgentApiAccessApi(Resource):
@setup_required @setup_required
@login_required @login_required
@account_initialization_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_current_tenant_id
@with_session(write=False) @with_session(write=False)
def get(self, session: Session, tenant_id: str, agent_id: UUID): def get(self, session: Session, tenant_id: str, agent_id: UUID):
@ -984,7 +988,7 @@ class AgentApiStatusApi(Resource):
@login_required @login_required
@is_admin_or_owner_required @is_admin_or_owner_required
@account_initialization_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_current_tenant_id
@with_session @with_session
@model_validate(AgentApiStatusPayload) @model_validate(AgentApiStatusPayload)
@ -1002,7 +1006,7 @@ class AgentApiKeyListApi(BaseApiKeyListResource):
token_prefix = "app-" token_prefix = "app-"
@console_ns.response(200, "Agent service API keys", console_ns.models[ApiKeyList.__name__]) @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 @with_current_tenant_id
@edit_permission_required @edit_permission_required
@with_session(write=False) @with_session(write=False)
@ -1014,7 +1018,7 @@ class AgentApiKeyListApi(BaseApiKeyListResource):
@console_ns.response(400, "Maximum keys exceeded") @console_ns.response(400, "Maximum keys exceeded")
@with_current_tenant_id @with_current_tenant_id
@edit_permission_required @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 @with_session
def post(self, session: Session, tenant_id: str, agent_id: UUID) -> tuple[dict[str, object], int]: 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) 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") @console_ns.response(204, "Agent service API key deleted")
@with_current_user @with_current_user
@with_current_tenant_id @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 @with_session
def delete( def delete(
self, self,
@ -1055,6 +1059,7 @@ class AgentInviteOptionsApi(Resource):
@setup_required @setup_required
@login_required @login_required
@account_initialization_required @account_initialization_required
@rbac_permission_required(RBACCheck(RBACPermission.AGENT_PREVIEW, Workspace()))
@with_current_tenant_id @with_current_tenant_id
@with_session(write=False) @with_session(write=False)
@model_validate(AgentInviteOptionsQuery) @model_validate(AgentInviteOptionsQuery)
@ -1078,7 +1083,7 @@ class AgentLogsApi(Resource):
@setup_required @setup_required
@login_required @login_required
@account_initialization_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_user
@with_current_tenant_id @with_current_tenant_id
@with_session(write=False) @with_session(write=False)
@ -1117,7 +1122,7 @@ class AgentLogMessagesApi(Resource):
@setup_required @setup_required
@login_required @login_required
@account_initialization_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_user
@with_current_tenant_id @with_current_tenant_id
@with_session(write=False) @with_session(write=False)
@ -1156,7 +1161,7 @@ class AgentLogSourcesApi(Resource):
@setup_required @setup_required
@login_required @login_required
@account_initialization_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_user
@with_current_tenant_id @with_current_tenant_id
@with_session(write=False) @with_session(write=False)
@ -1177,7 +1182,7 @@ class AgentStatisticsSummaryApi(Resource):
@setup_required @setup_required
@login_required @login_required
@account_initialization_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_user
@with_current_tenant_id @with_current_tenant_id
@with_session(write=False) @with_session(write=False)
@ -1210,7 +1215,7 @@ class AgentRosterVersionsApi(Resource):
@setup_required @setup_required
@login_required @login_required
@account_initialization_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_current_tenant_id
@with_session(write=False) @with_session(write=False)
def get(self, session: Session, tenant_id: str, agent_id: UUID): def get(self, session: Session, tenant_id: str, agent_id: UUID):
@ -1226,7 +1231,7 @@ class AgentRosterVersionDetailApi(Resource):
@setup_required @setup_required
@login_required @login_required
@account_initialization_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_current_tenant_id
@with_session(write=False) @with_session(write=False)
def get(self, session: Session, tenant_id: str, agent_id: UUID, version_id: UUID): def get(self, session: Session, tenant_id: str, agent_id: UUID, version_id: UUID):
@ -1247,7 +1252,7 @@ class AgentRosterVersionRestoreApi(Resource):
@login_required @login_required
@account_initialization_required @account_initialization_required
@edit_permission_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_user
@with_current_tenant_id @with_current_tenant_id
@with_session @with_session

View File

@ -11,9 +11,9 @@ from sqlalchemy.orm import Session
from werkzeug.exceptions import Forbidden from werkzeug.exceptions import Forbidden
from configs import dify_config 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.schema import register_response_schema_models
from controllers.common.session import with_session 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 fields.base import ResponseModel
from libs.helper import dump_response, to_timestamp from libs.helper import dump_response, to_timestamp
from libs.login import login_required from libs.login import login_required
@ -27,7 +27,6 @@ from services.app_service import AppService
from . import console_ns from . import console_ns
from .wraps import ( from .wraps import (
RBACPermission, RBACPermission,
RBACResourceScope,
account_initialization_required, account_initialization_required,
edit_permission_required, edit_permission_required,
rbac_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__]) @console_ns.response(200, "API keys retrieved successfully", console_ns.models[ApiKeyList.__name__])
@with_current_tenant_id @with_current_tenant_id
@edit_permission_required @edit_permission_required
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_RELEASE_AND_VERSION) @rbac_permission_required(
@agent_manage_required_for_agent_app RBACCheck(RBACPermission.APP_RELEASE_AND_VERSION, PlainApp("resource_id")),
RBACCheck(RBACPermission.AGENT_ACCESS_POINT_VIEW, AgentBehindApp("resource_id")),
)
@with_session(write=False) @with_session(write=False)
def get(self, session: Session, current_tenant_id: str, resource_id: UUID) -> dict[str, object]: def get(self, session: Session, current_tenant_id: str, resource_id: UUID) -> dict[str, object]:
"""Get all API keys for an app""" """Get all API keys for an app"""
@ -259,8 +260,10 @@ class AppApiKeyListResource(BaseApiKeyListResource):
@console_ns.response(400, "Maximum keys exceeded") @console_ns.response(400, "Maximum keys exceeded")
@with_current_tenant_id @with_current_tenant_id
@edit_permission_required @edit_permission_required
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_RELEASE_AND_VERSION) @rbac_permission_required(
@agent_manage_required_for_agent_app RBACCheck(RBACPermission.APP_RELEASE_AND_VERSION, PlainApp("resource_id")),
RBACCheck(RBACPermission.AGENT_ACCESS_POINT_MANAGE, AgentBehindApp("resource_id")),
)
@with_session @with_session
def post(self, session: Session, current_tenant_id: str, resource_id: UUID) -> tuple[dict[str, object], int]: 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""" """Create a new API key for an app"""
@ -283,8 +286,10 @@ class AppApiKeyResource(BaseApiKeyResource):
@console_ns.response(204, "API key deleted successfully") @console_ns.response(204, "API key deleted successfully")
@with_current_user @with_current_user
@with_current_tenant_id @with_current_tenant_id
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_RELEASE_AND_VERSION) @rbac_permission_required(
@agent_manage_required_for_agent_app RBACCheck(RBACPermission.APP_RELEASE_AND_VERSION, PlainApp("resource_id")),
RBACCheck(RBACPermission.AGENT_ACCESS_POINT_MANAGE, AgentBehindApp("resource_id")),
)
@with_session @with_session
def delete( def delete(
self, self,
@ -321,7 +326,7 @@ class DatasetApiKeyListResource(BaseApiKeyListResource):
@console_ns.response(200, "API keys retrieved successfully", console_ns.models[ApiKeyList.__name__]) @console_ns.response(200, "API keys retrieved successfully", console_ns.models[ApiKeyList.__name__])
@with_current_tenant_id @with_current_tenant_id
@edit_permission_required @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) @with_session(write=False)
def get(self, session: Session, current_tenant_id: str, resource_id: UUID) -> dict[str, object]: def get(self, session: Session, current_tenant_id: str, resource_id: UUID) -> dict[str, object]:
"""Get all API keys for a dataset""" """Get all API keys for a dataset"""
@ -337,7 +342,7 @@ class DatasetApiKeyListResource(BaseApiKeyListResource):
@console_ns.response(400, "Maximum keys exceeded") @console_ns.response(400, "Maximum keys exceeded")
@with_current_tenant_id @with_current_tenant_id
@edit_permission_required @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 @with_session
def post(self, session: Session, current_tenant_id: str, resource_id: UUID) -> tuple[dict[str, object], int]: 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""" """Create a new API key for a dataset"""
@ -360,7 +365,7 @@ class DatasetApiKeyResource(BaseApiKeyResource):
@console_ns.response(204, "API key deleted successfully") @console_ns.response(204, "API key deleted successfully")
@with_current_user @with_current_user
@with_current_tenant_id @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 @with_session
def delete( def delete(
self, self,

View File

@ -4,13 +4,13 @@ from flask_restx import Resource
from pydantic import BaseModel, Field, field_validator from pydantic import BaseModel, Field, field_validator
from sqlalchemy.orm import Session 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.schema import query_params_from_model, register_response_schema_models
from controllers.common.session import with_session from controllers.common.session import with_session
from controllers.console import console_ns from controllers.console import console_ns
from controllers.console.app.wraps import get_app_model from controllers.console.app.wraps import get_app_model
from controllers.console.wraps import ( from controllers.console.wraps import (
RBACPermission, RBACPermission,
RBACResourceScope,
account_initialization_required, account_initialization_required,
model_validate, model_validate,
rbac_permission_required, rbac_permission_required,
@ -84,7 +84,7 @@ class AgentLogApi(Resource):
@setup_required @setup_required
@login_required @login_required
@account_initialization_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) @with_session(write=False)
@get_app_model(mode=[AppMode.AGENT_CHAT]) @get_app_model(mode=[AppMode.AGENT_CHAT])
@model_validate(AgentLogQuery) @model_validate(AgentLogQuery)

View File

@ -11,11 +11,18 @@ from flask_restx import Resource
from pydantic import Field from pydantic import Field
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from controllers.common.rbac import AgentId, RBACCheck
from controllers.common.schema import register_response_schema_models from controllers.common.schema import register_response_schema_models
from controllers.common.session import with_session from controllers.common.session import with_session
from controllers.console import console_ns from controllers.console import console_ns
from controllers.console.agent.app_helpers import resolve_agent_app_model 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 fields.base import ResponseModel
from libs.login import login_required from libs.login import login_required
from services.agent.roster_service import AgentRosterService from services.agent.roster_service import AgentRosterService
@ -55,6 +62,7 @@ class AgentAppReferencingWorkflowsResource(Resource):
@setup_required @setup_required
@login_required @login_required
@account_initialization_required @account_initialization_required
@rbac_permission_required(RBACCheck(RBACPermission.AGENT_PREVIEW, AgentId()))
@with_current_tenant_id @with_current_tenant_id
@with_session(write=False) @with_session(write=False)
def get(self, session: Session, tenant_id: str, agent_id: UUID): def get(self, session: Session, tenant_id: str, agent_id: UUID):

View File

@ -16,13 +16,13 @@ from pydantic import BaseModel, Field
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from controllers.common.fields import SimpleResultResponse 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.schema import register_response_schema_models, register_schema_models
from controllers.common.session import with_session from controllers.common.session import with_session
from controllers.console import console_ns from controllers.console import console_ns
from controllers.console.agent.app_helpers import resolve_agent_runtime_app_model from controllers.console.agent.app_helpers import resolve_agent_runtime_app_model
from controllers.console.wraps import ( from controllers.console.wraps import (
RBACPermission, RBACPermission,
RBACResourceScope,
account_initialization_required, account_initialization_required,
edit_permission_required, edit_permission_required,
model_validate, model_validate,
@ -83,7 +83,7 @@ class AgentAppFeatureConfigResource(Resource):
@setup_required @setup_required
@login_required @login_required
@edit_permission_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 @account_initialization_required
@with_current_user @with_current_user
@with_current_tenant_id @with_current_tenant_id

View File

@ -15,6 +15,7 @@ from flask_restx import Resource
from pydantic import BaseModel, Field from pydantic import BaseModel, Field
from clients.agent_backend.errors import backend_error_detail, backend_reported_failure 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 ( from controllers.common.schema import (
query_params_from_model, query_params_from_model,
query_params_from_request, query_params_from_request,
@ -23,10 +24,9 @@ from controllers.common.schema import (
) )
from controllers.console import console_ns from controllers.console import console_ns
from controllers.console.app.error import AppNotFoundError 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 ( from controllers.console.wraps import (
RBACPermission, RBACPermission,
RBACResourceScope,
account_initialization_required, account_initialization_required,
model_validate, model_validate,
rbac_permission_required, rbac_permission_required,
@ -153,7 +153,7 @@ class AgentAppSandboxInfoResource(Resource):
@setup_required @setup_required
@login_required @login_required
@account_initialization_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_tenant_id
@with_current_user @with_current_user
def get(self, current_user: Account, tenant_id: str, agent_id: UUID): def get(self, current_user: Account, tenant_id: str, agent_id: UUID):
@ -183,7 +183,7 @@ class AgentAppSandboxListResource(Resource):
@setup_required @setup_required
@login_required @login_required
@account_initialization_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_tenant_id
@with_current_user @with_current_user
def get(self, current_user: Account, tenant_id: str, agent_id: UUID): def get(self, current_user: Account, tenant_id: str, agent_id: UUID):
@ -214,7 +214,7 @@ class AgentAppSandboxReadResource(Resource):
@setup_required @setup_required
@login_required @login_required
@account_initialization_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_tenant_id
@with_current_user @with_current_user
def get(self, current_user: Account, tenant_id: str, agent_id: UUID): def get(self, current_user: Account, tenant_id: str, agent_id: UUID):
@ -245,7 +245,7 @@ class AgentAppSandboxDownloadResource(Resource):
@setup_required @setup_required
@login_required @login_required
@account_initialization_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_tenant_id
@with_current_user @with_current_user
@model_validate(AgentSandboxDownloadPayload) @model_validate(AgentSandboxDownloadPayload)
@ -289,7 +289,7 @@ class WorkflowAgentSandboxListResource(Resource):
@setup_required @setup_required
@login_required @login_required
@account_initialization_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]) @get_app_model(mode=[AppMode.ADVANCED_CHAT, AppMode.WORKFLOW])
@with_current_tenant_id @with_current_tenant_id
def get(self, tenant_id: str, app_model: App, workflow_run_id: UUID, node_id: str): 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 @setup_required
@login_required @login_required
@account_initialization_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]) @get_app_model(mode=[AppMode.ADVANCED_CHAT, AppMode.WORKFLOW])
@with_current_tenant_id @with_current_tenant_id
def get(self, tenant_id: str, app_model: App, workflow_run_id: UUID, node_id: str): 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 @setup_required
@login_required @login_required
@account_initialization_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_user
@with_current_tenant_id @with_current_tenant_id
@model_validate(WorkflowAgentSandboxDownloadPayload) @model_validate(WorkflowAgentSandboxDownloadPayload)

View File

@ -15,6 +15,7 @@ from flask_restx import Resource
from pydantic import BaseModel, Field from pydantic import BaseModel, Field
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from controllers.common.rbac import AgentId, PlainApp, RBACCheck
from controllers.common.schema import ( from controllers.common.schema import (
query_params_from_model, query_params_from_model,
query_params_from_request, 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.app.wraps import get_app_model
from controllers.console.wraps import ( from controllers.console.wraps import (
RBACPermission, RBACPermission,
RBACResourceScope,
account_initialization_required, account_initialization_required,
edit_permission_required, edit_permission_required,
model_validate, model_validate,
@ -659,6 +659,7 @@ class AgentConfigManifestByAgentApi(Resource):
@setup_required @setup_required
@login_required @login_required
@account_initialization_required @account_initialization_required
@rbac_permission_required(RBACCheck(RBACPermission.AGENT_PREVIEW, AgentId()))
@with_current_user @with_current_user
@with_current_tenant_id @with_current_tenant_id
@with_session @with_session
@ -704,6 +705,7 @@ class AgentConfigSkillUploadByAgentApi(Resource):
@setup_required @setup_required
@login_required @login_required
@account_initialization_required @account_initialization_required
@rbac_permission_required(RBACCheck(RBACPermission.AGENT_EDIT, AgentId()))
@with_current_user @with_current_user
@with_current_tenant_id @with_current_tenant_id
@with_session @with_session
@ -733,7 +735,7 @@ class AgentConfigSkillUploadApi(Resource):
@login_required @login_required
@account_initialization_required @account_initialization_required
@edit_permission_required @edit_permission_required
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_EDIT) @rbac_permission_required(RBACCheck(RBACPermission.APP_EDIT, PlainApp()))
@with_current_user @with_current_user
@with_session @with_session
@get_app_model(mode=_WORKFLOW_APP_MODES) @get_app_model(mode=_WORKFLOW_APP_MODES)
@ -751,6 +753,7 @@ class AgentConfigSkillsByAgentApi(Resource):
@setup_required @setup_required
@login_required @login_required
@account_initialization_required @account_initialization_required
@rbac_permission_required(RBACCheck(RBACPermission.AGENT_PREVIEW, AgentId()))
@with_current_user @with_current_user
@with_current_tenant_id @with_current_tenant_id
@with_session @with_session
@ -789,6 +792,7 @@ class AgentConfigFilesByAgentApi(Resource):
@setup_required @setup_required
@login_required @login_required
@account_initialization_required @account_initialization_required
@rbac_permission_required(RBACCheck(RBACPermission.AGENT_PREVIEW, AgentId()))
@with_current_user @with_current_user
@with_current_tenant_id @with_current_tenant_id
@with_session @with_session
@ -808,6 +812,7 @@ class AgentConfigFilesByAgentApi(Resource):
@setup_required @setup_required
@login_required @login_required
@account_initialization_required @account_initialization_required
@rbac_permission_required(RBACCheck(RBACPermission.AGENT_EDIT, AgentId()))
@with_current_user @with_current_user
@with_current_tenant_id @with_current_tenant_id
@with_session @with_session
@ -853,7 +858,7 @@ class AgentConfigFilesApi(Resource):
@login_required @login_required
@account_initialization_required @account_initialization_required
@edit_permission_required @edit_permission_required
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_EDIT) @rbac_permission_required(RBACCheck(RBACPermission.APP_EDIT, PlainApp()))
@with_current_user @with_current_user
@with_session @with_session
@get_app_model(mode=_WORKFLOW_APP_MODES) @get_app_model(mode=_WORKFLOW_APP_MODES)
@ -877,6 +882,7 @@ class AgentConfigSkillInspectByAgentApi(Resource):
@setup_required @setup_required
@login_required @login_required
@account_initialization_required @account_initialization_required
@rbac_permission_required(RBACCheck(RBACPermission.AGENT_PREVIEW, AgentId()))
@with_current_user @with_current_user
@with_current_tenant_id @with_current_tenant_id
@with_session @with_session
@ -928,6 +934,7 @@ class AgentConfigSkillFilePreviewByAgentApi(Resource):
@setup_required @setup_required
@login_required @login_required
@account_initialization_required @account_initialization_required
@rbac_permission_required(RBACCheck(RBACPermission.AGENT_PREVIEW, AgentId()))
@with_current_user @with_current_user
@with_current_tenant_id @with_current_tenant_id
@with_session @with_session
@ -988,6 +995,7 @@ class AgentConfigSkillDownloadByAgentApi(Resource):
@setup_required @setup_required
@login_required @login_required
@account_initialization_required @account_initialization_required
@rbac_permission_required(RBACCheck(RBACPermission.AGENT_PREVIEW, AgentId()))
@with_current_user @with_current_user
@with_current_tenant_id @with_current_tenant_id
@with_session @with_session
@ -1037,6 +1045,7 @@ class AgentConfigSkillFileDownloadByAgentApi(Resource):
@setup_required @setup_required
@login_required @login_required
@account_initialization_required @account_initialization_required
@rbac_permission_required(RBACCheck(RBACPermission.AGENT_PREVIEW, AgentId()))
@with_current_user @with_current_user
@with_current_tenant_id @with_current_tenant_id
@with_session @with_session
@ -1106,6 +1115,7 @@ class AgentConfigSkillFileDownloadContentByAgentApi(Resource):
@setup_required @setup_required
@login_required @login_required
@account_initialization_required @account_initialization_required
@rbac_permission_required(RBACCheck(RBACPermission.AGENT_PREVIEW, AgentId()))
@with_current_user @with_current_user
@with_current_tenant_id @with_current_tenant_id
@with_session @with_session
@ -1158,6 +1168,7 @@ class AgentConfigSkillByAgentApi(Resource):
@setup_required @setup_required
@login_required @login_required
@account_initialization_required @account_initialization_required
@rbac_permission_required(RBACCheck(RBACPermission.AGENT_EDIT, AgentId()))
@with_current_user @with_current_user
@with_current_tenant_id @with_current_tenant_id
@with_session @with_session
@ -1182,7 +1193,7 @@ class AgentConfigSkillApi(Resource):
@login_required @login_required
@account_initialization_required @account_initialization_required
@edit_permission_required @edit_permission_required
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_EDIT) @rbac_permission_required(RBACCheck(RBACPermission.APP_EDIT, PlainApp()))
@with_current_user @with_current_user
@with_session @with_session
@get_app_model(mode=_WORKFLOW_APP_MODES) @get_app_model(mode=_WORKFLOW_APP_MODES)
@ -1205,6 +1216,7 @@ class AgentConfigFilePreviewByAgentApi(Resource):
@setup_required @setup_required
@login_required @login_required
@account_initialization_required @account_initialization_required
@rbac_permission_required(RBACCheck(RBACPermission.AGENT_PREVIEW, AgentId()))
@with_current_user @with_current_user
@with_current_tenant_id @with_current_tenant_id
@with_session @with_session
@ -1250,6 +1262,7 @@ class AgentConfigFileDownloadByAgentApi(Resource):
@setup_required @setup_required
@login_required @login_required
@account_initialization_required @account_initialization_required
@rbac_permission_required(RBACCheck(RBACPermission.AGENT_PREVIEW, AgentId()))
@with_current_user @with_current_user
@with_current_tenant_id @with_current_tenant_id
@with_session @with_session
@ -1295,6 +1308,7 @@ class AgentConfigFileByAgentApi(Resource):
@setup_required @setup_required
@login_required @login_required
@account_initialization_required @account_initialization_required
@rbac_permission_required(RBACCheck(RBACPermission.AGENT_EDIT, AgentId()))
@with_current_user @with_current_user
@with_current_tenant_id @with_current_tenant_id
@with_session @with_session
@ -1319,7 +1333,7 @@ class AgentConfigFileApi(Resource):
@login_required @login_required
@account_initialization_required @account_initialization_required
@edit_permission_required @edit_permission_required
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_EDIT) @rbac_permission_required(RBACCheck(RBACPermission.APP_EDIT, PlainApp()))
@with_current_user @with_current_user
@with_session @with_session
@get_app_model(mode=_WORKFLOW_APP_MODES) @get_app_model(mode=_WORKFLOW_APP_MODES)

View File

@ -9,12 +9,12 @@ from sqlalchemy.orm import Session
from werkzeug.exceptions import NotFound from werkzeug.exceptions import NotFound
from controllers.common.errors import NoFileUploadedError, TooManyFilesError 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.schema import query_params_from_model, register_response_schema_models, register_schema_models
from controllers.common.session import with_session from controllers.common.session import with_session
from controllers.console import console_ns from controllers.console import console_ns
from controllers.console.wraps import ( from controllers.console.wraps import (
RBACPermission, RBACPermission,
RBACResourceScope,
account_initialization_required, account_initialization_required,
annotation_import_concurrency_limit, annotation_import_concurrency_limit,
annotation_import_rate_limit, annotation_import_rate_limit,
@ -180,7 +180,7 @@ class AnnotationReplyActionApi(Resource):
@account_initialization_required @account_initialization_required
@cloud_edition_billing_resource_check("annotation") @cloud_edition_billing_resource_check("annotation")
@edit_permission_required @edit_permission_required
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_EDIT) @rbac_permission_required(RBACCheck(RBACPermission.APP_EDIT, PlainApp()))
@model_validate(AnnotationReplyPayload) @model_validate(AnnotationReplyPayload)
def post(self, req_data: AnnotationReplyPayload, app_id: UUID, action: Literal["enable", "disable"]): def post(self, req_data: AnnotationReplyPayload, app_id: UUID, action: Literal["enable", "disable"]):
match action: match action:
@ -211,7 +211,7 @@ class AppAnnotationSettingDetailApi(Resource):
@login_required @login_required
@account_initialization_required @account_initialization_required
@edit_permission_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) @with_session(write=False)
def get(self, session: Session, app_id: UUID): def get(self, session: Session, app_id: UUID):
result = AppAnnotationService.get_app_annotation_setting_by_app_id(str(app_id), session) result = AppAnnotationService.get_app_annotation_setting_by_app_id(str(app_id), session)
@ -230,7 +230,7 @@ class AppAnnotationSettingUpdateApi(Resource):
@login_required @login_required
@account_initialization_required @account_initialization_required
@edit_permission_required @edit_permission_required
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_EDIT) @rbac_permission_required(RBACCheck(RBACPermission.APP_EDIT, PlainApp()))
@with_session @with_session
@model_validate(AnnotationSettingUpdatePayload) @model_validate(AnnotationSettingUpdatePayload)
def post( def post(
@ -263,7 +263,7 @@ class AnnotationReplyActionStatusApi(Resource):
@account_initialization_required @account_initialization_required
@cloud_edition_billing_resource_check("annotation") @cloud_edition_billing_resource_check("annotation")
@edit_permission_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_id: UUID, job_id: UUID, action: str): def get(self, app_id: UUID, job_id: UUID, action: str):
job_id_str = str(job_id) job_id_str = str(job_id)
app_annotation_job_key = f"{action}_app_annotation_job_{job_id_str}" app_annotation_job_key = f"{action}_app_annotation_job_{job_id_str}"
@ -294,7 +294,7 @@ class AnnotationApi(Resource):
@login_required @login_required
@account_initialization_required @account_initialization_required
@edit_permission_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) @with_session(write=False)
@model_validate(AnnotationListQuery) @model_validate(AnnotationListQuery)
def get(self, req_data: AnnotationListQuery, session: Session, app_id: UUID): def get(self, req_data: AnnotationListQuery, session: Session, app_id: UUID):
@ -321,7 +321,7 @@ class AnnotationApi(Resource):
@account_initialization_required @account_initialization_required
@cloud_edition_billing_resource_check("annotation") @cloud_edition_billing_resource_check("annotation")
@edit_permission_required @edit_permission_required
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_EDIT) @rbac_permission_required(RBACCheck(RBACPermission.APP_EDIT, PlainApp()))
@with_session @with_session
@model_validate(CreateAnnotationPayload) @model_validate(CreateAnnotationPayload)
def post(self, req_data: CreateAnnotationPayload, session: Session, app_id: UUID): def post(self, req_data: CreateAnnotationPayload, session: Session, app_id: UUID):
@ -341,7 +341,7 @@ class AnnotationApi(Resource):
@login_required @login_required
@account_initialization_required @account_initialization_required
@edit_permission_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") @console_ns.response(204, "Annotations deleted successfully")
@with_session @with_session
def delete(self, session: Session, app_id: UUID): def delete(self, session: Session, app_id: UUID):
@ -382,7 +382,7 @@ class AnnotationExportApi(Resource):
@login_required @login_required
@account_initialization_required @account_initialization_required
@edit_permission_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) @with_session(write=False)
def get(self, session: Session, app_id: UUID): def get(self, session: Session, app_id: UUID):
annotation_list = AppAnnotationService.export_annotation_list_by_app_id(str(app_id), session) annotation_list = AppAnnotationService.export_annotation_list_by_app_id(str(app_id), session)
@ -411,7 +411,7 @@ class AnnotationUpdateDeleteApi(Resource):
@account_initialization_required @account_initialization_required
@cloud_edition_billing_resource_check("annotation") @cloud_edition_billing_resource_check("annotation")
@edit_permission_required @edit_permission_required
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_EDIT) @rbac_permission_required(RBACCheck(RBACPermission.APP_EDIT, PlainApp()))
@with_session @with_session
@model_validate(UpdateAnnotationPayload) @model_validate(UpdateAnnotationPayload)
def post(self, req_data: UpdateAnnotationPayload, session: Session, app_id: UUID, annotation_id: UUID): def post(self, req_data: UpdateAnnotationPayload, session: Session, app_id: UUID, annotation_id: UUID):
@ -429,7 +429,7 @@ class AnnotationUpdateDeleteApi(Resource):
@login_required @login_required
@account_initialization_required @account_initialization_required
@edit_permission_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") @console_ns.response(204, "Annotation deleted successfully")
@with_session @with_session
def delete(self, session: Session, app_id: UUID, annotation_id: UUID): def delete(self, session: Session, app_id: UUID, annotation_id: UUID):
@ -458,7 +458,7 @@ class AnnotationBatchImportApi(Resource):
@annotation_import_rate_limit @annotation_import_rate_limit
@annotation_import_concurrency_limit @annotation_import_concurrency_limit
@edit_permission_required @edit_permission_required
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_EDIT) @rbac_permission_required(RBACCheck(RBACPermission.APP_EDIT, PlainApp()))
@with_session @with_session
def post(self, session: Session, app_id: UUID): def post(self, session: Session, app_id: UUID):
from configs import dify_config from configs import dify_config
@ -513,7 +513,7 @@ class AnnotationBatchImportStatusApi(Resource):
@account_initialization_required @account_initialization_required
@cloud_edition_billing_resource_check("annotation") @cloud_edition_billing_resource_check("annotation")
@edit_permission_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_id: UUID, job_id: UUID): def get(self, app_id: UUID, job_id: UUID):
indexing_cache_key = f"app_annotation_batch_import_{str(job_id)}" indexing_cache_key = f"app_annotation_batch_import_{str(job_id)}"
cache_result = redis_client.get(indexing_cache_key) cache_result = redis_client.get(indexing_cache_key)
@ -546,7 +546,7 @@ class AnnotationHitHistoryListApi(Resource):
@login_required @login_required
@account_initialization_required @account_initialization_required
@edit_permission_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) @with_session(write=False)
def get(self, session: Session, app_id: UUID, annotation_id: UUID): def get(self, session: Session, app_id: UUID, annotation_id: UUID):
page = request.args.get("page", default=1, type=int) page = request.args.get("page", default=1, type=int)

View File

@ -13,6 +13,7 @@ from werkzeug.exceptions import BadRequest, Forbidden, NotFound
from configs import dify_config from configs import dify_config
from controllers.common.app_access import resolve_app_access_filter from controllers.common.app_access import resolve_app_access_filter
from controllers.common.fields import RedirectUrlResponse, SimpleResultResponse from controllers.common.fields import RedirectUrlResponse, SimpleResultResponse
from controllers.common.rbac import AgentBehindApp, PlainApp, RBACCheck, Workspace
from controllers.common.schema import ( from controllers.common.schema import (
query_params_from_model, query_params_from_model,
query_params_from_request, query_params_from_request,
@ -21,11 +22,10 @@ from controllers.common.schema import (
register_schema_models, register_schema_models,
) )
from controllers.console import console_ns 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.workspace.models import LoadBalancingPayload
from controllers.console.wraps import ( from controllers.console.wraps import (
RBACPermission, RBACPermission,
RBACResourceScope,
account_initialization_required, account_initialization_required,
cloud_edition_billing_resource_check, cloud_edition_billing_resource_check,
edit_permission_required, edit_permission_required,
@ -691,7 +691,7 @@ class AppListApi(Resource):
@setup_required @setup_required
@login_required @login_required
@account_initialization_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") @cloud_edition_billing_resource_check("apps")
@edit_permission_required @edit_permission_required
@with_current_user @with_current_user
@ -868,7 +868,7 @@ class AppApi(Resource):
@enterprise_license_required @enterprise_license_required
@with_current_user @with_current_user
@with_current_tenant_id @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) @with_session(write=False)
@get_app_model(mode=None) @get_app_model(mode=None)
def get(self, session: Session, current_tenant_id: str, current_user: Account, app_model: App): def get(self, session: Session, current_tenant_id: str, current_user: Account, app_model: App):
@ -907,8 +907,9 @@ class AppApi(Resource):
@login_required @login_required
@account_initialization_required @account_initialization_required
@edit_permission_required @edit_permission_required
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_EDIT) @rbac_permission_required(
@agent_manage_required_for_agent_app RBACCheck(RBACPermission.APP_EDIT, PlainApp()), RBACCheck(RBACPermission.AGENT_EDIT, AgentBehindApp())
)
@with_session @with_session
@get_app_model(mode=None) @get_app_model(mode=None)
@model_validate(UpdateAppPayload) @model_validate(UpdateAppPayload)
@ -942,8 +943,9 @@ class AppApi(Resource):
@login_required @login_required
@account_initialization_required @account_initialization_required
@edit_permission_required @edit_permission_required
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_DELETE) @rbac_permission_required(
@agent_manage_required_for_agent_app RBACCheck(RBACPermission.APP_DELETE, PlainApp()), RBACCheck(RBACPermission.AGENT_DELETE, AgentBehindApp())
)
@with_session @with_session
@get_app_model @get_app_model
def delete(self, session: Session, app_model: App): def delete(self, session: Session, app_model: App):
@ -967,8 +969,7 @@ class AppCopyApi(Resource):
@login_required @login_required
@account_initialization_required @account_initialization_required
@edit_permission_required @edit_permission_required
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_CREATE_AND_MANAGEMENT) @rbac_permission_required(RBACCheck(RBACPermission.APP_CREATE_AND_MANAGEMENT, PlainApp()))
@agent_manage_required_for_agent_app
@with_current_user @with_current_user
@with_current_tenant_id @with_current_tenant_id
@get_app_model(mode=None) @get_app_model(mode=None)
@ -1045,8 +1046,10 @@ class AppExportApi(Resource):
@login_required @login_required
@account_initialization_required @account_initialization_required
@edit_permission_required @edit_permission_required
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_IMPORT_EXPORT_DSL) @rbac_permission_required(
@agent_manage_required_for_agent_app RBACCheck(RBACPermission.APP_IMPORT_EXPORT_DSL, PlainApp()),
RBACCheck(RBACPermission.AGENT_IMPORT_EXPORT_DSL, AgentBehindApp()),
)
@get_app_model @get_app_model
@model_validate(AppExportQuery) @model_validate(AppExportQuery)
def get(self, req_data: AppExportQuery, app_model: App): def get(self, req_data: AppExportQuery, app_model: App):
@ -1070,8 +1073,10 @@ class AppPublishToCreatorsPlatformApi(Resource):
@login_required @login_required
@account_initialization_required @account_initialization_required
@edit_permission_required @edit_permission_required
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_IMPORT_EXPORT_DSL) @rbac_permission_required(
@agent_manage_required_for_agent_app RBACCheck(RBACPermission.APP_IMPORT_EXPORT_DSL, PlainApp()),
RBACCheck(RBACPermission.AGENT_RELEASE_AND_VERSION, AgentBehindApp()),
)
@with_current_user_id @with_current_user_id
@get_app_model(mode=None) @get_app_model(mode=None)
def post(self, current_user_id: str, app_model: App): def post(self, current_user_id: str, app_model: App):
@ -1111,8 +1116,9 @@ class AppNameApi(Resource):
@login_required @login_required
@account_initialization_required @account_initialization_required
@edit_permission_required @edit_permission_required
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_EDIT) @rbac_permission_required(
@agent_manage_required_for_agent_app RBACCheck(RBACPermission.APP_EDIT, PlainApp()), RBACCheck(RBACPermission.AGENT_EDIT, AgentBehindApp())
)
@with_session @with_session
@get_app_model(mode=None) @get_app_model(mode=None)
@model_validate(AppNamePayload) @model_validate(AppNamePayload)
@ -1139,8 +1145,9 @@ class AppIconApi(Resource):
@login_required @login_required
@account_initialization_required @account_initialization_required
@edit_permission_required @edit_permission_required
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_EDIT) @rbac_permission_required(
@agent_manage_required_for_agent_app RBACCheck(RBACPermission.APP_EDIT, PlainApp()), RBACCheck(RBACPermission.AGENT_EDIT, AgentBehindApp())
)
@with_session @with_session
@get_app_model(mode=None) @get_app_model(mode=None)
@model_validate(AppIconPayload) @model_validate(AppIconPayload)
@ -1173,7 +1180,10 @@ class AppSiteStatus(Resource):
@login_required @login_required
@account_initialization_required @account_initialization_required
@edit_permission_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 @with_session
@get_app_model(mode=None) @get_app_model(mode=None)
@model_validate(AppSiteStatusPayload) @model_validate(AppSiteStatusPayload)
@ -1200,8 +1210,10 @@ class AppApiStatus(Resource):
@login_required @login_required
@is_admin_or_owner_required @is_admin_or_owner_required
@account_initialization_required @account_initialization_required
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_RELEASE_AND_VERSION) @rbac_permission_required(
@agent_manage_required_for_agent_app RBACCheck(RBACPermission.APP_RELEASE_AND_VERSION, PlainApp()),
RBACCheck(RBACPermission.AGENT_ACCESS_POINT_MANAGE, AgentBehindApp()),
)
@with_session @with_session
@get_app_model(mode=None) @get_app_model(mode=None)
@model_validate(AppApiStatusPayload) @model_validate(AppApiStatusPayload)
@ -1230,7 +1242,7 @@ class AppTraceApi(Resource):
@login_required @login_required
@account_initialization_required @account_initialization_required
@with_session @with_session
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_VIEW_LAYOUT) @rbac_permission_required(RBACCheck(RBACPermission.APP_VIEW_LAYOUT, PlainApp()))
@get_app_model @get_app_model
def get(self, session: Session, app_model: App): def get(self, session: Session, app_model: App):
"""Get app trace""" """Get app trace"""
@ -1252,7 +1264,7 @@ class AppTraceApi(Resource):
@login_required @login_required
@account_initialization_required @account_initialization_required
@edit_permission_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 @get_app_model
@model_validate(AppTracePayload) @model_validate(AppTracePayload)
def post(self, req_data: AppTracePayload, app_model: App): def post(self, req_data: AppTracePayload, app_model: App):

View File

@ -4,11 +4,11 @@ from sqlalchemy.orm import Session
from werkzeug.exceptions import Forbidden from werkzeug.exceptions import Forbidden
from configs import dify_config 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.common.schema import register_enum_models, register_schema_models
from controllers.console.app.wraps import get_app_model from controllers.console.app.wraps import get_app_model
from controllers.console.wraps import ( from controllers.console.wraps import (
RBACPermission, RBACPermission,
RBACResourceScope,
account_initialization_required, account_initialization_required,
cloud_edition_billing_resource_check, cloud_edition_billing_resource_check,
edit_permission_required, edit_permission_required,
@ -82,7 +82,7 @@ class AppImportApi(Resource):
@account_initialization_required @account_initialization_required
@cloud_edition_billing_resource_check("apps") @cloud_edition_billing_resource_check("apps")
@edit_permission_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 @with_current_user
@model_validate(AppImportPayload) @model_validate(AppImportPayload)
def post(self, req_data: AppImportPayload, current_user: Account | None = None): def post(self, req_data: AppImportPayload, current_user: Account | None = None):
@ -149,7 +149,7 @@ class AppImportConfirmApi(Resource):
@login_required @login_required
@account_initialization_required @account_initialization_required
@edit_permission_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 @with_current_user
def post(self, current_user: Account | None = None, import_id: str = ""): 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] 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 @get_app_model
@account_initialization_required @account_initialization_required
@edit_permission_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): def get(self, app_model: App):
with Session(db.engine, expire_on_commit=False) as session: with Session(db.engine, expire_on_commit=False) as session:
import_service = AppDslService(session) import_service = AppDslService(session)

View File

@ -9,8 +9,8 @@ from werkzeug.datastructures import FileStorage
from werkzeug.exceptions import HTTPException, InternalServerError from werkzeug.exceptions import HTTPException, InternalServerError
import services 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.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 import console_ns
from controllers.console.agent.app_helpers import resolve_agent_runtime_app_model from controllers.console.agent.app_helpers import resolve_agent_runtime_app_model
from controllers.console.app.error import ( 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.app.wraps import get_app_model, with_session
from controllers.console.wraps import ( from controllers.console.wraps import (
RBACPermission, RBACPermission,
RBACResourceScope,
account_initialization_required, account_initialization_required,
edit_permission_required, edit_permission_required,
model_validate, model_validate,
@ -242,11 +241,10 @@ class AgentChatMessageAudioApi(Resource):
agent_id=agent_id, agent_id=agent_id,
) )
# Agent routes expose Agent ids, while APP RBAC is keyed by the resolved runtime App 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, tenant_id=current_tenant_id,
account_id=current_user.id, account_id=current_user.id,
resource_type=RBACResourceScope.APP, checks=[RBACCheck(RBACPermission.APP_TEST_AND_RUN, PlainApp())],
scene=RBACPermission.APP_TEST_AND_RUN,
path_args={"app_id": app_model.id}, path_args={"app_id": app_model.id},
) )
agent_soul = AgentComposerService.load_agent_soul_for_debug( agent_soul = AgentComposerService.load_agent_soul_for_debug(
@ -338,7 +336,7 @@ class TextModesApi(Resource):
@setup_required @setup_required
@login_required @login_required
@account_initialization_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 @get_app_model
@model_validate(TextToSpeechVoiceQuery) @model_validate(TextToSpeechVoiceQuery)
def get(self, req_data: TextToSpeechVoiceQuery, app_model: App): def get(self, req_data: TextToSpeechVoiceQuery, app_model: App):

View File

@ -12,6 +12,7 @@ from werkzeug.exceptions import BadRequest, InternalServerError, NotFound
import services import services
from controllers.common.fields import SimpleResultResponse 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.common.schema import register_response_schema_models, register_schema_models
from controllers.console import console_ns from controllers.console import console_ns
from controllers.console.agent.app_helpers import resolve_agent_runtime_app_model from controllers.console.agent.app_helpers import resolve_agent_runtime_app_model
@ -24,10 +25,9 @@ from controllers.console.app.error import (
ProviderNotInitializeError, ProviderNotInitializeError,
ProviderQuotaExceededError, 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 ( from controllers.console.wraps import (
RBACPermission, RBACPermission,
RBACResourceScope,
account_initialization_required, account_initialization_required,
edit_permission_required, edit_permission_required,
model_validate, model_validate,
@ -159,7 +159,7 @@ class CompletionMessageApi(Resource):
@login_required @login_required
@account_initialization_required @account_initialization_required
@with_current_user @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 @with_session
@get_app_model(mode=AppMode.COMPLETION) @get_app_model(mode=AppMode.COMPLETION)
@model_validate(CompletionMessagePayload) @model_validate(CompletionMessagePayload)
@ -241,7 +241,7 @@ class ChatMessageApi(Resource):
@edit_permission_required @edit_permission_required
@with_current_user @with_current_user
@with_current_tenant_id @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 @with_session
@get_app_model(mode=[AppMode.CHAT, AppMode.AGENT_CHAT, AppMode.AGENT]) @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): def post(self, session: Session, current_tenant_id: str, current_user: Account, app_model: App):
@ -263,7 +263,7 @@ class AgentChatMessageApi(Resource):
@login_required @login_required
@account_initialization_required @account_initialization_required
@edit_permission_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_user
@with_current_tenant_id @with_current_tenant_id
@with_session @with_session
@ -292,7 +292,7 @@ class AgentBuildChatFinalizeApi(Resource):
@login_required @login_required
@account_initialization_required @account_initialization_required
@edit_permission_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_user
@with_current_tenant_id @with_current_tenant_id
@with_session @with_session
@ -333,6 +333,7 @@ class AgentChatMessageStopApi(Resource):
@setup_required @setup_required
@login_required @login_required
@account_initialization_required @account_initialization_required
@rbac_permission_required(RBACCheck(RBACPermission.AGENT_TEST_AND_RUN, AgentId()))
@with_current_user_id @with_current_user_id
@with_current_tenant_id @with_current_tenant_id
@with_session(write=False) @with_session(write=False)

View File

@ -9,13 +9,13 @@ from sqlalchemy import func, or_
from sqlalchemy.orm import Session, selectinload from sqlalchemy.orm import Session, selectinload
from werkzeug.exceptions import NotFound 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.schema import query_params_from_model, register_response_schema_models, register_schema_models
from controllers.common.session import with_session from controllers.common.session import with_session
from controllers.console import console_ns from controllers.console import console_ns
from controllers.console.app.wraps import get_app_model from controllers.console.app.wraps import get_app_model
from controllers.console.wraps import ( from controllers.console.wraps import (
RBACPermission, RBACPermission,
RBACResourceScope,
account_initialization_required, account_initialization_required,
edit_permission_required, edit_permission_required,
model_validate, model_validate,
@ -106,7 +106,7 @@ class CompletionConversationApi(Resource):
@account_initialization_required @account_initialization_required
@edit_permission_required @edit_permission_required
@with_current_user @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) @with_session(write=False)
@get_app_model(mode=AppMode.COMPLETION) @get_app_model(mode=AppMode.COMPLETION)
@model_validate(CompletionConversationQuery) @model_validate(CompletionConversationQuery)
@ -187,7 +187,7 @@ class CompletionConversationDetailApi(Resource):
@account_initialization_required @account_initialization_required
@edit_permission_required @edit_permission_required
@with_current_user @with_current_user
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_VIEW_LAYOUT) @rbac_permission_required(RBACCheck(RBACPermission.APP_VIEW_LAYOUT, PlainApp()))
@with_session @with_session
@get_app_model(mode=AppMode.COMPLETION) @get_app_model(mode=AppMode.COMPLETION)
def get(self, session: Session, current_user: Account, app_model: App, conversation_id: UUID): def get(self, session: Session, current_user: Account, app_model: App, conversation_id: UUID):
@ -209,7 +209,7 @@ class CompletionConversationDetailApi(Resource):
@login_required @login_required
@account_initialization_required @account_initialization_required
@edit_permission_required @edit_permission_required
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_EDIT) @rbac_permission_required(RBACCheck(RBACPermission.APP_EDIT, PlainApp()))
@with_current_user @with_current_user
@with_session @with_session
@get_app_model(mode=AppMode.COMPLETION) @get_app_model(mode=AppMode.COMPLETION)
@ -236,7 +236,7 @@ class ChatConversationApi(Resource):
@account_initialization_required @account_initialization_required
@edit_permission_required @edit_permission_required
@with_current_user @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) @with_session(write=False)
@get_app_model(mode=[AppMode.CHAT, AppMode.AGENT_CHAT, AppMode.ADVANCED_CHAT, AppMode.AGENT]) @get_app_model(mode=[AppMode.CHAT, AppMode.AGENT_CHAT, AppMode.ADVANCED_CHAT, AppMode.AGENT])
@model_validate(ChatConversationQuery) @model_validate(ChatConversationQuery)
@ -356,7 +356,7 @@ class ChatConversationDetailApi(Resource):
@account_initialization_required @account_initialization_required
@edit_permission_required @edit_permission_required
@with_current_user @with_current_user
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_VIEW_LAYOUT) @rbac_permission_required(RBACCheck(RBACPermission.APP_VIEW_LAYOUT, PlainApp()))
@with_session @with_session
@get_app_model(mode=[AppMode.CHAT, AppMode.AGENT_CHAT, AppMode.ADVANCED_CHAT, AppMode.AGENT]) @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): def get(self, session: Session, current_user: Account, app_model: App, conversation_id: UUID):
@ -378,7 +378,7 @@ class ChatConversationDetailApi(Resource):
@login_required @login_required
@account_initialization_required @account_initialization_required
@edit_permission_required @edit_permission_required
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_EDIT) @rbac_permission_required(RBACCheck(RBACPermission.APP_EDIT, PlainApp()))
@with_current_user @with_current_user
@with_session @with_session
@get_app_model(mode=[AppMode.CHAT, AppMode.AGENT_CHAT, AppMode.ADVANCED_CHAT, AppMode.AGENT]) @get_app_model(mode=[AppMode.CHAT, AppMode.AGENT_CHAT, AppMode.ADVANCED_CHAT, AppMode.AGENT])

View File

@ -8,12 +8,12 @@ from pydantic import BaseModel, Field, field_validator
from sqlalchemy import select from sqlalchemy import select
from sqlalchemy.orm import sessionmaker 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.common.schema import query_params_from_model, register_schema_models
from controllers.console import console_ns from controllers.console import console_ns
from controllers.console.app.wraps import get_app_model from controllers.console.app.wraps import get_app_model
from controllers.console.wraps import ( from controllers.console.wraps import (
RBACPermission, RBACPermission,
RBACResourceScope,
account_initialization_required, account_initialization_required,
model_validate, model_validate,
rbac_permission_required, rbac_permission_required,
@ -99,7 +99,7 @@ class ConversationVariablesApi(Resource):
@setup_required @setup_required
@login_required @login_required
@account_initialization_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) @get_app_model(mode=AppMode.ADVANCED_CHAT)
@model_validate(ConversationVariablesQuery) @model_validate(ConversationVariablesQuery)
def get(self, req_data: ConversationVariablesQuery, app_model: App): def get(self, req_data: ConversationVariablesQuery, app_model: App):

View File

@ -7,12 +7,12 @@ from pydantic import BaseModel, Field, field_validator
from sqlalchemy import select from sqlalchemy import select
from werkzeug.exceptions import NotFound from werkzeug.exceptions import NotFound
from controllers.common.rbac import PlainApp, RBACCheck
from controllers.common.schema import register_schema_models from controllers.common.schema import register_schema_models
from controllers.console import console_ns from controllers.console import console_ns
from controllers.console.app.wraps import get_app_model from controllers.console.app.wraps import get_app_model
from controllers.console.wraps import ( from controllers.console.wraps import (
RBACPermission, RBACPermission,
RBACResourceScope,
account_initialization_required, account_initialization_required,
edit_permission_required, edit_permission_required,
model_validate, model_validate,
@ -87,7 +87,7 @@ class AppMCPServerController(Resource):
@login_required @login_required
@account_initialization_required @account_initialization_required
@setup_required @setup_required
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_VIEW_LAYOUT) @rbac_permission_required(RBACCheck(RBACPermission.APP_VIEW_LAYOUT, PlainApp()))
@get_app_model @get_app_model
def get(self, app_model: App): def get(self, app_model: App):
server = db.session.scalar(select(AppMCPServer).where(AppMCPServer.app_id == app_model.id).limit(1)) server = db.session.scalar(select(AppMCPServer).where(AppMCPServer.app_id == app_model.id).limit(1))
@ -107,7 +107,7 @@ class AppMCPServerController(Resource):
@login_required @login_required
@setup_required @setup_required
@edit_permission_required @edit_permission_required
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_EDIT) @rbac_permission_required(RBACCheck(RBACPermission.APP_EDIT, PlainApp()))
@with_current_tenant_id @with_current_tenant_id
@get_app_model @get_app_model
@model_validate(MCPServerCreatePayload) @model_validate(MCPServerCreatePayload)
@ -142,7 +142,7 @@ class AppMCPServerController(Resource):
@setup_required @setup_required
@account_initialization_required @account_initialization_required
@edit_permission_required @edit_permission_required
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_EDIT) @rbac_permission_required(RBACCheck(RBACPermission.APP_EDIT, PlainApp()))
@get_app_model @get_app_model
@model_validate(MCPServerUpdatePayload) @model_validate(MCPServerUpdatePayload)
def put(self, req_data: MCPServerUpdatePayload, app_model: App): def put(self, req_data: MCPServerUpdatePayload, app_model: App):
@ -190,7 +190,7 @@ class AppMCPServerRefreshController(Resource):
@login_required @login_required
@account_initialization_required @account_initialization_required
@edit_permission_required @edit_permission_required
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_EDIT) @rbac_permission_required(RBACCheck(RBACPermission.APP_EDIT, PlainApp()))
@with_current_tenant_id @with_current_tenant_id
@get_app_model @get_app_model
def post(self, current_tenant_id: str, app_model: App): def post(self, current_tenant_id: str, app_model: App):

View File

@ -11,6 +11,7 @@ from werkzeug.exceptions import InternalServerError, NotFound
from controllers.common.controller_schemas import MessageFeedbackPayload as _MessageFeedbackPayloadBase from controllers.common.controller_schemas import MessageFeedbackPayload as _MessageFeedbackPayloadBase
from controllers.common.fields import SimpleResultResponse, TextFileResponse 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.schema import query_params_from_model, register_response_schema_models, register_schema_models
from controllers.common.session import with_session from controllers.common.session import with_session
from controllers.console import console_ns from controllers.console import console_ns
@ -21,11 +22,10 @@ from controllers.console.app.error import (
ProviderNotInitializeError, ProviderNotInitializeError,
ProviderQuotaExceededError, 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.explore.error import AppSuggestedQuestionsAfterAnswerDisabledError
from controllers.console.wraps import ( from controllers.console.wraps import (
RBACPermission, RBACPermission,
RBACResourceScope,
account_initialization_required, account_initialization_required,
edit_permission_required, edit_permission_required,
model_validate, model_validate,
@ -155,7 +155,7 @@ class ChatMessageListApi(Resource):
@setup_required @setup_required
@edit_permission_required @edit_permission_required
@with_current_user @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) @with_session(write=False)
@get_app_model(mode=[AppMode.CHAT, AppMode.AGENT_CHAT, AppMode.ADVANCED_CHAT, AppMode.AGENT]) @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): def get(self, session: Session, current_user: Account, app_model: App):
@ -174,7 +174,7 @@ class AgentChatMessageListApi(Resource):
@account_initialization_required @account_initialization_required
@setup_required @setup_required
@edit_permission_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_user
@with_current_tenant_id @with_current_tenant_id
@with_session(write=False) @with_session(write=False)
@ -217,6 +217,7 @@ class AgentMessageFeedbackApi(Resource):
@setup_required @setup_required
@login_required @login_required
@account_initialization_required @account_initialization_required
@rbac_permission_required(RBACCheck(RBACPermission.AGENT_TEST_AND_RUN, AgentId()))
@with_current_user @with_current_user
@with_current_tenant_id @with_current_tenant_id
@with_session @with_session
@ -242,7 +243,7 @@ class MessageAnnotationCountApi(Resource):
@setup_required @setup_required
@login_required @login_required
@account_initialization_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 @get_app_model
def get(self, app_model: App): def get(self, app_model: App):
count = db.session.scalar( count = db.session.scalar(
@ -267,7 +268,7 @@ class MessageSuggestedQuestionApi(Resource):
@login_required @login_required
@account_initialization_required @account_initialization_required
@with_current_user @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) @with_session(write=False)
@get_app_model(mode=[AppMode.CHAT, AppMode.AGENT_CHAT, AppMode.ADVANCED_CHAT, AppMode.AGENT]) @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): def get(self, session: Session, current_user: Account, app_model: App, message_id: UUID):
@ -290,6 +291,7 @@ class AgentMessageSuggestedQuestionApi(Resource):
@setup_required @setup_required
@login_required @login_required
@account_initialization_required @account_initialization_required
@rbac_permission_required(RBACCheck(RBACPermission.AGENT_TEST_AND_RUN, AgentId()))
@with_current_user @with_current_user
@with_current_tenant_id @with_current_tenant_id
@with_session(write=False) @with_session(write=False)
@ -319,7 +321,7 @@ class MessageFeedbackExportApi(Resource):
@setup_required @setup_required
@login_required @login_required
@account_initialization_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 @get_app_model
@model_validate(FeedbackExportQuery) @model_validate(FeedbackExportQuery)
def get(self, req_data: FeedbackExportQuery, app_model: App): def get(self, req_data: FeedbackExportQuery, app_model: App):
@ -358,7 +360,7 @@ class MessageApi(Resource):
@setup_required @setup_required
@login_required @login_required
@account_initialization_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) @with_session(write=False)
@get_app_model @get_app_model
def get(self, session: Session, app_model: App, message_id: UUID): def get(self, session: Session, app_model: App, message_id: UUID):
@ -375,6 +377,7 @@ class AgentMessageApi(Resource):
@setup_required @setup_required
@login_required @login_required
@account_initialization_required @account_initialization_required
@rbac_permission_required(RBACCheck(RBACPermission.AGENT_TEST_AND_RUN, AgentId()))
@with_current_tenant_id @with_current_tenant_id
@with_session(write=False) @with_session(write=False)
def get(self, session: Session, current_tenant_id: str, agent_id: UUID, message_id: UUID): def get(self, session: Session, current_tenant_id: str, agent_id: UUID, message_id: UUID):

View File

@ -7,13 +7,13 @@ from pydantic import BaseModel, Field
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from controllers.common.fields import SimpleResultResponse 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.schema import register_response_schema_models, register_schema_models
from controllers.common.session import with_session from controllers.common.session import with_session
from controllers.console import console_ns from controllers.console import console_ns
from controllers.console.app.wraps import get_app_model from controllers.console.app.wraps import get_app_model
from controllers.console.wraps import ( from controllers.console.wraps import (
RBACPermission, RBACPermission,
RBACResourceScope,
account_initialization_required, account_initialization_required,
edit_permission_required, edit_permission_required,
rbac_permission_required, rbac_permission_required,
@ -90,7 +90,7 @@ class ModelConfigResource(Resource):
@setup_required @setup_required
@login_required @login_required
@edit_permission_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 @account_initialization_required
@with_current_user_id @with_current_user_id
@with_current_tenant_id @with_current_tenant_id

View File

@ -4,6 +4,7 @@ from uuid import UUID
from flask_restx import Resource from flask_restx import Resource
from pydantic import BaseModel, Field 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.common.schema import query_params_from_model, register_response_schema_models, register_schema_models
from controllers.console import console_ns from controllers.console import console_ns
from controllers.console.app.error import ( 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.flask_admission import console_account_admission
from controllers.console.wraps import ( from controllers.console.wraps import (
RBACPermission, RBACPermission,
RBACResourceScope,
model_validate, model_validate,
) )
from extensions.ext_application_services import application_services 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(404, "Application not found")
@console_ns.response(500, "Tracing configuration processing failed") @console_ns.response(500, "Tracing configuration processing failed")
@console_account_admission( @console_account_admission(
rbac_resource_scope=RBACResourceScope.APP, rbac_checks=[RBACCheck(RBACPermission.APP_TRACING_CONFIG, PlainApp())],
rbac_permission=RBACPermission.APP_TRACING_CONFIG,
) )
@model_validate(TraceProviderQuery) @model_validate(TraceProviderQuery)
def get( def get(
@ -155,8 +154,7 @@ class TraceAppConfigApi(Resource):
@console_ns.response(500, "Tracing configuration processing failed") @console_ns.response(500, "Tracing configuration processing failed")
@console_account_admission( @console_account_admission(
allowed_roles=_APP_TRACING_CONFIG_EDIT_ROLES, allowed_roles=_APP_TRACING_CONFIG_EDIT_ROLES,
rbac_resource_scope=RBACResourceScope.APP, rbac_checks=[RBACCheck(RBACPermission.APP_TRACING_CONFIG, PlainApp())],
rbac_permission=RBACPermission.APP_TRACING_CONFIG,
) )
@model_validate(TraceConfigPayload) @model_validate(TraceConfigPayload)
def post( def post(
@ -205,8 +203,7 @@ class TraceAppConfigApi(Resource):
@console_ns.response(500, "Tracing configuration processing failed") @console_ns.response(500, "Tracing configuration processing failed")
@console_account_admission( @console_account_admission(
allowed_roles=_APP_TRACING_CONFIG_EDIT_ROLES, allowed_roles=_APP_TRACING_CONFIG_EDIT_ROLES,
rbac_resource_scope=RBACResourceScope.APP, rbac_checks=[RBACCheck(RBACPermission.APP_TRACING_CONFIG, PlainApp())],
rbac_permission=RBACPermission.APP_TRACING_CONFIG,
) )
@model_validate(TraceConfigPayload) @model_validate(TraceConfigPayload)
def patch( def patch(
@ -251,8 +248,7 @@ class TraceAppConfigApi(Resource):
@console_ns.response(500, "Tracing configuration processing failed") @console_ns.response(500, "Tracing configuration processing failed")
@console_account_admission( @console_account_admission(
allowed_roles=_APP_TRACING_CONFIG_EDIT_ROLES, allowed_roles=_APP_TRACING_CONFIG_EDIT_ROLES,
rbac_resource_scope=RBACResourceScope.APP, rbac_checks=[RBACCheck(RBACPermission.APP_TRACING_CONFIG, PlainApp())],
rbac_permission=RBACPermission.APP_TRACING_CONFIG,
) )
@model_validate(TraceProviderQuery) @model_validate(TraceProviderQuery)
def delete( def delete(

View File

@ -5,13 +5,13 @@ from pydantic import BaseModel, Field, field_validator
from werkzeug.exceptions import NotFound from werkzeug.exceptions import NotFound
from constants.languages import supported_language from constants.languages import supported_language
from controllers.common.rbac import AgentBehindApp, PlainApp, RBACCheck
from controllers.common.schema import register_schema_models from controllers.common.schema import register_schema_models
from controllers.console import console_ns from controllers.console import console_ns
from controllers.console.app.error import AppNotFoundError from controllers.console.app.error import AppNotFoundError
from controllers.console.flask_admission import console_account_admission from controllers.console.flask_admission import console_account_admission
from controllers.console.wraps import ( from controllers.console.wraps import (
RBACPermission, RBACPermission,
RBACResourceScope,
model_validate, model_validate,
) )
from extensions.ext_application_services import application_services from extensions.ext_application_services import application_services
@ -105,9 +105,10 @@ class AppSite(Resource):
@console_ns.response(404, "App not found") @console_ns.response(404, "App not found")
@console_account_admission( @console_account_admission(
allowed_roles=_APP_SITE_EDIT_ROLES, allowed_roles=_APP_SITE_EDIT_ROLES,
rbac_resource_scope=RBACResourceScope.APP, rbac_checks=[
rbac_permission=RBACPermission.APP_RELEASE_AND_VERSION, RBACCheck(RBACPermission.APP_RELEASE_AND_VERSION, PlainApp()),
agent_manage_fallback=True, RBACCheck(RBACPermission.AGENT_ACCESS_POINT_MANAGE, AgentBehindApp()),
],
) )
@model_validate(AppSiteUpdatePayload) @model_validate(AppSiteUpdatePayload)
def post( def post(
@ -136,9 +137,10 @@ class AppSiteAccessTokenReset(Resource):
@console_ns.response(404, "App or site not found") @console_ns.response(404, "App or site not found")
@console_account_admission( @console_account_admission(
allowed_roles=_APP_SITE_TOKEN_RESET_ROLES, allowed_roles=_APP_SITE_TOKEN_RESET_ROLES,
rbac_resource_scope=RBACResourceScope.APP, rbac_checks=[
rbac_permission=RBACPermission.APP_RELEASE_AND_VERSION, RBACCheck(RBACPermission.APP_RELEASE_AND_VERSION, PlainApp()),
agent_manage_fallback=True, RBACCheck(RBACPermission.AGENT_ACCESS_POINT_MANAGE, AgentBehindApp()),
],
) )
def post(self, request_context: RequestContext, app_id: UUID): def post(self, request_context: RequestContext, app_id: UUID):
try: try:

View File

@ -5,11 +5,12 @@ from flask_restx import Resource
from pydantic import BaseModel, Field, field_validator from pydantic import BaseModel, Field, field_validator
from werkzeug.exceptions import BadRequest 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.common.schema import query_params_from_model, register_response_schema_models, register_schema_models
from controllers.console import console_ns from controllers.console import console_ns
from controllers.console.app.wraps import get_app_model from controllers.console.app.wraps import get_app_model
from controllers.console.flask_admission import console_account_admission 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 extensions.ext_application_services import application_services
from fields.base import ResponseModel from fields.base import ResponseModel
from libs.datetime_utils import parse_time_range from libs.datetime_utils import parse_time_range
@ -158,8 +159,7 @@ class DailyMessageStatistic(Resource):
console_ns.models[DailyMessageStatisticResponse.__name__], console_ns.models[DailyMessageStatisticResponse.__name__],
) )
@console_account_admission( @console_account_admission(
rbac_resource_scope=RBACResourceScope.APP, rbac_checks=[RBACCheck(RBACPermission.APP_MONITOR, PlainApp())],
rbac_permission=RBACPermission.APP_MONITOR,
) )
@get_app_model @get_app_model
@model_validate(StatisticTimeRangeQuery) @model_validate(StatisticTimeRangeQuery)
@ -186,8 +186,7 @@ class DailyConversationStatistic(Resource):
console_ns.models[DailyConversationStatisticResponse.__name__], console_ns.models[DailyConversationStatisticResponse.__name__],
) )
@console_account_admission( @console_account_admission(
rbac_resource_scope=RBACResourceScope.APP, rbac_checks=[RBACCheck(RBACPermission.APP_MONITOR, PlainApp())],
rbac_permission=RBACPermission.APP_MONITOR,
) )
@get_app_model @get_app_model
@model_validate(StatisticTimeRangeQuery) @model_validate(StatisticTimeRangeQuery)
@ -214,8 +213,7 @@ class DailyTerminalsStatistic(Resource):
console_ns.models[DailyTerminalStatisticResponse.__name__], console_ns.models[DailyTerminalStatisticResponse.__name__],
) )
@console_account_admission( @console_account_admission(
rbac_resource_scope=RBACResourceScope.APP, rbac_checks=[RBACCheck(RBACPermission.APP_MONITOR, PlainApp())],
rbac_permission=RBACPermission.APP_MONITOR,
) )
@get_app_model @get_app_model
@model_validate(StatisticTimeRangeQuery) @model_validate(StatisticTimeRangeQuery)
@ -242,8 +240,7 @@ class DailyTokenCostStatistic(Resource):
console_ns.models[DailyTokenCostStatisticResponse.__name__], console_ns.models[DailyTokenCostStatisticResponse.__name__],
) )
@console_account_admission( @console_account_admission(
rbac_resource_scope=RBACResourceScope.APP, rbac_checks=[RBACCheck(RBACPermission.APP_MONITOR, PlainApp())],
rbac_permission=RBACPermission.APP_MONITOR,
) )
@get_app_model @get_app_model
@model_validate(StatisticTimeRangeQuery) @model_validate(StatisticTimeRangeQuery)
@ -270,8 +267,7 @@ class AverageSessionInteractionStatistic(Resource):
console_ns.models[AverageSessionInteractionStatisticResponse.__name__], console_ns.models[AverageSessionInteractionStatisticResponse.__name__],
) )
@console_account_admission( @console_account_admission(
rbac_resource_scope=RBACResourceScope.APP, rbac_checks=[RBACCheck(RBACPermission.APP_MONITOR, PlainApp())],
rbac_permission=RBACPermission.APP_MONITOR,
) )
@get_app_model(mode=[AppMode.CHAT, AppMode.AGENT_CHAT, AppMode.ADVANCED_CHAT, AppMode.AGENT]) @get_app_model(mode=[AppMode.CHAT, AppMode.AGENT_CHAT, AppMode.ADVANCED_CHAT, AppMode.AGENT])
@model_validate(StatisticTimeRangeQuery) @model_validate(StatisticTimeRangeQuery)
@ -298,8 +294,7 @@ class UserSatisfactionRateStatistic(Resource):
console_ns.models[UserSatisfactionRateStatisticResponse.__name__], console_ns.models[UserSatisfactionRateStatisticResponse.__name__],
) )
@console_account_admission( @console_account_admission(
rbac_resource_scope=RBACResourceScope.APP, rbac_checks=[RBACCheck(RBACPermission.APP_MONITOR, PlainApp())],
rbac_permission=RBACPermission.APP_MONITOR,
) )
@get_app_model @get_app_model
@model_validate(StatisticTimeRangeQuery) @model_validate(StatisticTimeRangeQuery)
@ -326,8 +321,7 @@ class AverageResponseTimeStatistic(Resource):
console_ns.models[AverageResponseTimeStatisticResponse.__name__], console_ns.models[AverageResponseTimeStatisticResponse.__name__],
) )
@console_account_admission( @console_account_admission(
rbac_resource_scope=RBACResourceScope.APP, rbac_checks=[RBACCheck(RBACPermission.APP_MONITOR, PlainApp())],
rbac_permission=RBACPermission.APP_MONITOR,
) )
@get_app_model(mode=AppMode.COMPLETION) @get_app_model(mode=AppMode.COMPLETION)
@model_validate(StatisticTimeRangeQuery) @model_validate(StatisticTimeRangeQuery)
@ -354,8 +348,7 @@ class TokensPerSecondStatistic(Resource):
console_ns.models[TokensPerSecondStatisticResponse.__name__], console_ns.models[TokensPerSecondStatisticResponse.__name__],
) )
@console_account_admission( @console_account_admission(
rbac_resource_scope=RBACResourceScope.APP, rbac_checks=[RBACCheck(RBACPermission.APP_MONITOR, PlainApp())],
rbac_permission=RBACPermission.APP_MONITOR,
) )
@get_app_model @get_app_model
@model_validate(StatisticTimeRangeQuery) @model_validate(StatisticTimeRangeQuery)

View File

@ -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.controller_schemas import DefaultBlockConfigQuery, WorkflowListQuery, WorkflowUpdatePayload
from controllers.common.errors import InvalidArgumentError from controllers.common.errors import InvalidArgumentError
from controllers.common.fields import GeneratedAppResponse, NewAppResponse, SimpleResultResponse from controllers.common.fields import GeneratedAppResponse, NewAppResponse, SimpleResultResponse
from controllers.common.rbac import PlainApp, RBACCheck
from controllers.common.schema import ( from controllers.common.schema import (
query_params_from_model, query_params_from_model,
register_response_schema_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.app.wraps import get_app_model, with_session
from controllers.console.wraps import ( from controllers.console.wraps import (
RBACPermission, RBACPermission,
RBACResourceScope,
account_initialization_required, account_initialization_required,
edit_permission_required, edit_permission_required,
model_validate, model_validate,
@ -582,7 +582,7 @@ class DraftWorkflowApi(Resource):
@login_required @login_required
@account_initialization_required @account_initialization_required
@edit_permission_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]) @get_app_model(mode=[AppMode.ADVANCED_CHAT, AppMode.WORKFLOW])
def get(self, app_model: App): def get(self, app_model: App):
""" """
@ -625,7 +625,7 @@ class DraftWorkflowApi(Resource):
@console_ns.response(403, "Permission denied") @console_ns.response(403, "Permission denied")
@with_current_user @with_current_user
@edit_permission_required @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): def post(self, current_user: Account, app_model: App):
""" """
Sync draft workflow Sync draft workflow
@ -704,7 +704,7 @@ class AdvancedChatDraftWorkflowRunApi(Resource):
@setup_required @setup_required
@login_required @login_required
@account_initialization_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 @with_current_user
@edit_permission_required @edit_permission_required
@with_session @with_session
@ -760,7 +760,7 @@ class AdvancedChatDraftRunIterationNodeApi(Resource):
@setup_required @setup_required
@login_required @login_required
@account_initialization_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]) @get_app_model(mode=[AppMode.ADVANCED_CHAT])
@with_current_user @with_current_user
@edit_permission_required @edit_permission_required
@ -809,7 +809,7 @@ class WorkflowDraftRunIterationNodeApi(Resource):
@setup_required @setup_required
@login_required @login_required
@account_initialization_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]) @get_app_model(mode=[AppMode.WORKFLOW])
@with_current_user @with_current_user
@edit_permission_required @edit_permission_required
@ -854,7 +854,7 @@ class AdvancedChatDraftRunLoopNodeApi(Resource):
@setup_required @setup_required
@login_required @login_required
@account_initialization_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]) @get_app_model(mode=[AppMode.ADVANCED_CHAT])
@with_current_user @with_current_user
@edit_permission_required @edit_permission_required
@ -902,7 +902,7 @@ class WorkflowDraftRunLoopNodeApi(Resource):
@setup_required @setup_required
@login_required @login_required
@account_initialization_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]) @get_app_model(mode=[AppMode.WORKFLOW])
@with_current_user @with_current_user
@edit_permission_required @edit_permission_required
@ -982,7 +982,7 @@ class AdvancedChatDraftHumanInputFormPreviewApi(Resource):
@get_app_model(mode=[AppMode.ADVANCED_CHAT]) @get_app_model(mode=[AppMode.ADVANCED_CHAT])
@with_current_user @with_current_user
@edit_permission_required @edit_permission_required
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_VIEW_LAYOUT) @rbac_permission_required(RBACCheck(RBACPermission.APP_VIEW_LAYOUT, PlainApp()))
@model_validate(HumanInputFormPreviewPayload) @model_validate(HumanInputFormPreviewPayload)
def post(self, args: HumanInputFormPreviewPayload, current_user: Account, app_model: App, node_id: str): def post(self, args: HumanInputFormPreviewPayload, current_user: Account, app_model: App, node_id: str):
""" """
@ -1015,7 +1015,7 @@ class AdvancedChatDraftHumanInputFormRunApi(Resource):
@setup_required @setup_required
@login_required @login_required
@account_initialization_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]) @get_app_model(mode=[AppMode.ADVANCED_CHAT])
@with_current_user @with_current_user
@edit_permission_required @edit_permission_required
@ -1050,7 +1050,7 @@ class WorkflowDraftHumanInputFormPreviewApi(Resource):
@get_app_model(mode=[AppMode.WORKFLOW]) @get_app_model(mode=[AppMode.WORKFLOW])
@with_current_user @with_current_user
@edit_permission_required @edit_permission_required
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_VIEW_LAYOUT) @rbac_permission_required(RBACCheck(RBACPermission.APP_VIEW_LAYOUT, PlainApp()))
@model_validate(HumanInputFormPreviewPayload) @model_validate(HumanInputFormPreviewPayload)
def post(self, args: HumanInputFormPreviewPayload, current_user: Account, app_model: App, node_id: str): def post(self, args: HumanInputFormPreviewPayload, current_user: Account, app_model: App, node_id: str):
""" """
@ -1083,7 +1083,7 @@ class WorkflowDraftHumanInputFormRunApi(Resource):
@setup_required @setup_required
@login_required @login_required
@account_initialization_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]) @get_app_model(mode=[AppMode.WORKFLOW])
@with_current_user @with_current_user
@edit_permission_required @edit_permission_required
@ -1118,7 +1118,7 @@ class WorkflowDraftHumanInputDeliveryTestApi(Resource):
@get_app_model(mode=[AppMode.WORKFLOW, AppMode.ADVANCED_CHAT]) @get_app_model(mode=[AppMode.WORKFLOW, AppMode.ADVANCED_CHAT])
@with_current_user @with_current_user
@edit_permission_required @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) @model_validate(HumanInputDeliveryTestPayload)
def post(self, args: HumanInputDeliveryTestPayload, current_user: Account, app_model: App, node_id: str): def post(self, args: HumanInputDeliveryTestPayload, current_user: Account, app_model: App, node_id: str):
""" """
@ -1151,7 +1151,7 @@ class DraftWorkflowRunApi(Resource):
@setup_required @setup_required
@login_required @login_required
@account_initialization_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 @with_current_user
@edit_permission_required @edit_permission_required
@with_session @with_session
@ -1194,7 +1194,7 @@ class WorkflowTaskStopApi(Resource):
@login_required @login_required
@account_initialization_required @account_initialization_required
@edit_permission_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]) @get_app_model(mode=[AppMode.ADVANCED_CHAT, AppMode.WORKFLOW])
def post(self, app_model: App, task_id: str): def post(self, app_model: App, task_id: str):
""" """
@ -1226,7 +1226,7 @@ class DraftWorkflowNodeRunApi(Resource):
@setup_required @setup_required
@login_required @login_required
@account_initialization_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]) @get_app_model(mode=[AppMode.ADVANCED_CHAT, AppMode.WORKFLOW])
@with_current_user @with_current_user
@edit_permission_required @edit_permission_required
@ -1278,7 +1278,7 @@ class PublishedWorkflowApi(Resource):
@login_required @login_required
@account_initialization_required @account_initialization_required
@edit_permission_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]) @get_app_model(mode=[AppMode.ADVANCED_CHAT, AppMode.WORKFLOW])
def get(self, app_model: App): def get(self, app_model: App):
""" """
@ -1300,7 +1300,7 @@ class PublishedWorkflowApi(Resource):
@setup_required @setup_required
@login_required @login_required
@account_initialization_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]) @get_app_model(mode=[AppMode.ADVANCED_CHAT, AppMode.WORKFLOW])
@with_current_user @with_current_user
@edit_permission_required @edit_permission_required
@ -1349,7 +1349,7 @@ class DefaultBlockConfigsApi(Resource):
@login_required @login_required
@account_initialization_required @account_initialization_required
@edit_permission_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]) @get_app_model(mode=[AppMode.ADVANCED_CHAT, AppMode.WORKFLOW])
def get(self, app_model: App): def get(self, app_model: App):
""" """
@ -1376,7 +1376,7 @@ class DefaultBlockConfigApi(Resource):
@login_required @login_required
@account_initialization_required @account_initialization_required
@edit_permission_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]) @get_app_model(mode=[AppMode.ADVANCED_CHAT, AppMode.WORKFLOW])
@model_validate(DefaultBlockConfigQuery) @model_validate(DefaultBlockConfigQuery)
def get(self, args: DefaultBlockConfigQuery, app_model: App, block_type: str): def get(self, args: DefaultBlockConfigQuery, app_model: App, block_type: str):
@ -1416,7 +1416,7 @@ class ConvertToWorkflowApi(Resource):
@with_current_user @with_current_user
@with_current_tenant_id @with_current_tenant_id
@edit_permission_required @edit_permission_required
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_EDIT) @rbac_permission_required(RBACCheck(RBACPermission.APP_EDIT, PlainApp()))
@model_validate(ConvertToWorkflowPayload) @model_validate(ConvertToWorkflowPayload)
def post(self, payload: ConvertToWorkflowPayload, current_tenant_id: str, current_user: Account, app_model: App): 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]) @get_app_model(mode=[AppMode.ADVANCED_CHAT, AppMode.WORKFLOW])
@with_current_user @with_current_user
@edit_permission_required @edit_permission_required
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_VIEW_LAYOUT) @rbac_permission_required(RBACCheck(RBACPermission.APP_VIEW_LAYOUT, PlainApp()))
@model_validate(WorkflowFeaturesPayload) @model_validate(WorkflowFeaturesPayload)
def post(self, args: WorkflowFeaturesPayload, current_user: Account, app_model: App): def post(self, args: WorkflowFeaturesPayload, current_user: Account, app_model: App):
features = args.features.model_dump(mode="json", exclude_unset=True) features = args.features.model_dump(mode="json", exclude_unset=True)
@ -1485,7 +1485,7 @@ class PublishedAllWorkflowApi(Resource):
@setup_required @setup_required
@login_required @login_required
@account_initialization_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]) @get_app_model(mode=[AppMode.ADVANCED_CHAT, AppMode.WORKFLOW])
@with_current_user @with_current_user
@edit_permission_required @edit_permission_required
@ -1538,7 +1538,7 @@ class DraftWorkflowRestoreApi(Resource):
@get_app_model(mode=[AppMode.ADVANCED_CHAT, AppMode.WORKFLOW]) @get_app_model(mode=[AppMode.ADVANCED_CHAT, AppMode.WORKFLOW])
@with_current_user @with_current_user
@edit_permission_required @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): def post(self, current_user: Account, app_model: App, workflow_id: str):
workflow_service = WorkflowService() workflow_service = WorkflowService()
@ -1578,7 +1578,7 @@ class WorkflowByIdApi(Resource):
@get_app_model(mode=[AppMode.ADVANCED_CHAT, AppMode.WORKFLOW]) @get_app_model(mode=[AppMode.ADVANCED_CHAT, AppMode.WORKFLOW])
@with_current_user @with_current_user
@edit_permission_required @edit_permission_required
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_EDIT) @rbac_permission_required(RBACCheck(RBACPermission.APP_EDIT, PlainApp()))
@model_validate(WorkflowUpdatePayload) @model_validate(WorkflowUpdatePayload)
def patch(self, args: WorkflowUpdatePayload, current_user: Account, app_model: App, workflow_id: str): 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]) @get_app_model(mode=[AppMode.ADVANCED_CHAT, AppMode.WORKFLOW])
@with_current_user @with_current_user
@edit_permission_required @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") @console_ns.response(204, "Workflow deleted successfully")
def delete(self, current_user: Account, app_model: App, workflow_id: str): def delete(self, current_user: Account, app_model: App, workflow_id: str):
""" """
@ -1666,7 +1666,7 @@ class DraftWorkflowNodeLastRunApi(Resource):
@setup_required @setup_required
@login_required @login_required
@account_initialization_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]) @get_app_model(mode=[AppMode.ADVANCED_CHAT, AppMode.WORKFLOW])
def get(self, app_model: App, node_id: str): def get(self, app_model: App, node_id: str):
srv = WorkflowService() srv = WorkflowService()
@ -1711,7 +1711,7 @@ class DraftWorkflowTriggerRunApi(Resource):
@setup_required @setup_required
@login_required @login_required
@account_initialization_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 @with_current_user
@edit_permission_required @edit_permission_required
@with_session @with_session
@ -1782,7 +1782,7 @@ class DraftWorkflowTriggerNodeApi(Resource):
@setup_required @setup_required
@login_required @login_required
@account_initialization_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]) @get_app_model(mode=[AppMode.WORKFLOW])
@with_current_user @with_current_user
@edit_permission_required @edit_permission_required
@ -1865,7 +1865,7 @@ class DraftWorkflowTriggerRunAllApi(Resource):
@account_initialization_required @account_initialization_required
@with_current_user @with_current_user
@edit_permission_required @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 @with_session
@get_app_model(mode=[AppMode.WORKFLOW]) @get_app_model(mode=[AppMode.WORKFLOW])
@model_validate(DraftWorkflowTriggerRunAllPayload) @model_validate(DraftWorkflowTriggerRunAllPayload)

View File

@ -5,15 +5,12 @@ from dateutil.parser import isoparse
from flask_restx import Resource from flask_restx import Resource
from pydantic import BaseModel, Field, field_validator 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.common.schema import query_params_from_model, register_schema_models
from controllers.console import console_ns from controllers.console import console_ns
from controllers.console.app.wraps import get_app_model from controllers.console.app.wraps import get_app_model
from controllers.console.flask_admission import console_account_admission from controllers.console.flask_admission import console_account_admission
from controllers.console.wraps import ( from controllers.console.wraps import RBACPermission, model_validate
RBACPermission,
RBACResourceScope,
model_validate,
)
from extensions.ext_application_services import application_services from extensions.ext_application_services import application_services
from fields.base import ResponseModel from fields.base import ResponseModel
from fields.end_user_fields import SimpleEndUser from fields.end_user_fields import SimpleEndUser
@ -133,8 +130,7 @@ class WorkflowAppLogApi(Resource):
console_ns.models[WorkflowAppLogPaginationResponse.__name__], console_ns.models[WorkflowAppLogPaginationResponse.__name__],
) )
@console_account_admission( @console_account_admission(
rbac_resource_scope=RBACResourceScope.APP, rbac_checks=[RBACCheck(RBACPermission.APP_LOG_AND_ANNOTATION, PlainApp())],
rbac_permission=RBACPermission.APP_LOG_AND_ANNOTATION,
) )
@get_app_model(mode=[AppMode.WORKFLOW]) @get_app_model(mode=[AppMode.WORKFLOW])
@model_validate(WorkflowAppLogQuery) @model_validate(WorkflowAppLogQuery)

View File

@ -11,6 +11,7 @@ from sqlalchemy.orm import sessionmaker
from controllers.common.errors import InvalidArgumentError, NotFoundError from controllers.common.errors import InvalidArgumentError, NotFoundError
from controllers.common.fields import SimpleResultResponse 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.common.schema import query_params_from_model, register_response_schema_models, register_schema_models
from controllers.console import console_ns from controllers.console import console_ns
from controllers.console.app.error import ( 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.app.wraps import get_app_model
from controllers.console.wraps import ( from controllers.console.wraps import (
RBACPermission, RBACPermission,
RBACResourceScope,
account_initialization_required, account_initialization_required,
edit_permission_required, edit_permission_required,
model_validate, model_validate,
@ -337,7 +337,7 @@ def _api_prerequisite[T, **P, R](
@login_required @login_required
@account_initialization_required @account_initialization_required
@edit_permission_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]) @get_app_model(mode=[AppMode.ADVANCED_CHAT, AppMode.WORKFLOW])
@with_current_user @with_current_user
@wraps(f) @wraps(f)
@ -359,7 +359,7 @@ class WorkflowVariableCollectionApi(Resource):
) )
@_api_prerequisite @_api_prerequisite
@marshal_with(workflow_draft_variable_list_without_value_model) @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) @model_validate(WorkflowDraftVariableListQuery)
def get(self, req_data: WorkflowDraftVariableListQuery, current_user: Account, app_model: App): 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) @console_ns.response(200, "Node variables retrieved successfully", workflow_draft_variable_list_model)
@_api_prerequisite @_api_prerequisite
@marshal_with(workflow_draft_variable_list_model) @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): def get(self, current_user: Account, app_model: App, node_id: str):
validate_node_id(node_id) validate_node_id(node_id)
with sessionmaker(bind=db.engine, expire_on_commit=False).begin() as session: 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") @console_ns.response(404, "Variable not found")
@_api_prerequisite @_api_prerequisite
@marshal_with(workflow_draft_variable_model) @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): def get(self, current_user: Account, app_model: App, variable_id: UUID):
draft_var_srv = WorkflowDraftVariableService( draft_var_srv = WorkflowDraftVariableService(
session=db.session(), session=db.session(),
@ -638,7 +638,7 @@ class ConversationVariableCollectionApi(Resource):
@console_ns.response(404, "Draft workflow not found") @console_ns.response(404, "Draft workflow not found")
@_api_prerequisite @_api_prerequisite
@marshal_with(workflow_draft_variable_list_model) @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): def get(self, current_user: Account, app_model: App):
# NOTE(QuantumGhost): Prefill conversation variables into the draft variables table # NOTE(QuantumGhost): Prefill conversation variables into the draft variables table
# so their IDs can be returned to the caller. # so their IDs can be returned to the caller.
@ -664,7 +664,7 @@ class ConversationVariableCollectionApi(Resource):
@login_required @login_required
@account_initialization_required @account_initialization_required
@edit_permission_required @edit_permission_required
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_EDIT) @rbac_permission_required(RBACCheck(RBACPermission.APP_EDIT, PlainApp()))
@with_current_user @with_current_user
@get_app_model(mode=AppMode.ADVANCED_CHAT) @get_app_model(mode=AppMode.ADVANCED_CHAT)
@model_validate(ConversationVariableUpdatePayload) @model_validate(ConversationVariableUpdatePayload)
@ -697,7 +697,7 @@ class SystemVariableCollectionApi(Resource):
@console_ns.response(200, "System variables retrieved successfully", workflow_draft_variable_list_model) @console_ns.response(200, "System variables retrieved successfully", workflow_draft_variable_list_model)
@_api_prerequisite @_api_prerequisite
@marshal_with(workflow_draft_variable_list_model) @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): def get(self, current_user: Account, app_model: App):
return _get_variable_list(app_model, SYSTEM_VARIABLE_NODE_ID, current_user.id) 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") @console_ns.response(404, "Draft workflow not found")
@_api_prerequisite @_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): def get(self, _current_user: Account, app_model: App):
""" """
Get draft workflow Get draft workflow
@ -759,7 +759,7 @@ class EnvironmentVariableCollectionApi(Resource):
@login_required @login_required
@account_initialization_required @account_initialization_required
@edit_permission_required @edit_permission_required
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_EDIT) @rbac_permission_required(RBACCheck(RBACPermission.APP_EDIT, PlainApp()))
@with_current_user @with_current_user
@get_app_model(mode=[AppMode.ADVANCED_CHAT, AppMode.WORKFLOW]) @get_app_model(mode=[AppMode.ADVANCED_CHAT, AppMode.WORKFLOW])
@model_validate(EnvironmentVariableUpdatePayload) @model_validate(EnvironmentVariableUpdatePayload)

View File

@ -31,12 +31,12 @@ from flask import Response
from flask_restx import Resource from flask_restx import Resource
from controllers.common.fields import EventStreamResponse from controllers.common.fields import EventStreamResponse
from controllers.common.rbac import PlainApp, RBACCheck
from controllers.common.schema import register_response_schema_models from controllers.common.schema import register_response_schema_models
from controllers.console import console_ns from controllers.console import console_ns
from controllers.console.app.wraps import get_app_model from controllers.console.app.wraps import get_app_model
from controllers.console.wraps import ( from controllers.console.wraps import (
RBACPermission, RBACPermission,
RBACResourceScope,
account_initialization_required, account_initialization_required,
rbac_permission_required, rbac_permission_required,
setup_required, setup_required,
@ -157,7 +157,7 @@ class WorkflowDraftRunNodeOutputsApi(Resource):
@setup_required @setup_required
@login_required @login_required
@account_initialization_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]) @get_app_model(mode=[AppMode.ADVANCED_CHAT, AppMode.WORKFLOW])
def get(self, app_model: App, run_id: UUID): def get(self, app_model: App, run_id: UUID):
return _serve_snapshot(app_model, run_id) return _serve_snapshot(app_model, run_id)
@ -181,7 +181,7 @@ class WorkflowDraftRunNodeOutputDetailApi(Resource):
@setup_required @setup_required
@login_required @login_required
@account_initialization_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]) @get_app_model(mode=[AppMode.ADVANCED_CHAT, AppMode.WORKFLOW])
def get(self, app_model: App, run_id: UUID, node_id: str): def get(self, app_model: App, run_id: UUID, node_id: str):
return _serve_node_detail(app_model, run_id, node_id) return _serve_node_detail(app_model, run_id, node_id)
@ -208,7 +208,7 @@ class WorkflowDraftRunNodeOutputPreviewApi(Resource):
@setup_required @setup_required
@login_required @login_required
@account_initialization_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]) @get_app_model(mode=[AppMode.ADVANCED_CHAT, AppMode.WORKFLOW])
def get(self, app_model: App, run_id: UUID, node_id: str, output_name: str): 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) return _serve_output_preview(app_model, run_id, node_id, output_name)
@ -353,7 +353,7 @@ class WorkflowDraftRunNodeOutputEventsApi(Resource):
@setup_required @setup_required
@login_required @login_required
@account_initialization_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]) @get_app_model(mode=[AppMode.ADVANCED_CHAT, AppMode.WORKFLOW])
def get(self, app_model: App, run_id: UUID): def get(self, app_model: App, run_id: UUID):
return Response( return Response(
@ -384,7 +384,7 @@ class WorkflowPublishedRunNodeOutputsApi(Resource):
@setup_required @setup_required
@login_required @login_required
@account_initialization_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]) @get_app_model(mode=[AppMode.ADVANCED_CHAT, AppMode.WORKFLOW])
def get(self, app_model: App, run_id: UUID): def get(self, app_model: App, run_id: UUID):
return _serve_snapshot(app_model, run_id) return _serve_snapshot(app_model, run_id)
@ -408,7 +408,7 @@ class WorkflowPublishedRunNodeOutputDetailApi(Resource):
@setup_required @setup_required
@login_required @login_required
@account_initialization_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]) @get_app_model(mode=[AppMode.ADVANCED_CHAT, AppMode.WORKFLOW])
def get(self, app_model: App, run_id: UUID, node_id: str): def get(self, app_model: App, run_id: UUID, node_id: str):
return _serve_node_detail(app_model, run_id, node_id) return _serve_node_detail(app_model, run_id, node_id)
@ -436,7 +436,7 @@ class WorkflowPublishedRunNodeOutputPreviewApi(Resource):
@setup_required @setup_required
@login_required @login_required
@account_initialization_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]) @get_app_model(mode=[AppMode.ADVANCED_CHAT, AppMode.WORKFLOW])
def get(self, app_model: App, run_id: UUID, node_id: str, output_name: str): 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) return _serve_output_preview(app_model, run_id, node_id, output_name)
@ -458,7 +458,7 @@ class WorkflowPublishedRunNodeOutputEventsApi(Resource):
@setup_required @setup_required
@login_required @login_required
@account_initialization_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]) @get_app_model(mode=[AppMode.ADVANCED_CHAT, AppMode.WORKFLOW])
def get(self, app_model: App, run_id: UUID): def get(self, app_model: App, run_id: UUID):
return Response( return Response(

View File

@ -6,13 +6,13 @@ from pydantic import BaseModel, Field, field_validator
from configs import dify_config from configs import dify_config
from controllers.common.errors import NotFoundError 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.common.schema import query_params_from_model, register_response_schema_models, register_schema_models
from controllers.console import console_ns from controllers.console import console_ns
from controllers.console.app.wraps import get_app_model from controllers.console.app.wraps import get_app_model
from controllers.console.flask_admission import console_account_admission from controllers.console.flask_admission import console_account_admission
from controllers.console.wraps import ( from controllers.console.wraps import (
RBACPermission, RBACPermission,
RBACResourceScope,
model_validate, model_validate,
) )
from extensions.ext_application_services import application_services from extensions.ext_application_services import application_services
@ -143,8 +143,7 @@ class AdvancedChatAppWorkflowRunListApi(Resource):
console_ns.models[AdvancedChatWorkflowRunPaginationResponse.__name__], console_ns.models[AdvancedChatWorkflowRunPaginationResponse.__name__],
) )
@console_account_admission( @console_account_admission(
rbac_resource_scope=RBACResourceScope.APP, rbac_checks=[RBACCheck(RBACPermission.APP_CREATE_AND_MANAGEMENT, PlainApp())],
rbac_permission=RBACPermission.APP_CREATE_AND_MANAGEMENT,
) )
@get_app_model(mode=[AppMode.ADVANCED_CHAT]) @get_app_model(mode=[AppMode.ADVANCED_CHAT])
@model_validate(WorkflowRunListQuery) @model_validate(WorkflowRunListQuery)
@ -174,8 +173,7 @@ class AdvancedChatAppWorkflowRunCountApi(Resource):
console_ns.models[WorkflowRunCountResponse.__name__], console_ns.models[WorkflowRunCountResponse.__name__],
) )
@console_account_admission( @console_account_admission(
rbac_resource_scope=RBACResourceScope.APP, rbac_checks=[RBACCheck(RBACPermission.APP_CREATE_AND_MANAGEMENT, PlainApp())],
rbac_permission=RBACPermission.APP_CREATE_AND_MANAGEMENT,
) )
@get_app_model(mode=[AppMode.ADVANCED_CHAT]) @get_app_model(mode=[AppMode.ADVANCED_CHAT])
@model_validate(WorkflowRunCountQuery) @model_validate(WorkflowRunCountQuery)
@ -206,8 +204,7 @@ class WorkflowRunListApi(Resource):
console_ns.models[WorkflowRunPaginationResponse.__name__], console_ns.models[WorkflowRunPaginationResponse.__name__],
) )
@console_account_admission( @console_account_admission(
rbac_resource_scope=RBACResourceScope.APP, rbac_checks=[RBACCheck(RBACPermission.APP_CREATE_AND_MANAGEMENT, PlainApp())],
rbac_permission=RBACPermission.APP_CREATE_AND_MANAGEMENT,
) )
@get_app_model(mode=[AppMode.ADVANCED_CHAT, AppMode.WORKFLOW]) @get_app_model(mode=[AppMode.ADVANCED_CHAT, AppMode.WORKFLOW])
@model_validate(WorkflowRunListQuery) @model_validate(WorkflowRunListQuery)
@ -237,8 +234,7 @@ class WorkflowRunCountApi(Resource):
console_ns.models[WorkflowRunCountResponse.__name__], console_ns.models[WorkflowRunCountResponse.__name__],
) )
@console_account_admission( @console_account_admission(
rbac_resource_scope=RBACResourceScope.APP, rbac_checks=[RBACCheck(RBACPermission.APP_CREATE_AND_MANAGEMENT, PlainApp())],
rbac_permission=RBACPermission.APP_CREATE_AND_MANAGEMENT,
) )
@get_app_model(mode=[AppMode.ADVANCED_CHAT, AppMode.WORKFLOW]) @get_app_model(mode=[AppMode.ADVANCED_CHAT, AppMode.WORKFLOW])
@model_validate(WorkflowRunCountQuery) @model_validate(WorkflowRunCountQuery)
@ -269,8 +265,7 @@ class WorkflowRunDetailApi(Resource):
) )
@console_ns.response(404, "Workflow run not found") @console_ns.response(404, "Workflow run not found")
@console_account_admission( @console_account_admission(
rbac_resource_scope=RBACResourceScope.APP, rbac_checks=[RBACCheck(RBACPermission.APP_CREATE_AND_MANAGEMENT, PlainApp())],
rbac_permission=RBACPermission.APP_CREATE_AND_MANAGEMENT,
) )
@get_app_model(mode=[AppMode.ADVANCED_CHAT, AppMode.WORKFLOW]) @get_app_model(mode=[AppMode.ADVANCED_CHAT, AppMode.WORKFLOW])
def get(self, request_context: RequestContext, app_model: App, run_id: UUID): 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_ns.response(404, "Workflow run not found")
@console_account_admission( @console_account_admission(
rbac_resource_scope=RBACResourceScope.APP, rbac_checks=[RBACCheck(RBACPermission.APP_CREATE_AND_MANAGEMENT, PlainApp())],
rbac_permission=RBACPermission.APP_CREATE_AND_MANAGEMENT,
) )
@get_app_model(mode=[AppMode.ADVANCED_CHAT, AppMode.WORKFLOW]) @get_app_model(mode=[AppMode.ADVANCED_CHAT, AppMode.WORKFLOW])
def get(self, request_context: RequestContext, app_model: App, run_id: UUID): def get(self, request_context: RequestContext, app_model: App, run_id: UUID):

View File

@ -4,13 +4,13 @@ from flask_restx import Resource
from pydantic import BaseModel, Field, field_validator from pydantic import BaseModel, Field, field_validator
from werkzeug.exceptions import BadRequest 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.common.schema import query_params_from_model, register_response_schema_models, register_schema_models
from controllers.console import console_ns from controllers.console import console_ns
from controllers.console.app.wraps import get_app_model from controllers.console.app.wraps import get_app_model
from controllers.console.flask_admission import console_account_admission from controllers.console.flask_admission import console_account_admission
from controllers.console.wraps import ( from controllers.console.wraps import (
RBACPermission, RBACPermission,
RBACResourceScope,
model_validate, model_validate,
) )
from extensions.ext_application_services import application_services from extensions.ext_application_services import application_services
@ -106,8 +106,7 @@ class WorkflowDailyRunsStatistic(Resource):
console_ns.models[WorkflowDailyRunsStatisticResponse.__name__], console_ns.models[WorkflowDailyRunsStatisticResponse.__name__],
) )
@console_account_admission( @console_account_admission(
rbac_resource_scope=RBACResourceScope.APP, rbac_checks=[RBACCheck(RBACPermission.APP_MONITOR, PlainApp())],
rbac_permission=RBACPermission.APP_MONITOR,
) )
@get_app_model @get_app_model
@model_validate(WorkflowStatisticQuery) @model_validate(WorkflowStatisticQuery)
@ -136,8 +135,7 @@ class WorkflowDailyTerminalsStatistic(Resource):
console_ns.models[WorkflowDailyTerminalsStatisticResponse.__name__], console_ns.models[WorkflowDailyTerminalsStatisticResponse.__name__],
) )
@console_account_admission( @console_account_admission(
rbac_resource_scope=RBACResourceScope.APP, rbac_checks=[RBACCheck(RBACPermission.APP_MONITOR, PlainApp())],
rbac_permission=RBACPermission.APP_MONITOR,
) )
@get_app_model @get_app_model
@model_validate(WorkflowStatisticQuery) @model_validate(WorkflowStatisticQuery)
@ -166,8 +164,7 @@ class WorkflowDailyTokenCostStatistic(Resource):
console_ns.models[WorkflowDailyTokenCostStatisticResponse.__name__], console_ns.models[WorkflowDailyTokenCostStatisticResponse.__name__],
) )
@console_account_admission( @console_account_admission(
rbac_resource_scope=RBACResourceScope.APP, rbac_checks=[RBACCheck(RBACPermission.APP_MONITOR, PlainApp())],
rbac_permission=RBACPermission.APP_MONITOR,
) )
@get_app_model @get_app_model
@model_validate(WorkflowStatisticQuery) @model_validate(WorkflowStatisticQuery)
@ -196,8 +193,7 @@ class WorkflowAverageAppInteractionStatistic(Resource):
console_ns.models[WorkflowAverageAppInteractionStatisticResponse.__name__], console_ns.models[WorkflowAverageAppInteractionStatisticResponse.__name__],
) )
@console_account_admission( @console_account_admission(
rbac_resource_scope=RBACResourceScope.APP, rbac_checks=[RBACCheck(RBACPermission.APP_MONITOR, PlainApp())],
rbac_permission=RBACPermission.APP_MONITOR,
) )
@get_app_model(mode=[AppMode.WORKFLOW]) @get_app_model(mode=[AppMode.WORKFLOW])
@model_validate(WorkflowStatisticQuery) @model_validate(WorkflowStatisticQuery)

View File

@ -8,6 +8,7 @@ from sqlalchemy.orm import sessionmaker
from werkzeug.exceptions import NotFound from werkzeug.exceptions import NotFound
from configs import dify_config 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 controllers.common.schema import query_params_from_model, register_schema_models
from extensions.ext_database import db from extensions.ext_database import db
from fields.base import ResponseModel from fields.base import ResponseModel
@ -21,7 +22,6 @@ from .. import console_ns
from ..app.wraps import get_app_model from ..app.wraps import get_app_model
from ..wraps import ( from ..wraps import (
RBACPermission, RBACPermission,
RBACResourceScope,
account_initialization_required, account_initialization_required,
edit_permission_required, edit_permission_required,
model_validate, model_validate,
@ -100,7 +100,7 @@ class WebhookTriggerApi(Resource):
@login_required @login_required
@account_initialization_required @account_initialization_required
@console_ns.response(200, "Success", console_ns.models[WebhookTriggerResponse.__name__]) @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) @get_app_model(mode=AppMode.WORKFLOW)
@model_validate(Parser) @model_validate(Parser)
def get(self, req_data: Parser, app_model: App): def get(self, req_data: Parser, app_model: App):
@ -134,7 +134,7 @@ class AppTriggersApi(Resource):
@account_initialization_required @account_initialization_required
@console_ns.response(200, "Success", console_ns.models[WorkflowTriggerListResponse.__name__]) @console_ns.response(200, "Success", console_ns.models[WorkflowTriggerListResponse.__name__])
@with_current_tenant_id @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) @get_app_model(mode=AppMode.WORKFLOW)
def get(self, current_tenant_id: str, app_model: App): def get(self, current_tenant_id: str, app_model: App):
"""Get app triggers list""" """Get app triggers list"""
@ -171,7 +171,7 @@ class AppTriggerEnableApi(Resource):
@login_required @login_required
@account_initialization_required @account_initialization_required
@edit_permission_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__]) @console_ns.response(200, "Success", console_ns.models[WorkflowTriggerResponse.__name__])
@with_current_tenant_id @with_current_tenant_id
@get_app_model(mode=AppMode.WORKFLOW) @get_app_model(mode=AppMode.WORKFLOW)

View File

@ -11,35 +11,37 @@ from functools import wraps
from typing import cast, overload from typing import cast, overload
from sqlalchemy import select from sqlalchemy import select
from sqlalchemy.orm import Session from sqlalchemy.orm import Session, scoped_session
from werkzeug.exceptions import Forbidden
from configs import dify_config
from controllers.common.session import with_session 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 controllers.console.app.error import AppNotFoundError
from extensions.ext_application_services import application_services from extensions.ext_application_services import application_services
from extensions.ext_database import db from extensions.ext_database import db
from libs.login import current_account_with_tenant from libs.login import current_account_with_tenant
from models import App, AppMode from models import App, AppMode
from models.agent import Agent, AgentScope from models.agent import AgentScope
from services.app_service import AppService from services.app_service import AppService
__all__ = [ __all__ = [
"agent_manage_required_for_agent_app",
"enforce_agent_manage_or_app_scene",
"get_app_model", "get_app_model",
"get_previewable_app_model", "get_previewable_app_model",
"with_session", "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: 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`.""" """Load the tenant-scoped app row with the request session owned by `with_session`."""
_, current_tenant_id = current_account_with_tenant() _, current_tenant_id = current_account_with_tenant()
app_model = session.scalar( app_model = session.scalar(
select(App).where(App.id == app_id, App.tenant_id == current_tenant_id, App.status == "normal").limit(1) 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 return app_model
@ -49,6 +51,8 @@ def _load_app_model_from_scoped_session(app_id: str) -> App | None:
app_model = db.session.scalar( app_model = db.session.scalar(
select(App).where(App.id == app_id, App.tenant_id == current_tenant_id, App.status == "normal").limit(1) 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 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) 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: def _get_injected_session(args: tuple[object, ...]) -> Session | None:
"""Return the request session inserted by `with_session`, if this handler has been migrated.""" """Return the request session inserted by `with_session`, if this handler has been migrated."""
if len(args) < 2: if len(args) < 2:

View File

@ -5,6 +5,7 @@ from flask_restx import Resource
from pydantic import BaseModel, ConfigDict, Field from pydantic import BaseModel, ConfigDict, Field
from controllers.common.fields import SimpleResultResponse 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.common.schema import register_response_schema_models, register_schema_models
from controllers.console import console_ns from controllers.console import console_ns
from controllers.console.auth.error import ( from controllers.console.auth.error import (
@ -14,7 +15,7 @@ from controllers.console.auth.error import (
InvalidDataSourceApiKeyAuthCredentialsRequestError, InvalidDataSourceApiKeyAuthCredentialsRequestError,
) )
from controllers.console.flask_admission import console_account_admission 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 extensions.ext_application_services import application_services
from fields.base import ResponseModel from fields.base import ResponseModel
from libs.helper import dump_response from libs.helper import dump_response
@ -104,9 +105,7 @@ class ApiKeyAuthDataSourceBinding(Resource):
@console_ns.expect(console_ns.models[ApiKeyAuthBindingPayload.__name__]) @console_ns.expect(console_ns.models[ApiKeyAuthBindingPayload.__name__])
@console_account_admission( @console_account_admission(
allowed_roles=_ADMIN_OR_OWNER_ROLES, allowed_roles=_ADMIN_OR_OWNER_ROLES,
rbac_resource_scope=RBACResourceScope.WORKSPACE, rbac_checks=[RBACCheck(RBACPermission.CREDENTIAL_CREATE, Workspace())],
rbac_permission=RBACPermission.CREDENTIAL_CREATE,
rbac_resource_required=False,
) )
@model_validate(ApiKeyAuthBindingPayload) @model_validate(ApiKeyAuthBindingPayload)
def post(self, req_data: ApiKeyAuthBindingPayload, request_context: RequestContext): 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_ns.response(HTTPStatus.NO_CONTENT, "Binding deleted successfully")
@console_account_admission( @console_account_admission(
allowed_roles=_ADMIN_OR_OWNER_ROLES, allowed_roles=_ADMIN_OR_OWNER_ROLES,
rbac_resource_scope=RBACResourceScope.WORKSPACE, rbac_checks=[RBACCheck(RBACPermission.CREDENTIAL_MANAGE, Workspace())],
rbac_permission=RBACPermission.CREDENTIAL_MANAGE,
rbac_resource_required=False,
) )
def delete(self, request_context: RequestContext, binding_id: UUID): def delete(self, request_context: RequestContext, binding_id: UUID):
application_services().data_source_api_key_auth.delete_binding(request_context, str(binding_id)) application_services().data_source_api_key_auth.delete_binding(request_context, str(binding_id))

View File

@ -9,6 +9,7 @@ from pydantic import BaseModel, Field, ValidationError
from configs import dify_config from configs import dify_config
from controllers.common.fields import RedirectResponse from controllers.common.fields import RedirectResponse
from controllers.common.rbac import RBACCheck, Workspace
from controllers.common.schema import ( from controllers.common.schema import (
query_params_from_model, query_params_from_model,
query_params_from_request, query_params_from_request,
@ -17,7 +18,7 @@ from controllers.common.schema import (
register_schema_models, register_schema_models,
) )
from controllers.console.flask_admission import console_account_admission 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 extensions.ext_application_services import application_services
from fields.base import ResponseModel from fields.base import ResponseModel
from libs.helper import dump_response from libs.helper import dump_response
@ -111,9 +112,7 @@ class OAuthDataSource(Resource):
@console_ns.response(HTTPStatus.FORBIDDEN, "Admin privileges required") @console_ns.response(HTTPStatus.FORBIDDEN, "Admin privileges required")
@console_account_admission( @console_account_admission(
allowed_roles=_ADMIN_OR_OWNER_ROLES, allowed_roles=_ADMIN_OR_OWNER_ROLES,
rbac_resource_scope=RBACResourceScope.WORKSPACE, rbac_checks=[RBACCheck(RBACPermission.CREDENTIAL_MANAGE, Workspace())],
rbac_permission=RBACPermission.CREDENTIAL_MANAGE,
rbac_resource_required=False,
) )
def get(self, request_context: RequestContext, provider: str): def get(self, request_context: RequestContext, provider: str):
try: try:

View File

@ -12,6 +12,7 @@ from sqlalchemy.orm import Session
from werkzeug.exceptions import NotFound from werkzeug.exceptions import NotFound
from controllers.common.fields import SimpleResultResponse, TextContentResponse 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.schema import query_params_from_model, register_response_schema_models, register_schema_models
from controllers.common.session import with_session from controllers.common.session import with_session
from core.datasource.entities.datasource_entities import DatasourceProviderType, OnlineDocumentPagesMessage 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 .. import console_ns
from ..wraps import ( from ..wraps import (
RBACPermission, RBACPermission,
RBACResourceScope,
account_initialization_required, account_initialization_required,
is_admin_or_owner_required, is_admin_or_owner_required,
model_validate, model_validate,
@ -140,7 +140,7 @@ class DataSourceApi(Resource):
@setup_required @setup_required
@login_required @login_required
@is_admin_or_owner_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 @account_initialization_required
@console_ns.response(200, "Success", console_ns.models[DataSourceIntegrateListResponse.__name__]) @console_ns.response(200, "Success", console_ns.models[DataSourceIntegrateListResponse.__name__])
@with_current_tenant_id @with_current_tenant_id
@ -191,7 +191,7 @@ class DataSourceApi(Resource):
@setup_required @setup_required
@login_required @login_required
@is_admin_or_owner_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 @account_initialization_required
@console_ns.response(200, "Success", console_ns.models[SimpleResultResponse.__name__]) @console_ns.response(200, "Success", console_ns.models[SimpleResultResponse.__name__])
@with_current_tenant_id @with_current_tenant_id
@ -420,7 +420,7 @@ class DataSourceNotionDatasetSyncApi(Resource):
@login_required @login_required
@account_initialization_required @account_initialization_required
@console_ns.response(200, "Success", console_ns.models[SimpleResultResponse.__name__]) @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) @with_session(write=False)
def get(self, session: Session, dataset_id: UUID) -> tuple[dict[str, str], int]: def get(self, session: Session, dataset_id: UUID) -> tuple[dict[str, str], int]:
dataset_id_str = str(dataset_id) dataset_id_str = str(dataset_id)
@ -440,7 +440,7 @@ class DataSourceNotionDocumentSyncApi(Resource):
@login_required @login_required
@account_initialization_required @account_initialization_required
@console_ns.response(200, "Success", console_ns.models[SimpleResultResponse.__name__]) @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) @with_session(write=False)
def get(self, session: Session, dataset_id: UUID, document_id: UUID) -> tuple[dict[str, str], int]: def get(self, session: Session, dataset_id: UUID, document_id: UUID) -> tuple[dict[str, str], int]:
dataset_id_str = str(dataset_id) dataset_id_str = str(dataset_id)

View File

@ -13,6 +13,7 @@ from werkzeug.exceptions import Forbidden, NotFound
import services import services
from configs import dify_config from configs import dify_config
from controllers.common.fields import ApiBaseUrlResponse, SimpleResultResponse, UsageCheckResponse 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.schema import query_params_from_model, register_response_schema_models, register_schema_models
from controllers.common.session import with_session from controllers.common.session import with_session
from controllers.console import console_ns 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.datasets.error import DatasetInUseError, DatasetNameDuplicateError, IndexingEstimateError
from controllers.console.wraps import ( from controllers.console.wraps import (
RBACPermission, RBACPermission,
RBACResourceScope,
account_initialization_required, account_initialization_required,
cloud_edition_billing_rate_limit_check, cloud_edition_billing_rate_limit_check,
enterprise_license_required, enterprise_license_required,
@ -589,9 +589,7 @@ class DatasetListApi(Resource):
@setup_required @setup_required
@login_required @login_required
@account_initialization_required @account_initialization_required
@rbac_permission_required( @rbac_permission_required(RBACCheck(RBACPermission.DATASET_CREATE_AND_MANAGEMENT, Workspace()))
RBACResourceScope.DATASET, RBACPermission.DATASET_CREATE_AND_MANAGEMENT, resource_required=False
)
@cloud_edition_billing_rate_limit_check("knowledge") @cloud_edition_billing_rate_limit_check("knowledge")
@with_current_user @with_current_user
@with_current_tenant_id @with_current_tenant_id
@ -662,7 +660,7 @@ class DatasetApi(Resource):
@setup_required @setup_required
@login_required @login_required
@account_initialization_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_user
@with_current_tenant_id @with_current_tenant_id
@with_session(write=False) @with_session(write=False)
@ -731,7 +729,7 @@ class DatasetApi(Resource):
@cloud_edition_billing_rate_limit_check("knowledge") @cloud_edition_billing_rate_limit_check("knowledge")
@with_current_user @with_current_user
@with_current_tenant_id @with_current_tenant_id
@rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_EDIT) @rbac_permission_required(RBACCheck(RBACPermission.DATASET_EDIT, DatasetId()))
@with_session @with_session
@model_validate(DatasetUpdatePayload) @model_validate(DatasetUpdatePayload)
def patch( def patch(
@ -798,7 +796,7 @@ class DatasetApi(Resource):
@cloud_edition_billing_rate_limit_check("knowledge") @cloud_edition_billing_rate_limit_check("knowledge")
@console_ns.response(204, "Dataset deleted successfully") @console_ns.response(204, "Dataset deleted successfully")
@with_current_user @with_current_user
@rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_EDIT) @rbac_permission_required(RBACCheck(RBACPermission.DATASET_EDIT, DatasetId()))
@with_session @with_session
def delete(self, session: Session, current_user: Account, dataset_id: UUID): def delete(self, session: Session, current_user: Account, dataset_id: UUID):
dataset_id_str = str(dataset_id) dataset_id_str = str(dataset_id)
@ -831,7 +829,7 @@ class DatasetUseCheckApi(Resource):
@account_initialization_required @account_initialization_required
@with_current_user @with_current_user
@with_current_tenant_id @with_current_tenant_id
@rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_READONLY) @rbac_permission_required(RBACCheck(RBACPermission.DATASET_READONLY, DatasetId()))
@with_session(write=False) @with_session(write=False)
def get(self, session: Session, current_tenant_id: str, current_user: Account, dataset_id: UUID): 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) dataset = _get_accessible_dataset(dataset_id, current_tenant_id, current_user, session)
@ -853,7 +851,7 @@ class DatasetQueryApi(Resource):
@login_required @login_required
@account_initialization_required @account_initialization_required
@with_current_user @with_current_user
@rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_READONLY) @rbac_permission_required(RBACCheck(RBACPermission.DATASET_READONLY, DatasetId()))
@with_session(write=False) @with_session(write=False)
def get(self, session: Session, current_user: Account, dataset_id: UUID): def get(self, session: Session, current_user: Account, dataset_id: UUID):
dataset_id_str = str(dataset_id) dataset_id_str = str(dataset_id)
@ -1009,7 +1007,7 @@ class DatasetRelatedAppListApi(Resource):
@login_required @login_required
@account_initialization_required @account_initialization_required
@with_current_user @with_current_user
@rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_READONLY) @rbac_permission_required(RBACCheck(RBACPermission.DATASET_READONLY, DatasetId()))
@with_session(write=False) @with_session(write=False)
def get(self, session: Session, current_user: Account, dataset_id: UUID): def get(self, session: Session, current_user: Account, dataset_id: UUID):
dataset_id_str = str(dataset_id) dataset_id_str = str(dataset_id)
@ -1048,7 +1046,7 @@ class DatasetIndexingStatusApi(Resource):
@account_initialization_required @account_initialization_required
@with_current_user @with_current_user
@with_current_tenant_id @with_current_tenant_id
@rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_READONLY) @rbac_permission_required(RBACCheck(RBACPermission.DATASET_READONLY, DatasetId()))
@with_session(write=False) @with_session(write=False)
def get(self, session: Session, current_tenant_id: str, current_user: Account, dataset_id: UUID): 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) dataset = _get_accessible_dataset(dataset_id, current_tenant_id, current_user, session)
@ -1112,7 +1110,7 @@ class DatasetApiKeyApi(Resource):
@setup_required @setup_required
@login_required @login_required
@is_admin_or_owner_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 @account_initialization_required
@with_current_tenant_id @with_current_tenant_id
@with_session(write=False) @with_session(write=False)
@ -1130,7 +1128,7 @@ class DatasetApiKeyApi(Resource):
@setup_required @setup_required
@login_required @login_required
@is_admin_or_owner_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 @account_initialization_required
@with_current_tenant_id @with_current_tenant_id
@with_session @with_session
@ -1191,7 +1189,7 @@ class DatasetApiDeleteApi(Resource):
@setup_required @setup_required
@login_required @login_required
@is_admin_or_owner_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 @account_initialization_required
@with_current_tenant_id @with_current_tenant_id
@with_session @with_session
@ -1228,7 +1226,7 @@ class DatasetEnableApiApi(Resource):
@console_ns.response(200, "Success", console_ns.models[SimpleResultResponse.__name__]) @console_ns.response(200, "Success", console_ns.models[SimpleResultResponse.__name__])
@with_current_user @with_current_user
@with_current_tenant_id @with_current_tenant_id
@rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_EDIT) @rbac_permission_required(RBACCheck(RBACPermission.DATASET_EDIT, DatasetId()))
@with_session @with_session
def post(self, session: Session, current_tenant_id: str, current_user: Account, dataset_id: UUID, status: str): 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) dataset = _get_accessible_dataset(dataset_id, current_tenant_id, current_user, session)
@ -1303,7 +1301,7 @@ class DatasetErrorDocs(Resource):
@account_initialization_required @account_initialization_required
@with_current_user @with_current_user
@with_current_tenant_id @with_current_tenant_id
@rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_READONLY) @rbac_permission_required(RBACCheck(RBACPermission.DATASET_READONLY, DatasetId()))
@with_session(write=False) @with_session(write=False)
def get(self, session: Session, current_tenant_id: str, current_user: Account, dataset_id: UUID): 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) dataset = _get_accessible_dataset(dataset_id, current_tenant_id, current_user, session)
@ -1330,7 +1328,7 @@ class DatasetPermissionUserListApi(Resource):
@login_required @login_required
@account_initialization_required @account_initialization_required
@with_current_user @with_current_user
@rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_READONLY) @rbac_permission_required(RBACCheck(RBACPermission.DATASET_READONLY, DatasetId()))
@with_session(write=False) @with_session(write=False)
def get(self, session: Session, current_user: Account, dataset_id: UUID): def get(self, session: Session, current_user: Account, dataset_id: UUID):
dataset_id_str = str(dataset_id) dataset_id_str = str(dataset_id)
@ -1363,7 +1361,7 @@ class DatasetAutoDisableLogApi(Resource):
@account_initialization_required @account_initialization_required
@with_current_user @with_current_user
@with_current_tenant_id @with_current_tenant_id
@rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_READONLY) @rbac_permission_required(RBACCheck(RBACPermission.DATASET_READONLY, DatasetId()))
@with_session(write=False) @with_session(write=False)
def get(self, session: Session, current_tenant_id: str, current_user: Account, dataset_id: UUID): 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) dataset = _get_accessible_dataset(dataset_id, current_tenant_id, current_user, session)

View File

@ -19,10 +19,11 @@ import services
from configs import dify_config from configs import dify_config
from controllers.common.controller_schemas import DocumentBatchDownloadZipPayload from controllers.common.controller_schemas import DocumentBatchDownloadZipPayload
from controllers.common.fields import SimpleResultMessageResponse, SimpleResultResponse, UrlResponse 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.schema import register_response_schema_models, register_schema_models
from controllers.common.session import with_session from controllers.common.session import with_session
from controllers.console import console_ns 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.entities.knowledge_entities import IndexingEstimate
from core.errors.error import ( from core.errors.error import (
LLMBadRequestError, LLMBadRequestError,
@ -405,7 +406,7 @@ class DatasetDocumentListApi(Resource):
@account_initialization_required @account_initialization_required
@with_current_user @with_current_user
@with_current_tenant_id @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) @with_session(write=False)
def get(self, session: Session, current_tenant_id: str, current_user: Account, dataset_id: UUID): def get(self, session: Session, current_tenant_id: str, current_user: Account, dataset_id: UUID):
dataset_id_str = str(dataset_id) dataset_id_str = str(dataset_id)
@ -534,7 +535,7 @@ class DatasetDocumentListApi(Resource):
@console_ns.expect(console_ns.models[KnowledgeConfig.__name__]) @console_ns.expect(console_ns.models[KnowledgeConfig.__name__])
@console_ns.response(200, "Documents created successfully", console_ns.models[DatasetAndDocumentResponse.__name__]) @console_ns.response(200, "Documents created successfully", console_ns.models[DatasetAndDocumentResponse.__name__])
@with_current_user @with_current_user
@rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_EDIT) @rbac_permission_required(RBACCheck(RBACPermission.DATASET_EDIT, DatasetId()))
@with_session @with_session
def post(self, session: Session, current_user: Account, dataset_id: UUID): def post(self, session: Session, current_user: Account, dataset_id: UUID):
dataset_id_str = str(dataset_id) dataset_id_str = str(dataset_id)
@ -585,7 +586,7 @@ class DatasetDocumentListApi(Resource):
@console_ns.response(204, "Documents deleted successfully") @console_ns.response(204, "Documents deleted successfully")
@with_current_user @with_current_user
@with_current_tenant_id @with_current_tenant_id
@rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_EDIT) @rbac_permission_required(RBACCheck(RBACPermission.DATASET_EDIT, DatasetId()))
@with_session @with_session
def delete( def delete(
self, self,
@ -719,7 +720,7 @@ class DocumentIndexingEstimateApi(DocumentResource):
@account_initialization_required @account_initialization_required
@with_current_user @with_current_user
@with_current_tenant_id @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 @with_session
def get(self, session: Session, current_tenant_id: str, current_user: Account, dataset_id: UUID, document_id: UUID): def get(self, session: Session, current_tenant_id: str, current_user: Account, dataset_id: UUID, document_id: UUID):
dataset_id_str = str(dataset_id) dataset_id_str = str(dataset_id)
@ -811,7 +812,7 @@ class DocumentBatchIndexingEstimateApi(DocumentResource):
@account_initialization_required @account_initialization_required
@with_current_user @with_current_user
@with_current_tenant_id @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 @with_session
def get(self, session: Session, current_tenant_id: str, current_user: Account, dataset_id: UUID, batch: str): def get(self, session: Session, current_tenant_id: str, current_user: Account, dataset_id: UUID, batch: str):
dataset_id_str = str(dataset_id) dataset_id_str = str(dataset_id)
@ -933,7 +934,7 @@ class DocumentBatchIndexingStatusApi(DocumentResource):
@login_required @login_required
@account_initialization_required @account_initialization_required
@with_current_user @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) @with_session(write=False)
def get(self, session: Session, current_user: Account, dataset_id: UUID, batch: str): def get(self, session: Session, current_user: Account, dataset_id: UUID, batch: str):
dataset_id_str = str(dataset_id) dataset_id_str = str(dataset_id)
@ -993,7 +994,7 @@ class DocumentIndexingStatusApi(DocumentResource):
@account_initialization_required @account_initialization_required
@with_current_user @with_current_user
@with_current_tenant_id @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) @with_session(write=False)
def get(self, session: Session, current_tenant_id: str, current_user: Account, dataset_id: UUID, document_id: UUID): def get(self, session: Session, current_tenant_id: str, current_user: Account, dataset_id: UUID, document_id: UUID):
dataset_id_str = str(dataset_id) dataset_id_str = str(dataset_id)
@ -1059,7 +1060,7 @@ class DocumentApi(DocumentResource):
@account_initialization_required @account_initialization_required
@with_current_user @with_current_user
@with_current_tenant_id @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) @with_session(write=False)
def get(self, session: Session, current_tenant_id: str, current_user: Account, dataset_id: UUID, document_id: UUID): def get(self, session: Session, current_tenant_id: str, current_user: Account, dataset_id: UUID, document_id: UUID):
dataset_id_str = str(dataset_id) dataset_id_str = str(dataset_id)
@ -1133,7 +1134,7 @@ class DocumentApi(DocumentResource):
@console_ns.response(204, "Document deleted successfully") @console_ns.response(204, "Document deleted successfully")
@with_current_user @with_current_user
@with_current_tenant_id @with_current_tenant_id
@rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_EDIT) @rbac_permission_required(RBACCheck(RBACPermission.DATASET_EDIT, DatasetId()))
@with_session @with_session
def delete( def delete(
self, session: Session, current_tenant_id: str, current_user: Account, dataset_id: UUID, document_id: UUID 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") @cloud_edition_billing_rate_limit_check("knowledge")
@with_current_user @with_current_user
@with_current_tenant_id @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) @with_session(write=False)
def get( def get(
self, session: Session, current_tenant_id: str, current_user: Account, dataset_id: UUID, document_id: UUID 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__]) @console_ns.expect(console_ns.models[DocumentBatchDownloadZipPayload.__name__])
@with_current_user @with_current_user
@with_current_tenant_id @with_current_tenant_id
@rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_EDIT) @rbac_permission_required(RBACCheck(RBACPermission.DATASET_EDIT, DatasetId()))
@with_session(write=False) @with_session(write=False)
def post(self, session: Session, current_tenant_id: str, current_user: Account, dataset_id: UUID): def post(self, session: Session, current_tenant_id: str, current_user: Account, dataset_id: UUID):
"""Stream a ZIP archive containing the requested uploaded documents.""" """Stream a ZIP archive containing the requested uploaded documents."""
@ -1245,7 +1246,7 @@ class DocumentProcessingApi(DocumentResource):
@cloud_edition_billing_rate_limit_check("knowledge") @cloud_edition_billing_rate_limit_check("knowledge")
@with_current_user @with_current_user
@with_current_tenant_id @with_current_tenant_id
@rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_EDIT) @rbac_permission_required(RBACCheck(RBACPermission.DATASET_EDIT, DatasetId()))
@with_session @with_session
def patch( def patch(
self, self,
@ -1302,7 +1303,7 @@ class DocumentMetadataApi(DocumentResource):
@account_initialization_required @account_initialization_required
@with_current_user @with_current_user
@with_current_tenant_id @with_current_tenant_id
@rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_EDIT) @rbac_permission_required(RBACCheck(RBACPermission.DATASET_EDIT, DatasetId()))
@with_session @with_session
@model_validate(DocumentMetadataUpdatePayload) @model_validate(DocumentMetadataUpdatePayload)
def put( def put(
@ -1361,7 +1362,7 @@ class DocumentStatusApi(DocumentResource):
@cloud_edition_billing_rate_limit_check("knowledge") @cloud_edition_billing_rate_limit_check("knowledge")
@console_ns.response(200, "Success", console_ns.models[SimpleResultResponse.__name__]) @console_ns.response(200, "Success", console_ns.models[SimpleResultResponse.__name__])
@with_current_user @with_current_user
@rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_EDIT) @rbac_permission_required(RBACCheck(RBACPermission.DATASET_EDIT, DatasetId()))
@with_session @with_session
def patch( def patch(
self, self,
@ -1407,7 +1408,7 @@ class DocumentPauseApi(DocumentResource):
@console_ns.response(204, "Document paused successfully") @console_ns.response(204, "Document paused successfully")
@with_current_user @with_current_user
@with_current_tenant_id @with_current_tenant_id
@rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_EDIT) @rbac_permission_required(RBACCheck(RBACPermission.DATASET_EDIT, DatasetId()))
@with_session @with_session
def patch( def patch(
self, self,
@ -1447,7 +1448,7 @@ class DocumentRecoverApi(DocumentResource):
@console_ns.response(204, "Document resumed successfully") @console_ns.response(204, "Document resumed successfully")
@with_current_user @with_current_user
@with_current_tenant_id @with_current_tenant_id
@rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_EDIT) @rbac_permission_required(RBACCheck(RBACPermission.DATASET_EDIT, DatasetId()))
@with_session @with_session
def patch( def patch(
self, self,
@ -1487,7 +1488,7 @@ class DocumentRetryApi(DocumentResource):
@console_ns.response(204, "Documents retry started successfully") @console_ns.response(204, "Documents retry started successfully")
@with_current_user @with_current_user
@with_current_tenant_id @with_current_tenant_id
@rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_EDIT) @rbac_permission_required(RBACCheck(RBACPermission.DATASET_EDIT, DatasetId()))
@with_session @with_session
@model_validate(DocumentRetryPayload) @model_validate(DocumentRetryPayload)
def post( def post(
@ -1552,7 +1553,7 @@ class DocumentRenameApi(DocumentResource):
@console_ns.response(200, "Document renamed successfully", console_ns.models[DocumentResponse.__name__]) @console_ns.response(200, "Document renamed successfully", console_ns.models[DocumentResponse.__name__])
@console_ns.expect(console_ns.models[DocumentRenamePayload.__name__]) @console_ns.expect(console_ns.models[DocumentRenamePayload.__name__])
@with_current_user @with_current_user
@rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_EDIT) @rbac_permission_required(RBACCheck(RBACPermission.DATASET_EDIT, DatasetId()))
@with_session @with_session
@model_validate(DocumentRenamePayload) @model_validate(DocumentRenamePayload)
def post( def post(
@ -1587,7 +1588,7 @@ class WebsiteDocumentSyncApi(DocumentResource):
@console_ns.response(200, "Success", console_ns.models[SimpleResultResponse.__name__]) @console_ns.response(200, "Success", console_ns.models[SimpleResultResponse.__name__])
@with_current_user @with_current_user
@with_current_tenant_id @with_current_tenant_id
@rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_EDIT) @rbac_permission_required(RBACCheck(RBACPermission.DATASET_EDIT, DatasetId()))
@with_session @with_session
def get( def get(
self, self,
@ -1629,7 +1630,7 @@ class DocumentPipelineExecutionLogApi(DocumentResource):
@account_initialization_required @account_initialization_required
@with_current_user @with_current_user
@with_current_tenant_id @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) @with_session(write=False)
def get( def get(
self, self,
@ -1681,7 +1682,7 @@ class DocumentGenerateSummaryApi(Resource):
@account_initialization_required @account_initialization_required
@cloud_edition_billing_rate_limit_check("knowledge") @cloud_edition_billing_rate_limit_check("knowledge")
@with_current_user @with_current_user
@rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_EDIT) @rbac_permission_required(RBACCheck(RBACPermission.DATASET_EDIT, DatasetId()))
@with_session @with_session
@model_validate(GenerateSummaryPayload) @model_validate(GenerateSummaryPayload)
def post(self, req_data: GenerateSummaryPayload, session: Session, current_user: Account, dataset_id: UUID): def post(self, req_data: GenerateSummaryPayload, session: Session, current_user: Account, dataset_id: UUID):
@ -1782,7 +1783,7 @@ class DocumentSummaryStatusApi(DocumentResource):
@login_required @login_required
@account_initialization_required @account_initialization_required
@with_current_user @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) @with_session(write=False)
def get(self, session: Session, current_user: Account, dataset_id: UUID, document_id: UUID): def get(self, session: Session, current_user: Account, dataset_id: UUID, document_id: UUID):
""" """

View File

@ -15,6 +15,7 @@ import services
from configs import dify_config from configs import dify_config
from controllers.common.controller_schemas import ChildChunkCreatePayload, ChildChunkUpdatePayload from controllers.common.controller_schemas import ChildChunkCreatePayload, ChildChunkUpdatePayload
from controllers.common.fields import SimpleResultResponse from controllers.common.fields import SimpleResultResponse
from controllers.common.rbac import DatasetId, RBACCheck
from controllers.common.schema import ( from controllers.common.schema import (
query_params_from_model, query_params_from_model,
query_params_from_request, query_params_from_request,
@ -31,7 +32,6 @@ from controllers.console.datasets.error import (
) )
from controllers.console.wraps import ( from controllers.console.wraps import (
RBACPermission, RBACPermission,
RBACResourceScope,
account_initialization_required, account_initialization_required,
cloud_edition_billing_knowledge_limit_check, cloud_edition_billing_knowledge_limit_check,
cloud_edition_billing_rate_limit_check, cloud_edition_billing_rate_limit_check,
@ -191,7 +191,7 @@ class DatasetDocumentSegmentListApi(Resource):
@account_initialization_required @account_initialization_required
@with_current_user @with_current_user
@with_current_tenant_id @with_current_tenant_id
@rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_READONLY) @rbac_permission_required(RBACCheck(RBACPermission.DATASET_READONLY, DatasetId()))
@with_session(write=False) @with_session(write=False)
def get(self, session: Session, current_tenant_id: str, current_user: Account, dataset_id: UUID, document_id: UUID): def get(self, session: Session, current_tenant_id: str, current_user: Account, dataset_id: UUID, document_id: UUID):
dataset_id_str = str(dataset_id) dataset_id_str = str(dataset_id)
@ -302,7 +302,7 @@ class DatasetDocumentSegmentListApi(Resource):
@console_ns.doc(params=query_params_from_model(SegmentIdListQuery)) @console_ns.doc(params=query_params_from_model(SegmentIdListQuery))
@console_ns.response(204, "Segments deleted successfully") @console_ns.response(204, "Segments deleted successfully")
@with_current_user @with_current_user
@rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_EDIT) @rbac_permission_required(RBACCheck(RBACPermission.DATASET_EDIT, DatasetId()))
@with_session @with_session
def delete(self, session: Session, current_user: Account, dataset_id: UUID, document_id: UUID): def delete(self, session: Session, current_user: Account, dataset_id: UUID, document_id: UUID):
# check dataset # check dataset
@ -342,7 +342,7 @@ class DatasetDocumentSegmentApi(Resource):
@console_ns.response(200, "Success", console_ns.models[SimpleResultResponse.__name__]) @console_ns.response(200, "Success", console_ns.models[SimpleResultResponse.__name__])
@with_current_user @with_current_user
@with_current_tenant_id @with_current_tenant_id
@rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_EDIT) @rbac_permission_required(RBACCheck(RBACPermission.DATASET_EDIT, DatasetId()))
@with_session @with_session
def patch( def patch(
self, self,
@ -413,7 +413,7 @@ class DatasetDocumentSegmentAddApi(Resource):
@console_ns.response(200, "Segment created successfully", console_ns.models[SegmentDetailResponse.__name__]) @console_ns.response(200, "Segment created successfully", console_ns.models[SegmentDetailResponse.__name__])
@with_current_user @with_current_user
@with_current_tenant_id @with_current_tenant_id
@rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_EDIT) @rbac_permission_required(RBACCheck(RBACPermission.DATASET_EDIT, DatasetId()))
@with_session @with_session
@model_validate(SegmentCreatePayload) @model_validate(SegmentCreatePayload)
def post( def post(
@ -485,7 +485,7 @@ class DatasetDocumentSegmentUpdateApi(Resource):
@console_ns.response(200, "Segment updated successfully", console_ns.models[SegmentDetailResponse.__name__]) @console_ns.response(200, "Segment updated successfully", console_ns.models[SegmentDetailResponse.__name__])
@with_current_user @with_current_user
@with_current_tenant_id @with_current_tenant_id
@rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_EDIT) @rbac_permission_required(RBACCheck(RBACPermission.DATASET_EDIT, DatasetId()))
@with_session @with_session
@model_validate(SegmentUpdatePayload) @model_validate(SegmentUpdatePayload)
def patch( def patch(
@ -566,7 +566,7 @@ class DatasetDocumentSegmentUpdateApi(Resource):
@console_ns.response(204, "Segment deleted successfully") @console_ns.response(204, "Segment deleted successfully")
@with_current_user @with_current_user
@with_current_tenant_id @with_current_tenant_id
@rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_EDIT) @rbac_permission_required(RBACCheck(RBACPermission.DATASET_EDIT, DatasetId()))
@with_session @with_session
def delete( def delete(
self, self,
@ -617,7 +617,7 @@ class DatasetDocumentSegmentBatchImportApi(Resource):
@console_ns.expect(console_ns.models[BatchImportPayload.__name__]) @console_ns.expect(console_ns.models[BatchImportPayload.__name__])
@with_current_user @with_current_user
@with_current_tenant_id @with_current_tenant_id
@rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_EDIT) @rbac_permission_required(RBACCheck(RBACPermission.DATASET_EDIT, DatasetId()))
@with_session @with_session
@model_validate(BatchImportPayload) @model_validate(BatchImportPayload)
def post( def post(
@ -676,7 +676,7 @@ class DatasetDocumentSegmentBatchImportApi(Resource):
@setup_required @setup_required
@login_required @login_required
@account_initialization_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): def get(self, job_id=None, dataset_id: UUID | None = None, document_id: UUID | None = None):
if job_id is None: if job_id is None:
raise NotFound("The job does not exist.") 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__]) @console_ns.response(200, "Child chunk created successfully", console_ns.models[ChildChunkDetailResponse.__name__])
@with_current_user @with_current_user
@with_current_tenant_id @with_current_tenant_id
@rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_EDIT) @rbac_permission_required(RBACCheck(RBACPermission.DATASET_EDIT, DatasetId()))
@with_session @with_session
@model_validate(ChildChunkCreatePayload) @model_validate(ChildChunkCreatePayload)
def post( def post(
@ -764,7 +764,7 @@ class ChildChunkAddApi(Resource):
@login_required @login_required
@account_initialization_required @account_initialization_required
@with_current_tenant_id @with_current_tenant_id
@rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_READONLY) @rbac_permission_required(RBACCheck(RBACPermission.DATASET_READONLY, DatasetId()))
@with_session(write=False) @with_session(write=False)
def get(self, session: Session, current_tenant_id: str, dataset_id: UUID, document_id: UUID, segment_id: UUID): def get(self, session: Session, current_tenant_id: str, dataset_id: UUID, document_id: UUID, segment_id: UUID):
# check dataset # check dataset
@ -819,7 +819,7 @@ class ChildChunkAddApi(Resource):
@console_ns.expect(console_ns.models[ChildChunkBatchUpdatePayload.__name__]) @console_ns.expect(console_ns.models[ChildChunkBatchUpdatePayload.__name__])
@with_current_user @with_current_user
@with_current_tenant_id @with_current_tenant_id
@rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_EDIT) @rbac_permission_required(RBACCheck(RBACPermission.DATASET_EDIT, DatasetId()))
@with_session @with_session
@model_validate(ChildChunkBatchUpdatePayload) @model_validate(ChildChunkBatchUpdatePayload)
def patch( def patch(
@ -873,7 +873,7 @@ class ChildChunkUpdateApi(Resource):
@console_ns.response(204, "Child chunk deleted successfully") @console_ns.response(204, "Child chunk deleted successfully")
@with_current_user @with_current_user
@with_current_tenant_id @with_current_tenant_id
@rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_EDIT) @rbac_permission_required(RBACCheck(RBACPermission.DATASET_EDIT, DatasetId()))
@with_session @with_session
def delete( def delete(
self, self,
@ -926,7 +926,7 @@ class ChildChunkUpdateApi(Resource):
@console_ns.response(200, "Child chunk updated successfully", console_ns.models[ChildChunkDetailResponse.__name__]) @console_ns.response(200, "Child chunk updated successfully", console_ns.models[ChildChunkDetailResponse.__name__])
@with_current_user @with_current_user
@with_current_tenant_id @with_current_tenant_id
@rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_EDIT) @rbac_permission_required(RBACCheck(RBACPermission.DATASET_EDIT, DatasetId()))
@with_session @with_session
@model_validate(ChildChunkUpdatePayload) @model_validate(ChildChunkUpdatePayload)
def patch( def patch(

View File

@ -12,6 +12,7 @@ from werkzeug.exceptions import Forbidden, InternalServerError, NotFound
import services import services
from controllers.common.fields import UsageCountResponse from controllers.common.fields import UsageCountResponse
from controllers.common.rbac import DatasetId, RBACCheck, Workspace
from controllers.common.schema import ( from controllers.common.schema import (
query_params_from_model, query_params_from_model,
register_response_schema_models, register_response_schema_models,
@ -22,7 +23,6 @@ from controllers.console import console_ns
from controllers.console.datasets.error import DatasetNameDuplicateError from controllers.console.datasets.error import DatasetNameDuplicateError
from controllers.console.wraps import ( from controllers.console.wraps import (
RBACPermission, RBACPermission,
RBACResourceScope,
account_initialization_required, account_initialization_required,
edit_permission_required, edit_permission_required,
model_validate, model_validate,
@ -350,9 +350,7 @@ class ExternalDatasetCreateApi(Resource):
@login_required @login_required
@account_initialization_required @account_initialization_required
@edit_permission_required @edit_permission_required
@rbac_permission_required( @rbac_permission_required(RBACCheck(RBACPermission.DATASET_EXTERNAL_CONNECT, Workspace()))
RBACResourceScope.DATASET, RBACPermission.DATASET_EXTERNAL_CONNECT, resource_required=False
)
@with_current_user @with_current_user
@with_current_tenant_id @with_current_tenant_id
@with_session @with_session
@ -411,7 +409,7 @@ class ExternalKnowledgeHitTestingApi(Resource):
@login_required @login_required
@account_initialization_required @account_initialization_required
@with_current_user @with_current_user
@rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_PIPELINE_TEST) @rbac_permission_required(RBACCheck(RBACPermission.DATASET_PIPELINE_TEST, DatasetId()))
@with_session @with_session
@model_validate(ExternalHitTestingPayload) @model_validate(ExternalHitTestingPayload)
def post(self, req_data: ExternalHitTestingPayload, session: Session, current_user: Account, dataset_id: UUID): def post(self, req_data: ExternalHitTestingPayload, session: Session, current_user: Account, dataset_id: UUID):

View File

@ -5,9 +5,10 @@ from uuid import UUID
from flask_restx import Resource from flask_restx import Resource
from sqlalchemy.orm import Session 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.common.schema import register_response_schema_models, register_schema_models
from controllers.console.app.wraps import with_session 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 fields.hit_testing_fields import HitTestingResponse
from libs.helper import dump_response from libs.helper import dump_response
from libs.login import login_required from libs.login import login_required
@ -46,7 +47,7 @@ class HitTestingApi(Resource, DatasetsHitTestingBase):
@cloud_edition_billing_rate_limit_check("knowledge") @cloud_edition_billing_rate_limit_check("knowledge")
@with_current_tenant_id @with_current_tenant_id
@with_current_user @with_current_user
@rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_PIPELINE_TEST) @rbac_permission_required(RBACCheck(RBACPermission.DATASET_PIPELINE_TEST, DatasetId()))
@with_session @with_session
def post( def post(
self, session: Session, current_user: Account, current_tenant_id: str, dataset_id: UUID self, session: Session, current_user: Account, current_tenant_id: str, dataset_id: UUID

View File

@ -8,12 +8,12 @@ from werkzeug.exceptions import Forbidden, NotFound
import services import services
from configs import dify_config from configs import dify_config
from controllers.common.controller_schemas import MetadataUpdatePayload 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.schema import register_response_schema_models, register_schema_models
from controllers.common.session import with_session from controllers.common.session import with_session
from controllers.console import console_ns from controllers.console import console_ns
from controllers.console.wraps import ( from controllers.console.wraps import (
RBACPermission, RBACPermission,
RBACResourceScope,
account_initialization_required, account_initialization_required,
enterprise_license_required, enterprise_license_required,
model_validate, model_validate,
@ -61,7 +61,7 @@ class DatasetMetadataCreateApi(Resource):
@console_ns.expect(console_ns.models[MetadataArgs.__name__]) @console_ns.expect(console_ns.models[MetadataArgs.__name__])
@with_current_user @with_current_user
@with_current_tenant_id @with_current_tenant_id
@rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_EDIT) @rbac_permission_required(RBACCheck(RBACPermission.DATASET_EDIT, DatasetId()))
@with_session @with_session
@model_validate(MetadataArgs) @model_validate(MetadataArgs)
def post( def post(
@ -92,7 +92,7 @@ class DatasetMetadataCreateApi(Resource):
) )
@with_current_user @with_current_user
@with_current_tenant_id @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) @with_session(write=False)
def get(self, session: Session, current_tenant_id: str, current_user: Account, dataset_id: UUID): def get(self, session: Session, current_tenant_id: str, current_user: Account, dataset_id: UUID):
dataset_id_str = str(dataset_id) dataset_id_str = str(dataset_id)
@ -118,7 +118,7 @@ class DatasetMetadataApi(Resource):
@console_ns.expect(console_ns.models[MetadataUpdatePayload.__name__]) @console_ns.expect(console_ns.models[MetadataUpdatePayload.__name__])
@with_current_user @with_current_user
@with_current_tenant_id @with_current_tenant_id
@rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_EDIT) @rbac_permission_required(RBACCheck(RBACPermission.DATASET_EDIT, DatasetId()))
@with_session @with_session
@model_validate(MetadataUpdatePayload) @model_validate(MetadataUpdatePayload)
def patch( def patch(
@ -149,7 +149,7 @@ class DatasetMetadataApi(Resource):
@console_ns.response(204, "Metadata deleted successfully") @console_ns.response(204, "Metadata deleted successfully")
@with_current_user @with_current_user
@with_current_tenant_id @with_current_tenant_id
@rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_EDIT) @rbac_permission_required(RBACCheck(RBACPermission.DATASET_EDIT, DatasetId()))
@with_session @with_session
def delete( def delete(
self, self,
@ -195,7 +195,7 @@ class DatasetMetadataBuiltInFieldActionApi(Resource):
@enterprise_license_required @enterprise_license_required
@console_ns.response(204, "Action completed successfully") @console_ns.response(204, "Action completed successfully")
@with_current_user @with_current_user
@rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_EDIT) @rbac_permission_required(RBACCheck(RBACPermission.DATASET_EDIT, DatasetId()))
@with_session @with_session
def post(self, session: Session, current_user: Account, dataset_id: UUID, action: Literal["enable", "disable"]): def post(self, session: Session, current_user: Account, dataset_id: UUID, action: Literal["enable", "disable"]):
dataset_id_str = str(dataset_id) dataset_id_str = str(dataset_id)
@ -227,7 +227,7 @@ class DocumentMetadataEditApi(Resource):
@console_ns.response(404, "Dataset, document, or metadata not found") @console_ns.response(404, "Dataset, document, or metadata not found")
@with_current_user @with_current_user
@with_current_tenant_id @with_current_tenant_id
@rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_EDIT) @rbac_permission_required(RBACCheck(RBACPermission.DATASET_EDIT, DatasetId()))
@with_session @with_session
@model_validate(MetadataOperationData) @model_validate(MetadataOperationData)
def post( def post(

View File

@ -7,11 +7,11 @@ from werkzeug.exceptions import Forbidden, NotFound
from configs import dify_config from configs import dify_config
from controllers.common.fields import SimpleResultResponse 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.common.schema import query_params_from_model, register_response_schema_models, register_schema_models
from controllers.console import console_ns from controllers.console import console_ns
from controllers.console.wraps import ( from controllers.console.wraps import (
RBACPermission, RBACPermission,
RBACResourceScope,
account_initialization_required, account_initialization_required,
edit_permission_required, edit_permission_required,
model_validate, model_validate,
@ -167,7 +167,7 @@ class DatasourcePluginOAuthAuthorizationUrl(Resource):
@login_required @login_required
@account_initialization_required @account_initialization_required
@edit_permission_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_user
@with_current_tenant_id @with_current_tenant_id
def get(self, current_tenant_id: str, current_user: Account, provider_id: str): def get(self, current_tenant_id: str, current_user: Account, provider_id: str):
@ -310,7 +310,7 @@ class DatasourceAuth(Resource):
@login_required @login_required
@account_initialization_required @account_initialization_required
@edit_permission_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 @with_current_tenant_id
@model_validate(DatasourceCredentialPayload) @model_validate(DatasourceCredentialPayload)
def post(self, req_data: DatasourceCredentialPayload, current_tenant_id: str, provider_id: str): def post(self, req_data: DatasourceCredentialPayload, current_tenant_id: str, provider_id: str):
@ -337,7 +337,7 @@ class DatasourceAuth(Resource):
@login_required @login_required
@account_initialization_required @account_initialization_required
@edit_permission_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_user
@with_current_tenant_id @with_current_tenant_id
def get(self, current_tenant_id: str, user: Account, provider_id: str): def get(self, current_tenant_id: str, user: Account, provider_id: str):
@ -362,7 +362,7 @@ class DatasourceAuthDeleteApi(Resource):
@login_required @login_required
@account_initialization_required @account_initialization_required
@edit_permission_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 @with_current_tenant_id
@model_validate(DatasourceCredentialDeletePayload) @model_validate(DatasourceCredentialDeletePayload)
def post(self, req_data: DatasourceCredentialDeletePayload, current_tenant_id: str, provider_id: str): def post(self, req_data: DatasourceCredentialDeletePayload, current_tenant_id: str, provider_id: str):
@ -391,7 +391,7 @@ class DatasourceAuthUpdateApi(Resource):
@login_required @login_required
@account_initialization_required @account_initialization_required
@edit_permission_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 @with_current_tenant_id
@model_validate(DatasourceCredentialUpdatePayload) @model_validate(DatasourceCredentialUpdatePayload)
def post(self, req_data: DatasourceCredentialUpdatePayload, current_tenant_id: str, provider_id: str): def post(self, req_data: DatasourceCredentialUpdatePayload, current_tenant_id: str, provider_id: str):
@ -459,7 +459,7 @@ class DatasourceAuthOauthCustomClient(Resource):
@login_required @login_required
@account_initialization_required @account_initialization_required
@edit_permission_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 @with_current_tenant_id
@model_validate(DatasourceCustomClientPayload) @model_validate(DatasourceCustomClientPayload)
def post(self, req_data: DatasourceCustomClientPayload, current_tenant_id: str, provider_id: str): def post(self, req_data: DatasourceCustomClientPayload, current_tenant_id: str, provider_id: str):
@ -477,7 +477,7 @@ class DatasourceAuthOauthCustomClient(Resource):
@login_required @login_required
@account_initialization_required @account_initialization_required
@edit_permission_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__]) @console_ns.response(200, "Success", console_ns.models[SimpleResultResponse.__name__])
@with_current_tenant_id @with_current_tenant_id
def delete(self, current_tenant_id: str, provider_id: str): def delete(self, current_tenant_id: str, provider_id: str):
@ -498,7 +498,7 @@ class DatasourceAuthDefaultApi(Resource):
@login_required @login_required
@account_initialization_required @account_initialization_required
@edit_permission_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 @with_current_tenant_id
@model_validate(DatasourceDefaultPayload) @model_validate(DatasourceDefaultPayload)
def post(self, req_data: DatasourceDefaultPayload, current_tenant_id: str, provider_id: str): def post(self, req_data: DatasourceDefaultPayload, current_tenant_id: str, provider_id: str):
@ -520,7 +520,7 @@ class DatasourceUpdateProviderNameApi(Resource):
@login_required @login_required
@account_initialization_required @account_initialization_required
@edit_permission_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 @with_current_tenant_id
@model_validate(DatasourceUpdateNamePayload) @model_validate(DatasourceUpdateNamePayload)
def post(self, req_data: DatasourceUpdateNamePayload, current_tenant_id: str, provider_id: str): def post(self, req_data: DatasourceUpdateNamePayload, current_tenant_id: str, provider_id: str):

View File

@ -8,6 +8,7 @@ from werkzeug.exceptions import Forbidden, NotFound
from configs import dify_config from configs import dify_config
from controllers.common.fields import SimpleDataResponse from controllers.common.fields import SimpleDataResponse
from controllers.common.rbac import DatasetByPipeline, RBACCheck
from controllers.common.schema import ( from controllers.common.schema import (
JsonResponseWithStatus, JsonResponseWithStatus,
query_params_from_model, 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.datasets.wraps import get_rag_pipeline
from controllers.console.wraps import ( from controllers.console.wraps import (
RBACPermission, RBACPermission,
RBACResourceScope,
account_initialization_required, account_initialization_required,
enterprise_license_required, enterprise_license_required,
knowledge_pipeline_publish_enabled, knowledge_pipeline_publish_enabled,
@ -223,7 +223,7 @@ class PublishCustomizedPipelineTemplateApi(Resource):
@knowledge_pipeline_publish_enabled @knowledge_pipeline_publish_enabled
@with_current_user @with_current_user
@get_rag_pipeline @get_rag_pipeline
@rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_PIPELINE_RELEASE) @rbac_permission_required(RBACCheck(RBACPermission.DATASET_PIPELINE_RELEASE, DatasetByPipeline()))
@model_validate(CustomizedPipelineTemplatePayload) @model_validate(CustomizedPipelineTemplatePayload)
def post( def post(
self, self,

View File

@ -10,6 +10,7 @@ from pydantic import BaseModel, Field
from sqlalchemy.orm import sessionmaker from sqlalchemy.orm import sessionmaker
from controllers.common.errors import InvalidArgumentError, NotFoundError 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.common.schema import query_params_from_model, register_schema_models
from controllers.console import console_ns from controllers.console import console_ns
from controllers.console.app.error import ( 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.datasets.wraps import get_rag_pipeline
from controllers.console.wraps import ( from controllers.console.wraps import (
RBACPermission, RBACPermission,
RBACResourceScope,
account_initialization_required, account_initialization_required,
edit_permission_required, edit_permission_required,
model_validate, model_validate,
@ -82,7 +82,7 @@ def _api_prerequisite[T, **P, R](
@account_initialization_required @account_initialization_required
@get_rag_pipeline @get_rag_pipeline
@edit_permission_required @edit_permission_required
@rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_EDIT) @rbac_permission_required(RBACCheck(RBACPermission.DATASET_EDIT, DatasetByPipeline()))
@with_current_user @with_current_user
@wraps(f) @wraps(f)
def wrapper(self: T, current_user: Account, *args: P.args, **kwargs: P.kwargs) -> R | Response: def wrapper(self: T, current_user: Account, *args: P.args, **kwargs: P.kwargs) -> R | Response:

View File

@ -3,6 +3,7 @@ from pydantic import BaseModel, Field
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from controllers.common.fields import SimpleDataResponse from controllers.common.fields import SimpleDataResponse
from controllers.common.rbac import DatasetByPipeline, RBACCheck, Workspace
from controllers.common.schema import ( from controllers.common.schema import (
JsonResponseWithStatus, JsonResponseWithStatus,
query_params_from_model, 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.datasets.wraps import get_rag_pipeline
from controllers.console.wraps import ( from controllers.console.wraps import (
RBACPermission, RBACPermission,
RBACResourceScope,
account_initialization_required, account_initialization_required,
edit_permission_required, edit_permission_required,
model_validate, model_validate,
@ -81,9 +81,7 @@ class RagPipelineImportApi(Resource):
@login_required @login_required
@account_initialization_required @account_initialization_required
@edit_permission_required @edit_permission_required
@rbac_permission_required( @rbac_permission_required(RBACCheck(RBACPermission.DATASET_CREATE_AND_MANAGEMENT, Workspace()))
RBACResourceScope.DATASET, RBACPermission.DATASET_CREATE_AND_MANAGEMENT, resource_required=False
)
@with_current_user @with_current_user
@model_validate(RagPipelineImportPayload) @model_validate(RagPipelineImportPayload)
def post(self, req_data: RagPipelineImportPayload, current_user: Account) -> JsonResponseWithStatus: def post(self, req_data: RagPipelineImportPayload, current_user: Account) -> JsonResponseWithStatus:
@ -128,9 +126,7 @@ class RagPipelineImportConfirmApi(Resource):
@login_required @login_required
@account_initialization_required @account_initialization_required
@edit_permission_required @edit_permission_required
@rbac_permission_required( @rbac_permission_required(RBACCheck(RBACPermission.DATASET_CREATE_AND_MANAGEMENT, Workspace()))
RBACResourceScope.DATASET, RBACPermission.DATASET_CREATE_AND_MANAGEMENT, resource_required=False
)
@with_current_user @with_current_user
def post(self, current_user: Account, import_id: str) -> JsonResponseWithStatus: def post(self, current_user: Account, import_id: str) -> JsonResponseWithStatus:
with Session(db.engine, expire_on_commit=False) as session: with Session(db.engine, expire_on_commit=False) as session:
@ -160,7 +156,7 @@ class RagPipelineImportCheckDependenciesApi(Resource):
@get_rag_pipeline @get_rag_pipeline
@account_initialization_required @account_initialization_required
@edit_permission_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: def get(self, pipeline: Pipeline) -> JsonResponseWithStatus:
with Session(db.engine, expire_on_commit=False) as session: with Session(db.engine, expire_on_commit=False) as session:
import_service = RagPipelineDslService(session) import_service = RagPipelineDslService(session)
@ -178,7 +174,7 @@ class RagPipelineExportApi(Resource):
@get_rag_pipeline @get_rag_pipeline
@account_initialization_required @account_initialization_required
@edit_permission_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) @model_validate(IncludeSecretQuery)
def get(self, req_data: IncludeSecretQuery, pipeline: Pipeline) -> JsonResponseWithStatus: def get(self, req_data: IncludeSecretQuery, pipeline: Pipeline) -> JsonResponseWithStatus:
# Add include_secret params # Add include_secret params

View File

@ -13,6 +13,7 @@ import services
from configs import dify_config from configs import dify_config
from controllers.common.controller_schemas import DefaultBlockConfigQuery, WorkflowListQuery, WorkflowUpdatePayload from controllers.common.controller_schemas import DefaultBlockConfigQuery, WorkflowListQuery, WorkflowUpdatePayload
from controllers.common.fields import SimpleResultResponse 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.common.schema import query_params_from_model, register_response_schema_models, register_schema_models
from controllers.console import console_ns from controllers.console import console_ns
from controllers.console.app.error import ( 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.datasets.wraps import get_rag_pipeline, load_rag_pipeline
from controllers.console.wraps import ( from controllers.console.wraps import (
RBACPermission, RBACPermission,
RBACResourceScope,
account_initialization_required, account_initialization_required,
edit_permission_required, edit_permission_required,
model_validate, model_validate,
@ -197,7 +197,7 @@ class DraftRagPipelineApi(Resource):
@account_initialization_required @account_initialization_required
@get_rag_pipeline @get_rag_pipeline
@edit_permission_required @edit_permission_required
@rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_EDIT) @rbac_permission_required(RBACCheck(RBACPermission.DATASET_EDIT, DatasetByPipeline()))
def get(self, pipeline: Pipeline): def get(self, pipeline: Pipeline):
""" """
Get draft rag pipeline's workflow Get draft rag pipeline's workflow
@ -219,7 +219,7 @@ class DraftRagPipelineApi(Resource):
@with_current_user @with_current_user
@get_rag_pipeline @get_rag_pipeline
@edit_permission_required @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.expect(console_ns.models[DraftWorkflowSyncPayload.__name__])
@console_ns.response(200, "Success", console_ns.models[RagPipelineWorkflowSyncResponse.__name__]) @console_ns.response(200, "Success", console_ns.models[RagPipelineWorkflowSyncResponse.__name__])
def post(self, current_user: Account, pipeline: Pipeline): def post(self, current_user: Account, pipeline: Pipeline):
@ -280,7 +280,7 @@ class RagPipelineDraftRunIterationNodeApi(Resource):
@with_current_user @with_current_user
@get_rag_pipeline @get_rag_pipeline
@edit_permission_required @edit_permission_required
@rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_EDIT) @rbac_permission_required(RBACCheck(RBACPermission.DATASET_EDIT, DatasetByPipeline()))
@model_validate(NodeRunPayload) @model_validate(NodeRunPayload)
def post(self, req_data: NodeRunPayload, current_user: Account, pipeline: Pipeline, node_id: str): def post(self, req_data: NodeRunPayload, current_user: Account, pipeline: Pipeline, node_id: str):
""" """
@ -313,7 +313,7 @@ class RagPipelineDraftRunLoopNodeApi(Resource):
@login_required @login_required
@account_initialization_required @account_initialization_required
@edit_permission_required @edit_permission_required
@rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_EDIT) @rbac_permission_required(RBACCheck(RBACPermission.DATASET_EDIT, DatasetByPipeline()))
@with_current_user @with_current_user
@get_rag_pipeline @get_rag_pipeline
@model_validate(NodeRunPayload) @model_validate(NodeRunPayload)
@ -348,7 +348,7 @@ class DraftRagPipelineRunApi(Resource):
@login_required @login_required
@account_initialization_required @account_initialization_required
@edit_permission_required @edit_permission_required
@rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_EDIT) @rbac_permission_required(RBACCheck(RBACPermission.DATASET_EDIT, DatasetByPipeline()))
@with_current_user @with_current_user
@with_session @with_session
@model_validate(DraftWorkflowRunPayload) @model_validate(DraftWorkflowRunPayload)
@ -382,7 +382,7 @@ class PublishedRagPipelineRunApi(Resource):
@login_required @login_required
@account_initialization_required @account_initialization_required
@edit_permission_required @edit_permission_required
@rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_EDIT) @rbac_permission_required(RBACCheck(RBACPermission.DATASET_EDIT, DatasetByPipeline()))
@with_current_user @with_current_user
@with_session @with_session
@model_validate(PublishedWorkflowRunPayload) @model_validate(PublishedWorkflowRunPayload)
@ -417,7 +417,7 @@ class RagPipelinePublishedDatasourceNodeRunApi(Resource):
@login_required @login_required
@account_initialization_required @account_initialization_required
@edit_permission_required @edit_permission_required
@rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_EDIT) @rbac_permission_required(RBACCheck(RBACPermission.DATASET_EDIT, DatasetByPipeline()))
@with_current_user @with_current_user
@get_rag_pipeline @get_rag_pipeline
@model_validate(DatasourceNodeRunPayload) @model_validate(DatasourceNodeRunPayload)
@ -449,7 +449,7 @@ class RagPipelineDraftDatasourceNodeRunApi(Resource):
@setup_required @setup_required
@login_required @login_required
@edit_permission_required @edit_permission_required
@rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_EDIT) @rbac_permission_required(RBACCheck(RBACPermission.DATASET_EDIT, DatasetByPipeline()))
@account_initialization_required @account_initialization_required
@with_current_user @with_current_user
@get_rag_pipeline @get_rag_pipeline
@ -486,7 +486,7 @@ class RagPipelineDraftNodeRunApi(Resource):
@setup_required @setup_required
@login_required @login_required
@edit_permission_required @edit_permission_required
@rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_EDIT) @rbac_permission_required(RBACCheck(RBACPermission.DATASET_EDIT, DatasetByPipeline()))
@account_initialization_required @account_initialization_required
@with_current_user @with_current_user
@get_rag_pipeline @get_rag_pipeline
@ -516,7 +516,7 @@ class RagPipelineTaskStopApi(Resource):
@setup_required @setup_required
@login_required @login_required
@edit_permission_required @edit_permission_required
@rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_EDIT) @rbac_permission_required(RBACCheck(RBACPermission.DATASET_EDIT, DatasetByPipeline()))
@account_initialization_required @account_initialization_required
@with_current_user @with_current_user
@get_rag_pipeline @get_rag_pipeline
@ -540,7 +540,7 @@ class PublishedRagPipelineApi(Resource):
@login_required @login_required
@account_initialization_required @account_initialization_required
@edit_permission_required @edit_permission_required
@rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_EDIT) @rbac_permission_required(RBACCheck(RBACPermission.DATASET_EDIT, DatasetByPipeline()))
@get_rag_pipeline @get_rag_pipeline
def get(self, pipeline: Pipeline): def get(self, pipeline: Pipeline):
""" """
@ -565,7 +565,7 @@ class PublishedRagPipelineApi(Resource):
@login_required @login_required
@account_initialization_required @account_initialization_required
@edit_permission_required @edit_permission_required
@rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_EDIT) @rbac_permission_required(RBACCheck(RBACPermission.DATASET_EDIT, DatasetByPipeline()))
@with_current_user @with_current_user
@get_rag_pipeline @get_rag_pipeline
def post(self, current_user: Account, pipeline: Pipeline): def post(self, current_user: Account, pipeline: Pipeline):
@ -600,7 +600,7 @@ class DefaultRagPipelineBlockConfigsApi(Resource):
@login_required @login_required
@account_initialization_required @account_initialization_required
@edit_permission_required @edit_permission_required
@rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_EDIT) @rbac_permission_required(RBACCheck(RBACPermission.DATASET_EDIT, DatasetByPipeline()))
@get_rag_pipeline @get_rag_pipeline
def get(self, pipeline: Pipeline): def get(self, pipeline: Pipeline):
""" """
@ -623,7 +623,7 @@ class DefaultRagPipelineBlockConfigApi(Resource):
@login_required @login_required
@account_initialization_required @account_initialization_required
@edit_permission_required @edit_permission_required
@rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_EDIT) @rbac_permission_required(RBACCheck(RBACPermission.DATASET_EDIT, DatasetByPipeline()))
@get_rag_pipeline @get_rag_pipeline
@model_validate(DefaultBlockConfigQuery) @model_validate(DefaultBlockConfigQuery)
def get(self, req_data: DefaultBlockConfigQuery, pipeline: Pipeline, block_type: str): def get(self, req_data: DefaultBlockConfigQuery, pipeline: Pipeline, block_type: str):
@ -656,7 +656,7 @@ class PublishedAllRagPipelineApi(Resource):
@login_required @login_required
@account_initialization_required @account_initialization_required
@edit_permission_required @edit_permission_required
@rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_EDIT) @rbac_permission_required(RBACCheck(RBACPermission.DATASET_EDIT, DatasetByPipeline()))
@with_current_user @with_current_user
@get_rag_pipeline @get_rag_pipeline
@model_validate(WorkflowListQuery) @model_validate(WorkflowListQuery)
@ -702,7 +702,7 @@ class RagPipelineDraftWorkflowRestoreApi(Resource):
@login_required @login_required
@account_initialization_required @account_initialization_required
@edit_permission_required @edit_permission_required
@rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_EDIT) @rbac_permission_required(RBACCheck(RBACPermission.DATASET_EDIT, DatasetByPipeline()))
@with_current_user @with_current_user
@get_rag_pipeline @get_rag_pipeline
def post(self, current_user: Account, pipeline: Pipeline, workflow_id: str): def post(self, current_user: Account, pipeline: Pipeline, workflow_id: str):
@ -737,7 +737,7 @@ class RagPipelineByIdApi(Resource):
@login_required @login_required
@account_initialization_required @account_initialization_required
@edit_permission_required @edit_permission_required
@rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_EDIT) @rbac_permission_required(RBACCheck(RBACPermission.DATASET_EDIT, DatasetByPipeline()))
@with_current_user @with_current_user
@get_rag_pipeline @get_rag_pipeline
@console_ns.expect(console_ns.models[WorkflowUpdatePayload.__name__]) @console_ns.expect(console_ns.models[WorkflowUpdatePayload.__name__])
@ -773,7 +773,7 @@ class RagPipelineByIdApi(Resource):
@login_required @login_required
@account_initialization_required @account_initialization_required
@edit_permission_required @edit_permission_required
@rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_EDIT) @rbac_permission_required(RBACCheck(RBACPermission.DATASET_EDIT, DatasetByPipeline()))
@with_current_user @with_current_user
@get_rag_pipeline @get_rag_pipeline
def delete(self, current_user: Account, pipeline: Pipeline, workflow_id: str): def delete(self, current_user: Account, pipeline: Pipeline, workflow_id: str):
@ -816,7 +816,7 @@ class PublishedRagPipelineSecondStepApi(Resource):
@account_initialization_required @account_initialization_required
@get_rag_pipeline @get_rag_pipeline
@edit_permission_required @edit_permission_required
@rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_EDIT) @rbac_permission_required(RBACCheck(RBACPermission.DATASET_EDIT, DatasetByPipeline()))
@model_validate(NodeIdQuery) @model_validate(NodeIdQuery)
def get(self, req_data: NodeIdQuery, pipeline: Pipeline): def get(self, req_data: NodeIdQuery, pipeline: Pipeline):
""" """
@ -839,7 +839,7 @@ class PublishedRagPipelineFirstStepApi(Resource):
@account_initialization_required @account_initialization_required
@get_rag_pipeline @get_rag_pipeline
@edit_permission_required @edit_permission_required
@rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_EDIT) @rbac_permission_required(RBACCheck(RBACPermission.DATASET_EDIT, DatasetByPipeline()))
@model_validate(NodeIdQuery) @model_validate(NodeIdQuery)
def get(self, req_data: NodeIdQuery, pipeline: Pipeline): def get(self, req_data: NodeIdQuery, pipeline: Pipeline):
""" """
@ -862,7 +862,7 @@ class DraftRagPipelineFirstStepApi(Resource):
@account_initialization_required @account_initialization_required
@get_rag_pipeline @get_rag_pipeline
@edit_permission_required @edit_permission_required
@rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_EDIT) @rbac_permission_required(RBACCheck(RBACPermission.DATASET_EDIT, DatasetByPipeline()))
@model_validate(NodeIdQuery) @model_validate(NodeIdQuery)
def get(self, req_data: NodeIdQuery, pipeline: Pipeline): def get(self, req_data: NodeIdQuery, pipeline: Pipeline):
""" """
@ -885,7 +885,7 @@ class DraftRagPipelineSecondStepApi(Resource):
@account_initialization_required @account_initialization_required
@get_rag_pipeline @get_rag_pipeline
@edit_permission_required @edit_permission_required
@rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_EDIT) @rbac_permission_required(RBACCheck(RBACPermission.DATASET_EDIT, DatasetByPipeline()))
@model_validate(NodeIdQuery) @model_validate(NodeIdQuery)
def get(self, req_data: NodeIdQuery, pipeline: Pipeline): def get(self, req_data: NodeIdQuery, pipeline: Pipeline):
""" """
@ -1035,7 +1035,7 @@ class RagPipelineTransformApi(Resource):
@account_initialization_required @account_initialization_required
@with_current_user @with_current_user
@with_current_tenant_id @with_current_tenant_id
@rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_EDIT) @rbac_permission_required(RBACCheck(RBACPermission.DATASET_EDIT, DatasetId()))
@with_session @with_session
def post(self, session: Session, current_tenant_id: str, current_user: Account, dataset_id: UUID): 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) dataset = DatasetService.get_dataset_for_tenant(str(dataset_id), current_tenant_id, session=session)
@ -1070,7 +1070,7 @@ class RagPipelineDatasourceVariableApi(Resource):
@with_current_user @with_current_user
@get_rag_pipeline @get_rag_pipeline
@edit_permission_required @edit_permission_required
@rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_EDIT) @rbac_permission_required(RBACCheck(RBACPermission.DATASET_EDIT, DatasetByPipeline()))
@model_validate(DatasourceVariablesPayload) @model_validate(DatasourceVariablesPayload)
def post(self, req_data: DatasourceVariablesPayload, current_user: Account, pipeline: Pipeline): def post(self, req_data: DatasourceVariablesPayload, current_user: Account, pipeline: Pipeline):
""" """

View File

@ -1,6 +1,6 @@
"""Flask adapter for Console API admission.""" """Flask adapter for Console API admission."""
from collections.abc import Callable from collections.abc import Callable, Sequence
from functools import wraps from functools import wraps
from typing import Concatenate from typing import Concatenate
@ -8,8 +8,7 @@ from flask import Response, abort, request
from werkzeug.exceptions import Forbidden from werkzeug.exceptions import Forbidden
from configs import dify_config from configs import dify_config
from controllers.common.wraps import enforce_rbac_access from controllers.common.rbac import RBAC_CHECKS_ATTR, RBACCheck, enforce_rbac_checks
from controllers.console.app.wraps import enforce_agent_manage_or_app_scene
from controllers.console.wraps import ( from controllers.console.wraps import (
account_initialization_required, account_initialization_required,
enable_change_email, enable_change_email,
@ -17,11 +16,9 @@ from controllers.console.wraps import (
setup_required, setup_required,
) )
from core.logging.context import get_request_id, get_trace_id from core.logging.context import get_request_id, get_trace_id
from core.rbac import RBACPermission, RBACResourceScope
from enums import DeploymentEdition from enums import DeploymentEdition
from libs.login import current_account_with_tenant, login_required from libs.login import current_account_with_tenant, login_required
from machinery.context import RequestContext from machinery.context import RequestContext
from machinery.errors import AdmissionConfigurationError
from models.account import TenantAccountRole from models.account import TenantAccountRole
from services.system_feature_service import SystemFeatureService from services.system_feature_service import SystemFeatureService
@ -50,10 +47,7 @@ def console_account_admission[T, **P, R](
require_initialized: bool = True, require_initialized: bool = True,
require_valid_enterprise_license: bool = False, require_valid_enterprise_license: bool = False,
allowed_roles: frozenset[TenantAccountRole] | None = None, allowed_roles: frozenset[TenantAccountRole] | None = None,
rbac_resource_scope: RBACResourceScope | None = None, rbac_checks: Sequence[RBACCheck] | None = None,
rbac_permission: RBACPermission | None = None,
rbac_resource_required: bool = True,
agent_manage_fallback: bool = False,
) -> Callable[ ) -> Callable[
[Callable[Concatenate[T, RequestContext, P], R]], [Callable[Concatenate[T, RequestContext, P], R]],
Callable[Concatenate[T, P], R | Response], Callable[Concatenate[T, P], R | Response],
@ -66,40 +60,23 @@ def console_account_admission[T, **P, R](
context construction. 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( def decorator(
view: Callable[Concatenate[T, RequestContext, P], R], view: Callable[Concatenate[T, RequestContext, P], R],
) -> Callable[Concatenate[T, P], R | Response]: ) -> Callable[Concatenate[T, P], R | Response]:
@wraps(view) @wraps(view, updated=())
def inject_request_context(self: T, /, *args: P.args, **kwargs: P.kwargs) -> R: def inject_request_context(self: T, /, *args: P.args, **kwargs: P.kwargs) -> R:
account_with_tenant = current_account_with_tenant() account_with_tenant = current_account_with_tenant()
account = account_with_tenant.account account = account_with_tenant.account
tenant_id = account_with_tenant.tenant_id 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: if allowed_roles is not None and not dify_config.RBAC_ENABLED and account.role not in allowed_roles:
raise Forbidden() raise Forbidden()
if rbac_resource_scope is not None and rbac_permission is not None: if rbac_checks is not None:
if agent_manage_fallback: enforce_rbac_checks(
enforce_agent_manage_or_app_scene( tenant_id=tenant_id,
tenant_id=tenant_id, account_id=account.id,
account_id=account.id, checks=rbac_checks,
scene=rbac_permission, path_args=kwargs,
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,
)
request_context = RequestContext( request_context = RequestContext(
account_id=account.id, account_id=account.id,
active_workspace_id=tenant_id, active_workspace_id=tenant_id,
@ -108,6 +85,9 @@ def console_account_admission[T, **P, R](
) )
return view(self, request_context, *args, **kwargs) 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 admitted: Callable[Concatenate[T, P], R | Response] = inject_request_context
if require_change_email_enabled: if require_change_email_enabled:
admitted = enable_change_email(admitted) admitted = enable_change_email(admitted)

View File

@ -10,6 +10,7 @@ from werkzeug.exceptions import BadRequest, InternalServerError, NotFound
from controllers.common.controller_schemas import WorkflowUpdatePayload from controllers.common.controller_schemas import WorkflowUpdatePayload
from controllers.common.fields import GeneratedAppResponse, SimpleResultResponse 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.common.schema import query_params_from_model, register_response_schema_models, register_schema_models
from controllers.console import console_ns from controllers.console import console_ns
from controllers.console.app.error import DraftWorkflowNotExist, DraftWorkflowNotSync from controllers.console.app.error import DraftWorkflowNotExist, DraftWorkflowNotSync
@ -34,7 +35,6 @@ from controllers.console.snippets.payloads import (
) )
from controllers.console.wraps import ( from controllers.console.wraps import (
RBACPermission, RBACPermission,
RBACResourceScope,
account_initialization_required, account_initialization_required,
edit_permission_required, edit_permission_required,
model_validate, model_validate,
@ -205,9 +205,7 @@ class SnippetDraftWorkflowApi(Resource):
@with_current_user @with_current_user
@get_snippet @get_snippet
@edit_permission_required @edit_permission_required
@rbac_permission_required( @rbac_permission_required(RBACCheck(RBACPermission.SNIPPETS_CREATE_AND_MODIFY, Workspace()))
RBACResourceScope.WORKSPACE, RBACPermission.SNIPPETS_CREATE_AND_MODIFY, resource_required=False
)
@model_validate(SnippetDraftSyncPayload) @model_validate(SnippetDraftSyncPayload)
def post(self, req_data: SnippetDraftSyncPayload, current_user: Account, snippet: CustomizedSnippet): def post(self, req_data: SnippetDraftSyncPayload, current_user: Account, snippet: CustomizedSnippet):
"""Sync draft workflow for snippet.""" """Sync draft workflow for snippet."""
@ -294,9 +292,7 @@ class SnippetPublishedWorkflowApi(Resource):
@with_current_user @with_current_user
@get_snippet @get_snippet
@edit_permission_required @edit_permission_required
@rbac_permission_required( @rbac_permission_required(RBACCheck(RBACPermission.SNIPPETS_CREATE_AND_MODIFY, Workspace()))
RBACResourceScope.WORKSPACE, RBACPermission.SNIPPETS_CREATE_AND_MODIFY, resource_required=False
)
def post(self, current_user: Account, snippet: CustomizedSnippet): def post(self, current_user: Account, snippet: CustomizedSnippet):
"""Publish snippet workflow.""" """Publish snippet workflow."""
snippet_service = _snippet_service() snippet_service = _snippet_service()
@ -355,9 +351,7 @@ class SnippetPublishedAllWorkflowApi(Resource):
@account_initialization_required @account_initialization_required
@get_snippet @get_snippet
@edit_permission_required @edit_permission_required
@rbac_permission_required( @rbac_permission_required(RBACCheck(RBACPermission.SNIPPETS_CREATE_AND_MODIFY, Workspace()))
RBACResourceScope.WORKSPACE, RBACPermission.SNIPPETS_CREATE_AND_MODIFY, resource_required=False
)
@model_validate(SnippetWorkflowListQuery) @model_validate(SnippetWorkflowListQuery)
def get(self, req_data: SnippetWorkflowListQuery, snippet: CustomizedSnippet): def get(self, req_data: SnippetWorkflowListQuery, snippet: CustomizedSnippet):
"""Get all published workflow versions for snippet.""" """Get all published workflow versions for snippet."""
@ -399,9 +393,7 @@ class SnippetDraftWorkflowRestoreApi(Resource):
@with_current_user @with_current_user
@get_snippet @get_snippet
@edit_permission_required @edit_permission_required
@rbac_permission_required( @rbac_permission_required(RBACCheck(RBACPermission.SNIPPETS_CREATE_AND_MODIFY, Workspace()))
RBACResourceScope.WORKSPACE, RBACPermission.SNIPPETS_CREATE_AND_MODIFY, resource_required=False
)
def post(self, current_user: Account, snippet: CustomizedSnippet, workflow_id: str): def post(self, current_user: Account, snippet: CustomizedSnippet, workflow_id: str):
"""Restore a published snippet workflow version into the draft workflow.""" """Restore a published snippet workflow version into the draft workflow."""
snippet_service = _snippet_service() snippet_service = _snippet_service()
@ -441,9 +433,7 @@ class SnippetWorkflowByIdApi(Resource):
@with_current_user @with_current_user
@get_snippet @get_snippet
@edit_permission_required @edit_permission_required
@rbac_permission_required( @rbac_permission_required(RBACCheck(RBACPermission.SNIPPETS_CREATE_AND_MODIFY, Workspace()))
RBACResourceScope.WORKSPACE, RBACPermission.SNIPPETS_CREATE_AND_MODIFY, resource_required=False
)
@model_validate(WorkflowUpdatePayload) @model_validate(WorkflowUpdatePayload)
def patch( def patch(
self, self,
@ -487,9 +477,7 @@ class SnippetWorkflowByIdApi(Resource):
@account_initialization_required @account_initialization_required
@get_snippet @get_snippet
@edit_permission_required @edit_permission_required
@rbac_permission_required( @rbac_permission_required(RBACCheck(RBACPermission.SNIPPETS_CREATE_AND_MODIFY, Workspace()))
RBACResourceScope.WORKSPACE, RBACPermission.SNIPPETS_CREATE_AND_MODIFY, resource_required=False
)
def delete(self, snippet: CustomizedSnippet, workflow_id: str): def delete(self, snippet: CustomizedSnippet, workflow_id: str):
"""Delete a published snippet workflow version.""" """Delete a published snippet workflow version."""
snippet_service = _snippet_service() snippet_service = _snippet_service()

View File

@ -7,13 +7,12 @@ from werkzeug.exceptions import Forbidden, NotFound
from configs import dify_config from configs import dify_config
from controllers.common.fields import SimpleResultResponse 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.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 import console_ns
from controllers.console.flask_admission import console_account_admission from controllers.console.flask_admission import console_account_admission
from controllers.console.wraps import ( from controllers.console.wraps import (
RBACPermission, RBACPermission,
RBACResourceScope,
model_validate, model_validate,
) )
from extensions.ext_application_services import application_services 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: if not dify_config.RBAC_ENABLED:
return return
enforce_rbac_access( enforce_rbac_checks(
tenant_id=context.active_workspace_id, tenant_id=context.active_workspace_id,
account_id=context.account_id, account_id=context.account_id,
resource_type=RBACResourceScope.WORKSPACE, checks=[RBACCheck(RBACPermission.SNIPPETS_CREATE_AND_MODIFY, Workspace())],
scene=RBACPermission.SNIPPETS_CREATE_AND_MODIFY,
resource_required=False,
) )

View File

@ -15,11 +15,11 @@ from flask_restx import Resource
from pydantic import BaseModel, Field from pydantic import BaseModel, Field
from controllers.common.fields import SuccessResponse 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.common.schema import query_params_from_model, register_response_schema_models, register_schema_models
from controllers.console import console_ns from controllers.console import console_ns
from controllers.console.wraps import ( from controllers.console.wraps import (
RBACPermission, RBACPermission,
RBACResourceScope,
account_initialization_required, account_initialization_required,
is_admin_or_owner_required, is_admin_or_owner_required,
model_validate, model_validate,
@ -235,7 +235,7 @@ class EndpointCollectionApi(Resource):
@setup_required @setup_required
@login_required @login_required
@is_admin_or_owner_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 @account_initialization_required
@with_current_user_id @with_current_user_id
@with_current_tenant_id @with_current_tenant_id
@ -267,7 +267,7 @@ class DeprecatedEndpointCreateApi(Resource):
@setup_required @setup_required
@login_required @login_required
@is_admin_or_owner_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 @account_initialization_required
@with_current_user_id @with_current_user_id
@with_current_tenant_id @with_current_tenant_id
@ -291,7 +291,7 @@ class EndpointListApi(Resource):
@setup_required @setup_required
@login_required @login_required
@is_admin_or_owner_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 @account_initialization_required
@with_current_user_id @with_current_user_id
@with_current_tenant_id @with_current_tenant_id
@ -321,7 +321,7 @@ class EndpointListForSinglePluginApi(Resource):
@setup_required @setup_required
@login_required @login_required
@is_admin_or_owner_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 @account_initialization_required
@with_current_user_id @with_current_user_id
@with_current_tenant_id @with_current_tenant_id
@ -355,7 +355,7 @@ class EndpointItemApi(Resource):
@setup_required @setup_required
@login_required @login_required
@is_admin_or_owner_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 @account_initialization_required
@with_current_user_id @with_current_user_id
@with_current_tenant_id @with_current_tenant_id
@ -377,7 +377,7 @@ class EndpointItemApi(Resource):
@setup_required @setup_required
@login_required @login_required
@is_admin_or_owner_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 @account_initialization_required
@with_current_user_id @with_current_user_id
@with_current_tenant_id @with_current_tenant_id
@ -410,7 +410,7 @@ class DeprecatedEndpointDeleteApi(Resource):
@setup_required @setup_required
@login_required @login_required
@is_admin_or_owner_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 @account_initialization_required
@with_current_user_id @with_current_user_id
@with_current_tenant_id @with_current_tenant_id
@ -443,7 +443,7 @@ class DeprecatedEndpointUpdateApi(Resource):
@setup_required @setup_required
@login_required @login_required
@is_admin_or_owner_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 @account_initialization_required
@with_current_user_id @with_current_user_id
@with_current_tenant_id @with_current_tenant_id
@ -468,7 +468,7 @@ class EndpointEnableApi(Resource):
@setup_required @setup_required
@login_required @login_required
@is_admin_or_owner_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 @account_initialization_required
@with_current_user_id @with_current_user_id
@with_current_tenant_id @with_current_tenant_id
@ -493,7 +493,7 @@ class EndpointDisableApi(Resource):
@setup_required @setup_required
@login_required @login_required
@is_admin_or_owner_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 @account_initialization_required
@with_current_user_id @with_current_user_id
@with_current_tenant_id @with_current_tenant_id

View File

@ -7,12 +7,12 @@ from pydantic import BaseModel, Field, field_validator
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from controllers.common.fields import SimpleResultResponse, ValidationResultResponse 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.schema import query_params_from_model, register_response_schema_models, register_schema_models
from controllers.common.session import with_session from controllers.common.session import with_session
from controllers.console import console_ns from controllers.console import console_ns
from controllers.console.wraps import ( from controllers.console.wraps import (
RBACPermission, RBACPermission,
RBACResourceScope,
account_initialization_required, account_initialization_required,
is_admin_or_owner_required, is_admin_or_owner_required,
model_validate, model_validate,
@ -208,7 +208,7 @@ class ModelProviderCredentialApi(Resource):
@setup_required @setup_required
@login_required @login_required
@is_admin_or_owner_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 @account_initialization_required
@with_current_tenant_id @with_current_tenant_id
@model_validate(ParserCredentialId) @model_validate(ParserCredentialId)
@ -226,7 +226,7 @@ class ModelProviderCredentialApi(Resource):
@setup_required @setup_required
@login_required @login_required
@is_admin_or_owner_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 @account_initialization_required
@with_current_tenant_id @with_current_tenant_id
@model_validate(ParserCredentialCreate) @model_validate(ParserCredentialCreate)
@ -250,7 +250,7 @@ class ModelProviderCredentialApi(Resource):
@setup_required @setup_required
@login_required @login_required
@is_admin_or_owner_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 @account_initialization_required
@with_current_tenant_id @with_current_tenant_id
@model_validate(ParserCredentialUpdate) @model_validate(ParserCredentialUpdate)
@ -275,7 +275,7 @@ class ModelProviderCredentialApi(Resource):
@setup_required @setup_required
@login_required @login_required
@is_admin_or_owner_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 @account_initialization_required
@with_current_tenant_id @with_current_tenant_id
@model_validate(ParserCredentialDelete) @model_validate(ParserCredentialDelete)
@ -295,7 +295,7 @@ class ModelProviderCredentialSwitchApi(Resource):
@setup_required @setup_required
@login_required @login_required
@is_admin_or_owner_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 @account_initialization_required
@with_current_tenant_id @with_current_tenant_id
@model_validate(ParserCredentialSwitch) @model_validate(ParserCredentialSwitch)
@ -372,7 +372,7 @@ class PreferredProviderTypeUpdateApi(Resource):
@setup_required @setup_required
@login_required @login_required
@is_admin_or_owner_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 @account_initialization_required
@with_current_tenant_id @with_current_tenant_id
@model_validate(ParserPreferredProviderType) @model_validate(ParserPreferredProviderType)

View File

@ -5,6 +5,7 @@ from flask_restx import Resource
from pydantic import BaseModel, Field, field_validator from pydantic import BaseModel, Field, field_validator
from controllers.common.fields import SimpleResultResponse, ValidationResultResponse from controllers.common.fields import SimpleResultResponse, ValidationResultResponse
from controllers.common.rbac import RBACCheck, Workspace
from controllers.common.schema import ( from controllers.common.schema import (
query_params_from_model, query_params_from_model,
register_enum_models, register_enum_models,
@ -14,7 +15,6 @@ from controllers.common.schema import (
from controllers.console import console_ns from controllers.console import console_ns
from controllers.console.wraps import ( from controllers.console.wraps import (
RBACPermission, RBACPermission,
RBACResourceScope,
account_initialization_required, account_initialization_required,
is_admin_or_owner_required, is_admin_or_owner_required,
model_validate, model_validate,
@ -227,7 +227,7 @@ class DefaultModelApi(Resource):
@setup_required @setup_required
@login_required @login_required
@is_admin_or_owner_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 @account_initialization_required
@with_current_tenant_id @with_current_tenant_id
@model_validate(ParserPostDefault) @model_validate(ParserPostDefault)
@ -276,7 +276,7 @@ class ModelProviderModelApi(Resource):
@setup_required @setup_required
@login_required @login_required
@is_admin_or_owner_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 @account_initialization_required
@with_current_tenant_id @with_current_tenant_id
@model_validate(ParserPostModels) @model_validate(ParserPostModels)
@ -325,7 +325,7 @@ class ModelProviderModelApi(Resource):
@setup_required @setup_required
@login_required @login_required
@is_admin_or_owner_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 @account_initialization_required
@with_current_tenant_id @with_current_tenant_id
@model_validate(ParserDeleteModels) @model_validate(ParserDeleteModels)
@ -350,7 +350,7 @@ class ModelProviderModelCredentialApi(Resource):
@setup_required @setup_required
@login_required @login_required
@is_admin_or_owner_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 @account_initialization_required
@with_current_user @with_current_user
@with_current_tenant_id @with_current_tenant_id
@ -413,7 +413,7 @@ class ModelProviderModelCredentialApi(Resource):
@setup_required @setup_required
@login_required @login_required
@is_admin_or_owner_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 @account_initialization_required
@with_current_tenant_id @with_current_tenant_id
@model_validate(ParserCreateCredential) @model_validate(ParserCreateCredential)
@ -446,7 +446,7 @@ class ModelProviderModelCredentialApi(Resource):
@setup_required @setup_required
@login_required @login_required
@is_admin_or_owner_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 @account_initialization_required
@with_current_tenant_id @with_current_tenant_id
@model_validate(ParserUpdateCredential) @model_validate(ParserUpdateCredential)
@ -474,7 +474,7 @@ class ModelProviderModelCredentialApi(Resource):
@setup_required @setup_required
@login_required @login_required
@is_admin_or_owner_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 @account_initialization_required
@with_current_tenant_id @with_current_tenant_id
@model_validate(ParserDeleteCredential) @model_validate(ParserDeleteCredential)
@ -499,7 +499,7 @@ class ModelProviderModelCredentialSwitchApi(Resource):
@setup_required @setup_required
@login_required @login_required
@is_admin_or_owner_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 @account_initialization_required
@with_current_tenant_id @with_current_tenant_id
@model_validate(ParserSwitch) @model_validate(ParserSwitch)
@ -526,7 +526,7 @@ class ModelProviderModelEnableApi(Resource):
@login_required @login_required
@account_initialization_required @account_initialization_required
@with_current_tenant_id @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) @model_validate(ParserDeleteModels)
def patch(self, req_data: ParserDeleteModels, tenant_id: str, provider: str): def patch(self, req_data: ParserDeleteModels, tenant_id: str, provider: str):
@ -548,7 +548,7 @@ class ModelProviderModelDisableApi(Resource):
@login_required @login_required
@account_initialization_required @account_initialization_required
@with_current_tenant_id @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) @model_validate(ParserDeleteModels)
def patch(self, req_data: ParserDeleteModels, tenant_id: str, provider: str): def patch(self, req_data: ParserDeleteModels, tenant_id: str, provider: str):

View File

@ -11,6 +11,7 @@ from werkzeug.exceptions import Forbidden
from configs import dify_config from configs import dify_config
from controllers.common.fields import BinaryFileResponse, SuccessResponse from controllers.common.fields import BinaryFileResponse, SuccessResponse
from controllers.common.rbac import RBACCheck, Workspace
from controllers.common.schema import ( from controllers.common.schema import (
query_params_from_model, query_params_from_model,
register_enum_models, register_enum_models,
@ -21,7 +22,6 @@ from controllers.console import console_ns
from controllers.console.workspace import plugin_permission_required from controllers.console.workspace import plugin_permission_required
from controllers.console.wraps import ( from controllers.console.wraps import (
RBACPermission, RBACPermission,
RBACResourceScope,
account_initialization_required, account_initialization_required,
is_admin_or_owner_required, is_admin_or_owner_required,
model_validate, model_validate,
@ -570,7 +570,7 @@ class PluginDebuggingKeyApi(Resource):
@setup_required @setup_required
@login_required @login_required
@account_initialization_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) @plugin_permission_required(debug_required=True)
@with_current_tenant_id @with_current_tenant_id
def get(self, tenant_id: str): def get(self, tenant_id: str):
@ -750,7 +750,7 @@ class PluginUploadFromPkgApi(Resource):
@setup_required @setup_required
@login_required @login_required
@account_initialization_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) @plugin_permission_required(install_required=True)
@with_current_tenant_id @with_current_tenant_id
def post(self, tenant_id: str): def post(self, tenant_id: str):
@ -771,7 +771,7 @@ class PluginUploadFromGithubApi(Resource):
@setup_required @setup_required
@login_required @login_required
@account_initialization_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) @plugin_permission_required(install_required=True)
@with_current_tenant_id @with_current_tenant_id
@model_validate(ParserGithubUpload) @model_validate(ParserGithubUpload)
@ -793,7 +793,7 @@ class PluginUploadFromBundleApi(Resource):
@setup_required @setup_required
@login_required @login_required
@account_initialization_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) @plugin_permission_required(install_required=True)
@with_current_tenant_id @with_current_tenant_id
def post(self, tenant_id: str): def post(self, tenant_id: str):
@ -814,7 +814,7 @@ class PluginInstallFromPkgApi(Resource):
@setup_required @setup_required
@login_required @login_required
@account_initialization_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) @plugin_permission_required(install_required=True)
@with_current_tenant_id @with_current_tenant_id
@model_validate(ParserPluginIdentifiers) @model_validate(ParserPluginIdentifiers)
@ -835,7 +835,7 @@ class PluginInstallFromGithubApi(Resource):
@setup_required @setup_required
@login_required @login_required
@account_initialization_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) @plugin_permission_required(install_required=True)
@with_current_tenant_id @with_current_tenant_id
@model_validate(ParserGithubInstall) @model_validate(ParserGithubInstall)
@ -862,7 +862,7 @@ class PluginInstallFromMarketplaceApi(Resource):
@setup_required @setup_required
@login_required @login_required
@account_initialization_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) @plugin_permission_required(install_required=True)
@with_current_tenant_id @with_current_tenant_id
@model_validate(ParserPluginIdentifiers) @model_validate(ParserPluginIdentifiers)
@ -883,7 +883,7 @@ class PluginFetchMarketplacePkgApi(Resource):
@setup_required @setup_required
@login_required @login_required
@account_initialization_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) @plugin_permission_required(install_required=True)
@with_current_tenant_id @with_current_tenant_id
@model_validate(ParserPluginIdentifierQuery) @model_validate(ParserPluginIdentifierQuery)
@ -909,7 +909,7 @@ class PluginFetchManifestApi(Resource):
@setup_required @setup_required
@login_required @login_required
@account_initialization_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) @plugin_permission_required(install_required=True)
@with_current_tenant_id @with_current_tenant_id
@model_validate(ParserPluginIdentifierQuery) @model_validate(ParserPluginIdentifierQuery)
@ -1014,7 +1014,7 @@ class PluginUpgradeFromMarketplaceApi(Resource):
@setup_required @setup_required
@login_required @login_required
@account_initialization_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) @plugin_permission_required(install_required=True)
@with_current_tenant_id @with_current_tenant_id
@model_validate(ParserMarketplaceUpgrade) @model_validate(ParserMarketplaceUpgrade)
@ -1037,7 +1037,7 @@ class PluginUpgradeFromGithubApi(Resource):
@setup_required @setup_required
@login_required @login_required
@account_initialization_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) @plugin_permission_required(install_required=True)
@with_current_tenant_id @with_current_tenant_id
@model_validate(ParserGithubUpgrade) @model_validate(ParserGithubUpgrade)
@ -1065,7 +1065,7 @@ class PluginUninstallApi(Resource):
@setup_required @setup_required
@login_required @login_required
@account_initialization_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) @plugin_permission_required(install_required=True)
@with_current_tenant_id @with_current_tenant_id
@model_validate(ParserUninstall) @model_validate(ParserUninstall)
@ -1138,7 +1138,7 @@ class PluginFetchDynamicSelectOptionsApi(Resource):
@setup_required @setup_required
@login_required @login_required
@is_admin_or_owner_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 @account_initialization_required
@with_current_user @with_current_user
@with_current_tenant_id @with_current_tenant_id
@ -1169,7 +1169,7 @@ class PluginFetchDynamicSelectOptionsWithCredentialsApi(Resource):
@setup_required @setup_required
@login_required @login_required
@is_admin_or_owner_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 @account_initialization_required
@with_current_user @with_current_user
@with_current_tenant_id @with_current_tenant_id
@ -1201,7 +1201,7 @@ class PluginChangeAutoUpgradeApi(Resource):
@setup_required @setup_required
@login_required @login_required
@account_initialization_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_user
@with_current_tenant_id @with_current_tenant_id
@model_validate(ParserAutoUpgradeChange) @model_validate(ParserAutoUpgradeChange)
@ -1256,7 +1256,7 @@ class PluginAutoUpgradeExcludePluginApi(Resource):
@setup_required @setup_required
@login_required @login_required
@account_initialization_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 @with_current_tenant_id
@model_validate(ParserExcludePlugin) @model_validate(ParserExcludePlugin)
def post(self, req_data: ParserExcludePlugin, tenant_id: str): def post(self, req_data: ParserExcludePlugin, tenant_id: str):

View File

@ -1,5 +1,7 @@
from __future__ import annotations from __future__ import annotations
from collections.abc import Callable, Mapping
from dataclasses import dataclass
from typing import Any, Literal from typing import Any, Literal
from flask import request from flask import request
@ -9,9 +11,10 @@ from sqlalchemy import select
from werkzeug.exceptions import NotFound from werkzeug.exceptions import NotFound
from configs import dify_config 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.common.schema import query_params_from_model, register_response_schema_models, register_schema_models
from controllers.console import console_ns 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 core.db.session_factory import session_factory
from enums import DeploymentEdition from enums import DeploymentEdition
from extensions.ext_database import db from extensions.ext_database import db
@ -50,6 +53,7 @@ register_response_schema_models(
svc.MyPermissionsResponse, svc.MyPermissionsResponse,
svc.AppAccessMatrix, svc.AppAccessMatrix,
svc.DatasetAccessMatrix, svc.DatasetAccessMatrix,
svc.AgentAccessMatrix,
svc.WorkspaceAccessMatrix, svc.WorkspaceAccessMatrix,
svc.ResourceWhitelist, svc.ResourceWhitelist,
svc.ResourceWhitelistConfig, svc.ResourceWhitelistConfig,
@ -282,24 +286,6 @@ class RBACWorkspaceCatalogApi(Resource):
return _dump(svc.RBACService.Catalog.workspace(tenant_id, account_id)) 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. # Roles.
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
@ -323,9 +309,7 @@ class _RoleUpsertRequest(BaseModel):
@console_ns.route("/workspaces/current/rbac/roles") @console_ns.route("/workspaces/current/rbac/roles")
class RBACRolesApi(Resource): class RBACRolesApi(Resource):
@login_required @login_required
@rbac_permission_required( @rbac_permission_required(RBACCheck(RBACPermission.WORKSPACE_ROLE_MANAGE, Workspace()))
RBACResourceScope.WORKSPACE, RBACPermission.WORKSPACE_ROLE_MANAGE, resource_required=False
)
@console_ns.response(200, "Success", console_ns.models[_RBACRoleList.__name__]) @console_ns.response(200, "Success", console_ns.models[_RBACRoleList.__name__])
@model_validate(_RolesListQuery) @model_validate(_RolesListQuery)
def get(self, req_data: _RolesListQuery): def get(self, req_data: _RolesListQuery):
@ -349,9 +333,7 @@ class RBACRolesApi(Resource):
return _dump(result) return _dump(result)
@login_required @login_required
@rbac_permission_required( @rbac_permission_required(RBACCheck(RBACPermission.WORKSPACE_ROLE_MANAGE, Workspace()))
RBACResourceScope.WORKSPACE, RBACPermission.WORKSPACE_ROLE_MANAGE, resource_required=False
)
@console_ns.response(201, "Role created", console_ns.models[svc.RBACRole.__name__]) @console_ns.response(201, "Role created", console_ns.models[svc.RBACRole.__name__])
def post(self): def post(self):
tenant_id, account_id = _current_ids() tenant_id, account_id = _current_ids()
@ -363,9 +345,7 @@ class RBACRolesApi(Resource):
@console_ns.route("/workspaces/current/rbac/roles/<uuid:role_id>") @console_ns.route("/workspaces/current/rbac/roles/<uuid:role_id>")
class RBACRoleItemApi(Resource): class RBACRoleItemApi(Resource):
@login_required @login_required
@rbac_permission_required( @rbac_permission_required(RBACCheck(RBACPermission.WORKSPACE_ROLE_MANAGE, Workspace()))
RBACResourceScope.WORKSPACE, RBACPermission.WORKSPACE_ROLE_MANAGE, resource_required=False
)
@console_ns.response(200, "Success", console_ns.models[svc.RBACRole.__name__]) @console_ns.response(200, "Success", console_ns.models[svc.RBACRole.__name__])
def get(self, role_id): def get(self, role_id):
tenant_id, account_id = _current_ids() tenant_id, account_id = _current_ids()
@ -379,9 +359,7 @@ class RBACRoleItemApi(Resource):
) )
@login_required @login_required
@rbac_permission_required( @rbac_permission_required(RBACCheck(RBACPermission.WORKSPACE_ROLE_MANAGE, Workspace()))
RBACResourceScope.WORKSPACE, RBACPermission.WORKSPACE_ROLE_MANAGE, resource_required=False
)
@console_ns.response(200, "Success", console_ns.models[svc.RBACRole.__name__]) @console_ns.response(200, "Success", console_ns.models[svc.RBACRole.__name__])
def put(self, role_id): def put(self, role_id):
tenant_id, account_id = _current_ids() tenant_id, account_id = _current_ids()
@ -390,9 +368,7 @@ class RBACRoleItemApi(Resource):
return _dump(role) return _dump(role)
@login_required @login_required
@rbac_permission_required( @rbac_permission_required(RBACCheck(RBACPermission.WORKSPACE_ROLE_MANAGE, Workspace()))
RBACResourceScope.WORKSPACE, RBACPermission.WORKSPACE_ROLE_MANAGE, resource_required=False
)
@console_ns.response(200, "Success", console_ns.models[svc.RBACRole.__name__]) @console_ns.response(200, "Success", console_ns.models[svc.RBACRole.__name__])
def delete(self, role_id): def delete(self, role_id):
tenant_id, account_id = _current_ids() tenant_id, account_id = _current_ids()
@ -403,9 +379,7 @@ class RBACRoleItemApi(Resource):
@console_ns.route("/workspaces/current/rbac/roles/<uuid:role_id>/copy") @console_ns.route("/workspaces/current/rbac/roles/<uuid:role_id>/copy")
class RBACRoleCopyApi(Resource): class RBACRoleCopyApi(Resource):
@login_required @login_required
@rbac_permission_required( @rbac_permission_required(RBACCheck(RBACPermission.WORKSPACE_ROLE_MANAGE, Workspace()))
RBACResourceScope.WORKSPACE, RBACPermission.WORKSPACE_ROLE_MANAGE, resource_required=False
)
@console_ns.response(201, "Role copied", console_ns.models[svc.RBACRole.__name__]) @console_ns.response(201, "Role copied", console_ns.models[svc.RBACRole.__name__])
def post(self, role_id): def post(self, role_id):
tenant_id, account_id = _current_ids() tenant_id, account_id = _current_ids()
@ -430,9 +404,7 @@ class _AccessPolicyUpdateRequest(BaseModel):
@console_ns.route("/workspaces/current/rbac/access-policies") @console_ns.route("/workspaces/current/rbac/access-policies")
class RBACAccessPoliciesApi(Resource): class RBACAccessPoliciesApi(Resource):
@login_required @login_required
@rbac_permission_required( @rbac_permission_required(RBACCheck(RBACPermission.WORKSPACE_ROLE_MANAGE, Workspace()))
RBACResourceScope.WORKSPACE, RBACPermission.WORKSPACE_ROLE_MANAGE, resource_required=False
)
@console_ns.response(200, "Success", console_ns.models[_AccessPolicyList.__name__]) @console_ns.response(200, "Success", console_ns.models[_AccessPolicyList.__name__])
def get(self): def get(self):
tenant_id, account_id = _current_ids() tenant_id, account_id = _current_ids()
@ -449,9 +421,7 @@ class RBACAccessPoliciesApi(Resource):
) )
@login_required @login_required
@rbac_permission_required( @rbac_permission_required(RBACCheck(RBACPermission.WORKSPACE_ROLE_MANAGE, Workspace()))
RBACResourceScope.WORKSPACE, RBACPermission.WORKSPACE_ROLE_MANAGE, resource_required=False
)
@console_ns.response(201, "Policy created", console_ns.models[svc.AccessPolicy.__name__]) @console_ns.response(201, "Policy created", console_ns.models[svc.AccessPolicy.__name__])
def post(self): def post(self):
tenant_id, account_id = _current_ids() tenant_id, account_id = _current_ids()
@ -472,18 +442,14 @@ class RBACAccessPoliciesApi(Resource):
@console_ns.route("/workspaces/current/rbac/access-policies/<uuid:policy_id>") @console_ns.route("/workspaces/current/rbac/access-policies/<uuid:policy_id>")
class RBACAccessPolicyItemApi(Resource): class RBACAccessPolicyItemApi(Resource):
@login_required @login_required
@rbac_permission_required( @rbac_permission_required(RBACCheck(RBACPermission.WORKSPACE_ROLE_MANAGE, Workspace()))
RBACResourceScope.WORKSPACE, RBACPermission.WORKSPACE_ROLE_MANAGE, resource_required=False
)
@console_ns.response(200, "Success", console_ns.models[svc.AccessPolicy.__name__]) @console_ns.response(200, "Success", console_ns.models[svc.AccessPolicy.__name__])
def get(self, policy_id): def get(self, policy_id):
tenant_id, account_id = _current_ids() tenant_id, account_id = _current_ids()
return _dump(svc.RBACService.AccessPolicies.get(tenant_id, account_id, str(policy_id))) return _dump(svc.RBACService.AccessPolicies.get(tenant_id, account_id, str(policy_id)))
@login_required @login_required
@rbac_permission_required( @rbac_permission_required(RBACCheck(RBACPermission.WORKSPACE_ROLE_MANAGE, Workspace()))
RBACResourceScope.WORKSPACE, RBACPermission.WORKSPACE_ROLE_MANAGE, resource_required=False
)
@console_ns.response(200, "Success", console_ns.models[svc.AccessPolicy.__name__]) @console_ns.response(200, "Success", console_ns.models[svc.AccessPolicy.__name__])
def put(self, policy_id): def put(self, policy_id):
tenant_id, account_id = _current_ids() tenant_id, account_id = _current_ids()
@ -501,9 +467,7 @@ class RBACAccessPolicyItemApi(Resource):
return _dump(policy) return _dump(policy)
@login_required @login_required
@rbac_permission_required( @rbac_permission_required(RBACCheck(RBACPermission.WORKSPACE_ROLE_MANAGE, Workspace()))
RBACResourceScope.WORKSPACE, RBACPermission.WORKSPACE_ROLE_MANAGE, resource_required=False
)
@console_ns.response(200, "Success", console_ns.models[svc.AccessPolicy.__name__]) @console_ns.response(200, "Success", console_ns.models[svc.AccessPolicy.__name__])
def delete(self, policy_id): def delete(self, policy_id):
tenant_id, account_id = _current_ids() tenant_id, account_id = _current_ids()
@ -514,9 +478,7 @@ class RBACAccessPolicyItemApi(Resource):
@console_ns.route("/workspaces/current/rbac/access-policies/<uuid:policy_id>/copy") @console_ns.route("/workspaces/current/rbac/access-policies/<uuid:policy_id>/copy")
class RBACAccessPolicyCopyApi(Resource): class RBACAccessPolicyCopyApi(Resource):
@login_required @login_required
@rbac_permission_required( @rbac_permission_required(RBACCheck(RBACPermission.WORKSPACE_ROLE_MANAGE, Workspace()))
RBACResourceScope.WORKSPACE, RBACPermission.WORKSPACE_ROLE_MANAGE, resource_required=False
)
@console_ns.response(201, "Policy copied", console_ns.models[svc.AccessPolicy.__name__]) @console_ns.response(201, "Policy copied", console_ns.models[svc.AccessPolicy.__name__])
def post(self, policy_id): def post(self, policy_id):
tenant_id, account_id = _current_ids() tenant_id, account_id = _current_ids()
@ -527,9 +489,7 @@ class RBACAccessPolicyCopyApi(Resource):
@console_ns.route("/workspaces/current/rbac/access-policy-bindings/<uuid:binding_id>/lock") @console_ns.route("/workspaces/current/rbac/access-policy-bindings/<uuid:binding_id>/lock")
class RBACAccessPolicyBindingLockApi(Resource): class RBACAccessPolicyBindingLockApi(Resource):
@login_required @login_required
@rbac_permission_required( @rbac_permission_required(RBACCheck(RBACPermission.WORKSPACE_ROLE_MANAGE, Workspace()))
RBACResourceScope.WORKSPACE, RBACPermission.WORKSPACE_ROLE_MANAGE, resource_required=False
)
@console_ns.response(200, "Success", console_ns.models[svc.AccessPolicyBindingState.__name__]) @console_ns.response(200, "Success", console_ns.models[svc.AccessPolicyBindingState.__name__])
def put(self, binding_id): def put(self, binding_id):
tenant_id, account_id = _current_ids() tenant_id, account_id = _current_ids()
@ -539,20 +499,13 @@ class RBACAccessPolicyBindingLockApi(Resource):
@console_ns.route("/workspaces/current/rbac/access-policy-bindings/<uuid:binding_id>/unlock") @console_ns.route("/workspaces/current/rbac/access-policy-bindings/<uuid:binding_id>/unlock")
class RBACAccessPolicyBindingUnlockApi(Resource): class RBACAccessPolicyBindingUnlockApi(Resource):
@login_required @login_required
@rbac_permission_required( @rbac_permission_required(RBACCheck(RBACPermission.WORKSPACE_ROLE_MANAGE, Workspace()))
RBACResourceScope.WORKSPACE, RBACPermission.WORKSPACE_ROLE_MANAGE, resource_required=False
)
@console_ns.response(200, "Success", console_ns.models[svc.AccessPolicyBindingState.__name__]) @console_ns.response(200, "Success", console_ns.models[svc.AccessPolicyBindingState.__name__])
def put(self, binding_id): def put(self, binding_id):
tenant_id, account_id = _current_ids() tenant_id, account_id = _current_ids()
return _dump(svc.RBACService.AccessPolicyBindings.unlock(tenant_id, account_id, str(binding_id))) return _dump(svc.RBACService.AccessPolicyBindings.unlock(tenant_id, account_id, str(binding_id)))
# ---------------------------------------------------------------------------
# Per-app access (App Access Config).
# ---------------------------------------------------------------------------
class _ResourceAccessScopeRequest(BaseModel): class _ResourceAccessScopeRequest(BaseModel):
automatic_include_workspace_members: bool automatic_include_workspace_members: bool
@ -611,346 +564,337 @@ class RBACMyPermissionsApi(Resource):
account_id, account_id,
app_id=request.args.get("app_id") or None, app_id=request.args.get("app_id") or None,
dataset_id=request.args.get("dataset_id") or None, dataset_id=request.args.get("dataset_id") or None,
agent_id=request.args.get("agent_id") or None,
session=db.session(), session=db.session(),
) )
) )
@console_ns.route("/workspaces/current/rbac/apps/<uuid:app_id>/access-policy") type _ResourceAccessClient = (
class RBACAppMatrixApi(Resource): type[svc.RBACService.AppAccess] | type[svc.RBACService.DatasetAccess] | type[svc.RBACService.AgentAccess]
@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/<uuid:app_id>/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/<uuid:app_id>/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/<uuid:app_id>/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/<uuid:app_id>/users/<uuid:target_account_id>/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/<uuid:app_id>/access-policies/<uuid:policy_id>/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/<uuid:app_id>/access-policies/<string:policy_id>/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/<uuid:dataset_id>/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/<uuid:dataset_id>/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/<uuid:dataset_id>/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/<uuid:dataset_id>/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/<uuid:dataset_id>/users/<uuid:target_account_id>/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/<uuid:dataset_id>/access-policies/<uuid:policy_id>/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/<uuid:dataset_id>/access-policies/<string:policy_id>/member-bindings"
) )
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") @dataclass(frozen=True)
class RBACWorkspaceAppMatrixApi(Resource): class _ResourceAccessRoutes:
@login_required resource_type: svc.RBACResourceType
@console_ns.response(200, "Success", console_ns.models[svc.WorkspaceAccessMatrix.__name__]) class_prefix: str
def get(self): matrix_model: type[BaseModel]
tenant_id, account_id = _current_ids() access: _ResourceAccessClient
options = _pagination_options() catalog: Callable[[str, str], svc.PermissionCatalogResponse]
result = svc.RBACService.WorkspaceAccess.app_matrix(tenant_id, account_id, options=options) workspace_matrix: Callable[[str, str, svc.ListOption], svc.WorkspaceAccessMatrix]
_hydrate_access_matrix_account_names(result.items) workspace_role_bindings: Callable[[str, str, str], svc.RoleBindingsResponse]
return _dump(result) 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/<uuid:policy_id>/role-bindings") @dataclass(frozen=True)
class RBACWorkspaceAppRoleBindingsApi(Resource): class _ResourceAccessApis:
@login_required catalog: type[Resource]
@console_ns.response(200, "Success", console_ns.models[svc.RoleBindingsResponse.__name__]) matrix: type[Resource]
def get(self, policy_id): whitelist: type[Resource]
tenant_id, account_id = _current_ids() whitelist_config: type[Resource]
return _dump(svc.RBACService.WorkspaceAccess.list_app_role_bindings(tenant_id, account_id, str(policy_id))) 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/<uuid:policy_id>/bindings") _RESOURCE_ACCESS_ROUTES = (
class RBACWorkspaceAppBindingsApi(Resource): _ResourceAccessRoutes(
@login_required resource_type=svc.RBACResourceType.APP,
@console_ns.expect(console_ns.models[_ReplaceBindingsRequest.__name__]) class_prefix="App",
@console_ns.response(200, "Success", console_ns.models[svc.AccessMatrixItem.__name__]) matrix_model=svc.AppAccessMatrix,
def put(self, policy_id): access=svc.RBACService.AppAccess,
tenant_id, account_id = _current_ids() catalog=lambda tenant_id, account_id: svc.RBACService.Catalog.app(tenant_id, account_id),
request = _payload(_ReplaceBindingsRequest) workspace_matrix=lambda tenant_id, account_id, options: svc.RBACService.WorkspaceAccess.app_matrix(
return _dump( tenant_id, account_id, options=options
svc.RBACService.WorkspaceAccess.replace_app_bindings( ),
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}/<uuid:{id_param}>"
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, tenant_id,
account_id, account_id,
str(policy_id), target_id,
svc.ReplaceBindings(role_ids=list(request.role_ids), account_ids=list(request.account_ids)), 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/<uuid:policy_id>/member-bindings") class UserAccessPoliciesApi(Resource):
class RBACWorkspaceAppMemberBindingsApi(Resource): @login_required
@login_required @console_ns.doc(params=query_params_from_model(_ResourceUserAccessPoliciesQuery))
@console_ns.response(200, "Success", console_ns.models[svc.MemberBindingsResponse.__name__]) @console_ns.response(200, "Success", console_ns.models[svc.ResourceUserAccessPoliciesResponse.__name__])
def get(self, policy_id): def get(self, **path_params):
tenant_id, account_id = _current_ids() tenant_id, account_id = _current_ids()
return _dump(svc.RBACService.WorkspaceAccess.list_app_member_bindings(tenant_id, account_id, str(policy_id))) 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 RoleBindingsApi(Resource):
class RBACWorkspaceDatasetMatrixApi(Resource): @login_required
@login_required @console_ns.response(200, "Success", console_ns.models[svc.RoleBindingsResponse.__name__])
@console_ns.response(200, "Success", console_ns.models[svc.WorkspaceAccessMatrix.__name__]) def get(self, policy_id, **path_params):
def get(self): tenant_id, account_id = _current_ids()
tenant_id, account_id = _current_ids() return _dump(
options = _pagination_options() spec.access.list_role_bindings(tenant_id, account_id, resource_id(path_params), str(policy_id))
result = svc.RBACService.WorkspaceAccess.dataset_matrix(tenant_id, account_id, options=options) )
_hydrate_access_matrix_account_names(result.items)
return _dump(result)
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/<uuid:policy_id>/role-bindings") @login_required
class RBACWorkspaceDatasetRoleBindingsApi(Resource): @console_ns.expect(console_ns.models[_DeleteMemberBindingsRequest.__name__])
@login_required @console_ns.response(200, "Success", console_ns.models[svc.MemberBindingsResponse.__name__])
@console_ns.response(200, "Success", console_ns.models[svc.RoleBindingsResponse.__name__]) def delete(self, policy_id, **path_params):
def get(self, policy_id): tenant_id, account_id = _current_ids()
tenant_id, account_id = _current_ids() body = _payload(_DeleteMemberBindingsRequest)
return _dump(svc.RBACService.WorkspaceAccess.list_dataset_role_bindings(tenant_id, account_id, str(policy_id))) spec.access.delete_member_bindings(
@console_ns.route("/workspaces/current/rbac/workspace/datasets/access-policies/<uuid:policy_id>/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(
tenant_id, tenant_id,
account_id, account_id,
resource_id(path_params),
str(policy_id), 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/<uuid:target_account_id>/access-policies",
),
role_bindings=register(
RoleBindingsApi,
f"RBAC{prefix}RoleBindingsApi",
f"{resource_prefix}/access-policies/<uuid:policy_id>/role-bindings",
),
member_bindings=register(
MemberBindingsApi,
f"RBAC{prefix}MemberBindingsApi",
f"{resource_prefix}/access-policies/<string:policy_id>/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/<uuid:policy_id>/role-bindings",
),
workspace_bindings=register(
WorkspaceBindingsApi,
f"RBACWorkspace{prefix}BindingsApi",
f"{workspace_prefix}/access-policies/<uuid:policy_id>/bindings",
),
workspace_member_bindings=register(
WorkspaceMemberBindingsApi,
f"RBACWorkspace{prefix}MemberBindingsApi",
f"{workspace_prefix}/access-policies/<uuid:policy_id>/member-bindings",
),
)
@console_ns.route("/workspaces/current/rbac/workspace/datasets/access-policies/<uuid:policy_id>/member-bindings") _RESOURCE_ACCESS_APIS = {spec.resource_type: _build_resource_access_apis(spec) for spec in _RESOURCE_ACCESS_ROUTES}
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))
)
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------

View File

@ -10,13 +10,13 @@ from pydantic import BaseModel, ConfigDict, Field, ValidationError
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from controllers.common.fields import BinaryFileResponse 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.schema import query_params_from_model, register_response_schema_models, register_schema_models
from controllers.common.session import with_session from controllers.common.session import with_session
from controllers.console import console_ns from controllers.console import console_ns
from controllers.console.flask_admission import console_account_admission from controllers.console.flask_admission import console_account_admission
from controllers.console.wraps import ( from controllers.console.wraps import (
RBACPermission, RBACPermission,
RBACResourceScope,
edit_permission_required, edit_permission_required,
) )
from fields.base import ResponseModel from fields.base import ResponseModel
@ -298,9 +298,7 @@ class WorkspaceSkillsApi(Resource):
@console_ns.doc(params=query_params_from_model(WorkspaceSkillsQuery)) @console_ns.doc(params=query_params_from_model(WorkspaceSkillsQuery))
@console_ns.response(200, "Workspace skills", console_ns.models[SkillListResponse.__name__]) @console_ns.response(200, "Workspace skills", console_ns.models[SkillListResponse.__name__])
@console_account_admission( @console_account_admission(
rbac_resource_scope=RBACResourceScope.WORKSPACE, rbac_checks=[RBACCheck(RBACPermission.SKILL_VIEW, Workspace())],
rbac_permission=RBACPermission.SKILL_VIEW,
rbac_resource_required=False,
) )
@with_session(write=False) @with_session(write=False)
def get(self, session: Session, request_context: RequestContext): 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.expect(console_ns.models[SkillCreatePayload.__name__])
@console_ns.response(201, "Skill created", console_ns.models[SkillDetailResponse.__name__]) @console_ns.response(201, "Skill created", console_ns.models[SkillDetailResponse.__name__])
@console_account_admission( @console_account_admission(
rbac_resource_scope=RBACResourceScope.WORKSPACE, rbac_checks=[RBACCheck(RBACPermission.SKILL_EDIT, Workspace())],
rbac_permission=RBACPermission.SKILL_EDIT,
rbac_resource_required=False,
) )
@edit_permission_required @edit_permission_required
@with_session @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.doc(description="Import a Skill zip package from multipart form field `file`.")
@console_ns.response(201, "Skill imported", console_ns.models[SkillDetailResponse.__name__]) @console_ns.response(201, "Skill imported", console_ns.models[SkillDetailResponse.__name__])
@console_account_admission( @console_account_admission(
rbac_resource_scope=RBACResourceScope.WORKSPACE, rbac_checks=[RBACCheck(RBACPermission.SKILL_EDIT, Workspace())],
rbac_permission=RBACPermission.SKILL_EDIT,
rbac_resource_required=False,
) )
@edit_permission_required @edit_permission_required
@with_session @with_session
@ -419,9 +413,7 @@ class WorkspaceSkillImportApi(Resource):
class WorkspaceSkillApi(Resource): class WorkspaceSkillApi(Resource):
@console_ns.response(200, "Skill detail", console_ns.models[SkillDetailResponse.__name__]) @console_ns.response(200, "Skill detail", console_ns.models[SkillDetailResponse.__name__])
@console_account_admission( @console_account_admission(
rbac_resource_scope=RBACResourceScope.WORKSPACE, rbac_checks=[RBACCheck(RBACPermission.SKILL_VIEW, Workspace())],
rbac_permission=RBACPermission.SKILL_VIEW,
rbac_resource_required=False,
) )
@with_session(write=False) @with_session(write=False)
def get(self, session: Session, request_context: RequestContext, skill_id: str): 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.expect(console_ns.models[SkillMetadataPayload.__name__])
@console_ns.response(200, "Skill updated", console_ns.models[SkillResponse.__name__]) @console_ns.response(200, "Skill updated", console_ns.models[SkillResponse.__name__])
@console_account_admission( @console_account_admission(
rbac_resource_scope=RBACResourceScope.WORKSPACE, rbac_checks=[RBACCheck(RBACPermission.SKILL_EDIT, Workspace())],
rbac_permission=RBACPermission.SKILL_EDIT,
rbac_resource_required=False,
) )
@edit_permission_required @edit_permission_required
@with_session @with_session
@ -462,9 +452,7 @@ class WorkspaceSkillApi(Resource):
@console_ns.expect(console_ns.models[SkillDeletePayload.__name__]) @console_ns.expect(console_ns.models[SkillDeletePayload.__name__])
@console_ns.response(200, "Skill deleted", console_ns.models[SkillDeleteResponse.__name__]) @console_ns.response(200, "Skill deleted", console_ns.models[SkillDeleteResponse.__name__])
@console_account_admission( @console_account_admission(
rbac_resource_scope=RBACResourceScope.WORKSPACE, rbac_checks=[RBACCheck(RBACPermission.SKILL_DELETE, Workspace())],
rbac_permission=RBACPermission.SKILL_DELETE,
rbac_resource_required=False,
) )
@edit_permission_required @edit_permission_required
@with_session @with_session
@ -487,9 +475,7 @@ class WorkspaceSkillApi(Resource):
class WorkspaceSkillDuplicateApi(Resource): class WorkspaceSkillDuplicateApi(Resource):
@console_ns.response(201, "Skill duplicated", console_ns.models[SkillDetailResponse.__name__]) @console_ns.response(201, "Skill duplicated", console_ns.models[SkillDetailResponse.__name__])
@console_account_admission( @console_account_admission(
rbac_resource_scope=RBACResourceScope.WORKSPACE, rbac_checks=[RBACCheck(RBACPermission.SKILL_EDIT, Workspace())],
rbac_permission=RBACPermission.SKILL_EDIT,
rbac_resource_required=False,
) )
@edit_permission_required @edit_permission_required
@with_session @with_session
@ -509,9 +495,7 @@ class WorkspaceSkillDuplicateApi(Resource):
class WorkspaceSkillExportApi(Resource): class WorkspaceSkillExportApi(Resource):
@console_ns.response(200, "Published Skill zip archive") @console_ns.response(200, "Published Skill zip archive")
@console_account_admission( @console_account_admission(
rbac_resource_scope=RBACResourceScope.WORKSPACE, rbac_checks=[RBACCheck(RBACPermission.SKILL_VIEW, Workspace())],
rbac_permission=RBACPermission.SKILL_VIEW,
rbac_resource_required=False,
) )
@with_session(write=False) @with_session(write=False)
def get(self, session: Session, request_context: RequestContext, skill_id: str): 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.expect(console_ns.models[SkillPublishPayload.__name__])
@console_ns.response(200, "Skill published", console_ns.models[SkillVersionResponse.__name__]) @console_ns.response(200, "Skill published", console_ns.models[SkillVersionResponse.__name__])
@console_account_admission( @console_account_admission(
rbac_resource_scope=RBACResourceScope.WORKSPACE, rbac_checks=[RBACCheck(RBACPermission.SKILL_PUBLISH, Workspace())],
rbac_permission=RBACPermission.SKILL_PUBLISH,
rbac_resource_required=False,
) )
@edit_permission_required @edit_permission_required
@with_session @with_session
@ -723,9 +705,7 @@ class WorkspaceSkillRestoreApi(Resource):
@console_ns.expect(console_ns.models[SkillRestorePayload.__name__]) @console_ns.expect(console_ns.models[SkillRestorePayload.__name__])
@console_ns.response(200, "Skill version restored to draft", console_ns.models[SkillDetailResponse.__name__]) @console_ns.response(200, "Skill version restored to draft", console_ns.models[SkillDetailResponse.__name__])
@console_account_admission( @console_account_admission(
rbac_resource_scope=RBACResourceScope.WORKSPACE, rbac_checks=[RBACCheck(RBACPermission.SKILL_PUBLISH, Workspace())],
rbac_permission=RBACPermission.SKILL_PUBLISH,
rbac_resource_required=False,
) )
@edit_permission_required @edit_permission_required
@with_session @with_session

View File

@ -8,6 +8,7 @@ from sqlalchemy.orm import Session, sessionmaker
from werkzeug.exceptions import NotFound from werkzeug.exceptions import NotFound
from controllers.common.fields import TextFileResponse from controllers.common.fields import TextFileResponse
from controllers.common.rbac import RBACCheck, Workspace
from controllers.common.schema import ( from controllers.common.schema import (
query_params_from_model, query_params_from_model,
register_response_schema_models, register_response_schema_models,
@ -24,7 +25,6 @@ from controllers.console.snippets.payloads import (
) )
from controllers.console.wraps import ( from controllers.console.wraps import (
RBACPermission, RBACPermission,
RBACResourceScope,
account_initialization_required, account_initialization_required,
edit_permission_required, edit_permission_required,
model_validate, model_validate,
@ -147,9 +147,7 @@ class CustomizedSnippetsApi(Resource):
@login_required @login_required
@account_initialization_required @account_initialization_required
@edit_permission_required @edit_permission_required
@rbac_permission_required( @rbac_permission_required(RBACCheck(RBACPermission.SNIPPETS_CREATE_AND_MODIFY, Workspace()))
RBACResourceScope.WORKSPACE, RBACPermission.SNIPPETS_CREATE_AND_MODIFY, resource_required=False
)
@with_current_user @with_current_user
@with_current_tenant_id @with_current_tenant_id
@model_validate(CreateSnippetPayload) @model_validate(CreateSnippetPayload)
@ -211,9 +209,7 @@ class CustomizedSnippetDetailApi(Resource):
@login_required @login_required
@account_initialization_required @account_initialization_required
@edit_permission_required @edit_permission_required
@rbac_permission_required( @rbac_permission_required(RBACCheck(RBACPermission.SNIPPETS_CREATE_AND_MODIFY, Workspace()))
RBACResourceScope.WORKSPACE, RBACPermission.SNIPPETS_CREATE_AND_MODIFY, resource_required=False
)
@with_current_user @with_current_user
@with_current_tenant_id @with_current_tenant_id
@model_validate(UpdateSnippetPayload) @model_validate(UpdateSnippetPayload)
@ -258,7 +254,7 @@ class CustomizedSnippetDetailApi(Resource):
@login_required @login_required
@account_initialization_required @account_initialization_required
@edit_permission_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_user
@with_current_tenant_id @with_current_tenant_id
def delete(self, current_tenant_id: str, current_user: Account, snippet_id: str): def delete(self, current_tenant_id: str, current_user: Account, snippet_id: str):
@ -296,9 +292,7 @@ class CustomizedSnippetExportApi(Resource):
@login_required @login_required
@account_initialization_required @account_initialization_required
@edit_permission_required @edit_permission_required
@rbac_permission_required( @rbac_permission_required(RBACCheck(RBACPermission.SNIPPETS_CREATE_AND_MODIFY, Workspace()))
RBACResourceScope.WORKSPACE, RBACPermission.SNIPPETS_CREATE_AND_MODIFY, resource_required=False
)
@with_current_tenant_id @with_current_tenant_id
def get(self, current_tenant_id: str, snippet_id: str): def get(self, current_tenant_id: str, snippet_id: str):
"""Export snippet as DSL.""" """Export snippet as DSL."""
@ -351,9 +345,7 @@ class CustomizedSnippetImportApi(Resource):
@login_required @login_required
@account_initialization_required @account_initialization_required
@edit_permission_required @edit_permission_required
@rbac_permission_required( @rbac_permission_required(RBACCheck(RBACPermission.SNIPPETS_CREATE_AND_MODIFY, Workspace()))
RBACResourceScope.WORKSPACE, RBACPermission.SNIPPETS_CREATE_AND_MODIFY, resource_required=False
)
@with_current_user @with_current_user
@with_session @with_session
@model_validate(SnippetImportPayload) @model_validate(SnippetImportPayload)
@ -390,9 +382,7 @@ class CustomizedSnippetImportConfirmApi(Resource):
@login_required @login_required
@account_initialization_required @account_initialization_required
@edit_permission_required @edit_permission_required
@rbac_permission_required( @rbac_permission_required(RBACCheck(RBACPermission.SNIPPETS_CREATE_AND_MODIFY, Workspace()))
RBACResourceScope.WORKSPACE, RBACPermission.SNIPPETS_CREATE_AND_MODIFY, resource_required=False
)
@with_current_user @with_current_user
@with_session @with_session
def post(self, session: Session, current_user: Account, import_id: str): def post(self, session: Session, current_user: Account, import_id: str):
@ -420,9 +410,7 @@ class CustomizedSnippetCheckDependenciesApi(Resource):
@login_required @login_required
@account_initialization_required @account_initialization_required
@edit_permission_required @edit_permission_required
@rbac_permission_required( @rbac_permission_required(RBACCheck(RBACPermission.SNIPPETS_CREATE_AND_MODIFY, Workspace()))
RBACResourceScope.WORKSPACE, RBACPermission.SNIPPETS_CREATE_AND_MODIFY, resource_required=False
)
@with_current_tenant_id @with_current_tenant_id
def get(self, current_tenant_id: str, snippet_id: str): def get(self, current_tenant_id: str, snippet_id: str):
"""Check dependencies for a snippet.""" """Check dependencies for a snippet."""

View File

@ -20,6 +20,7 @@ from werkzeug.exceptions import Forbidden
from configs import dify_config from configs import dify_config
from controllers.common.fields import SimpleResultResponse from controllers.common.fields import SimpleResultResponse
from controllers.common.rbac import RBACCheck, Workspace
from controllers.common.schema import ( from controllers.common.schema import (
query_params_from_model, query_params_from_model,
query_params_from_request, query_params_from_request,
@ -29,7 +30,6 @@ from controllers.common.schema import (
from controllers.console import console_ns from controllers.console import console_ns
from controllers.console.wraps import ( from controllers.console.wraps import (
RBACPermission, RBACPermission,
RBACResourceScope,
account_initialization_required, account_initialization_required,
enterprise_license_required, enterprise_license_required,
is_admin_or_owner_required, is_admin_or_owner_required,
@ -563,7 +563,7 @@ class ToolBuiltinProviderDeleteApi(Resource):
@setup_required @setup_required
@login_required @login_required
@is_admin_or_owner_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 @account_initialization_required
@with_current_tenant_id @with_current_tenant_id
@model_validate(BuiltinToolCredentialDeletePayload) @model_validate(BuiltinToolCredentialDeletePayload)
@ -590,7 +590,7 @@ class ToolBuiltinProviderAddApi(Resource):
@setup_required @setup_required
@login_required @login_required
@is_admin_or_owner_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 @account_initialization_required
@with_current_user @with_current_user
@with_current_tenant_id @with_current_tenant_id
@ -621,7 +621,7 @@ class ToolBuiltinProviderUpdateApi(Resource):
@setup_required @setup_required
@login_required @login_required
@is_admin_or_owner_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 @account_initialization_required
@with_current_user @with_current_user
@with_current_tenant_id @with_current_tenant_id
@ -689,7 +689,7 @@ class ToolApiProviderAddApi(Resource):
@setup_required @setup_required
@login_required @login_required
@is_admin_or_owner_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 @account_initialization_required
@with_current_user @with_current_user
@with_current_tenant_id @with_current_tenant_id
@ -769,7 +769,7 @@ class ToolApiProviderUpdateApi(Resource):
@setup_required @setup_required
@login_required @login_required
@is_admin_or_owner_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 @account_initialization_required
@with_current_user @with_current_user
@with_current_tenant_id @with_current_tenant_id
@ -800,7 +800,7 @@ class ToolApiProviderDeleteApi(Resource):
@setup_required @setup_required
@login_required @login_required
@is_admin_or_owner_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 @account_initialization_required
@with_current_user @with_current_user
@with_current_tenant_id @with_current_tenant_id
@ -908,7 +908,7 @@ class ToolWorkflowProviderCreateApi(Resource):
@setup_required @setup_required
@login_required @login_required
@is_admin_or_owner_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 @account_initialization_required
@with_current_user @with_current_user
@with_current_tenant_id @with_current_tenant_id
@ -938,7 +938,7 @@ class ToolWorkflowProviderUpdateApi(Resource):
@setup_required @setup_required
@login_required @login_required
@is_admin_or_owner_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 @account_initialization_required
@with_current_user @with_current_user
@with_current_tenant_id @with_current_tenant_id
@ -968,7 +968,7 @@ class ToolWorkflowProviderDeleteApi(Resource):
@setup_required @setup_required
@login_required @login_required
@is_admin_or_owner_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 @account_initialization_required
@with_current_user @with_current_user
@with_current_tenant_id @with_current_tenant_id
@ -1125,7 +1125,7 @@ class ToolPluginOAuthApi(Resource):
@setup_required @setup_required
@login_required @login_required
@is_admin_or_owner_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 @account_initialization_required
@with_current_user @with_current_user
@with_current_tenant_id @with_current_tenant_id
@ -1251,7 +1251,7 @@ class ToolBuiltinProviderSetDefaultApi(Resource):
@setup_required @setup_required
@login_required @login_required
@is_admin_or_owner_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 @account_initialization_required
@with_current_tenant_id @with_current_tenant_id
@model_validate(BuiltinProviderDefaultCredentialPayload) @model_validate(BuiltinProviderDefaultCredentialPayload)
@ -1273,7 +1273,7 @@ class ToolOAuthCustomClient(Resource):
@setup_required @setup_required
@login_required @login_required
@is_admin_or_owner_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 @account_initialization_required
@with_current_tenant_id @with_current_tenant_id
@model_validate(ToolOAuthCustomClientPayload) @model_validate(ToolOAuthCustomClientPayload)
@ -1297,7 +1297,7 @@ class ToolOAuthCustomClient(Resource):
@setup_required @setup_required
@login_required @login_required
@is_admin_or_owner_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 @account_initialization_required
@with_current_tenant_id @with_current_tenant_id
def get(self, current_tenant_id: str, provider: str): def get(self, current_tenant_id: str, provider: str):
@ -1309,7 +1309,7 @@ class ToolOAuthCustomClient(Resource):
@setup_required @setup_required
@login_required @login_required
@is_admin_or_owner_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 @account_initialization_required
@with_current_tenant_id @with_current_tenant_id
def delete(self, current_tenant_id: str, provider: str): def delete(self, current_tenant_id: str, provider: str):
@ -1379,7 +1379,7 @@ class ToolProviderMCPApi(Resource):
@setup_required @setup_required
@login_required @login_required
@account_initialization_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_user
@with_current_tenant_id @with_current_tenant_id
@model_validate(MCPProviderCreatePayload) @model_validate(MCPProviderCreatePayload)
@ -1433,7 +1433,7 @@ class ToolProviderMCPApi(Resource):
@setup_required @setup_required
@login_required @login_required
@account_initialization_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 @with_current_tenant_id
@model_validate(MCPProviderUpdatePayload) @model_validate(MCPProviderUpdatePayload)
def put(self, req_data: MCPProviderUpdatePayload, current_tenant_id: str): def put(self, req_data: MCPProviderUpdatePayload, current_tenant_id: str):
@ -1487,7 +1487,7 @@ class ToolProviderMCPApi(Resource):
@setup_required @setup_required
@login_required @login_required
@account_initialization_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 @with_current_tenant_id
@model_validate(MCPProviderDeletePayload) @model_validate(MCPProviderDeletePayload)
def delete(self, req_data: MCPProviderDeletePayload, current_tenant_id: str): def delete(self, req_data: MCPProviderDeletePayload, current_tenant_id: str):
@ -1505,7 +1505,7 @@ class ToolMCPAuthApi(Resource):
@setup_required @setup_required
@login_required @login_required
@account_initialization_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 @with_current_tenant_id
@model_validate(MCPAuthPayload) @model_validate(MCPAuthPayload)
def post(self, req_data: MCPAuthPayload, tenant_id: str): def post(self, req_data: MCPAuthPayload, tenant_id: str):
@ -1617,7 +1617,7 @@ class ToolMCPUpdateApi(Resource):
@setup_required @setup_required
@login_required @login_required
@account_initialization_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 @with_current_tenant_id
def get(self, tenant_id: str, provider_id: str): def get(self, tenant_id: str, provider_id: str):
with sessionmaker(db.engine).begin() as session: with sessionmaker(db.engine).begin() as session:

View File

@ -10,6 +10,7 @@ from werkzeug.exceptions import BadRequest, Forbidden
from configs import dify_config from configs import dify_config
from controllers.common.errors import NotFoundError from controllers.common.errors import NotFoundError
from controllers.common.fields import SimpleResultResponse 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.common.schema import register_response_schema_models, register_schema_models
from core.entities.provider_entities import ProviderConfig from core.entities.provider_entities import ProviderConfig
from core.plugin.entities.plugin_daemon import CredentialType 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 .. import console_ns
from ..wraps import ( from ..wraps import (
RBACPermission, RBACPermission,
RBACResourceScope,
account_initialization_required, account_initialization_required,
edit_permission_required, edit_permission_required,
is_admin_or_owner_required, is_admin_or_owner_required,
@ -199,7 +199,7 @@ class TriggerSubscriptionListApi(Resource):
@setup_required @setup_required
@login_required @login_required
@edit_permission_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 @account_initialization_required
@with_current_user @with_current_user
@with_current_tenant_id @with_current_tenant_id
@ -235,7 +235,7 @@ class TriggerSubscriptionBuilderCreateApi(Resource):
@setup_required @setup_required
@login_required @login_required
@edit_permission_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 @account_initialization_required
@with_current_user @with_current_user
@with_current_tenant_id @with_current_tenant_id
@ -271,7 +271,7 @@ class TriggerSubscriptionBuilderGetApi(Resource):
@setup_required @setup_required
@login_required @login_required
@edit_permission_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 @account_initialization_required
@with_current_user @with_current_user
@with_current_tenant_id @with_current_tenant_id
@ -299,7 +299,7 @@ class TriggerSubscriptionBuilderVerifyApi(Resource):
@setup_required @setup_required
@login_required @login_required
@edit_permission_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 @account_initialization_required
@with_current_user @with_current_user
@with_current_tenant_id @with_current_tenant_id
@ -344,7 +344,7 @@ class TriggerSubscriptionBuilderUpdateApi(Resource):
@setup_required @setup_required
@login_required @login_required
@edit_permission_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 @account_initialization_required
@with_current_user @with_current_user
@with_current_tenant_id @with_current_tenant_id
@ -389,7 +389,7 @@ class TriggerSubscriptionBuilderLogsApi(Resource):
@setup_required @setup_required
@login_required @login_required
@edit_permission_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 @account_initialization_required
@with_current_user @with_current_user
@with_current_tenant_id @with_current_tenant_id
@ -420,7 +420,7 @@ class TriggerSubscriptionBuilderBuildApi(Resource):
@setup_required @setup_required
@login_required @login_required
@edit_permission_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 @account_initialization_required
@with_current_user @with_current_user
@with_current_tenant_id @with_current_tenant_id
@ -464,7 +464,7 @@ class TriggerSubscriptionUpdateApi(Resource):
@setup_required @setup_required
@login_required @login_required
@edit_permission_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 @account_initialization_required
@with_current_tenant_id @with_current_tenant_id
@model_validate(TriggerSubscriptionBuilderUpdatePayload) @model_validate(TriggerSubscriptionBuilderUpdatePayload)
@ -524,7 +524,7 @@ class TriggerSubscriptionDeleteApi(Resource):
@setup_required @setup_required
@login_required @login_required
@is_admin_or_owner_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 @account_initialization_required
@with_current_tenant_id @with_current_tenant_id
def post(self, tenant_id: str, subscription_id: str): def post(self, tenant_id: str, subscription_id: str):
@ -716,7 +716,7 @@ class TriggerOAuthClientManageApi(Resource):
@setup_required @setup_required
@login_required @login_required
@is_admin_or_owner_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 @account_initialization_required
@with_current_tenant_id @with_current_tenant_id
def get(self, tenant_id: str, provider: str): def get(self, tenant_id: str, provider: str):
@ -763,7 +763,7 @@ class TriggerOAuthClientManageApi(Resource):
@setup_required @setup_required
@login_required @login_required
@is_admin_or_owner_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 @account_initialization_required
@with_current_tenant_id @with_current_tenant_id
@model_validate(TriggerOAuthClientPayload) @model_validate(TriggerOAuthClientPayload)
@ -792,7 +792,7 @@ class TriggerOAuthClientManageApi(Resource):
@setup_required @setup_required
@login_required @login_required
@is_admin_or_owner_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 @account_initialization_required
@with_current_tenant_id @with_current_tenant_id
def delete(self, tenant_id: str, provider: str): def delete(self, tenant_id: str, provider: str):
@ -826,7 +826,7 @@ class TriggerSubscriptionVerifyApi(Resource):
@setup_required @setup_required
@login_required @login_required
@edit_permission_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 @account_initialization_required
@with_current_user @with_current_user
@with_current_tenant_id @with_current_tenant_id

View File

@ -11,11 +11,8 @@ from sqlalchemy import select
from werkzeug.exceptions import Forbidden, UnprocessableEntity from werkzeug.exceptions import Forbidden, UnprocessableEntity
from configs import dify_config from configs import dify_config
from controllers.common.wraps import ( from controllers.common.rbac import RBACPermission, RBACResourceScope
RBACPermission, from controllers.common.wraps import rbac_permission_required
RBACResourceScope,
rbac_permission_required,
)
from controllers.console.auth.error import AuthenticationFailedError, EmailCodeError from controllers.console.auth.error import AuthenticationFailedError, EmailCodeError
from controllers.console.workspace.error import AccountNotInitializedError from controllers.console.workspace.error import AccountNotInitializedError
from enums import CloudPlan, DeploymentEdition from enums import CloudPlan, DeploymentEdition

View File

@ -6,12 +6,12 @@ from flask_restx import Resource
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from werkzeug.exceptions import Forbidden 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 import openapi_ns
from controllers.openapi._contract import accepts, returns from controllers.openapi._contract import accepts, returns
from controllers.openapi._models import AppDslExportQuery, AppDslExportResponse, AppDslImportPayload from controllers.openapi._models import AppDslExportQuery, AppDslExportResponse, AppDslImportPayload
from controllers.openapi.auth.composition import auth_router 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 extensions.ext_database import db
from libs.oauth_bearer import Scope, TokenType from libs.oauth_bearer import Scope, TokenType
from models import Account, App from models import Account, App
@ -40,11 +40,7 @@ class AppDslImportApi(Resource):
scope=Scope.WORKSPACE_WRITE, scope=Scope.WORKSPACE_WRITE,
allowed_token_types=frozenset({TokenType.OAUTH_ACCOUNT}), allowed_token_types=frozenset({TokenType.OAUTH_ACCOUNT}),
allowed_roles=frozenset({TenantAccountRole.EDITOR, TenantAccountRole.ADMIN, TenantAccountRole.OWNER}), allowed_roles=frozenset({TenantAccountRole.EDITOR, TenantAccountRole.ADMIN, TenantAccountRole.OWNER}),
rbac=RBACRequirement( rbac=RBACCheck(RBACPermission.APP_IMPORT_EXPORT_DSL, Workspace()),
resource_type=RBACResourceScope.APP,
scene=RBACPermission.APP_IMPORT_EXPORT_DSL,
resource_required=False,
),
) )
@returns(200, Import, "Import completed") @returns(200, Import, "Import completed")
@returns(202, Import, "Import pending confirmation") @returns(202, Import, "Import pending confirmation")
@ -100,11 +96,7 @@ class AppDslImportConfirmApi(Resource):
scope=Scope.WORKSPACE_WRITE, scope=Scope.WORKSPACE_WRITE,
allowed_token_types=frozenset({TokenType.OAUTH_ACCOUNT}), allowed_token_types=frozenset({TokenType.OAUTH_ACCOUNT}),
allowed_roles=frozenset({TenantAccountRole.EDITOR, TenantAccountRole.ADMIN, TenantAccountRole.OWNER}), allowed_roles=frozenset({TenantAccountRole.EDITOR, TenantAccountRole.ADMIN, TenantAccountRole.OWNER}),
rbac=RBACRequirement( rbac=RBACCheck(RBACPermission.APP_IMPORT_EXPORT_DSL, Workspace()),
resource_type=RBACResourceScope.APP,
scene=RBACPermission.APP_IMPORT_EXPORT_DSL,
resource_required=False,
),
) )
@returns(200, Import, "Import confirmed") @returns(200, Import, "Import confirmed")
@returns(400, Import, "Import failed") @returns(400, Import, "Import failed")
@ -144,7 +136,7 @@ class AppDslExportApi(Resource):
scope=Scope.APPS_READ, scope=Scope.APPS_READ,
allowed_token_types=frozenset({TokenType.OAUTH_ACCOUNT}), allowed_token_types=frozenset({TokenType.OAUTH_ACCOUNT}),
allowed_roles=frozenset({TenantAccountRole.EDITOR, TenantAccountRole.ADMIN, TenantAccountRole.OWNER}), 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) @accepts(query=AppDslExportQuery)
@returns(200, AppDslExportResponse, "Export successful") @returns(200, AppDslExportResponse, "Export successful")
@ -176,7 +168,7 @@ class AppDslCheckDependenciesApi(Resource):
scope=Scope.APPS_READ, scope=Scope.APPS_READ,
allowed_token_types=frozenset({TokenType.OAUTH_ACCOUNT}), allowed_token_types=frozenset({TokenType.OAUTH_ACCOUNT}),
allowed_roles=frozenset({TenantAccountRole.EDITOR, TenantAccountRole.ADMIN, TenantAccountRole.OWNER}), 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") @returns(200, CheckDependenciesResult, "Dependencies checked")
def get(self, app_id: str, *, auth_data: AuthData): def get(self, app_id: str, *, auth_data: AuthData):

View File

@ -20,14 +20,14 @@ from werkzeug.exceptions import (
import services import services
from controllers.common.fields import EventStreamResponse 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.console.app.wraps import with_session
from controllers.openapi import openapi_ns from controllers.openapi import openapi_ns
from controllers.openapi._audit import emit_app_run from controllers.openapi._audit import emit_app_run
from controllers.openapi._contract import accepts, returns from controllers.openapi._contract import accepts, returns
from controllers.openapi._models import AppRunRequest, TaskStopResponse from controllers.openapi._models import AppRunRequest, TaskStopResponse
from controllers.openapi.auth.composition import auth_router 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 ( from controllers.service_api.app.error import (
AppUnavailableError, AppUnavailableError,
CompletionRequestError, CompletionRequestError,
@ -148,7 +148,7 @@ _DISPATCH: dict[AppMode, Callable[[App, Any, AppRunRequest, Session], Any]] = {
class AppRunApi(Resource): class AppRunApi(Resource):
@auth_router.guard( @auth_router.guard(
scope=Scope.APPS_RUN, 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__]) @openapi_ns.response(200, "Run result (SSE stream)", openapi_ns.models[EventStreamResponse.__name__])
@accepts(body=AppRunRequest) @accepts(body=AppRunRequest)
@ -184,7 +184,7 @@ class AppRunApi(Resource):
class AppRunTaskStopApi(Resource): class AppRunTaskStopApi(Resource):
@auth_router.guard( @auth_router.guard(
scope=Scope.APPS_RUN, 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") @returns(200, TaskStopResponse, description="Task stopped")
def post(self, app_id: str, task_id: str, *, auth_data: AuthData): def post(self, app_id: str, task_id: str, *, auth_data: AuthData):

View File

@ -12,8 +12,8 @@ from werkzeug.exceptions import Conflict, NotFound, UnprocessableEntity
from configs import dify_config from configs import dify_config
from controllers.common.app_access import AppAccessFilter, resolve_app_access_filter from controllers.common.app_access import AppAccessFilter, resolve_app_access_filter
from controllers.common.fields import Parameters from controllers.common.fields import Parameters
from controllers.common.rbac import PlainApp, RBACCheck, RBACPermission
from controllers.common.session import with_session from controllers.common.session import with_session
from controllers.common.wraps import RBACPermission, RBACResourceScope
from controllers.openapi import openapi_ns from controllers.openapi import openapi_ns
from controllers.openapi._contract import accepts, returns from controllers.openapi._contract import accepts, returns
from controllers.openapi._input_schema import EMPTY_INPUT_SCHEMA, build_input_schema, resolve_app_config 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, AppListRow,
) )
from controllers.openapi.auth.composition import auth_router 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 controllers.service_api.app.error import AppUnavailableError
from core.app.app_config.common.parameters_mapping import get_parameters_from_feature_dict from core.app.app_config.common.parameters_mapping import get_parameters_from_feature_dict
from libs.oauth_bearer import Scope, TokenType from libs.oauth_bearer import Scope, TokenType
@ -135,7 +135,7 @@ class AppDescribeApi(AppReadResource):
@auth_router.guard( @auth_router.guard(
scope=Scope.APPS_READ, scope=Scope.APPS_READ,
allowed_token_types=frozenset({TokenType.OAUTH_ACCOUNT}), 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") @returns(200, AppDescribeResponse, description="App description")
@accepts(query=AppDescribeQuery) @accepts(query=AppDescribeQuery)

View File

@ -6,7 +6,7 @@ from enum import StrEnum
from pydantic import BaseModel, ConfigDict, Field from pydantic import BaseModel, ConfigDict, Field
from werkzeug.exceptions import InternalServerError 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 libs.oauth_bearer import Scope, TokenType
from models.account import Account, Tenant, TenantAccountRole from models.account import Account, Tenant, TenantAccountRole
from models.model import App, EndUser from models.model import App, EndUser
@ -25,23 +25,15 @@ class ExternalIdentity(BaseModel):
issuer: str | None = None issuer: str | None = None
class RBACRequirement(BaseModel):
model_config = ConfigDict(frozen=True)
resource_type: RBACResourceScope
scene: RBACPermission
resource_required: bool = True
class RequestContext(BaseModel): class RequestContext(BaseModel):
model_config = ConfigDict(frozen=True) model_config = ConfigDict(frozen=True, arbitrary_types_allowed=True)
token_type: TokenType token_type: TokenType
scope: Scope | None = None scope: Scope | None = None
path_params: dict[str, str] path_params: dict[str, str]
workspace_membership: bool = False workspace_membership: bool = False
allowed_roles: frozenset[TenantAccountRole] | None = None allowed_roles: frozenset[TenantAccountRole] | None = None
rbac: RBACRequirement | None = None rbac: RBACCheck | None = None
class AuthData(BaseModel): class AuthData(BaseModel):
@ -58,7 +50,7 @@ class AuthData(BaseModel):
path_params: dict[str, str] = Field(default_factory=dict) path_params: dict[str, str] = Field(default_factory=dict)
allowed_roles: frozenset[TenantAccountRole] | None = None allowed_roles: frozenset[TenantAccountRole] | None = None
rbac: RBACRequirement | None = None rbac: RBACCheck | None = None
app: App | None = None app: App | None = None
tenant: Tenant | None = None tenant: Tenant | None = None

View File

@ -17,11 +17,11 @@ from flask_login import user_logged_in
from werkzeug.exceptions import Forbidden, NotFound, Unauthorized from werkzeug.exceptions import Forbidden, NotFound, Unauthorized
from configs import dify_config from configs import dify_config
from controllers.common.rbac import RBACCheck
from controllers.openapi._audit import emit_wrong_surface from controllers.openapi._audit import emit_wrong_surface
from controllers.openapi.auth.data import ( from controllers.openapi.auth.data import (
AuthData, AuthData,
ExternalIdentity, ExternalIdentity,
RBACRequirement,
RequestContext, RequestContext,
) )
from controllers.openapi.auth.flow import When from controllers.openapi.auth.flow import When
@ -61,7 +61,7 @@ class AuthPipeline:
scope: Scope | None, scope: Scope | None,
workspace_membership: bool = False, workspace_membership: bool = False,
allowed_roles: frozenset[TenantAccountRole] | None = None, allowed_roles: frozenset[TenantAccountRole] | None = None,
rbac: RBACRequirement | None = None, rbac: RBACCheck | None = None,
) -> Any: ) -> Any:
req_ctx = RequestContext( req_ctx = RequestContext(
token_type=identity.token_type, token_type=identity.token_type,
@ -135,7 +135,7 @@ class PipelineRouter:
require_valid_enterprise_license: bool = False, require_valid_enterprise_license: bool = False,
workspace_membership: bool = False, workspace_membership: bool = False,
allowed_roles: frozenset[TenantAccountRole] | None = None, allowed_roles: frozenset[TenantAccountRole] | None = None,
rbac: RBACRequirement | None = None, rbac: RBACCheck | None = None,
) -> Callable: ) -> Callable:
return self._make_decorator( return self._make_decorator(
scope=scope, scope=scope,
@ -155,7 +155,7 @@ class PipelineRouter:
edition: frozenset[DeploymentEdition] | None = None, edition: frozenset[DeploymentEdition] | None = None,
require_valid_enterprise_license: bool = False, require_valid_enterprise_license: bool = False,
allowed_roles: frozenset[TenantAccountRole] | None = None, allowed_roles: frozenset[TenantAccountRole] | None = None,
rbac: RBACRequirement | None = None, rbac: RBACCheck | None = None,
) -> Callable: ) -> Callable:
return self._make_decorator( return self._make_decorator(
scope=scope, scope=scope,
@ -176,7 +176,7 @@ class PipelineRouter:
require_valid_enterprise_license: bool, require_valid_enterprise_license: bool,
workspace_membership: bool, workspace_membership: bool,
allowed_roles: frozenset[TenantAccountRole] | None, allowed_roles: frozenset[TenantAccountRole] | None,
rbac: RBACRequirement | None, rbac: RBACCheck | None,
) -> Callable: ) -> Callable:
def decorator(view: Callable) -> Callable: def decorator(view: Callable) -> Callable:
@wraps(view) @wraps(view)
@ -210,7 +210,7 @@ class PipelineRouter:
require_valid_enterprise_license: bool, require_valid_enterprise_license: bool,
workspace_membership: bool = False, workspace_membership: bool = False,
allowed_roles: frozenset[TenantAccountRole] | None = None, allowed_roles: frozenset[TenantAccountRole] | None = None,
rbac: RBACRequirement | None = None, rbac: RBACCheck | None = None,
) -> Any: ) -> Any:
# 404 not 403 — this edition doesn't expose the feature at all # 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: if edition is not None and dify_config.DEPLOYMENT_EDITION not in edition:

View File

@ -4,7 +4,7 @@ from flask import request
from werkzeug.exceptions import Forbidden, NotFound, UnprocessableEntity from werkzeug.exceptions import Forbidden, NotFound, UnprocessableEntity
from configs import dify_config 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 controllers.openapi.auth.data import AuthData, CallerKind
from extensions.ext_database import db from extensions.ext_database import db
from libs.oauth_bearer import Scope, TokenType from libs.oauth_bearer import Scope, TokenType
@ -62,12 +62,10 @@ def check_rbac_permission(data: AuthData) -> None:
return return
if data.account_id is None or data.tenant is None: if data.account_id is None or data.tenant is None:
raise Forbidden("rbac context missing") raise Forbidden("rbac context missing")
enforce_rbac_access( enforce_rbac_checks(
tenant_id=str(data.tenant.id), tenant_id=str(data.tenant.id),
account_id=str(data.account_id), account_id=str(data.account_id),
resource_type=req.resource_type, checks=[req],
scene=req.scene,
resource_required=req.resource_required,
path_args=dict(data.path_params), path_args=dict(data.path_params),
) )

View File

@ -15,14 +15,14 @@ from flask_restx import Resource
from werkzeug.exceptions import BadRequest from werkzeug.exceptions import BadRequest
from controllers.common.human_input import HumanInputFormSubmitPayload, stringify_form_default_values 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.schema import register_schema_models
from controllers.common.wraps import RBACPermission, RBACResourceScope
from controllers.openapi import openapi_ns from controllers.openapi import openapi_ns
from controllers.openapi._contract import accepts, returns from controllers.openapi._contract import accepts, returns
from controllers.openapi._errors import HumanInputFormNotFound, RecipientSurfaceMismatch from controllers.openapi._errors import HumanInputFormNotFound, RecipientSurfaceMismatch
from controllers.openapi._models import FormSubmitResponse, HumanInputFormDefinitionResponse from controllers.openapi._models import FormSubmitResponse, HumanInputFormDefinitionResponse
from controllers.openapi.auth.composition import auth_router 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 ( from core.workflow.human_input_policy import (
HumanInputSurface, HumanInputSurface,
is_recipient_type_allowed_for_surface, is_recipient_type_allowed_for_surface,
@ -65,7 +65,7 @@ class OpenApiWorkflowHumanInputFormApi(Resource):
@openapi_ns.response(200, "Form definition", openapi_ns.models[HumanInputFormDefinitionResponse.__name__]) @openapi_ns.response(200, "Form definition", openapi_ns.models[HumanInputFormDefinitionResponse.__name__])
@auth_router.guard( @auth_router.guard(
scope=Scope.APPS_RUN, 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): def get(self, app_id: str, form_token: str, *, auth_data: AuthData):
app_model, _caller, _caller_kind = auth_data.require_app_context() app_model, _caller, _caller_kind = auth_data.require_app_context()
@ -84,7 +84,7 @@ class OpenApiWorkflowHumanInputFormApi(Resource):
class OpenApiWorkflowHumanInputFormSubmitApi(Resource): class OpenApiWorkflowHumanInputFormSubmitApi(Resource):
@auth_router.guard( @auth_router.guard(
scope=Scope.APPS_RUN, 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") @returns(200, FormSubmitResponse, description="Form submitted")
@accepts(body=HumanInputFormSubmitPayload) @accepts(body=HumanInputFormSubmitPayload)

View File

@ -18,11 +18,11 @@ from sqlalchemy.orm import sessionmaker
from werkzeug.exceptions import NotFound, UnprocessableEntity from werkzeug.exceptions import NotFound, UnprocessableEntity
from controllers.common.fields import EventStreamResponse 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.schema import query_params_from_model
from controllers.common.wraps import RBACPermission, RBACResourceScope
from controllers.openapi import openapi_ns from controllers.openapi import openapi_ns
from controllers.openapi.auth.composition import auth_router 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.advanced_chat.app_generator import AdvancedChatAppGenerator
from core.app.apps.base_app_generator import BaseAppGenerator from core.app.apps.base_app_generator import BaseAppGenerator
from core.app.apps.common.workflow_response_converter import WorkflowResponseConverter 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__]) @openapi_ns.response(200, "SSE event stream", openapi_ns.models[EventStreamResponse.__name__])
@auth_router.guard( @auth_router.guard(
scope=Scope.APPS_RUN, 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): def get(self, app_id: str, task_id: str, *, auth_data: AuthData):
app_model, caller, caller_kind = auth_data.require_app_context() app_model, caller, caller_kind = auth_data.require_app_context()

View File

@ -5,11 +5,12 @@ class RBACResourceScope(StrEnum):
"""Resource scopes accepted by the ``rbac_permission_required`` decorator. """Resource scopes accepted by the ``rbac_permission_required`` decorator.
``WORKSPACE`` denotes a workspace-level check that carries no concrete ``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" APP = "app"
DATASET = "dataset" DATASET = "dataset"
AGENT = "agent"
WORKSPACE = "workspace" WORKSPACE = "workspace"
@ -41,6 +42,19 @@ class RBACPermission(StrEnum):
APP_DELETE = "app_delete" APP_DELETE = "app_delete"
APP_ACCESS_CONFIG = "app_access_config" 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_PREVIEW = "dataset_preview"
DATASET_READONLY = "dataset_readonly" DATASET_READONLY = "dataset_readonly"
DATASET_EDIT = "dataset_edit" DATASET_EDIT = "dataset_edit"
@ -61,7 +75,6 @@ class RBACPermission(StrEnum):
WORKSPACE_ROLE_MANAGE = "workspace_role_manage" WORKSPACE_ROLE_MANAGE = "workspace_role_manage"
API_EXTENSION_MANAGE = "api_extension_manage" API_EXTENSION_MANAGE = "api_extension_manage"
CUSTOMIZATION_MANAGE = "customization_manage" CUSTOMIZATION_MANAGE = "customization_manage"
AGENT_MANAGE = "agent_manage"
SKILL_VIEW = "skill_view" SKILL_VIEW = "skill_view"
SKILL_EDIT = "skill_edit" SKILL_EDIT = "skill_edit"
SKILL_PUBLISH = "skill_publish" SKILL_PUBLISH = "skill_publish"
@ -83,3 +96,25 @@ class RBACPermission(StrEnum):
TOOL_MANAGE = "tool_manage" TOOL_MANAGE = "tool_manage"
MCP_MANAGE = "mcp_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,
}
)

View File

@ -27,6 +27,7 @@ def init_app(app: DifyApp):
import_migration_data, import_migration_data,
install_plugins, install_plugins,
install_rag_pipeline_plugins, install_rag_pipeline_plugins,
migrate_agent_permissions_to_rbac,
migrate_data_for_plugin, migrate_data_for_plugin,
migrate_dataset_permissions_to_rbac, migrate_dataset_permissions_to_rbac,
migrate_member_roles_to_rbac, migrate_member_roles_to_rbac,
@ -60,6 +61,7 @@ def init_app(app: DifyApp):
migrate_data_for_plugin, migrate_data_for_plugin,
migrate_dataset_permissions_to_rbac, migrate_dataset_permissions_to_rbac,
migrate_member_roles_to_rbac, migrate_member_roles_to_rbac,
migrate_agent_permissions_to_rbac,
backfill_plugin_auto_upgrade, backfill_plugin_auto_upgrade,
extract_plugins, extract_plugins,
extract_unique_plugins, extract_unique_plugins,

View File

@ -15,7 +15,7 @@ import sqlalchemy as sa
from flask import request from flask import request
from flask_login import UserMixin # type: ignore[import-untyped] from flask_login import UserMixin # type: ignore[import-untyped]
from sqlalchemy import BigInteger, Float, Index, PrimaryKeyConstraint, String, exists, func, select, text 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 configs import dify_config
from constants import DEFAULT_FILE_NUMBER_LIMITS from constants import DEFAULT_FILE_NUMBER_LIMITS
@ -508,7 +508,9 @@ class App(Base):
agent = self.agent_app_binding_with_session(session=session) agent = self.agent_app_binding_with_session(session=session)
return agent.id if agent else None 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. """For an Agent App (mode=agent), the Agent bound to it.
A roster Agent is bound through ``Agent.app_id``; a workflow-only Agent A roster Agent is bound through ``Agent.app_id``; a workflow-only Agent

View File

@ -2172,7 +2172,7 @@ Update annotation settings for an app
| Code | Description | | Code | Description |
| ---- | ----------- | | ---- | ----------- |
| 204 | Annotations deleted successfully | | 200 | Success |
### [GET] /apps/{app_id}/annotations ### [GET] /apps/{app_id}/annotations
Get annotations for an app with pagination Get annotations for an app with pagination
@ -2294,7 +2294,7 @@ Export all annotations for an app with CSV injection protection
| Code | Description | | Code | Description |
| ---- | ----------- | | ---- | ----------- |
| 204 | Annotation deleted successfully | | 200 | Success |
### [POST] /apps/{app_id}/annotations/{annotation_id} ### [POST] /apps/{app_id}/annotations/{annotation_id}
Update or delete an annotation Update or delete an annotation
@ -3324,9 +3324,9 @@ Create a new tracing configuration for an application
#### Responses #### Responses
| Code | Description | Schema | | Code | Description |
| ---- | ----------- | ------ | | ---- | ----------- |
| 200 | Success | **application/json**: [WorkflowTriggerResponse](#workflowtriggerresponse)<br> | | 200 | Success |
### [GET] /apps/{app_id}/triggers ### [GET] /apps/{app_id}/triggers
**Get app triggers list** **Get app triggers list**
@ -4713,7 +4713,7 @@ Full value for one declared output of a published run.
| Code | Description | | Code | Description |
| ---- | ----------- | | ---- | ----------- |
| 204 | Workflow deleted successfully | | 200 | Success |
### [PATCH] /apps/{app_id}/workflows/{workflow_id} ### [PATCH] /apps/{app_id}/workflows/{workflow_id}
**Update workflow attributes** **Update workflow attributes**
@ -4861,9 +4861,9 @@ Restore a published workflow version into the draft workflow
#### Responses #### Responses
| Code | Description | Schema | | Code | Description |
| ---- | ----------- | ------ | | ---- | ----------- |
| 200 | Success | **application/json**: [SimpleResultResponse](#simpleresultresponse)<br> | | 200 | Success |
### [POST] /auth/plugin/datasource/{provider_id}/custom-client ### [POST] /auth/plugin/datasource/{provider_id}/custom-client
#### Parameters #### Parameters
@ -5047,16 +5047,16 @@ Get compliance document download link
### [GET] /data-source/integrates ### [GET] /data-source/integrates
#### Responses #### Responses
| Code | Description | Schema | | Code | Description |
| ---- | ----------- | ------ | | ---- | ----------- |
| 200 | Success | **application/json**: [DataSourceIntegrateListResponse](#datasourceintegratelistresponse)<br> | | 200 | Success |
### [PATCH] /data-source/integrates ### [PATCH] /data-source/integrates
#### Responses #### Responses
| Code | Description | Schema | | Code | Description |
| ---- | ----------- | ------ | | ---- | ----------- |
| 200 | Success | **application/json**: [SimpleResultResponse](#simpleresultresponse)<br> | | 200 | Success |
### [GET] /data-source/integrates/{binding_id}/{action} ### [GET] /data-source/integrates/{binding_id}/{action}
#### Parameters #### Parameters
@ -5068,9 +5068,9 @@ Get compliance document download link
#### Responses #### Responses
| Code | Description | Schema | | Code | Description |
| ---- | ----------- | ------ | | ---- | ----------- |
| 200 | Success | **application/json**: [DataSourceIntegrateListResponse](#datasourceintegratelistresponse)<br> | | 200 | Success |
### [PATCH] /data-source/integrates/{binding_id}/{action} ### [PATCH] /data-source/integrates/{binding_id}/{action}
#### Parameters #### Parameters
@ -5082,9 +5082,9 @@ Get compliance document download link
#### Responses #### Responses
| Code | Description | Schema | | Code | Description |
| ---- | ----------- | ------ | | ---- | ----------- |
| 200 | Success | **application/json**: [SimpleResultResponse](#simpleresultresponse)<br> | | 200 | Success |
### [GET] /datasets ### [GET] /datasets
Get list of datasets Get list of datasets
@ -7869,17 +7869,11 @@ Update account-level Step-by-step Tour state
| ---- | ---------- | ----------- | -------- | ------ | | ---- | ---------- | ----------- | -------- | ------ |
| pipeline_id | path | | Yes | string (uuid) | | pipeline_id | path | | Yes | string (uuid) |
#### Request Body
| Required | Schema |
| -------- | ------ |
| Yes | **application/json**: [DraftWorkflowSyncPayload](#draftworkflowsyncpayload)<br> |
#### Responses #### Responses
| Code | Description | Schema | | Code | Description |
| ---- | ----------- | ------ | | ---- | ----------- |
| 200 | Success | **application/json**: [RagPipelineWorkflowSyncResponse](#ragpipelineworkflowsyncresponse)<br> | | 200 | Success |
### [POST] /rag/pipelines/{pipeline_id}/workflows/draft/datasource/nodes/{node_id}/run ### [POST] /rag/pipelines/{pipeline_id}/workflows/draft/datasource/nodes/{node_id}/run
**Run rag pipeline datasource** **Run rag pipeline datasource**
@ -8179,12 +8173,6 @@ Update account-level Step-by-step Tour state
| pipeline_id | path | | Yes | string (uuid) | | pipeline_id | path | | Yes | string (uuid) |
| variable_id | path | | Yes | string (uuid) | | variable_id | path | | Yes | string (uuid) |
#### Request Body
| Required | Schema |
| -------- | ------ |
| Yes | **application/json**: [WorkflowDraftVariablePatchPayload](#workflowdraftvariablepatchpayload)<br> |
#### Responses #### Responses
| Code | Description | Schema | | Code | Description | Schema |
@ -8359,12 +8347,6 @@ Update account-level Step-by-step Tour state
| pipeline_id | path | | Yes | string (uuid) | | pipeline_id | path | | Yes | string (uuid) |
| workflow_id | path | | Yes | string | | workflow_id | path | | Yes | string |
#### Request Body
| Required | Schema |
| -------- | ------ |
| Yes | **application/json**: [WorkflowUpdatePayload](#workflowupdatepayload)<br> |
#### Responses #### Responses
| Code | Description | Schema | | Code | Description | Schema |
@ -11159,16 +11141,16 @@ Returns permission flags that control workspace features like member invitations
### [GET] /workspaces/current/rbac/access-policies ### [GET] /workspaces/current/rbac/access-policies
#### Responses #### Responses
| Code | Description | Schema | | Code | Description |
| ---- | ----------- | ------ | | ---- | ----------- |
| 200 | Success | **application/json**: [_AccessPolicyList](#_accesspolicylist)<br> | | 200 | Success |
### [POST] /workspaces/current/rbac/access-policies ### [POST] /workspaces/current/rbac/access-policies
#### Responses #### Responses
| Code | Description | Schema | | Code | Description |
| ---- | ----------- | ------ | | ---- | ----------- |
| 201 | Policy created | **application/json**: [AccessPolicy](#accesspolicy)<br> | | 200 | Success |
### [DELETE] /workspaces/current/rbac/access-policies/{policy_id} ### [DELETE] /workspaces/current/rbac/access-policies/{policy_id}
#### Parameters #### Parameters
@ -11179,9 +11161,9 @@ Returns permission flags that control workspace features like member invitations
#### Responses #### Responses
| Code | Description | Schema | | Code | Description |
| ---- | ----------- | ------ | | ---- | ----------- |
| 200 | Success | **application/json**: [AccessPolicy](#accesspolicy)<br> | | 200 | Success |
### [GET] /workspaces/current/rbac/access-policies/{policy_id} ### [GET] /workspaces/current/rbac/access-policies/{policy_id}
#### Parameters #### Parameters
@ -11192,9 +11174,9 @@ Returns permission flags that control workspace features like member invitations
#### Responses #### Responses
| Code | Description | Schema | | Code | Description |
| ---- | ----------- | ------ | | ---- | ----------- |
| 200 | Success | **application/json**: [AccessPolicy](#accesspolicy)<br> | | 200 | Success |
### [PUT] /workspaces/current/rbac/access-policies/{policy_id} ### [PUT] /workspaces/current/rbac/access-policies/{policy_id}
#### Parameters #### Parameters
@ -11205,9 +11187,9 @@ Returns permission flags that control workspace features like member invitations
#### Responses #### Responses
| Code | Description | Schema | | Code | Description |
| ---- | ----------- | ------ | | ---- | ----------- |
| 200 | Success | **application/json**: [AccessPolicy](#accesspolicy)<br> | | 200 | Success |
### [POST] /workspaces/current/rbac/access-policies/{policy_id}/copy ### [POST] /workspaces/current/rbac/access-policies/{policy_id}/copy
#### Parameters #### Parameters
@ -11218,9 +11200,9 @@ Returns permission flags that control workspace features like member invitations
#### Responses #### Responses
| Code | Description | Schema | | Code | Description |
| ---- | ----------- | ------ | | ---- | ----------- |
| 201 | Policy copied | **application/json**: [AccessPolicy](#accesspolicy)<br> | | 200 | Success |
### [PUT] /workspaces/current/rbac/access-policy-bindings/{binding_id}/lock ### [PUT] /workspaces/current/rbac/access-policy-bindings/{binding_id}/lock
#### Parameters #### Parameters
@ -11231,9 +11213,9 @@ Returns permission flags that control workspace features like member invitations
#### Responses #### Responses
| Code | Description | Schema | | Code | Description |
| ---- | ----------- | ------ | | ---- | ----------- |
| 200 | Success | **application/json**: [AccessPolicyBindingState](#accesspolicybindingstate)<br> | | 200 | Success |
### [PUT] /workspaces/current/rbac/access-policy-bindings/{binding_id}/unlock ### [PUT] /workspaces/current/rbac/access-policy-bindings/{binding_id}/unlock
#### Parameters #### Parameters
@ -11244,9 +11226,153 @@ Returns permission flags that control workspace features like member invitations
#### Responses #### 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)<br> |
#### Responses
| Code | Description | Schema | | Code | Description | Schema |
| ---- | ----------- | ------ | | ---- | ----------- | ------ |
| 200 | Success | **application/json**: [AccessPolicyBindingState](#accesspolicybindingstate)<br> | | 200 | Success | **application/json**: [MemberBindingsResponse](#memberbindingsresponse)<br> |
### [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)<br> |
### [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)<br> |
### [GET] /workspaces/current/rbac/agents/{agent_id}/access-policy
#### Parameters
| Name | Located in | Description | Required | Schema |
| ---- | ---------- | ----------- | -------- | ------ |
| language | query | Localized policy label language | No | string, <br>**Available values:** "en", "ja", "zh" |
| agent_id | path | | Yes | string (uuid) |
#### Responses
| Code | Description | Schema |
| ---- | ----------- | ------ |
| 200 | Success | **application/json**: [AgentAccessMatrix](#agentaccessmatrix)<br> |
### [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, <br>**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)<br> |
### [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)<br> |
#### Responses
| Code | Description | Schema |
| ---- | ----------- | ------ |
| 200 | Success | **application/json**: [ReplaceUserAccessPoliciesResponse](#replaceuseraccesspoliciesresponse)<br> |
### [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)<br> |
### [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)<br> |
#### Responses
| Code | Description | Schema |
| ---- | ----------- | ------ |
| 200 | Success | **application/json**: [ResourceWhitelist](#resourcewhitelist)<br> |
### [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)<br> |
### [DELETE] /workspaces/current/rbac/apps/{app_id}/access-policies/{policy_id}/member-bindings ### [DELETE] /workspaces/current/rbac/apps/{app_id}/access-policies/{policy_id}/member-bindings
#### Parameters #### Parameters
@ -11582,6 +11708,13 @@ Returns permission flags that control workspace features like member invitations
| ---- | ----------- | ------ | | ---- | ----------- | ------ |
| 200 | Success | **application/json**: [PermissionCatalogResponse](#permissioncatalogresponse)<br> | | 200 | Success | **application/json**: [PermissionCatalogResponse](#permissioncatalogresponse)<br> |
### [GET] /workspaces/current/rbac/role-permissions/catalog/agent
#### Responses
| Code | Description | Schema |
| ---- | ----------- | ------ |
| 200 | Success | **application/json**: [PermissionCatalogResponse](#permissioncatalogresponse)<br> |
### [GET] /workspaces/current/rbac/role-permissions/catalog/app ### [GET] /workspaces/current/rbac/role-permissions/catalog/app
#### Responses #### Responses
@ -11599,16 +11732,16 @@ Returns permission flags that control workspace features like member invitations
### [GET] /workspaces/current/rbac/roles ### [GET] /workspaces/current/rbac/roles
#### Responses #### Responses
| Code | Description | Schema | | Code | Description |
| ---- | ----------- | ------ | | ---- | ----------- |
| 200 | Success | **application/json**: [_RBACRoleList](#_rbacrolelist)<br> | | 200 | Success |
### [POST] /workspaces/current/rbac/roles ### [POST] /workspaces/current/rbac/roles
#### Responses #### Responses
| Code | Description | Schema | | Code | Description |
| ---- | ----------- | ------ | | ---- | ----------- |
| 201 | Role created | **application/json**: [RBACRole](#rbacrole)<br> | | 200 | Success |
### [DELETE] /workspaces/current/rbac/roles/{role_id} ### [DELETE] /workspaces/current/rbac/roles/{role_id}
#### Parameters #### Parameters
@ -11619,9 +11752,9 @@ Returns permission flags that control workspace features like member invitations
#### Responses #### Responses
| Code | Description | Schema | | Code | Description |
| ---- | ----------- | ------ | | ---- | ----------- |
| 200 | Success | **application/json**: [RBACRole](#rbacrole)<br> | | 200 | Success |
### [GET] /workspaces/current/rbac/roles/{role_id} ### [GET] /workspaces/current/rbac/roles/{role_id}
#### Parameters #### Parameters
@ -11632,9 +11765,9 @@ Returns permission flags that control workspace features like member invitations
#### Responses #### Responses
| Code | Description | Schema | | Code | Description |
| ---- | ----------- | ------ | | ---- | ----------- |
| 200 | Success | **application/json**: [RBACRole](#rbacrole)<br> | | 200 | Success |
### [PUT] /workspaces/current/rbac/roles/{role_id} ### [PUT] /workspaces/current/rbac/roles/{role_id}
#### Parameters #### Parameters
@ -11645,9 +11778,9 @@ Returns permission flags that control workspace features like member invitations
#### Responses #### Responses
| Code | Description | Schema | | Code | Description |
| ---- | ----------- | ------ | | ---- | ----------- |
| 200 | Success | **application/json**: [RBACRole](#rbacrole)<br> | | 200 | Success |
### [POST] /workspaces/current/rbac/roles/{role_id}/copy ### [POST] /workspaces/current/rbac/roles/{role_id}/copy
#### Parameters #### Parameters
@ -11658,9 +11791,9 @@ Returns permission flags that control workspace features like member invitations
#### Responses #### Responses
| Code | Description | Schema | | Code | Description |
| ---- | ----------- | ------ | | ---- | ----------- |
| 201 | Role copied | **application/json**: [RBACRole](#rbacrole)<br> | | 200 | Success |
### [GET] /workspaces/current/rbac/roles/{role_id}/members ### [GET] /workspaces/current/rbac/roles/{role_id}/members
#### Parameters #### Parameters
@ -11675,6 +11808,58 @@ Returns permission flags that control workspace features like member invitations
| ---- | ----------- | ------ | | ---- | ----------- | ------ |
| 200 | Success | **application/json**: [_MembersInRoleList](#_membersinrolelist)<br> | | 200 | Success | **application/json**: [_MembersInRoleList](#_membersinrolelist)<br> |
### [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)<br> |
#### Responses
| Code | Description | Schema |
| ---- | ----------- | ------ |
| 200 | Success | **application/json**: [AccessMatrixItem](#accessmatrixitem)<br> |
### [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)<br> |
### [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)<br> |
### [GET] /workspaces/current/rbac/workspace/agents/access-policy
#### Responses
| Code | Description | Schema |
| ---- | ----------- | ------ |
| 200 | Success | **application/json**: [WorkspaceAccessMatrix](#workspaceaccessmatrix)<br> |
### [PUT] /workspaces/current/rbac/workspace/apps/access-policies/{policy_id}/bindings ### [PUT] /workspaces/current/rbac/workspace/apps/access-policies/{policy_id}/bindings
#### Parameters #### Parameters
@ -13401,6 +13586,13 @@ Model class for AI model.
| chat_prompt_config | object | | No | | chat_prompt_config | object | | No |
| completion_prompt_config | object | | No | | completion_prompt_config | object | | No |
#### AgentAccessMatrix
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| agent_id | string | | No |
| items | [ [AccessMatrixItem](#accessmatrixitem) ] | | No |
#### AgentApiAccessResponse #### AgentApiAccessResponse
| Name | Type | Description | Required | | Name | Type | Description | Required |
@ -19598,6 +19790,7 @@ Model with provider entity.
| Name | Type | Description | Required | | Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- | | ---- | ---- | ----------- | -------- |
| agent | [ResourcePermissionSnapshot](#resourcepermissionsnapshot) | | No |
| app | [ResourcePermissionSnapshot](#resourcepermissionsnapshot) | | No | | app | [ResourcePermissionSnapshot](#resourcepermissionsnapshot) | | No |
| dataset | [ResourcePermissionSnapshot](#resourcepermissionsnapshot) | | No | | dataset | [ResourcePermissionSnapshot](#resourcepermissionsnapshot) | | No |
| workspace | [WorkspacePermissionSnapshot](#workspacepermissionsnapshot) | | No | | workspace | [WorkspacePermissionSnapshot](#workspacepermissionsnapshot) | | No |

View File

@ -44,6 +44,7 @@ from services.agent.workspace_service import AgentWorkspaceNotFoundError, AgentW
from services.app_service import AppService, CreateAppParams from services.app_service import AppService, CreateAppParams
from services.enterprise.enterprise_service import EnterpriseService from services.enterprise.enterprise_service import EnterpriseService
from services.entities.agent_entities import RosterAgentCreatePayload, RosterAgentUpdatePayload 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 services.system_feature_service import SystemFeatureService
from tasks.collect_agent_resources_task import enqueue_agent_resource_collection from tasks.collect_agent_resources_task import enqueue_agent_resource_collection
@ -314,11 +315,13 @@ class AgentRosterService:
source=source, source=source,
) )
self._session.commit() self._session.commit()
return agent
except IntegrityError as exc: except IntegrityError as exc:
self._session.rollback() self._session.rollback()
raise AgentNameConflictError() from exc 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( def _create_roster_agent_in_transaction(
self, self,
*, *,

View File

@ -44,6 +44,7 @@ from graphon.nodes.question_classifier.entities import QuestionClassifierNodeDat
from graphon.nodes.tool.entities import ToolNodeData from graphon.nodes.tool.entities import ToolNodeData
from libs.datetime_utils import naive_utc_now from libs.datetime_utils import naive_utc_now
from models import Account, App, AppMode from models import Account, App, AppMode
from models.agent import AgentScope
from models.model import AppModelConfig, AppModelConfigDict, IconType, load_annotation_reply_config from models.model import AppModelConfig, AppModelConfigDict, IconType, load_annotation_reply_config
from models.workflow import Workflow from models.workflow import Workflow
from services.agent.dsl_service import AgentDslService, AgentPackage 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") raise NoPermissionError("You do not have permission to overwrite this app")
return app return app
@staticmethod def _ensure_agent_import_permission(self, account: Account, *, app: App | None) -> None:
def _ensure_agent_manage_permission(account: Account) -> None:
"""Importing an Agent DSL creates a roster Agent, which requires ``agent.manage``."""
if not dify_config.RBAC_ENABLED: if not dify_config.RBAC_ENABLED:
return return
if account.current_tenant_id is None: if account.current_tenant_id is None:
raise ValueError("Current tenant is not set") 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( allowed = RBACService.CheckAccess.check(
account.current_tenant_id, account.current_tenant_id,
account.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: 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( def _create_or_update_app(
self, self,
@ -492,7 +500,7 @@ class AppDslService:
raise ValueError("loss app mode") raise ValueError("loss app mode")
app_mode = AppMode(app_mode) app_mode = AppMode(app_mode)
if app_mode == AppMode.AGENT: if app_mode == AppMode.AGENT:
self._ensure_agent_manage_permission(account) self._ensure_agent_import_permission(account, app=app)
# Set icon type # Set icon type
icon_type_value = icon_type or app_data.get("icon_type") icon_type_value = icon_type or app_data.get("icon_type")

View File

@ -1,6 +1,6 @@
import json import json
import logging import logging
from collections.abc import Sequence from collections.abc import Callable, Sequence
from dataclasses import dataclass from dataclasses import dataclass
from datetime import datetime from datetime import datetime
from typing import Any, Literal, NotRequired, TypedDict, cast, override 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 import rbac_service as enterprise_rbac_service
from services.enterprise.enterprise_service import EnterpriseService from services.enterprise.enterprise_service import EnterpriseService
from services.openapi.visibility import apply_openapi_gate, is_openapi_visible 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.system_feature_service import SystemFeatureService
from services.tag_service import TagService from services.tag_service import TagService
from tasks.collect_agent_resources_task import enqueue_agent_resource_collection 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): class AppListBaseParams(BaseModel):
page: int = Field(default=1, ge=1) page: int = Field(default=1, ge=1)
limit: int = Field(default=20, ge=1, le=100) 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 # Created in the same transaction so the App and its backing Agent persist
# atomically; the Agent Soul (model/prompt/tools) is configured afterward # atomically; the Agent Soul (model/prompt/tools) is configured afterward
# in the Composer. # in the Composer.
backing_agent: Agent | None = None
if app_mode == AppMode.AGENT: if app_mode == AppMode.AGENT:
from services.agent.roster_service import AgentRosterService from services.agent.roster_service import AgentRosterService
icon_type = AgentIconType(params.icon_type) if params.icon_type else None icon_type = AgentIconType(params.icon_type) if params.icon_type else None
try: try:
AgentRosterService(session).create_backing_agent_for_app( backing_agent = AgentRosterService(session).create_backing_agent_for_app(
tenant_id=tenant_id, tenant_id=tenant_id,
account_id=account.id, account_id=account.id,
app_id=app.id, app_id=app.id,
@ -696,11 +730,14 @@ class AppService:
session.commit() session.commit()
app_was_created.send(app, account=account, session=session) app_was_created.send(app, account=account, session=session)
session.commit() session.commit()
enterprise_rbac_service.try_sync_creator_access_policy_member_bindings( initialize_access = _CREATED_APP_ACCESS_INITIALIZERS.get(app_mode, _initialize_created_app_access)
tenant_id, initialize_access(
account.id, _CreatedApp(
enterprise_rbac_service.RBACResourceType.APP, tenant_id=tenant_id,
app.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(): if SystemFeatureService.is_webapp_auth_enabled():

File diff suppressed because it is too large Load Diff

View File

@ -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,
)

View File

@ -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)

View File

@ -1,14 +1,15 @@
"""Initialize default RBAC access for existing workspace members after app creation.""" """Initialize default RBAC access for existing workspace members after app creation."""
import logging import logging
from collections.abc import Iterator from collections.abc import Callable, Iterator, Sequence
from dataclasses import dataclass
from celery import shared_task from celery import shared_task
from sqlalchemy import select from sqlalchemy import select
from configs import dify_config from configs import dify_config
from extensions.ext_database import db 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.account_service import TenantService
from services.enterprise import rbac_service as enterprise_rbac_service 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( def _iter_resource_config_batches(
tenant_id: str, tenant_id: str,
batch_size: int, batch_size: int,
) -> Iterator[list[enterprise_rbac_service.ResourceWhitelistConfigResource]]: ) -> Iterator[list[enterprise_rbac_service.ResourceWhitelistConfigResource]]:
last_app_id: str | None = None for kind in _WHITELIST_RESOURCE_KINDS:
while True: for ids in kind.iter_id_batches(tenant_id, batch_size):
stmt = select(App.id).where(App.tenant_id == tenant_id).order_by(App.id.asc()).limit(batch_size) yield [
if last_app_id: enterprise_rbac_service.ResourceWhitelistConfigResource(
stmt = stmt.where(App.id > last_app_id) resource_type=kind.resource_type, resource_id=resource_id
app_ids = [str(app_id) for app_id in db.session().scalars(stmt).all()] )
if not app_ids: for resource_id in 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]
def _chunks[T](items: list[T], chunk_size: int) -> Iterator[list[T]]: 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] 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) @shared_task(queue=APP_RBAC_QUEUE, bind=True, max_retries=3, default_retry_delay=60)
def initialize_created_app_rbac_access_task( 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: ) -> 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 member policies is idempotent, so retrying the whole synchronization is safe
when the enterprise RBAC service is temporarily unavailable. when the enterprise RBAC service is temporarily unavailable.
""" """
if not dify_config.RBAC_ENABLED: if not dify_config.RBAC_ENABLED:
return 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: try:
for account_ids in TenantService.iter_member_account_id_batches( for account_ids in TenantService.iter_member_account_id_batches(
tenant_id, tenant_id,
APP_RBAC_ACCOUNT_POLICY_BATCH_SIZE, APP_RBAC_ACCOUNT_POLICY_BATCH_SIZE,
session=db.session(), session=db.session(),
): ):
if app_id is not None: kind.replace_user_access_policies(
enterprise_rbac_service.RBACService.AppAccess.replace_user_access_policies( tenant_id,
tenant_id=tenant_id, account_id,
account_id=account_id, resource_id,
app_id=app_id, enterprise_rbac_service.ReplaceUserAccessPolicies(
target_account_id=None, access_policy_ids=[APP_RBAC_DEFAULT_ACCESS_POLICY_ID],
payload=enterprise_rbac_service.ReplaceUserAccessPolicies( account_ids=account_ids,
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,
),
)
except Exception as exc: except Exception as exc:
logger.exception( 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, tenant_id,
app_id, kind.resource_type.value,
resource_id,
self.request.retries + 1, self.request.retries + 1,
) )
raise self.retry(exc=exc) raise self.retry(exc=exc)
@ -146,8 +238,9 @@ def sync_joined_workspace_member_rbac_access_task(
) )
return return
app_ids: list[str] = [] auto_included: dict[enterprise_rbac_service.RBACResourceType, list[str]] = {
dataset_ids: 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): for resources in _iter_resource_config_batches(tenant_id, APP_RBAC_RESOURCE_CONFIG_BATCH_SIZE):
configs = enterprise_rbac_service.RBACService.ResourceWhitelistConfigs.batch_get( configs = enterprise_rbac_service.RBACService.ResourceWhitelistConfigs.batch_get(
tenant_id=tenant_id, tenant_id=tenant_id,
@ -157,38 +250,14 @@ def sync_joined_workspace_member_rbac_access_task(
for config in configs.data: for config in configs.data:
if not config.automatic_include_workspace_members: if not config.automatic_include_workspace_members:
continue continue
if config.resource_type == enterprise_rbac_service.RBACResourceType.APP: ids = auto_included.get(config.resource_type)
app_ids.append(config.resource_id) if ids is not None:
elif config.resource_type == enterprise_rbac_service.RBACResourceType.DATASET: ids.append(config.resource_id)
dataset_ids.append(config.resource_id)
for app_id_batch in _chunks(app_ids, APP_RBAC_MEMBER_APPEND_BATCH_SIZE): for resource_type, resource_ids in auto_included.items():
enterprise_rbac_service.RBACService.AppAccess.append_whitelist_members_batch( kind = _WHITELIST_RESOURCE_KIND_BY_TYPE[resource_type]
tenant_id=tenant_id, for id_batch in _chunks(resource_ids, APP_RBAC_MEMBER_APPEND_BATCH_SIZE):
account_id=actor_account_id, kind.append_member(tenant_id, actor_account_id, id_batch, member_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
],
)
except Exception as exc: except Exception as exc:
logger.exception( logger.exception(
"Failed to sync joined member RBAC access; retrying: tenant_id=%s member=%s attempt=%s", "Failed to sync joined member RBAC access; retrying: tenant_id=%s member=%s attempt=%s",

View File

@ -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

View File

@ -1,6 +1,6 @@
from __future__ import annotations from __future__ import annotations
from inspect import getclosurevars, unwrap from inspect import unwrap
from types import FunctionType, SimpleNamespace from types import FunctionType, SimpleNamespace
import pytest import pytest
@ -11,6 +11,7 @@ from controllers.console import agent_app_sandbox as module
from models.account import Account from models.account import Account
from models.model import App, AppMode, IconType from models.model import App, AppMode, IconType
from services.agent_app_sandbox_service import AgentSandboxDownload, AgentSandboxInfo, AgentSandboxInspectorError from services.agent_app_sandbox_service import AgentSandboxDownload, AgentSandboxInfo, AgentSandboxInspectorError
from tests.unit_tests.controllers.rbac_introspection import rbac_checks
class _AgentAppService: class _AgentAppService:
@ -155,12 +156,10 @@ def _account() -> Account:
], ],
) )
def test_sandbox_resources_require_app_view_layout(method: FunctionType) -> None: 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) [check] = rbac_checks(method)
config = getclosurevars(rbac_wrapper).nonlocals
assert config["resource_type"] == module.RBACResourceScope.APP assert isinstance(check.locator, module.PlainApp)
assert config["scene"] == module.RBACPermission.APP_VIEW_LAYOUT assert check.scene == module.RBACPermission.APP_VIEW_LAYOUT
assert config["resource_required"] is True
def test_handle_maps_sandbox_and_agent_backend_errors() -> None: def test_handle_maps_sandbox_and_agent_backend_errors() -> None:

View File

@ -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"}]

View File

@ -111,11 +111,11 @@ def test_agent_console_audio_api_uses_agent_draft(
calls["asr"] = kwargs calls["asr"] = kwargs
return {"text": "agent transcript"} return {"text": "agent transcript"}
def enforce_rbac_access(**kwargs): def enforce_rbac_checks(**kwargs):
calls["rbac"] = kwargs calls["rbac"] = kwargs
monkeypatch.setattr(audio_module, "resolve_agent_runtime_app_model", resolve_agent_runtime_app_model) 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(AgentComposerService, "load_agent_soul_for_debug", load_agent_soul_for_debug)
monkeypatch.setattr(AudioService, "transcript_agent_asr", transcript_agent_asr) 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 response == {"text": "agent transcript"}
assert calls["resolver"] == {"session": session, "tenant_id": "tenant-1", "agent_id": agent_id} assert calls["resolver"] == {"session": session, "tenant_id": "tenant-1", "agent_id": agent_id}
assert calls["rbac"] == { rbac_call = calls["rbac"]
"tenant_id": "tenant-1", assert rbac_call["tenant_id"] == "tenant-1"
"account_id": "account-1", assert rbac_call["account_id"] == "account-1"
"resource_type": audio_module.RBACResourceScope.APP, assert rbac_call["path_args"] == {"app_id": "backing-app-1"}
"scene": audio_module.RBACPermission.APP_TEST_AND_RUN, (rbac_check,) = rbac_call["checks"]
"path_args": {"app_id": "backing-app-1"}, assert rbac_check.scene is audio_module.RBACPermission.APP_TEST_AND_RUN
} assert isinstance(rbac_check.locator, audio_module.PlainApp)
assert calls["draft"] == { assert calls["draft"] == {
"tenant_id": "tenant-1", "tenant_id": "tenant-1",
"agent_id": str(agent_id), "agent_id": str(agent_id),
@ -216,7 +216,7 @@ def test_agent_console_audio_api_checks_rbac_with_backing_app_id(
soul_loaded = True soul_loaded = True
return AgentSoulConfig() 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) monkeypatch.setattr(AgentComposerService, "load_agent_soul_for_debug", load_agent_soul_for_debug)
api = AgentChatMessageAudioApi() api = AgentChatMessageAudioApi()

View File

@ -315,19 +315,20 @@ class TestAppMCPServerRefreshController:
"controllers.common.wraps.current_account_with_tenant", "controllers.common.wraps.current_account_with_tenant",
return_value=(current_user, "tenant-1"), 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( patch(
"controllers.common.wraps.enforce_rbac_access", "controllers.common.rbac.checks.RBACService.CheckAccess.check",
side_effect=PermissionCheckedError, side_effect=PermissionCheckedError,
) as enforce_rbac_access, ) as check_access,
pytest.raises(PermissionCheckedError), pytest.raises(PermissionCheckedError),
): ):
method(AppMCPServerRefreshController(), app_id="app-1") method(AppMCPServerRefreshController(), app_id="app-1")
enforce_rbac_access.assert_called_once_with( check_access.assert_called_once_with(
tenant_id="tenant-1", "tenant-1",
account_id="account-1", "account-1",
resource_type=RBACResourceScope.APP,
scene=RBACPermission.APP_EDIT, scene=RBACPermission.APP_EDIT,
resource_required=True, resource_type=RBACResourceScope.APP,
path_args={"app_id": "app-1"}, resource_id="app-1",
) )

View File

@ -11,6 +11,7 @@ import pytest
from flask import Flask from flask import Flask
from werkzeug.exceptions import Forbidden from werkzeug.exceptions import Forbidden
from controllers.common.rbac import PlainApp
from controllers.console import flask_admission from controllers.console import flask_admission
from controllers.console.app import ops_trace as ops_trace_module from controllers.console.app import ops_trace as ops_trace_module
from controllers.console.app.error import ( 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), lambda: AccountWithTenant(account=account, tenant_id=WORKSPACE_ID),
) )
denied = MagicMock(side_effect=Forbidden()) 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): with app.test_request_context(), pytest.raises(Forbidden):
_admission_injector(method)(None, app_id=UUID(APP_ID)) _admission_injector(method)(None, app_id=UUID(APP_ID))
denied.assert_called_once_with( denied.assert_called_once()
tenant_id=WORKSPACE_ID, call = denied.call_args.kwargs
account_id=ACCOUNT_ID, assert call["tenant_id"] == WORKSPACE_ID
resource_type=ops_trace_module.RBACResourceScope.APP, assert call["account_id"] == ACCOUNT_ID
scene=ops_trace_module.RBACPermission.APP_TRACING_CONFIG, assert call["path_args"] == {"app_id": UUID(APP_ID)}
resource_required=True, (check,) = call["checks"]
path_args={"app_id": UUID(APP_ID)}, 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: def test_trace_config_get_preserves_read_access_for_normal_member() -> None:

View File

@ -1,7 +1,7 @@
from __future__ import annotations from __future__ import annotations
from collections.abc import Iterator from collections.abc import Iterator
from inspect import getclosurevars, unwrap from inspect import unwrap
from unittest.mock import ANY, PropertyMock, patch from unittest.mock import ANY, PropertyMock, patch
import pytest 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.account import NoPermissionError
from services.errors.rag_pipeline import RagPipelineResourceNotFoundError from services.errors.rag_pipeline import RagPipelineResourceNotFoundError
from tests.unit_tests.config_override import config_overrides_context 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]: def _template_item() -> dict[str, object]:
@ -362,11 +363,8 @@ class TestCustomizedPipelineTemplateApi:
class TestPublishCustomizedPipelineTemplateApi: class TestPublishCustomizedPipelineTemplateApi:
def test_post_uses_pipeline_release_rbac_scene(self) -> None: def test_post_uses_pipeline_release_rbac_scene(self) -> None:
method = PublishCustomizedPipelineTemplateApi.post [check] = rbac_checks(PublishCustomizedPipelineTemplateApi.post)
while "scene" not in getclosurevars(method).nonlocals: assert check.scene == module.RBACPermission.DATASET_PIPELINE_RELEASE
method = method.__wrapped__
assert getclosurevars(method).nonlocals["scene"] == module.RBACPermission.DATASET_PIPELINE_RELEASE
def test_post_validates_payload_and_returns_empty_204(self) -> None: def test_post_validates_payload_and_returns_empty_204(self) -> None:
api = PublishCustomizedPipelineTemplateApi() api = PublishCustomizedPipelineTemplateApi()

View File

@ -1,5 +1,5 @@
from collections.abc import Callable, Iterator from collections.abc import Callable, Iterator
from inspect import getclosurevars, unwrap from inspect import unwrap
from types import SimpleNamespace from types import SimpleNamespace
from unittest.mock import MagicMock, patch from unittest.mock import MagicMock, patch
@ -9,6 +9,7 @@ from sqlalchemy import Engine
from sqlalchemy.orm import Session, scoped_session, sessionmaker from sqlalchemy.orm import Session, scoped_session, sessionmaker
from controllers.common.errors import InvalidArgumentError, NotFoundError from controllers.common.errors import InvalidArgumentError, NotFoundError
from controllers.common.rbac import RBACResourceScope
from controllers.console import console_ns from controllers.console import console_ns
from controllers.console.app.error import DraftWorkflowNotExist from controllers.console.app.error import DraftWorkflowNotExist
from controllers.console.datasets.rag_pipeline.rag_pipeline_draft_variable import ( 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, RagPipelineVariableResetApi,
WorkflowDraftVariablePatchPayload, 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.llm_environment_variable import LLMEnvironmentVariable
from core.workflow.variable_prefixes import SYSTEM_VARIABLE_NODE_ID from core.workflow.variable_prefixes import SYSTEM_VARIABLE_NODE_ID
from factories.variable_factory import build_segment 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.account import Account, TenantAccountRole
from models.dataset import Pipeline from models.dataset import Pipeline
from models.workflow import Workflow, WorkflowDraftVariable, WorkflowType from models.workflow import Workflow, WorkflowDraftVariable, WorkflowType
from tests.unit_tests.controllers.rbac_introspection import rbac_checks
@pytest.fixture @pytest.fixture
@ -102,12 +104,11 @@ def restx_config(app):
def test_rag_draft_variable_routes_require_dataset_edit_permission() -> None: def test_rag_draft_variable_routes_require_dataset_edit_permission() -> None:
route = RagPipelineVariableApi.get route = RagPipelineVariableApi.get
legacy_gate = unwrap(route, stop=lambda decorator: "edit_permission_required" in decorator.__code__.co_qualname) 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 assert "edit_permission_required" in legacy_gate.__code__.co_qualname
permissions = getclosurevars(rbac_gate).nonlocals
assert permissions["resource_type"] == RBACResourceScope.DATASET [check] = rbac_checks(route)
assert permissions["scene"] == RBACPermission.DATASET_EDIT assert check.locator.scope == RBACResourceScope.DATASET
assert check.scene == RBACPermission.DATASET_EDIT
class TestRagPipelineVariableCollectionApi: class TestRagPipelineVariableCollectionApi:

View File

@ -166,7 +166,7 @@ class TestTagListApi:
app.test_request_context("/"), app.test_request_context("/"),
config_overrides_context(RBAC_ENABLED=True), config_overrides_context(RBAC_ENABLED=True),
patch.object(module, "current_account_with_tenant", return_value=(owner, "tenant-1")), 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)( unwrap(TagListApi().post)(
TagListApi(), TagListApi(),
@ -174,13 +174,13 @@ class TestTagListApi:
request_context, request_context,
) )
enforce_rbac_access.assert_called_once_with( enforce_rbac_checks.assert_called_once()
tenant_id="tenant-1", rbac_kwargs = enforce_rbac_checks.call_args.kwargs
account_id="user-1", assert rbac_kwargs["tenant_id"] == "tenant-1"
resource_type=module.RBACResourceScope.WORKSPACE, assert rbac_kwargs["account_id"] == "user-1"
scene=module.RBACPermission.SNIPPETS_CREATE_AND_MODIFY, (rbac_check,) = rbac_kwargs["checks"]
resource_required=False, 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: def test_post_rejects_read_only_member(self, app: Flask, request_context: RequestContext) -> None:
readonly = _account(TenantAccountRole.NORMAL) readonly = _account(TenantAccountRole.NORMAL)
@ -249,7 +249,7 @@ class TestTagUpdateDeleteApi:
app.test_request_context("/"), app.test_request_context("/"),
config_overrides_context(RBAC_ENABLED=True), config_overrides_context(RBAC_ENABLED=True),
patch.object(module, "current_account_with_tenant", return_value=(owner, "tenant-1")), 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)( result, status = unwrap(TagUpdateDeleteApi().patch)(
TagUpdateDeleteApi(), TagUpdateDeleteApi(),
@ -258,13 +258,13 @@ class TestTagUpdateDeleteApi:
"tag-1", "tag-1",
) )
enforce_rbac_access.assert_called_once_with( enforce_rbac_checks.assert_called_once()
tenant_id="tenant-1", rbac_kwargs = enforce_rbac_checks.call_args.kwargs
account_id="user-1", assert rbac_kwargs["tenant_id"] == "tenant-1"
resource_type=module.RBACResourceScope.WORKSPACE, assert rbac_kwargs["account_id"] == "user-1"
scene=module.RBACPermission.SNIPPETS_CREATE_AND_MODIFY, (rbac_check,) = rbac_kwargs["checks"]
resource_required=False, 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")) tags_service.update_tag.assert_called_once_with(request_context, "tag-1", UpdateTagInput(name="Updated"))
assert status == 200 assert status == 200
assert result["binding_count"] == "3" assert result["binding_count"] == "3"
@ -345,12 +345,12 @@ class TestTagUpdateDeleteApi:
app.test_request_context("/"), app.test_request_context("/"),
config_overrides_context(RBAC_ENABLED=True), config_overrides_context(RBAC_ENABLED=True),
patch.object(module, "current_account_with_tenant", return_value=(owner, "tenant-1")), 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") unwrap(TagUpdateDeleteApi().delete)(TagUpdateDeleteApi(), request_context, "tag-1")
tags_service.get_tag_type.assert_called_once_with(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( def test_delete_does_not_authorize_tag_outside_current_workspace(
self, app: Flask, request_context: RequestContext, tags_service: MagicMock self, app: Flask, request_context: RequestContext, tags_service: MagicMock
@ -363,12 +363,12 @@ class TestTagUpdateDeleteApi:
app.test_request_context("/"), app.test_request_context("/"),
config_overrides_context(RBAC_ENABLED=True), config_overrides_context(RBAC_ENABLED=True),
patch.object(module, "current_account_with_tenant", return_value=(owner, "tenant-1")), 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): with pytest.raises(NotFound):
unwrap(TagUpdateDeleteApi().delete)(TagUpdateDeleteApi(), request_context, "tag-1") unwrap(TagUpdateDeleteApi().delete)(TagUpdateDeleteApi(), request_context, "tag-1")
enforce_rbac_access.assert_not_called() enforce_rbac_checks.assert_not_called()
class TestTagBindings: class TestTagBindings:

View File

@ -57,9 +57,9 @@ def _make_account(role: TenantAccountRole) -> Account:
return 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( app = App(
id="app-1", id=app_id,
tenant_id="tenant-1", tenant_id="tenant-1",
name="API key app", name="API key app",
mode=mode, mode=mode,
@ -260,15 +260,23 @@ def test_api_key_lists_require_matching_rbac_permission(config_overrides: Callab
cases = [ cases = [
( (
lambda: AppApiKeyListResource().get(resource_id=api_id), 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(), 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), 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("/"), app.test_request_context("/"),
patch("controllers.console.wraps.current_account_with_tenant", return_value=(account, "tenant-1")), 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.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, 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( with patch(
"controllers.common.wraps.enforce_rbac_access", "controllers.common.rbac.checks.RBACService.CheckAccess.check", return_value=False
side_effect=[None] * (len(expected_gates) - 1) + [Forbidden()], ) as check_access:
) as enforce_rbac_access:
with pytest.raises(Forbidden): with pytest.raises(Forbidden):
invoke() invoke()
assert [ check_access.assert_called_once_with(
(kwargs["resource_type"], kwargs["scene"], kwargs["resource_required"]) "tenant-1",
for _, kwargs in enforce_rbac_access.call_args_list account.id,
] == expected_gates scene=expected_kwargs["scene"],
resource_type=expected_kwargs["resource_type"],
resource_id=expected_kwargs["resource_id"],
)
get_api_key_list.assert_not_called() get_api_key_list.assert_not_called()

View File

@ -1,28 +1,27 @@
from inspect import getclosurevars, unwrap from inspect import unwrap
from types import FunctionType from types import FunctionType
import pytest 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.datasets.rag_pipeline.datasource_auth import DatasourceAuthOauthCustomClient
from controllers.console.workspace.tool_providers import ToolOAuthCustomClient from controllers.console.workspace.tool_providers import ToolOAuthCustomClient
from tests.unit_tests.controllers.rbac_introspection import rbac_checks
@pytest.mark.parametrize( @pytest.mark.parametrize(
("method", "legacy_gate", "resource_type"), ("method", "legacy_gate"),
[ [
(ToolOAuthCustomClient.delete, "is_admin_or_owner_required", RBACResourceScope.WORKSPACE), (ToolOAuthCustomClient.delete, "is_admin_or_owner_required"),
(DatasourceAuthOauthCustomClient.delete, "edit_permission_required", RBACResourceScope.DATASET), (DatasourceAuthOauthCustomClient.delete, "edit_permission_required"),
], ],
) )
def test_custom_oauth_client_delete_requires_management_permission( def test_custom_oauth_client_delete_requires_management_permission(method: FunctionType, legacy_gate: str) -> None:
method: FunctionType, legacy_gate: str, resource_type: RBACResourceScope
) -> None:
legacy_wrapper = unwrap(method, stop=lambda wrapper: legacy_gate in wrapper.__code__.co_qualname) legacy_wrapper = unwrap(method, stop=lambda wrapper: legacy_gate in wrapper.__code__.co_qualname)
assert legacy_gate in legacy_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) # Both routes pass resource_required=False, so their bundle resolves to a Workspace
rbac_config = getclosurevars(rbac_wrapper).nonlocals # locator regardless of the resource_type they declared (see _single_check).
assert rbac_config["resource_type"] == resource_type [check] = rbac_checks(method)
assert rbac_config["scene"] == RBACPermission.CREDENTIAL_MANAGE assert check.scene == RBACPermission.CREDENTIAL_MANAGE
assert rbac_config["resource_required"] is False assert isinstance(check.locator, Workspace)

View File

@ -1,9 +1,9 @@
from inspect import getclosurevars, unwrap from inspect import unwrap
from types import FunctionType from types import FunctionType
import pytest 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.composer import AgentComposerApi
from controllers.console.agent.roster import AgentAppApi from controllers.console.agent.roster import AgentAppApi
from controllers.console.datasets.data_source import DataSourceApi 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.model_providers import ModelProviderCredentialApi
from controllers.console.workspace.models import ModelProviderModelCredentialApi from controllers.console.workspace.models import ModelProviderModelCredentialApi
from controllers.console.workspace.tool_providers import ToolBuiltinProviderAddApi, ToolOAuthCustomClient from controllers.console.workspace.tool_providers import ToolBuiltinProviderAddApi, ToolOAuthCustomClient
from tests.unit_tests.controllers.rbac_introspection import rbac_checks
@pytest.mark.parametrize( @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) 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 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) [check] = rbac_checks(method)
rbac_config = getclosurevars(rbac_wrapper).nonlocals assert check.scene == permission
assert rbac_config["resource_type"] == RBACResourceScope.WORKSPACE assert isinstance(check.locator, Workspace)
assert rbac_config["scene"] == permission
assert rbac_config["resource_required"] is False
@pytest.mark.parametrize( @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) 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 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) [check] = rbac_checks(method)
rbac_config = getclosurevars(rbac_wrapper).nonlocals assert check.scene == RBACPermission.CREDENTIAL_MANAGE
assert rbac_config["resource_type"] == RBACResourceScope.WORKSPACE assert isinstance(check.locator, Workspace)
assert rbac_config["scene"] == RBACPermission.CREDENTIAL_MANAGE
assert rbac_config["resource_required"] is False
def test_tool_oauth_custom_client_get_requires_admin_and_rbac() -> None: 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) 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 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) [check] = rbac_checks(method)
rbac_config = getclosurevars(rbac_wrapper).nonlocals assert check.scene == RBACPermission.CREDENTIAL_MANAGE
assert rbac_config["resource_type"] == RBACResourceScope.WORKSPACE assert isinstance(check.locator, Workspace)
assert rbac_config["scene"] == RBACPermission.CREDENTIAL_MANAGE
assert rbac_config["resource_required"] is False
def test_datasource_auth_get_requires_edit_and_rbac() -> None: 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) 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 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) [check] = rbac_checks(method)
rbac_config = getclosurevars(rbac_wrapper).nonlocals assert check.scene == RBACPermission.CREDENTIAL_MANAGE
assert rbac_config["resource_type"] == RBACResourceScope.DATASET assert isinstance(check.locator, Workspace)
assert rbac_config["scene"] == RBACPermission.CREDENTIAL_MANAGE
assert rbac_config["resource_required"] is False
@pytest.mark.parametrize( @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: def test_agent_app_get_requires_rbac(method: FunctionType) -> None:
"""GET endpoints that return agent app details or composer state must enforce """GET endpoints that return agent app details or composer state must require
the same RBAC gates as their sibling PUT/DELETE methods.""" the agent preview scene on the agent resource, same as their sibling routes."""
rbac_wrapper = unwrap(method, stop=lambda wrapper: "rbac_permission_required" in wrapper.__code__.co_qualname) [check] = rbac_checks(method)
rbac_config = getclosurevars(rbac_wrapper).nonlocals assert check.scene == RBACPermission.AGENT_PREVIEW
assert rbac_config["resource_type"] == RBACResourceScope.WORKSPACE assert isinstance(check.locator, AgentId)
assert rbac_config["scene"] == RBACPermission.AGENT_MANAGE
assert rbac_config["resource_required"] is False

View File

@ -4,14 +4,14 @@ from typing import override
from unittest.mock import MagicMock, patch from unittest.mock import MagicMock, patch
import pytest import pytest
from flask import Flask, request from flask import Flask
from flask_login import LoginManager, UserMixin from flask_login import LoginManager, UserMixin
from pydantic import BaseModel from pydantic import BaseModel
from sqlalchemy import select from sqlalchemy import select
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from werkzeug.exceptions import HTTPException 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 api as console_api
from controllers.console import flask_admission from controllers.console import flask_admission
from controllers.console import wraps as wraps_module from controllers.console import wraps as wraps_module
@ -41,10 +41,10 @@ from controllers.console.wraps import (
from enums import DeploymentEdition from enums import DeploymentEdition
from libs.login import AccountWithTenant from libs.login import AccountWithTenant
from machinery.context import RequestContext from machinery.context import RequestContext
from machinery.errors import ActiveWorkspaceRequiredError, AdmissionConfigurationError from machinery.errors import ActiveWorkspaceRequiredError
from models import Account, DifySetup from models import Account, DifySetup
from models.account import AccountStatus, TenantAccountRole 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 services.entities.feature_entities import LicenseStatus
from tests.unit_tests.config_override import config_overrides_context 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_request_id", return_value="request-1"),
patch("controllers.console.flask_admission.get_trace_id", return_value=None), 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: class Handler:
@flask_admission.console_account_admission( @flask_admission.console_account_admission(
allowed_roles=frozenset({TenantAccountRole.ADMIN, TenantAccountRole.OWNER}), allowed_roles=frozenset({TenantAccountRole.ADMIN, TenantAccountRole.OWNER}),
rbac_resource_scope=RBACResourceScope.WORKSPACE, rbac_checks=[RBACCheck(RBACPermission.CREDENTIAL_CREATE, Workspace())],
rbac_permission=RBACPermission.CREDENTIAL_CREATE,
rbac_resource_required=False,
) )
def post(self, request_context: RequestContext): def post(self, request_context: RequestContext):
return request_context return request_context
@ -378,18 +376,14 @@ class TestCurrentContextInjection:
assert isinstance(result, RequestContext) assert isinstance(result, RequestContext)
assert result.active_workspace_id == "tenant-123" assert result.active_workspace_id == "tenant-123"
assert result.trace_id == "trace-1" assert result.trace_id == "trace-1"
enforce_rbac_access.assert_called_once_with( enforce_rbac_checks.assert_called_once()
tenant_id="tenant-123", call_kwargs = enforce_rbac_checks.call_args.kwargs
account_id=current_user.id, assert call_kwargs["tenant_id"] == "tenant-123"
resource_type=RBACResourceScope.WORKSPACE, assert call_kwargs["account_id"] == current_user.id
scene=RBACPermission.CREDENTIAL_CREATE, assert call_kwargs["path_args"] == {}
resource_required=False, (check,) = call_kwargs["checks"]
path_args={}, assert check.scene is RBACPermission.CREDENTIAL_CREATE
) assert isinstance(check.locator, Workspace)
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)
def test_console_account_admission_can_admit_uninitialized_accounts(self): def test_console_account_admission_can_admit_uninitialized_accounts(self):
current_user = make_account() current_user = make_account()
@ -501,67 +495,72 @@ class TestRbacPermissionRequired:
def test_resource_scoped_check_uses_resource_id(self): def test_resource_scoped_check_uses_resource_id(self):
current_user = make_account("account-1") 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): def protected_view(**kwargs):
return "ok" return "ok"
with ( with (
patch("controllers.common.wraps.current_account_with_tenant", return_value=(current_user, "tenant-1")), Flask(__name__).test_request_context("/"),
patch("controllers.common.wraps._extract_resource_id", return_value="app-123") as mock_extract, patch(
patch("controllers.common.wraps._is_resource_owned_by_current_user", return_value=False) as mock_owned, "controllers.common.wraps.current_account_with_tenant",
patch("controllers.common.wraps.RBACService.CheckAccess.check", return_value=True) as mock_check, 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" assert protected_view(app_id="app-123") == "ok"
mock_extract.assert_called_once_with(RBACResourceScope.APP, "tenant-1", {"app_id": "app-123"}) mock_binding.assert_called_once_with("tenant-1", "app-123")
mock_owned.assert_called_once_with("tenant-1", "account-1", "app", "app-123") mock_owner.assert_called_once()
mock_check.assert_called_once_with( mock_check.assert_called_once_with(
"tenant-1", "tenant-1",
"account-1", "account-1",
scene="app_delete", scene=RBACPermission.APP_DELETE,
resource_type="app", resource_type=RBACResourceScope.APP,
resource_id="app-123", resource_id="app-123",
) )
def test_workspace_scoped_check_skips_resource_id_extraction(self): def test_workspace_scoped_check_skips_resource_id_extraction(self):
current_user = make_account("account-2") current_user = make_account("account-2")
@rbac_permission_required( @rbac_permission_required(RBACCheck(RBACPermission.DATASET_CREATE_AND_MANAGEMENT, Workspace()))
RBACResourceScope.DATASET, RBACPermission.DATASET_CREATE_AND_MANAGEMENT, resource_required=False
)
def protected_view(): def protected_view():
return "ok" return "ok"
with ( with (
patch("controllers.common.wraps.current_account_with_tenant", return_value=(current_user, "tenant-2")), Flask(__name__).test_request_context("/"),
patch("controllers.common.wraps._extract_resource_id") as mock_extract, patch(
patch("controllers.common.wraps._is_resource_owned_by_current_user", return_value=False) as mock_owned, "controllers.common.wraps.current_account_with_tenant",
patch("controllers.common.wraps.RBACService.CheckAccess.check", return_value=True) as mock_check, 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" assert protected_view() == "ok"
mock_extract.assert_not_called() mock_owner.assert_not_called()
mock_owned.assert_not_called()
mock_check.assert_called_once_with( mock_check.assert_called_once_with(
"tenant-2", "tenant-2",
"account-2", "account-2",
scene="dataset_create_and_management", scene=RBACPermission.DATASET_CREATE_AND_MANAGEMENT,
resource_type="dataset", resource_type=None,
resource_id=None, resource_id=None,
) )
def test_workspace_scene_omits_resource_type(self): def test_workspace_scene_omits_resource_type(self):
current_user = make_account("account-3") current_user = make_account("account-3")
@rbac_permission_required( @rbac_permission_required(RBACCheck(RBACPermission.WORKSPACE_ROLE_MANAGE, Workspace()))
RBACResourceScope.WORKSPACE, RBACPermission.WORKSPACE_ROLE_MANAGE, resource_required=False
)
def protected_view(): def protected_view():
return "ok" return "ok"
with ( with (
patch("controllers.common.wraps.current_account_with_tenant", return_value=(current_user, "tenant-3")), patch(
patch("controllers.common.wraps.RBACService.CheckAccess.check", return_value=True) as mock_check, "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" assert protected_view() == "ok"
@ -576,137 +575,46 @@ class TestRbacPermissionRequired:
def test_resource_owned_app_skips_rbac_check(self): def test_resource_owned_app_skips_rbac_check(self):
current_user = make_account("account-4") 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): def protected_view(**kwargs):
return "ok" return "ok"
with ( with (
patch("controllers.common.wraps.current_account_with_tenant", return_value=(current_user, "tenant-4")), Flask(__name__).test_request_context("/"),
patch("controllers.common.wraps._extract_resource_id", return_value="app-123"), patch(
patch("controllers.common.wraps._is_resource_owned_by_current_user", return_value=True) as mock_owned, "controllers.common.wraps.current_account_with_tenant",
patch("controllers.common.wraps.RBACService.CheckAccess.check") as mock_check, 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" 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() mock_check.assert_not_called()
def test_resource_owned_dataset_skips_rbac_check(self): def test_resource_owned_dataset_skips_rbac_check(self):
current_user = make_account("account-5") 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): def protected_view(**kwargs):
return "ok" return "ok"
with ( with (
patch("controllers.common.wraps.current_account_with_tenant", return_value=(current_user, "tenant-5")), Flask(__name__).test_request_context("/"),
patch("controllers.common.wraps._extract_resource_id", return_value="dataset-123"), patch(
patch("controllers.common.wraps._is_resource_owned_by_current_user", return_value=True) as mock_owned, "controllers.common.wraps.current_account_with_tenant",
patch("controllers.common.wraps.RBACService.CheckAccess.check") as mock_check, 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" 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() 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): def test_legacy_admin_decorator_noops_when_rbac_enabled(self):
@is_admin_or_owner_required @is_admin_or_owner_required
def protected_view(): def protected_view():

View File

@ -6,6 +6,7 @@ from unittest.mock import patch
import pytest import pytest
from flask import Flask from flask import Flask
from controllers.common.rbac import Workspace
from controllers.console import console_ns from controllers.console import console_ns
from controllers.console.workspace.endpoint import ( from controllers.console.workspace.endpoint import (
DeprecatedEndpointCreateApi, DeprecatedEndpointCreateApi,
@ -24,10 +25,11 @@ from controllers.console.workspace.endpoint import (
EndpointUpdatePayload, EndpointUpdatePayload,
LegacyEndpointUpdatePayload, LegacyEndpointUpdatePayload,
) )
from controllers.console.wraps import RBACPermission, RBACResourceScope from controllers.console.wraps import RBACPermission
from core.entities.provider_entities import ProviderConfig, ProviderConfigType from core.entities.provider_entities import ProviderConfig, ProviderConfigType
from core.plugin.entities.endpoint import EndpointEntityWithInstance, EndpointProviderDeclaration from core.plugin.entities.endpoint import EndpointEntityWithInstance, EndpointProviderDeclaration
from core.plugin.impl.exc import PluginPermissionDeniedError from core.plugin.impl.exc import PluginPermissionDeniedError
from tests.unit_tests.controllers.rbac_introspection import rbac_checks
def _endpoint_entity() -> EndpointEntityWithInstance: 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 assert "is_admin_or_owner_required" in legacy_wrapper.__code__.co_qualname
rbac_wrapper = inspect.unwrap( [check] = rbac_checks(method)
method, stop=lambda wrapper: "rbac_permission_required" in wrapper.__code__.co_qualname assert check.scene == RBACPermission.PLUGIN_MODEL_CONFIG
) assert isinstance(check.locator, Workspace)
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
class TestEndpointCollectionApi: class TestEndpointCollectionApi:

View File

@ -85,6 +85,25 @@ class TestMyPermissions:
assert response["app"]["default_permission_keys"] == ["app.acl.deploy"] assert response["app"]["default_permission_keys"] == ["app.acl.deploy"]
mock_get.assert_called_once() 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: class TestAccessMatrixAccountNames:
def test_hydrates_missing_account_names(self): def test_hydrates_missing_account_names(self):
@ -332,172 +351,6 @@ class TestPaginationMapping:
assert options.reverse is True 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: class TestPaginationForwarding:
def test_access_policies_get_forwards_outer_pagination_params(self, app): def test_access_policies_get_forwards_outer_pagination_params(self, app):
with ( with (
@ -517,88 +370,6 @@ class TestPaginationForwarding:
assert options.results_per_page == 25 assert options.results_per_page == 25
assert options.reverse is False 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: class TestAccessPolicyBindingLockUnlock:
def test_lock_forwards_binding_id(self, app): def test_lock_forwards_binding_id(self, app):
@ -653,7 +424,7 @@ class TestWorkspaceRbacGuards:
"controllers.common.wraps.current_account_with_tenant", "controllers.common.wraps.current_account_with_tenant",
return_value=(_account(), "tenant-1"), 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, patch("controllers.console.workspace.rbac.svc.RBACService.Roles.create") as mock_create,
): ):
with pytest.raises(Forbidden): with pytest.raises(Forbidden):
@ -672,7 +443,7 @@ class TestWorkspaceRbacGuards:
"controllers.common.wraps.current_account_with_tenant", "controllers.common.wraps.current_account_with_tenant",
return_value=(_account(), "tenant-1"), 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, patch("controllers.console.workspace.rbac.svc.RBACService.AccessPolicies.create") as mock_create,
): ):
with pytest.raises(Forbidden): with pytest.raises(Forbidden):

View File

@ -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)}/<uuid:{_id_param(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/<uuid:target_account_id>/access-policies": (
f"RBAC{prefix}UserAccessPolicyAssignmentApi",
{"PUT"},
),
f"{resource}/access-policies/<uuid:policy_id>/role-bindings": (f"RBAC{prefix}RoleBindingsApi", {"GET"}),
f"{resource}/access-policies/<string:policy_id>/member-bindings": (
f"RBAC{prefix}MemberBindingsApi",
{"GET", "DELETE"},
),
f"{workspace}/access-policy": (f"RBACWorkspace{prefix}MatrixApi", {"GET"}),
f"{workspace}/access-policies/<uuid:policy_id>/role-bindings": (
f"RBACWorkspace{prefix}RoleBindingsApi",
{"GET"},
),
f"{workspace}/access-policies/<uuid:policy_id>/bindings": (f"RBACWorkspace{prefix}BindingsApi", {"PUT"}),
f"{workspace}/access-policies/<uuid:policy_id>/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()

View File

@ -1,7 +1,8 @@
import uuid 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.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.flow import When
from controllers.openapi.auth.pipeline import AuthPipeline, PipelineRoute, PipelineRouter from controllers.openapi.auth.pipeline import AuthPipeline, PipelineRoute, PipelineRouter
from controllers.openapi.auth.verify import ( from controllers.openapi.auth.verify import (
@ -12,7 +13,7 @@ from controllers.openapi.auth.verify import (
check_workspace_mismatch, check_workspace_mismatch,
check_workspace_role, check_workspace_role,
) )
from core.rbac import RBACPermission, RBACResourceScope from core.rbac import RBACPermission
from enums import DeploymentEdition from enums import DeploymentEdition
from libs.oauth_bearer import Scope, TokenType from libs.oauth_bearer import Scope, TokenType
from models.account import TenantAccountRole 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(): 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) assert check_rbac_permission in _selected_auth_steps_with_rbac(rbac)

View File

@ -1,5 +1,6 @@
from unittest.mock import patch from unittest.mock import patch
from controllers.common.rbac import PlainApp, RBACCheck
from controllers.openapi.auth.conditions import ( from controllers.openapi.auth.conditions import (
EDITION_CLOUD, EDITION_CLOUD,
EDITION_COMMUNITY, EDITION_COMMUNITY,
@ -18,8 +19,8 @@ from controllers.openapi.auth.conditions import (
data_cond, data_cond,
request_cond, request_cond,
) )
from controllers.openapi.auth.data import AuthData, RBACRequirement, RequestContext from controllers.openapi.auth.data import AuthData, RequestContext
from core.rbac import RBACPermission, RBACResourceScope from core.rbac import RBACPermission
from enums import DeploymentEdition from enums import DeploymentEdition
from libs.oauth_bearer import Scope, TokenType from libs.oauth_bearer import Scope, TokenType
from models.account import TenantAccountRole from models.account import TenantAccountRole
@ -153,7 +154,7 @@ def test_webapp_run_scoped_false_when_scope_none():
def _rbac_req(): 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(): def test_has_rbac_true():

View File

@ -6,7 +6,8 @@ import pytest
from flask import Flask from flask import Flask
from werkzeug.exceptions import Forbidden, NotFound 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 ( from controllers.openapi.auth.verify import (
check_acl, check_acl,
check_app_access, check_app_access,
@ -18,7 +19,7 @@ from controllers.openapi.auth.verify import (
check_workspace_mismatch, check_workspace_mismatch,
check_workspace_role, check_workspace_role,
) )
from core.rbac import RBACPermission, RBACResourceScope from core.rbac import RBACPermission
from libs.oauth_bearer import Scope, TokenType from libs.oauth_bearer import Scope, TokenType
from models.account import Tenant, TenantAccountRole from models.account import Tenant, TenantAccountRole
from models.model import App from models.model import App
@ -85,25 +86,25 @@ def test_check_app_access_raises_when_not_member():
# --- check_rbac_permission --- # --- 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(): 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")) check_rbac_permission(_data(rbac=None, caller_kind="account"))
mock_enforce.assert_not_called() mock_enforce.assert_not_called()
def test_check_rbac_noop_when_rbac_disabled(config_overrides: Callable[..., None]): def test_check_rbac_noop_when_rbac_disabled(config_overrides: Callable[..., None]):
config_overrides(RBAC_ENABLED=False) 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")) check_rbac_permission(_data(rbac=_RBAC_REQ, caller_kind="account"))
mock_enforce.assert_not_called() mock_enforce.assert_not_called()
def test_check_rbac_skips_end_user_caller(): def test_check_rbac_skips_end_user_caller():
with ( 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")) check_rbac_permission(_data(rbac=_RBAC_REQ, caller_kind="end_user"))
mock_enforce.assert_not_called() mock_enforce.assert_not_called()
@ -126,17 +127,18 @@ def test_check_rbac_enforces_for_account_caller():
path_params={"app_id": "app-1"}, path_params={"app_id": "app-1"},
) )
with ( 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) check_rbac_permission(data)
mock_enforce.assert_called_once_with( mock_enforce.assert_called_once()
tenant_id="t1", call_kwargs = mock_enforce.call_args.kwargs
account_id=str(account_id), assert call_kwargs["tenant_id"] == "t1"
resource_type=RBACResourceScope.APP, assert call_kwargs["account_id"] == str(account_id)
scene=RBACPermission.APP_VIEW_LAYOUT, assert call_kwargs["path_args"] == {"app_id": "app-1"}
resource_required=True, (check,) = call_kwargs["checks"]
path_args={"app_id": "app-1"}, 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(): def test_check_acl_raises_when_app_or_mode_missing():

Some files were not shown because too many files have changed in this diff Show More