mirror of
https://github.com/langgenius/dify.git
synced 2026-06-22 19:21:13 +08:00
feat: RBAC (#37107)
Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: fatelei <fatelei@gmail.com> Co-authored-by: Copilot <198982749+Copilot@users.noreply.github.com> Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> Co-authored-by: 盐粒 Yanli <yanli@dify.ai> Co-authored-by: Charles Yao <chongbinyao33@gmail.com> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: yunlu.wen <yunlu.wen@dify.ai> Co-authored-by: yyh <92089059+lyzno1@users.noreply.github.com> Co-authored-by: Jingyi <jingyi.qi@dify.ai> Co-authored-by: yyh <yuanyouhuilyz@gmail.com> Co-authored-by: Joel <iamjoel007@gmail.com> Co-authored-by: hjlarry <hjlarry@163.com> Co-authored-by: Asuka Minato <i@asukaminato.eu.org> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Xiyuan Chen <52963600+GareArc@users.noreply.github.com> Co-authored-by: gigglewang <gigglewang@dify.ai> Co-authored-by: chariri <w@chariri.moe> Co-authored-by: Evan <2869018789@qq.com> Co-authored-by: zyssyz123 <916125788@qq.com>
This commit is contained in:
parent
8732d1463a
commit
33edf97f81
1
.github/workflows/build-push.yml
vendored
1
.github/workflows/build-push.yml
vendored
@ -9,6 +9,7 @@ on:
|
||||
- "release/e-*"
|
||||
- "hotfix/**"
|
||||
- "feat/hitl-backend"
|
||||
- "feat/rbac"
|
||||
tags:
|
||||
- "*"
|
||||
|
||||
|
||||
@ -22,6 +22,7 @@ from .plugin import (
|
||||
setup_system_trigger_oauth_client,
|
||||
transform_datasource_credentials,
|
||||
)
|
||||
from .rbac import migrate_member_roles_to_rbac
|
||||
from .retention import (
|
||||
archive_workflow_runs,
|
||||
clean_expired_messages,
|
||||
@ -74,6 +75,7 @@ __all__ = [
|
||||
"migrate_annotation_vector_database",
|
||||
"migrate_data_for_plugin",
|
||||
"migrate_knowledge_vector_database",
|
||||
"migrate_member_roles_to_rbac",
|
||||
"migrate_oss",
|
||||
"migration_data_wizard",
|
||||
"old_metadata_migration",
|
||||
|
||||
112
api/commands/rbac.py
Normal file
112
api/commands/rbac.py
Normal file
@ -0,0 +1,112 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import click
|
||||
from sqlalchemy import select
|
||||
|
||||
from core.db.session_factory import session_factory
|
||||
from models import TenantAccountJoin, TenantAccountRole
|
||||
from services.enterprise.rbac_service import ListOption, RBACService
|
||||
|
||||
|
||||
def _resolve_builtin_role_id(tenant_id: str, operator_account_id: str, legacy_role: str) -> str:
|
||||
"""Resolve a legacy workspace role to the current tenant's builtin RBAC role id.
|
||||
|
||||
The migration replays the old `TenantAccountJoin.role` values onto the
|
||||
RBAC member-role binding API. Builtin RBAC roles are tenant-scoped and
|
||||
identified by runtime ids, so the command must look them up per tenant.
|
||||
"""
|
||||
expected_builtin_tag = {
|
||||
TenantAccountRole.OWNER.value: "owner",
|
||||
TenantAccountRole.ADMIN.value: "admin",
|
||||
TenantAccountRole.EDITOR.value: "editor",
|
||||
TenantAccountRole.NORMAL.value: "normal",
|
||||
TenantAccountRole.DATASET_OPERATOR.value: "dataset_operator",
|
||||
}.get(legacy_role)
|
||||
if not expected_builtin_tag:
|
||||
raise ValueError(f"Unsupported legacy workspace role: {legacy_role}")
|
||||
|
||||
roles = RBACService.Roles.list(
|
||||
tenant_id=tenant_id,
|
||||
account_id=operator_account_id,
|
||||
options=ListOption(page_number=1, results_per_page=100),
|
||||
).data
|
||||
for role in roles:
|
||||
if role.is_builtin and role.category == "global_system_default" and role.role_tag == expected_builtin_tag:
|
||||
return str(role.id)
|
||||
|
||||
raise ValueError(f"Builtin RBAC role not found for tenant={tenant_id}, legacy_role={legacy_role}")
|
||||
|
||||
|
||||
@click.command(
|
||||
"rbac-migrate-member-roles", help="Migrate legacy workspace member roles into RBAC member-role bindings."
|
||||
)
|
||||
@click.option("--tenant-id", help="Only migrate a single workspace.")
|
||||
@click.option("--dry-run", is_flag=True, default=False, help="Preview the migration without writing RBAC bindings.")
|
||||
def migrate_member_roles_to_rbac(tenant_id: str | None, dry_run: bool) -> None:
|
||||
"""Backfill RBAC member-role bindings from legacy `TenantAccountJoin.role` data.
|
||||
|
||||
This is an offline migration command for workspaces that already have
|
||||
members in the legacy role model but need matching records in the RBAC
|
||||
member-role binding store.
|
||||
"""
|
||||
click.echo(click.style("Starting RBAC member-role migration.", fg="green"))
|
||||
|
||||
with session_factory.create_session() as session:
|
||||
stmt = select(TenantAccountJoin).order_by(TenantAccountJoin.tenant_id.asc(), TenantAccountJoin.id.asc())
|
||||
if tenant_id:
|
||||
stmt = stmt.where(TenantAccountJoin.tenant_id == tenant_id)
|
||||
|
||||
joins = list(session.scalars(stmt).all())
|
||||
|
||||
if not joins:
|
||||
click.echo(click.style("No workspace members found for migration.", fg="yellow"))
|
||||
return
|
||||
|
||||
owner_account_by_tenant: dict[str, str] = {}
|
||||
resolved_role_ids: dict[tuple[str, str], str] = {}
|
||||
migrated_count = 0
|
||||
|
||||
for join in joins:
|
||||
workspace_id = str(join.tenant_id)
|
||||
member_account_id = str(join.account_id)
|
||||
legacy_role = str(join.role)
|
||||
|
||||
if workspace_id not in owner_account_by_tenant:
|
||||
owner_join = next(
|
||||
(
|
||||
item
|
||||
for item in joins
|
||||
if str(item.tenant_id) == workspace_id and str(item.role) == TenantAccountRole.OWNER.value
|
||||
),
|
||||
None,
|
||||
)
|
||||
if not owner_join:
|
||||
raise ValueError(f"Workspace owner not found for tenant={workspace_id}")
|
||||
owner_account_by_tenant[workspace_id] = str(owner_join.account_id)
|
||||
|
||||
operator_account_id = owner_account_by_tenant[workspace_id]
|
||||
cache_key = (workspace_id, legacy_role)
|
||||
if cache_key not in resolved_role_ids:
|
||||
resolved_role_ids[cache_key] = _resolve_builtin_role_id(workspace_id, operator_account_id, legacy_role)
|
||||
|
||||
resolved_role_id = resolved_role_ids[cache_key]
|
||||
click.echo(
|
||||
f"tenant={workspace_id} member={member_account_id} "
|
||||
f"legacy_role={legacy_role} -> rbac_role_id={resolved_role_id}"
|
||||
)
|
||||
|
||||
if dry_run:
|
||||
continue
|
||||
|
||||
RBACService.MemberRoles.replace(
|
||||
tenant_id=workspace_id,
|
||||
account_id=operator_account_id,
|
||||
member_account_id=member_account_id,
|
||||
role_ids=[resolved_role_id],
|
||||
)
|
||||
migrated_count += 1
|
||||
|
||||
if dry_run:
|
||||
click.echo(click.style("Dry run completed. No RBAC bindings were written.", fg="yellow"))
|
||||
else:
|
||||
click.echo(click.style(f"RBAC member-role migration completed. Migrated {migrated_count} members.", fg="green"))
|
||||
@ -29,6 +29,11 @@ class EnterpriseFeatureConfig(BaseSettings):
|
||||
"This helps gain runtime performance by trading off consistency.",
|
||||
)
|
||||
|
||||
RBAC_ENABLED: bool = Field(
|
||||
description="Enable enterprise RBAC APIs. When disabled, compatibility responses fall back to legacy roles.",
|
||||
default=False,
|
||||
)
|
||||
|
||||
|
||||
class EnterpriseTelemetryConfig(BaseSettings):
|
||||
"""
|
||||
|
||||
@ -153,6 +153,7 @@ class ApiBaseUrlResponse(ResponseModel):
|
||||
|
||||
class NewAppResponse(ResponseModel):
|
||||
new_app_id: str
|
||||
permission_keys: list[str] = Field(default_factory=list)
|
||||
|
||||
|
||||
class Parameters(BaseModel):
|
||||
|
||||
@ -1,7 +1,36 @@
|
||||
"""Shared decorator utilities for Dify controller layers.
|
||||
|
||||
This module provides decorators that are not tied to any single API group (e.g.
|
||||
console, inner, service). Currently it exposes the RBAC permission gate, which
|
||||
can be applied to any blueprint.
|
||||
|
||||
Key exports
|
||||
-----------
|
||||
``rbac_permission_required`` – decorator that enforces enterprise RBAC access
|
||||
control. When ``RBAC_ENABLED`` is ``False`` it is a no-op.
|
||||
|
||||
``RBACPermission``, ``RBACResourceScope`` – re-exported from ``core.rbac`` so
|
||||
callers only need a single import site.
|
||||
|
||||
Private helpers
|
||||
---------------
|
||||
``_extract_resource_id``, ``_is_resource_owned_by_current_user`` – kept module-
|
||||
private but accessible via the module namespace for unit-test patching.
|
||||
"""
|
||||
|
||||
from collections.abc import Callable
|
||||
from functools import wraps
|
||||
|
||||
from sqlalchemy import select
|
||||
from werkzeug.exceptions import Forbidden, NotFound
|
||||
|
||||
from configs import dify_config
|
||||
from core.rbac import RBACPermission, RBACResourceScope
|
||||
from extensions.ext_database import db
|
||||
from libs.login import current_account_with_tenant
|
||||
from models.dataset import Dataset
|
||||
from models.model import App
|
||||
from services.enterprise.rbac_service import RBACService
|
||||
|
||||
__all__ = ["RBACPermission", "RBACResourceScope", "rbac_permission_required"]
|
||||
|
||||
@ -14,17 +43,105 @@ def rbac_permission_required[**P, R](
|
||||
) -> Callable[[Callable[P, R]], Callable[P, R]]:
|
||||
"""Check enterprise RBAC permissions for the current user.
|
||||
|
||||
When ``RBAC_ENABLED`` is ``False`` the decorator is a no-op and the
|
||||
request passes through unchanged. When enabled it extracts the resource ID
|
||||
from ``request.view_args`` for resource-scoped checks, calls the RBAC
|
||||
service ``check-access`` endpoint, and raises ``Forbidden`` if the access
|
||||
is denied. For workspace-level checks, set ``resource_required=False`` so
|
||||
the RBAC request omits ``resource_id``.
|
||||
|
||||
Args:
|
||||
resource_type: The :class:`RBACResourceScope` member (app/dataset/workspace).
|
||||
scene: The :class:`RBACPermission` permission point.
|
||||
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]:
|
||||
@wraps(view)
|
||||
def decorated(*args: P.args, **kwargs: P.kwargs) -> R:
|
||||
if not dify_config.RBAC_ENABLED:
|
||||
return view(*args, **kwargs)
|
||||
|
||||
current_user, current_tenant_id = current_account_with_tenant()
|
||||
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, kwargs)
|
||||
if _is_resource_owned_by_current_user(current_tenant_id, current_user.id, resource_type, resource_id):
|
||||
return view(*args, **kwargs)
|
||||
allowed = RBACService.CheckAccess.check(
|
||||
current_tenant_id,
|
||||
current_user.id,
|
||||
scene=scene,
|
||||
resource_type=check_resource_type,
|
||||
resource_id=resource_id,
|
||||
)
|
||||
|
||||
if not allowed:
|
||||
raise Forbidden()
|
||||
|
||||
return view(*args, **kwargs)
|
||||
|
||||
return decorated
|
||||
|
||||
return decorator
|
||||
|
||||
|
||||
def _is_resource_owned_by_current_user(
|
||||
tenant_id: str, account_id: str, resource_type: RBACResourceScope, resource_id: str
|
||||
) -> bool:
|
||||
if resource_type == RBACResourceScope.APP:
|
||||
maintainer = db.session.scalar(
|
||||
select(App.maintainer).where(
|
||||
App.id == resource_id,
|
||||
App.tenant_id == tenant_id,
|
||||
App.status == "normal",
|
||||
)
|
||||
)
|
||||
return maintainer == account_id
|
||||
|
||||
if resource_type == RBACResourceScope.DATASET:
|
||||
maintainer = db.session.scalar(
|
||||
select(Dataset.maintainer).where(
|
||||
Dataset.id == resource_id,
|
||||
Dataset.tenant_id == tenant_id,
|
||||
)
|
||||
)
|
||||
return maintainer == account_id
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def _extract_resource_id(resource_type: RBACResourceScope, 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 App routes use ``agent_id`` as the app id.
|
||||
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") or matched_args.get("agent_id") or matched_args.get("resource_id")
|
||||
if not app_id:
|
||||
raise ValueError("Missing app_id in request path")
|
||||
return str(app_id)
|
||||
|
||||
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)))
|
||||
if not dataset:
|
||||
raise NotFound("Dataset not found for pipeline")
|
||||
return str(dataset.id)
|
||||
raise ValueError("Missing dataset_id or pipeline_id in request path")
|
||||
raise ValueError(f"Unknown resource_type: {resource_type}")
|
||||
|
||||
@ -139,6 +139,7 @@ from .workspace import (
|
||||
model_providers,
|
||||
models,
|
||||
plugin,
|
||||
rbac,
|
||||
snippets,
|
||||
tool_providers,
|
||||
trigger_providers,
|
||||
@ -212,6 +213,7 @@ __all__ = [
|
||||
"rag_pipeline_draft_variable",
|
||||
"rag_pipeline_import",
|
||||
"rag_pipeline_workflow",
|
||||
"rbac",
|
||||
"recommended_app",
|
||||
"saved_message",
|
||||
"setup",
|
||||
|
||||
@ -7,8 +7,11 @@ from controllers.console import console_ns
|
||||
from controllers.console.agent.app_helpers import resolve_agent_app_model
|
||||
from controllers.console.app.wraps import get_app_model
|
||||
from controllers.console.wraps import (
|
||||
RBACPermission,
|
||||
RBACResourceScope,
|
||||
account_initialization_required,
|
||||
edit_permission_required,
|
||||
rbac_permission_required,
|
||||
setup_required,
|
||||
with_current_tenant_id,
|
||||
with_current_user_id,
|
||||
@ -70,6 +73,7 @@ class WorkflowAgentComposerApi(Resource):
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@edit_permission_required
|
||||
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_EDIT)
|
||||
@get_app_model(mode=[AppMode.WORKFLOW, AppMode.ADVANCED_CHAT])
|
||||
@with_current_user_id
|
||||
@with_current_tenant_id
|
||||
@ -166,6 +170,7 @@ class WorkflowAgentComposerSaveToRosterApi(Resource):
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@edit_permission_required
|
||||
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_EDIT)
|
||||
@get_app_model(mode=[AppMode.WORKFLOW, AppMode.ADVANCED_CHAT])
|
||||
@with_current_user_id
|
||||
@with_current_tenant_id
|
||||
@ -203,6 +208,7 @@ class AgentComposerApi(Resource):
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@edit_permission_required
|
||||
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_EDIT)
|
||||
@with_current_user_id
|
||||
@with_current_tenant_id
|
||||
def put(self, tenant_id: str, account_id: str, agent_id: UUID):
|
||||
|
||||
@ -9,6 +9,7 @@ from sqlalchemy import delete, func, select
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
from werkzeug.exceptions import Forbidden
|
||||
|
||||
from configs import dify_config
|
||||
from controllers.common.schema import register_response_schema_models
|
||||
from extensions.ext_database import db
|
||||
from fields.base import ResponseModel
|
||||
@ -22,8 +23,11 @@ from services.api_token_service import ApiTokenCache
|
||||
|
||||
from . import console_ns
|
||||
from .wraps import (
|
||||
RBACPermission,
|
||||
RBACResourceScope,
|
||||
account_initialization_required,
|
||||
edit_permission_required,
|
||||
rbac_permission_required,
|
||||
setup_required,
|
||||
with_current_tenant_id,
|
||||
with_current_user,
|
||||
@ -143,7 +147,7 @@ class BaseApiKeyResource(Resource):
|
||||
assert self.resource_id_field is not None, "resource_id_field must be set"
|
||||
_get_resource(resource_id, current_tenant_id, self.resource_model)
|
||||
|
||||
if not current_user.is_admin_or_owner:
|
||||
if not dify_config.RBAC_ENABLED and not current_user.is_admin_or_owner:
|
||||
raise Forbidden()
|
||||
|
||||
key = db.session.scalar(
|
||||
@ -186,6 +190,7 @@ class AppApiKeyListResource(BaseApiKeyListResource):
|
||||
@console_ns.response(400, "Maximum keys exceeded")
|
||||
@with_current_tenant_id
|
||||
@edit_permission_required
|
||||
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_RELEASE_AND_VERSION)
|
||||
def post(self, current_tenant_id: str, resource_id: UUID) -> tuple[dict[str, object], int]:
|
||||
"""Create a new API key for an app"""
|
||||
return dump_response(ApiKeyItem, self._create_api_key(str(resource_id), current_tenant_id)), 201
|
||||
@ -204,6 +209,7 @@ class AppApiKeyResource(BaseApiKeyResource):
|
||||
@console_ns.response(204, "API key deleted successfully")
|
||||
@with_current_user
|
||||
@with_current_tenant_id
|
||||
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_RELEASE_AND_VERSION)
|
||||
def delete(
|
||||
self, current_tenant_id: str, current_user: Account, resource_id: UUID, api_key_id: UUID
|
||||
) -> tuple[str, int]:
|
||||
@ -234,6 +240,7 @@ class DatasetApiKeyListResource(BaseApiKeyListResource):
|
||||
@console_ns.response(400, "Maximum keys exceeded")
|
||||
@with_current_tenant_id
|
||||
@edit_permission_required
|
||||
@rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_API_KEY_MANAGE)
|
||||
def post(self, current_tenant_id: str, resource_id: UUID) -> tuple[dict[str, object], int]:
|
||||
"""Create a new API key for a dataset"""
|
||||
return dump_response(ApiKeyItem, self._create_api_key(str(resource_id), current_tenant_id)), 201
|
||||
@ -252,6 +259,7 @@ class DatasetApiKeyResource(BaseApiKeyResource):
|
||||
@console_ns.response(204, "API key deleted successfully")
|
||||
@with_current_user
|
||||
@with_current_tenant_id
|
||||
@rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_API_KEY_MANAGE)
|
||||
def delete(
|
||||
self, current_tenant_id: str, current_user: Account, resource_id: UUID, api_key_id: UUID
|
||||
) -> tuple[str, int]:
|
||||
|
||||
@ -17,7 +17,10 @@ from controllers.console import console_ns
|
||||
from controllers.console.agent.app_helpers import resolve_agent_app_model
|
||||
from controllers.console.app.wraps import get_app_model
|
||||
from controllers.console.wraps import (
|
||||
RBACPermission,
|
||||
RBACResourceScope,
|
||||
account_initialization_required,
|
||||
rbac_permission_required,
|
||||
setup_required,
|
||||
with_current_tenant_id,
|
||||
with_current_user,
|
||||
@ -351,6 +354,7 @@ class AgentLogApi(Resource):
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_VIEW_LAYOUT)
|
||||
@get_app_model(mode=[AppMode.AGENT_CHAT])
|
||||
def get(self, app_model: App):
|
||||
"""Get agent logs"""
|
||||
|
||||
@ -19,8 +19,11 @@ from controllers.common.schema import register_response_schema_models, register_
|
||||
from controllers.console import console_ns
|
||||
from controllers.console.agent.app_helpers import resolve_agent_app_model
|
||||
from controllers.console.wraps import (
|
||||
RBACPermission,
|
||||
RBACResourceScope,
|
||||
account_initialization_required,
|
||||
edit_permission_required,
|
||||
rbac_permission_required,
|
||||
setup_required,
|
||||
with_current_tenant_id,
|
||||
with_current_user,
|
||||
@ -78,6 +81,7 @@ class AgentAppFeatureConfigResource(Resource):
|
||||
@setup_required
|
||||
@login_required
|
||||
@edit_permission_required
|
||||
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_CREATE_AND_MANAGEMENT)
|
||||
@account_initialization_required
|
||||
@with_current_user
|
||||
@with_current_tenant_id
|
||||
|
||||
@ -9,11 +9,14 @@ from controllers.common.errors import NoFileUploadedError, TooManyFilesError
|
||||
from controllers.common.schema import query_params_from_model, register_response_schema_models, register_schema_models
|
||||
from controllers.console import console_ns
|
||||
from controllers.console.wraps import (
|
||||
RBACPermission,
|
||||
RBACResourceScope,
|
||||
account_initialization_required,
|
||||
annotation_import_concurrency_limit,
|
||||
annotation_import_rate_limit,
|
||||
cloud_edition_billing_resource_check,
|
||||
edit_permission_required,
|
||||
rbac_permission_required,
|
||||
setup_required,
|
||||
)
|
||||
from extensions.ext_redis import redis_client
|
||||
@ -155,6 +158,7 @@ class AnnotationReplyActionApi(Resource):
|
||||
@account_initialization_required
|
||||
@cloud_edition_billing_resource_check("annotation")
|
||||
@edit_permission_required
|
||||
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_EDIT)
|
||||
def post(self, app_id: UUID, action: Literal["enable", "disable"]):
|
||||
args = AnnotationReplyPayload.model_validate(console_ns.payload)
|
||||
match action:
|
||||
@ -185,6 +189,7 @@ class AppAnnotationSettingDetailApi(Resource):
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@edit_permission_required
|
||||
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_VIEW_LAYOUT)
|
||||
def get(self, app_id: UUID):
|
||||
result = AppAnnotationService.get_app_annotation_setting_by_app_id(str(app_id))
|
||||
return result, 200
|
||||
@ -202,6 +207,7 @@ class AppAnnotationSettingUpdateApi(Resource):
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@edit_permission_required
|
||||
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_EDIT)
|
||||
def post(self, app_id: UUID, annotation_setting_id: UUID):
|
||||
annotation_setting_id_str = str(annotation_setting_id)
|
||||
|
||||
@ -230,6 +236,7 @@ class AnnotationReplyActionStatusApi(Resource):
|
||||
@account_initialization_required
|
||||
@cloud_edition_billing_resource_check("annotation")
|
||||
@edit_permission_required
|
||||
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_VIEW_LAYOUT)
|
||||
def get(self, app_id: UUID, job_id: UUID, action: str):
|
||||
job_id_str = str(job_id)
|
||||
app_annotation_job_key = f"{action}_app_annotation_job_{job_id_str}"
|
||||
@ -258,6 +265,7 @@ class AnnotationApi(Resource):
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@edit_permission_required
|
||||
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_VIEW_LAYOUT)
|
||||
def get(self, app_id: UUID):
|
||||
args = AnnotationListQuery.model_validate(request.args.to_dict(flat=True))
|
||||
page = args.page
|
||||
@ -286,6 +294,7 @@ class AnnotationApi(Resource):
|
||||
@account_initialization_required
|
||||
@cloud_edition_billing_resource_check("annotation")
|
||||
@edit_permission_required
|
||||
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_EDIT)
|
||||
def post(self, app_id: UUID):
|
||||
args = CreateAnnotationPayload.model_validate(console_ns.payload)
|
||||
upsert_args: UpsertAnnotationArgs = {}
|
||||
@ -304,6 +313,7 @@ class AnnotationApi(Resource):
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@edit_permission_required
|
||||
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_CREATE_AND_MANAGEMENT)
|
||||
@console_ns.response(204, "Annotations deleted successfully")
|
||||
def delete(self, app_id: UUID):
|
||||
|
||||
@ -342,6 +352,7 @@ class AnnotationExportApi(Resource):
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@edit_permission_required
|
||||
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_VIEW_LAYOUT)
|
||||
def get(self, app_id: UUID):
|
||||
annotation_list = AppAnnotationService.export_annotation_list_by_app_id(str(app_id))
|
||||
annotation_models = TypeAdapter(list[Annotation]).validate_python(annotation_list, from_attributes=True)
|
||||
@ -369,6 +380,7 @@ class AnnotationUpdateDeleteApi(Resource):
|
||||
@account_initialization_required
|
||||
@cloud_edition_billing_resource_check("annotation")
|
||||
@edit_permission_required
|
||||
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_EDIT)
|
||||
def post(self, app_id: UUID, annotation_id: UUID):
|
||||
args = UpdateAnnotationPayload.model_validate(console_ns.payload)
|
||||
update_args: UpdateAnnotationArgs = {}
|
||||
@ -383,6 +395,7 @@ class AnnotationUpdateDeleteApi(Resource):
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@edit_permission_required
|
||||
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_EDIT)
|
||||
@console_ns.response(204, "Annotation deleted successfully")
|
||||
def delete(self, app_id: UUID, annotation_id: UUID):
|
||||
AppAnnotationService.delete_app_annotation(str(app_id), str(annotation_id))
|
||||
@ -410,6 +423,7 @@ class AnnotationBatchImportApi(Resource):
|
||||
@annotation_import_rate_limit
|
||||
@annotation_import_concurrency_limit
|
||||
@edit_permission_required
|
||||
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_EDIT)
|
||||
def post(self, app_id: UUID):
|
||||
from configs import dify_config
|
||||
|
||||
@ -462,6 +476,7 @@ class AnnotationBatchImportStatusApi(Resource):
|
||||
@account_initialization_required
|
||||
@cloud_edition_billing_resource_check("annotation")
|
||||
@edit_permission_required
|
||||
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_VIEW_LAYOUT)
|
||||
def get(self, app_id: UUID, job_id: UUID):
|
||||
indexing_cache_key = f"app_annotation_batch_import_{str(job_id)}"
|
||||
cache_result = redis_client.get(indexing_cache_key)
|
||||
@ -492,6 +507,7 @@ class AnnotationHitHistoryListApi(Resource):
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@edit_permission_required
|
||||
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_VIEW_LAYOUT)
|
||||
def get(self, app_id: UUID, annotation_id: UUID):
|
||||
page = request.args.get("page", default=1, type=int)
|
||||
limit = request.args.get("limit", default=20, type=int)
|
||||
|
||||
@ -3,7 +3,7 @@ import re
|
||||
import uuid
|
||||
from collections.abc import Sequence
|
||||
from datetime import datetime
|
||||
from typing import Any, Literal, cast
|
||||
from typing import Any, Literal
|
||||
|
||||
from flask import request
|
||||
from flask_restx import Resource
|
||||
@ -11,8 +11,9 @@ from pydantic import AliasChoices, BaseModel, Field, computed_field, field_valid
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.orm import Session
|
||||
from werkzeug.datastructures import MultiDict
|
||||
from werkzeug.exceptions import BadRequest
|
||||
from werkzeug.exceptions import BadRequest, NotFound
|
||||
|
||||
from configs import dify_config
|
||||
from controllers.common.fields import RedirectUrlResponse, SimpleResultResponse
|
||||
from controllers.common.helpers import FileInfo
|
||||
from controllers.common.schema import (
|
||||
@ -25,11 +26,14 @@ from controllers.console import console_ns
|
||||
from controllers.console.app.wraps import get_app_model, with_session
|
||||
from controllers.console.workspace.models import LoadBalancingPayload
|
||||
from controllers.console.wraps import (
|
||||
RBACPermission,
|
||||
RBACResourceScope,
|
||||
account_initialization_required,
|
||||
cloud_edition_billing_resource_check,
|
||||
edit_permission_required,
|
||||
enterprise_license_required,
|
||||
is_admin_or_owner_required,
|
||||
rbac_permission_required,
|
||||
setup_required,
|
||||
with_current_tenant_id,
|
||||
with_current_user,
|
||||
@ -48,6 +52,7 @@ from models import Account, App, DatasetPermissionEnum, Workflow
|
||||
from models.model import IconType
|
||||
from services.app_dsl_service import AppDslService
|
||||
from services.app_service import AppListParams, AppListSortBy, AppService, CreateAppParams, StarredAppListParams
|
||||
from services.enterprise import rbac_service as enterprise_rbac_service
|
||||
from services.enterprise.enterprise_service import EnterpriseService
|
||||
from services.entities.dsl_entities import ImportMode, ImportStatus
|
||||
from services.entities.knowledge_entities.knowledge_entities import (
|
||||
@ -72,12 +77,14 @@ _logger = logging.getLogger(__name__)
|
||||
_TAG_IDS_BRACKET_PATTERN = re.compile(r"^tag_ids\[(\d+)\]$")
|
||||
_CREATOR_IDS_BRACKET_PATTERN = re.compile(r"^creator_ids\[(\d+)\]$")
|
||||
AppListMode = Literal["completion", "chat", "advanced-chat", "workflow", "agent-chat", "agent", "channel", "all"]
|
||||
DEFAULT_APP_LIST_MODE: AppListMode = "all"
|
||||
APP_LIST_PERMISSION_KEYS = frozenset({"app.preview", "app.acl.preview", "app.full_access"})
|
||||
|
||||
|
||||
class AppListBaseQuery(BaseModel):
|
||||
page: int = Field(default=1, ge=1, le=99999, description="Page number (1-99999)")
|
||||
limit: int = Field(default=20, ge=1, le=100, description="Page size (1-100)")
|
||||
mode: AppListMode = Field(default=cast(AppListMode, "all"), description="App mode filter")
|
||||
mode: AppListMode = Field(default=DEFAULT_APP_LIST_MODE, description="App mode filter")
|
||||
sort_by: AppListSortBy = Field(
|
||||
default="last_modified",
|
||||
description="Sort apps by last modified, recently created, or earliest created",
|
||||
@ -160,6 +167,10 @@ def _normalize_app_list_query_args(query_args: MultiDict[str, str]) -> dict[str,
|
||||
return normalized
|
||||
|
||||
|
||||
def _has_app_list_permission(permission_keys: Sequence[str]) -> bool:
|
||||
return any(permission_key in APP_LIST_PERMISSION_KEYS for permission_key in permission_keys)
|
||||
|
||||
|
||||
class CreateAppPayload(BaseModel):
|
||||
name: str = Field(..., min_length=1, description="App name")
|
||||
description: str | None = Field(default=None, description="App description (max 400 chars)", max_length=400)
|
||||
@ -398,11 +409,13 @@ class AppPartial(ResponseModel):
|
||||
create_user_name: str | None = None
|
||||
author_name: str | None = None
|
||||
has_draft_trigger: bool | None = None
|
||||
permission_keys: list[str] = Field(default_factory=list)
|
||||
# For Agent App type: the roster Agent backing this app (None otherwise).
|
||||
bound_agent_id: str | None = None
|
||||
# For Agent App responses exposed through /agent.
|
||||
app_id: str | None = None
|
||||
is_starred: bool = False
|
||||
maintainer: str | None = None
|
||||
|
||||
@computed_field(return_type=str | None) # type: ignore
|
||||
@property
|
||||
@ -438,6 +451,8 @@ class AppDetail(ResponseModel):
|
||||
updated_at: int | None = None
|
||||
access_mode: str | None = None
|
||||
tags: list[Tag] = Field(default_factory=list)
|
||||
permission_keys: list[str] = Field(default_factory=list)
|
||||
maintainer: str | None = None
|
||||
|
||||
@field_validator("created_at", "updated_at", mode="before")
|
||||
@classmethod
|
||||
@ -592,6 +607,44 @@ class AppListApi(Resource):
|
||||
is_created_by_me=args.is_created_by_me,
|
||||
)
|
||||
|
||||
permissions = enterprise_rbac_service.RBACService.MyPermissions.get(
|
||||
str(current_tenant_id),
|
||||
current_user_id,
|
||||
)
|
||||
if dify_config.RBAC_ENABLED:
|
||||
whitelist_scope = enterprise_rbac_service.RBACService.AppAccess.whitelist_resources(
|
||||
str(current_tenant_id),
|
||||
current_user_id,
|
||||
)
|
||||
can_manage_own_apps = "app.create_and_management" in permissions.workspace.permission_keys
|
||||
has_default_preview = _has_app_list_permission(
|
||||
permissions.app.default_permission_keys
|
||||
) or _has_app_list_permission(permissions.workspace.permission_keys)
|
||||
permission_app_ids: set[str] | None = None
|
||||
if not has_default_preview:
|
||||
permission_app_ids = {
|
||||
override.resource_id
|
||||
for override in permissions.app.overrides
|
||||
if _has_app_list_permission(override.permission_keys)
|
||||
}
|
||||
|
||||
if getattr(whitelist_scope, "unrestricted", False):
|
||||
accessible_app_ids = permission_app_ids
|
||||
else:
|
||||
accessible_app_ids = set(whitelist_scope.resource_ids)
|
||||
if permission_app_ids is not None:
|
||||
accessible_app_ids |= permission_app_ids
|
||||
elif has_default_preview:
|
||||
accessible_app_ids = None
|
||||
|
||||
if accessible_app_ids:
|
||||
params.accessible_app_ids = sorted(accessible_app_ids)
|
||||
params.include_own_apps = can_manage_own_apps
|
||||
elif accessible_app_ids is not None and can_manage_own_apps:
|
||||
params.is_created_by_me = True
|
||||
elif accessible_app_ids is not None:
|
||||
params.accessible_app_ids = []
|
||||
|
||||
# get app list
|
||||
app_service = AppService()
|
||||
app_pagination = app_service.get_paginate_apps(current_user_id, current_tenant_id, params, db.session)
|
||||
@ -599,9 +652,20 @@ class AppListApi(Resource):
|
||||
empty = AppPagination(page=args.page, limit=args.limit, total=0, has_more=False, data=[])
|
||||
return empty.model_dump(mode="json"), 200
|
||||
|
||||
app_ids = [str(app.id) for app in app_pagination.items]
|
||||
permission_keys_map = permissions.app.permission_keys_by_resource_ids(app_ids)
|
||||
_enrich_app_list_items(session, apps=app_pagination.items, tenant_id=current_tenant_id)
|
||||
|
||||
pagination_model = AppPagination.model_validate(app_pagination, from_attributes=True)
|
||||
if app_pagination.items:
|
||||
pagination_model = pagination_model.model_copy(
|
||||
update={
|
||||
"data": [
|
||||
item.model_copy(update={"permission_keys": permission_keys_map.get(str(item.id), [])})
|
||||
for item in pagination_model.data
|
||||
]
|
||||
}
|
||||
)
|
||||
return pagination_model.model_dump(mode="json"), 200
|
||||
|
||||
@console_ns.doc("create_app")
|
||||
@ -613,6 +677,7 @@ class AppListApi(Resource):
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_CREATE_AND_MANAGEMENT, resource_required=False)
|
||||
@cloud_edition_billing_resource_check("apps")
|
||||
@edit_permission_required
|
||||
@with_current_user
|
||||
@ -631,7 +696,14 @@ class AppListApi(Resource):
|
||||
|
||||
app_service = AppService()
|
||||
app = app_service.create_app(current_tenant_id, params, current_user)
|
||||
app_detail = AppDetailWithSite.model_validate(app, from_attributes=True)
|
||||
permission_keys_map = enterprise_rbac_service.RBACService.AppPermissions.batch_get(
|
||||
str(current_tenant_id),
|
||||
current_user.id,
|
||||
[str(app.id)],
|
||||
)
|
||||
app_detail = AppDetailWithSite.model_validate(app, from_attributes=True).model_copy(
|
||||
update={"permission_keys": permission_keys_map.get(str(app.id), [])}
|
||||
)
|
||||
return app_detail.model_dump(mode="json"), 201
|
||||
|
||||
|
||||
@ -717,8 +789,11 @@ class AppApi(Resource):
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@enterprise_license_required
|
||||
@with_current_user
|
||||
@with_current_tenant_id
|
||||
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_VIEW_LAYOUT)
|
||||
@get_app_model(mode=None)
|
||||
def get(self, app_model: App):
|
||||
def get(self, current_tenant_id: str, current_user: Account, app_model: App):
|
||||
"""Get app detail"""
|
||||
app_service = AppService()
|
||||
|
||||
@ -728,7 +803,16 @@ class AppApi(Resource):
|
||||
app_setting = EnterpriseService.WebAppAuth.get_app_access_mode_by_id(app_id=str(app_model.id))
|
||||
app_model.access_mode = app_setting.access_mode
|
||||
|
||||
response_model = AppDetailWithSite.model_validate(app_model, from_attributes=True)
|
||||
permissions = enterprise_rbac_service.RBACService.MyPermissions.get(
|
||||
str(current_tenant_id),
|
||||
current_user.id,
|
||||
app_id=str(app_model.id),
|
||||
)
|
||||
permission_keys_map = permissions.app.permission_keys_by_resource_ids([str(app_model.id)])
|
||||
|
||||
response_model = AppDetailWithSite.model_validate(app_model, from_attributes=True).model_copy(
|
||||
update={"permission_keys": permission_keys_map.get(str(app_model.id), [])}
|
||||
)
|
||||
return response_model.model_dump(mode="json")
|
||||
|
||||
@console_ns.doc("update_app")
|
||||
@ -741,8 +825,9 @@ class AppApi(Resource):
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@get_app_model(mode=None)
|
||||
@edit_permission_required
|
||||
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_EDIT)
|
||||
@get_app_model(mode=None)
|
||||
def put(self, app_model: App):
|
||||
"""Update app"""
|
||||
args = UpdateAppPayload.model_validate(console_ns.payload)
|
||||
@ -767,11 +852,12 @@ class AppApi(Resource):
|
||||
@console_ns.doc(params={"app_id": "Application ID"})
|
||||
@console_ns.response(204, "App deleted successfully")
|
||||
@console_ns.response(403, "Insufficient permissions")
|
||||
@get_app_model
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@edit_permission_required
|
||||
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_DELETE)
|
||||
@get_app_model
|
||||
def delete(self, app_model: App):
|
||||
"""Delete app"""
|
||||
app_service = AppService()
|
||||
@ -791,10 +877,12 @@ class AppCopyApi(Resource):
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@get_app_model(mode=None)
|
||||
@edit_permission_required
|
||||
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_CREATE_AND_MANAGEMENT)
|
||||
@with_current_user
|
||||
def post(self, current_user: Account, app_model: App):
|
||||
@with_current_tenant_id
|
||||
@get_app_model(mode=None)
|
||||
def post(self, current_tenant_id: str, current_user: Account, app_model: App):
|
||||
"""Copy app"""
|
||||
# The role of the current user in the ta table must be admin, owner, or editor
|
||||
args = CopyAppPayload.model_validate(console_ns.payload or {})
|
||||
@ -836,7 +924,17 @@ class AppCopyApi(Resource):
|
||||
stmt = select(App).where(App.id == result.app_id)
|
||||
app = session.scalar(stmt)
|
||||
|
||||
response_model = AppDetailWithSite.model_validate(app, from_attributes=True)
|
||||
if not app:
|
||||
raise NotFound("App not found")
|
||||
|
||||
permission_keys_map = enterprise_rbac_service.RBACService.AppPermissions.batch_get(
|
||||
str(current_tenant_id),
|
||||
current_user.id,
|
||||
[str(app.id)],
|
||||
)
|
||||
response_model = AppDetailWithSite.model_validate(app, from_attributes=True).model_copy(
|
||||
update={"permission_keys": permission_keys_map.get(str(app.id), [])}
|
||||
)
|
||||
return response_model.model_dump(mode="json"), 201
|
||||
|
||||
|
||||
@ -848,11 +946,12 @@ class AppExportApi(Resource):
|
||||
@console_ns.doc(params=query_params_from_model(AppExportQuery))
|
||||
@console_ns.response(200, "App exported successfully", console_ns.models[AppExportResponse.__name__])
|
||||
@console_ns.response(403, "Insufficient permissions")
|
||||
@get_app_model
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@edit_permission_required
|
||||
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_IMPORT_EXPORT_DSL)
|
||||
@get_app_model
|
||||
def get(self, app_model: App):
|
||||
"""Export app"""
|
||||
args = AppExportQuery.model_validate(request.args.to_dict(flat=True))
|
||||
@ -873,12 +972,12 @@ class AppPublishToCreatorsPlatformApi(Resource):
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@get_app_model(mode=None)
|
||||
@edit_permission_required
|
||||
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_IMPORT_EXPORT_DSL)
|
||||
@with_current_user_id
|
||||
@get_app_model(mode=None)
|
||||
def post(self, current_user_id: str, app_model: App):
|
||||
"""Publish app to Creators Platform"""
|
||||
from configs import dify_config
|
||||
from core.helper.creators import get_redirect_url, upload_dsl
|
||||
|
||||
if not dify_config.CREATORS_PLATFORM_FEATURES_ENABLED:
|
||||
@ -903,8 +1002,9 @@ class AppNameApi(Resource):
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@get_app_model(mode=None)
|
||||
@edit_permission_required
|
||||
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_EDIT)
|
||||
@get_app_model(mode=None)
|
||||
def post(self, app_model: App):
|
||||
args = AppNamePayload.model_validate(console_ns.payload)
|
||||
|
||||
@ -925,8 +1025,9 @@ class AppIconApi(Resource):
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@get_app_model(mode=None)
|
||||
@edit_permission_required
|
||||
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_EDIT)
|
||||
@get_app_model(mode=None)
|
||||
def post(self, app_model: App):
|
||||
args = AppIconPayload.model_validate(console_ns.payload or {})
|
||||
|
||||
@ -952,8 +1053,9 @@ class AppSiteStatus(Resource):
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@get_app_model(mode=None)
|
||||
@edit_permission_required
|
||||
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_RELEASE_AND_VERSION)
|
||||
@get_app_model(mode=None)
|
||||
def post(self, app_model: App):
|
||||
args = AppSiteStatusPayload.model_validate(console_ns.payload)
|
||||
|
||||
@ -975,6 +1077,7 @@ class AppApiStatus(Resource):
|
||||
@login_required
|
||||
@is_admin_or_owner_required
|
||||
@account_initialization_required
|
||||
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_RELEASE_AND_VERSION)
|
||||
@get_app_model(mode=None)
|
||||
def post(self, app_model: App):
|
||||
args = AppApiStatusPayload.model_validate(console_ns.payload)
|
||||
@ -999,6 +1102,7 @@ class AppTraceApi(Resource):
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@with_session
|
||||
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_VIEW_LAYOUT)
|
||||
@get_app_model
|
||||
def get(self, session: Session, app_model: App):
|
||||
"""Get app trace"""
|
||||
@ -1020,6 +1124,7 @@ class AppTraceApi(Resource):
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@edit_permission_required
|
||||
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_MONITOR)
|
||||
@get_app_model
|
||||
def post(self, app_model: App):
|
||||
# add app trace
|
||||
|
||||
@ -2,25 +2,36 @@ from flask_restx import Resource
|
||||
from pydantic import BaseModel, Field
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from configs import dify_config
|
||||
from controllers.common.schema import register_enum_models, register_schema_models
|
||||
from controllers.console.app.wraps import get_app_model
|
||||
from controllers.console.wraps import (
|
||||
RBACPermission,
|
||||
RBACResourceScope,
|
||||
account_initialization_required,
|
||||
cloud_edition_billing_resource_check,
|
||||
edit_permission_required,
|
||||
rbac_permission_required,
|
||||
setup_required,
|
||||
with_current_user,
|
||||
)
|
||||
from extensions.ext_database import db
|
||||
from libs.login import login_required
|
||||
from extensions.ext_redis import redis_client
|
||||
from libs.login import current_account_with_tenant, login_required
|
||||
from models.account import Account
|
||||
from models.model import App
|
||||
from services.app_dsl_service import AppDslService, Import
|
||||
from services.app_dsl_service import (
|
||||
IMPORT_INFO_REDIS_KEY_PREFIX,
|
||||
AppDslService,
|
||||
Import,
|
||||
PendingData,
|
||||
)
|
||||
from services.enterprise.enterprise_service import EnterpriseService
|
||||
from services.entities.dsl_entities import CheckDependenciesResult, ImportStatus
|
||||
from services.feature_service import FeatureService
|
||||
|
||||
from .. import console_ns
|
||||
from .permission_keys import get_app_permission_keys
|
||||
|
||||
|
||||
class AppImportPayload(BaseModel):
|
||||
@ -39,6 +50,24 @@ register_enum_models(console_ns, ImportStatus)
|
||||
register_schema_models(console_ns, AppImportPayload, Import, CheckDependenciesResult)
|
||||
|
||||
|
||||
def _current_user_and_tenant_id(current_user: Account | None) -> tuple[Account, str | None]:
|
||||
if current_user is None:
|
||||
account, tenant_id = current_account_with_tenant()
|
||||
return account, str(tenant_id) if tenant_id else None
|
||||
|
||||
current_tenant_id = getattr(current_user, "current_tenant_id", None)
|
||||
if current_tenant_id:
|
||||
return current_user, str(current_tenant_id)
|
||||
|
||||
current_tenant = getattr(current_user, "current_tenant", None)
|
||||
current_tenant_object_id = getattr(current_tenant, "id", None)
|
||||
if current_tenant_object_id:
|
||||
return current_user, str(current_tenant_object_id)
|
||||
|
||||
account, fallback_tenant_id = current_account_with_tenant()
|
||||
return account, str(fallback_tenant_id) if fallback_tenant_id else None
|
||||
|
||||
|
||||
@console_ns.route("/apps/imports")
|
||||
class AppImportApi(Resource):
|
||||
@console_ns.expect(console_ns.models[AppImportPayload.__name__])
|
||||
@ -50,10 +79,11 @@ class AppImportApi(Resource):
|
||||
@account_initialization_required
|
||||
@cloud_edition_billing_resource_check("apps")
|
||||
@edit_permission_required
|
||||
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_IMPORT_EXPORT_DSL, resource_required=False)
|
||||
@with_current_user
|
||||
def post(self, current_user: Account):
|
||||
# Check user role first
|
||||
def post(self, current_user: Account | None = None):
|
||||
args = AppImportPayload.model_validate(console_ns.payload)
|
||||
current_user = current_user if current_user is not None else _current_user_and_tenant_id(None)[0]
|
||||
|
||||
# AppDslService performs internal commits for some creation paths, so use a plain
|
||||
# Session here instead of nesting it inside sessionmaker(...).begin().
|
||||
@ -77,6 +107,20 @@ class AppImportApi(Resource):
|
||||
session.rollback()
|
||||
else:
|
||||
session.commit()
|
||||
|
||||
is_created_app = args.app_id is None and result.status in {
|
||||
ImportStatus.COMPLETED,
|
||||
ImportStatus.COMPLETED_WITH_WARNINGS,
|
||||
}
|
||||
if dify_config.RBAC_ENABLED and is_created_app and result.app_id:
|
||||
current_user, current_tenant_id = _current_user_and_tenant_id(current_user)
|
||||
if current_tenant_id:
|
||||
result.permission_keys = get_app_permission_keys(
|
||||
current_tenant_id,
|
||||
current_user.id,
|
||||
result.app_id,
|
||||
)
|
||||
|
||||
if result.app_id and FeatureService.get_system_features().webapp_auth.enabled:
|
||||
# update web app setting as private
|
||||
EnterpriseService.WebAppAuth.update_app_access_mode(result.app_id, "private")
|
||||
@ -99,9 +143,16 @@ class AppImportConfirmApi(Resource):
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@edit_permission_required
|
||||
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_IMPORT_EXPORT_DSL, resource_required=False)
|
||||
@with_current_user
|
||||
def post(self, current_user: Account, import_id: str):
|
||||
# Check user role first
|
||||
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]
|
||||
redis_key = f"{IMPORT_INFO_REDIS_KEY_PREFIX}{import_id}"
|
||||
pending_data_raw = redis_client.get(redis_key)
|
||||
pending_data: PendingData | None = None
|
||||
if pending_data_raw:
|
||||
pending_data = PendingData.model_validate_json(pending_data_raw)
|
||||
|
||||
with Session(db.engine, expire_on_commit=False) as session:
|
||||
import_service = AppDslService(session)
|
||||
# Confirm import
|
||||
@ -112,6 +163,24 @@ class AppImportConfirmApi(Resource):
|
||||
else:
|
||||
session.commit()
|
||||
|
||||
is_created_app = bool(
|
||||
pending_data
|
||||
and pending_data.app_id is None
|
||||
and result.status
|
||||
in {
|
||||
ImportStatus.COMPLETED,
|
||||
ImportStatus.COMPLETED_WITH_WARNINGS,
|
||||
}
|
||||
)
|
||||
if dify_config.RBAC_ENABLED and is_created_app and result.app_id:
|
||||
current_user, current_tenant_id = _current_user_and_tenant_id(current_user)
|
||||
if current_tenant_id:
|
||||
result.permission_keys = get_app_permission_keys(
|
||||
current_tenant_id,
|
||||
current_user.id,
|
||||
result.app_id,
|
||||
)
|
||||
|
||||
# Return appropriate status code based on result
|
||||
if result.status == ImportStatus.FAILED:
|
||||
return result.model_dump(mode="json"), 400
|
||||
@ -120,12 +189,17 @@ class AppImportConfirmApi(Resource):
|
||||
|
||||
@console_ns.route("/apps/imports/<string:app_id>/check-dependencies")
|
||||
class AppImportCheckDependenciesApi(Resource):
|
||||
@console_ns.response(200, "Dependencies checked", console_ns.models[CheckDependenciesResult.__name__])
|
||||
@console_ns.response(
|
||||
200,
|
||||
"Dependencies checked",
|
||||
console_ns.models[CheckDependenciesResult.__name__],
|
||||
)
|
||||
@setup_required
|
||||
@login_required
|
||||
@get_app_model
|
||||
@account_initialization_required
|
||||
@edit_permission_required
|
||||
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_VIEW_LAYOUT)
|
||||
def get(self, app_model: App):
|
||||
with Session(db.engine, expire_on_commit=False) as session:
|
||||
import_service = AppDslService(session)
|
||||
|
||||
@ -22,7 +22,13 @@ from controllers.console.app.error import (
|
||||
UnsupportedAudioTypeError,
|
||||
)
|
||||
from controllers.console.app.wraps import get_app_model
|
||||
from controllers.console.wraps import account_initialization_required, setup_required
|
||||
from controllers.console.wraps import (
|
||||
RBACPermission,
|
||||
RBACResourceScope,
|
||||
account_initialization_required,
|
||||
rbac_permission_required,
|
||||
setup_required,
|
||||
)
|
||||
from core.errors.error import ModelCurrentlyNotSupportError, ProviderTokenNotInitError, QuotaExceededError
|
||||
from graphon.model_runtime.errors.invoke import InvokeError
|
||||
from libs.login import login_required
|
||||
@ -126,10 +132,10 @@ class ChatMessageTextApi(Resource):
|
||||
console_ns.models[AudioBinaryResponse.__name__],
|
||||
)
|
||||
@console_ns.response(400, "Bad request - Invalid parameters")
|
||||
@get_app_model
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@get_app_model
|
||||
def post(self, app_model: App):
|
||||
try:
|
||||
payload = TextToSpeechPayload.model_validate(console_ns.payload)
|
||||
@ -180,10 +186,11 @@ class TextModesApi(Resource):
|
||||
console_ns.models[TextToSpeechVoiceListResponse.__name__],
|
||||
)
|
||||
@console_ns.response(400, "Invalid language parameter")
|
||||
@get_app_model
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_VIEW_LAYOUT)
|
||||
@get_app_model
|
||||
def get(self, app_model: App):
|
||||
try:
|
||||
args = TextToSpeechVoiceQuery.model_validate(request.args.to_dict(flat=True))
|
||||
|
||||
@ -22,8 +22,11 @@ from controllers.console.app.error import (
|
||||
)
|
||||
from controllers.console.app.wraps import get_app_model
|
||||
from controllers.console.wraps import (
|
||||
RBACPermission,
|
||||
RBACResourceScope,
|
||||
account_initialization_required,
|
||||
edit_permission_required,
|
||||
rbac_permission_required,
|
||||
setup_required,
|
||||
with_current_tenant_id,
|
||||
with_current_user,
|
||||
@ -113,8 +116,9 @@ class CompletionMessageApi(Resource):
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@get_app_model(mode=AppMode.COMPLETION)
|
||||
@with_current_user
|
||||
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_TEST_AND_RUN)
|
||||
@get_app_model(mode=AppMode.COMPLETION)
|
||||
def post(self, current_user: Account, app_model: App):
|
||||
args_model = CompletionMessagePayload.model_validate(console_ns.payload)
|
||||
args = args_model.model_dump(exclude_none=True, by_alias=True)
|
||||
@ -159,8 +163,8 @@ class CompletionMessageStopApi(Resource):
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@get_app_model(mode=AppMode.COMPLETION)
|
||||
@with_current_user_id
|
||||
@get_app_model(mode=AppMode.COMPLETION)
|
||||
def post(self, current_user_id: str, app_model: App, task_id: str):
|
||||
|
||||
AppTaskService.stop_task(
|
||||
@ -185,9 +189,10 @@ class ChatMessageApi(Resource):
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@get_app_model(mode=[AppMode.CHAT, AppMode.AGENT_CHAT, AppMode.AGENT])
|
||||
@edit_permission_required
|
||||
@with_current_user
|
||||
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_TEST_AND_RUN)
|
||||
@get_app_model(mode=[AppMode.CHAT, AppMode.AGENT_CHAT, AppMode.AGENT])
|
||||
def post(self, current_user: Account, app_model: App):
|
||||
return _create_chat_message(current_user=current_user, app_model=app_model)
|
||||
|
||||
@ -205,6 +210,7 @@ class AgentChatMessageApi(Resource):
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@edit_permission_required
|
||||
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_TEST_AND_RUN)
|
||||
@with_current_user
|
||||
@with_current_tenant_id
|
||||
def post(self, current_tenant_id: str, current_user: Account, agent_id: UUID):
|
||||
@ -221,8 +227,8 @@ class ChatMessageStopApi(Resource):
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@get_app_model(mode=[AppMode.CHAT, AppMode.AGENT_CHAT, AppMode.ADVANCED_CHAT, AppMode.AGENT])
|
||||
@with_current_user_id
|
||||
@get_app_model(mode=[AppMode.CHAT, AppMode.AGENT_CHAT, AppMode.ADVANCED_CHAT, AppMode.AGENT])
|
||||
def post(self, current_user_id: str, app_model: App, task_id: str):
|
||||
return _stop_chat_message(current_user_id=current_user_id, app_model=app_model, task_id=task_id)
|
||||
|
||||
|
||||
@ -13,8 +13,11 @@ from controllers.common.schema import query_params_from_model, register_schema_m
|
||||
from controllers.console import console_ns
|
||||
from controllers.console.app.wraps import get_app_model
|
||||
from controllers.console.wraps import (
|
||||
RBACPermission,
|
||||
RBACResourceScope,
|
||||
account_initialization_required,
|
||||
edit_permission_required,
|
||||
rbac_permission_required,
|
||||
setup_required,
|
||||
with_current_user,
|
||||
)
|
||||
@ -97,9 +100,10 @@ class CompletionConversationApi(Resource):
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@get_app_model(mode=AppMode.COMPLETION)
|
||||
@edit_permission_required
|
||||
@with_current_user
|
||||
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_VIEW_LAYOUT)
|
||||
@get_app_model(mode=AppMode.COMPLETION)
|
||||
def get(self, current_user: Account, app_model: App):
|
||||
args = CompletionConversationQuery.model_validate(request.args.to_dict(flat=True))
|
||||
|
||||
@ -169,9 +173,10 @@ class CompletionConversationDetailApi(Resource):
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@get_app_model(mode=AppMode.COMPLETION)
|
||||
@edit_permission_required
|
||||
@with_current_user
|
||||
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_VIEW_LAYOUT)
|
||||
@get_app_model(mode=AppMode.COMPLETION)
|
||||
def get(self, current_user: Account, app_model: App, conversation_id: UUID):
|
||||
conversation_id_str = str(conversation_id)
|
||||
return ConversationMessageDetailResponse.model_validate(
|
||||
@ -187,9 +192,10 @@ class CompletionConversationDetailApi(Resource):
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@get_app_model(mode=AppMode.COMPLETION)
|
||||
@edit_permission_required
|
||||
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_EDIT)
|
||||
@with_current_user
|
||||
@get_app_model(mode=AppMode.COMPLETION)
|
||||
def delete(self, current_user: Account, app_model: App, conversation_id: UUID):
|
||||
conversation_id_str = str(conversation_id)
|
||||
|
||||
@ -212,9 +218,10 @@ class ChatConversationApi(Resource):
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@get_app_model(mode=[AppMode.CHAT, AppMode.AGENT_CHAT, AppMode.ADVANCED_CHAT, AppMode.AGENT])
|
||||
@edit_permission_required
|
||||
@with_current_user
|
||||
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_VIEW_LAYOUT)
|
||||
@get_app_model(mode=[AppMode.CHAT, AppMode.AGENT_CHAT, AppMode.ADVANCED_CHAT, AppMode.AGENT])
|
||||
def get(self, current_user: Account, app_model: App):
|
||||
args = ChatConversationQuery.model_validate(request.args.to_dict(flat=True))
|
||||
|
||||
@ -323,9 +330,10 @@ class ChatConversationDetailApi(Resource):
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@get_app_model(mode=[AppMode.CHAT, AppMode.AGENT_CHAT, AppMode.ADVANCED_CHAT, AppMode.AGENT])
|
||||
@edit_permission_required
|
||||
@with_current_user
|
||||
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_VIEW_LAYOUT)
|
||||
@get_app_model(mode=[AppMode.CHAT, AppMode.AGENT_CHAT, AppMode.ADVANCED_CHAT, AppMode.AGENT])
|
||||
def get(self, current_user: Account, app_model: App, conversation_id: UUID):
|
||||
conversation_id_str = str(conversation_id)
|
||||
return ConversationDetailResponse.model_validate(
|
||||
@ -340,10 +348,11 @@ class ChatConversationDetailApi(Resource):
|
||||
@console_ns.response(404, "Conversation not found")
|
||||
@setup_required
|
||||
@login_required
|
||||
@get_app_model(mode=[AppMode.CHAT, AppMode.AGENT_CHAT, AppMode.ADVANCED_CHAT, AppMode.AGENT])
|
||||
@account_initialization_required
|
||||
@edit_permission_required
|
||||
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_EDIT)
|
||||
@with_current_user
|
||||
@get_app_model(mode=[AppMode.CHAT, AppMode.AGENT_CHAT, AppMode.ADVANCED_CHAT, AppMode.AGENT])
|
||||
def delete(self, current_user: Account, app_model: App, conversation_id: UUID):
|
||||
conversation_id_str = str(conversation_id)
|
||||
|
||||
|
||||
@ -12,7 +12,13 @@ from sqlalchemy.orm import sessionmaker
|
||||
from controllers.common.schema import query_params_from_model, register_schema_models
|
||||
from controllers.console import console_ns
|
||||
from controllers.console.app.wraps import get_app_model
|
||||
from controllers.console.wraps import account_initialization_required, setup_required
|
||||
from controllers.console.wraps import (
|
||||
RBACPermission,
|
||||
RBACResourceScope,
|
||||
account_initialization_required,
|
||||
rbac_permission_required,
|
||||
setup_required,
|
||||
)
|
||||
from extensions.ext_database import db
|
||||
from fields._value_type_serializer import serialize_value_type
|
||||
from fields.base import ResponseModel
|
||||
@ -93,6 +99,7 @@ class ConversationVariablesApi(Resource):
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_CREATE_AND_MANAGEMENT)
|
||||
@get_app_model(mode=AppMode.ADVANCED_CHAT)
|
||||
def get(self, app_model: App):
|
||||
args = ConversationVariablesQuery.model_validate(request.args.to_dict(flat=True))
|
||||
|
||||
@ -12,8 +12,11 @@ from controllers.common.schema import register_schema_models
|
||||
from controllers.console import console_ns
|
||||
from controllers.console.app.wraps import get_app_model
|
||||
from controllers.console.wraps import (
|
||||
RBACPermission,
|
||||
RBACResourceScope,
|
||||
account_initialization_required,
|
||||
edit_permission_required,
|
||||
rbac_permission_required,
|
||||
setup_required,
|
||||
with_current_tenant_id,
|
||||
)
|
||||
@ -83,6 +86,7 @@ class AppMCPServerController(Resource):
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@setup_required
|
||||
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_VIEW_LAYOUT)
|
||||
@get_app_model
|
||||
def get(self, app_model: App):
|
||||
server = db.session.scalar(select(AppMCPServer).where(AppMCPServer.app_id == app_model.id).limit(1))
|
||||
@ -99,11 +103,12 @@ class AppMCPServerController(Resource):
|
||||
)
|
||||
@console_ns.response(403, "Insufficient permissions")
|
||||
@account_initialization_required
|
||||
@get_app_model
|
||||
@login_required
|
||||
@setup_required
|
||||
@edit_permission_required
|
||||
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_EDIT)
|
||||
@with_current_tenant_id
|
||||
@get_app_model
|
||||
def post(self, current_tenant_id: str, app_model: App):
|
||||
payload = MCPServerCreatePayload.model_validate(console_ns.payload or {})
|
||||
|
||||
@ -133,11 +138,12 @@ class AppMCPServerController(Resource):
|
||||
)
|
||||
@console_ns.response(403, "Insufficient permissions")
|
||||
@console_ns.response(404, "Server not found")
|
||||
@get_app_model
|
||||
@login_required
|
||||
@setup_required
|
||||
@account_initialization_required
|
||||
@edit_permission_required
|
||||
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_EDIT)
|
||||
@get_app_model
|
||||
def put(self, app_model: App):
|
||||
payload = MCPServerUpdatePayload.model_validate(console_ns.payload or {})
|
||||
server = db.session.get(AppMCPServer, payload.id)
|
||||
@ -174,6 +180,7 @@ class AppMCPServerRefreshController(Resource):
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@edit_permission_required
|
||||
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_VIEW_LAYOUT)
|
||||
@with_current_tenant_id
|
||||
def get(self, current_tenant_id: str, server_id: UUID):
|
||||
server = db.session.scalar(
|
||||
|
||||
@ -23,8 +23,11 @@ from controllers.console.app.error import (
|
||||
from controllers.console.app.wraps import get_app_model
|
||||
from controllers.console.explore.error import AppSuggestedQuestionsAfterAnswerDisabledError
|
||||
from controllers.console.wraps import (
|
||||
RBACPermission,
|
||||
RBACResourceScope,
|
||||
account_initialization_required,
|
||||
edit_permission_required,
|
||||
rbac_permission_required,
|
||||
setup_required,
|
||||
with_current_tenant_id,
|
||||
with_current_user,
|
||||
@ -182,8 +185,9 @@ class ChatMessageListApi(Resource):
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@setup_required
|
||||
@get_app_model(mode=[AppMode.CHAT, AppMode.AGENT_CHAT, AppMode.ADVANCED_CHAT, AppMode.AGENT])
|
||||
@edit_permission_required
|
||||
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_VIEW_LAYOUT)
|
||||
@get_app_model(mode=[AppMode.CHAT, AppMode.AGENT_CHAT, AppMode.ADVANCED_CHAT, AppMode.AGENT])
|
||||
def get(self, app_model: App):
|
||||
return _list_chat_messages(app_model=app_model)
|
||||
|
||||
@ -200,6 +204,7 @@ class AgentChatMessageListApi(Resource):
|
||||
@account_initialization_required
|
||||
@setup_required
|
||||
@edit_permission_required
|
||||
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_VIEW_LAYOUT)
|
||||
@with_current_tenant_id
|
||||
def get(self, current_tenant_id: str, agent_id: UUID):
|
||||
app_model = resolve_agent_app_model(tenant_id=current_tenant_id, agent_id=agent_id)
|
||||
@ -215,11 +220,11 @@ class MessageFeedbackApi(Resource):
|
||||
@console_ns.response(200, "Feedback updated successfully", console_ns.models[SimpleResultResponse.__name__])
|
||||
@console_ns.response(404, "Message not found")
|
||||
@console_ns.response(403, "Insufficient permissions")
|
||||
@get_app_model
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@with_current_user
|
||||
@get_app_model
|
||||
def post(self, current_user: Account, app_model: App):
|
||||
return _update_message_feedback(current_user=current_user, app_model=app_model)
|
||||
|
||||
@ -252,10 +257,11 @@ class MessageAnnotationCountApi(Resource):
|
||||
"Annotation count retrieved successfully",
|
||||
console_ns.models[AnnotationCountResponse.__name__],
|
||||
)
|
||||
@get_app_model
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_VIEW_LAYOUT)
|
||||
@get_app_model
|
||||
def get(self, app_model: App):
|
||||
count = db.session.scalar(
|
||||
select(func.count(MessageAnnotation.id)).where(MessageAnnotation.app_id == app_model.id)
|
||||
@ -278,8 +284,9 @@ class MessageSuggestedQuestionApi(Resource):
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@get_app_model(mode=[AppMode.CHAT, AppMode.AGENT_CHAT, AppMode.ADVANCED_CHAT, AppMode.AGENT])
|
||||
@with_current_user
|
||||
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_VIEW_LAYOUT)
|
||||
@get_app_model(mode=[AppMode.CHAT, AppMode.AGENT_CHAT, AppMode.ADVANCED_CHAT, AppMode.AGENT])
|
||||
def get(self, current_user: Account, app_model: App, message_id: UUID):
|
||||
return _get_message_suggested_questions(current_user=current_user, app_model=app_model, message_id=message_id)
|
||||
|
||||
@ -318,10 +325,11 @@ class MessageFeedbackExportApi(Resource):
|
||||
)
|
||||
@console_ns.response(400, "Invalid parameters")
|
||||
@console_ns.response(500, "Internal server error")
|
||||
@get_app_model
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_VIEW_LAYOUT)
|
||||
@get_app_model
|
||||
def get(self, app_model: App):
|
||||
args = FeedbackExportQuery.model_validate(request.args.to_dict())
|
||||
|
||||
@ -356,10 +364,11 @@ class MessageApi(Resource):
|
||||
@console_ns.doc(params={"app_id": "Application ID", "message_id": "Message ID"})
|
||||
@console_ns.response(200, "Message retrieved successfully", console_ns.models[MessageDetailResponse.__name__])
|
||||
@console_ns.response(404, "Message not found")
|
||||
@get_app_model
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_VIEW_LAYOUT)
|
||||
@get_app_model
|
||||
def get(self, app_model: App, message_id: UUID):
|
||||
return _get_message_detail(app_model=app_model, message_id=message_id)
|
||||
|
||||
|
||||
@ -10,8 +10,11 @@ from controllers.common.schema import register_response_schema_models, register_
|
||||
from controllers.console import console_ns
|
||||
from controllers.console.app.wraps import get_app_model
|
||||
from controllers.console.wraps import (
|
||||
RBACPermission,
|
||||
RBACResourceScope,
|
||||
account_initialization_required,
|
||||
edit_permission_required,
|
||||
rbac_permission_required,
|
||||
setup_required,
|
||||
with_current_tenant_id,
|
||||
with_current_user_id,
|
||||
@ -86,10 +89,11 @@ class ModelConfigResource(Resource):
|
||||
@setup_required
|
||||
@login_required
|
||||
@edit_permission_required
|
||||
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_VIEW_LAYOUT)
|
||||
@account_initialization_required
|
||||
@get_app_model(mode=[AppMode.AGENT_CHAT, AppMode.CHAT, AppMode.COMPLETION])
|
||||
@with_current_user_id
|
||||
@with_current_tenant_id
|
||||
@get_app_model(mode=[AppMode.AGENT_CHAT, AppMode.CHAT, AppMode.COMPLETION])
|
||||
def post(self, current_tenant_id: str, current_user_id: str, app_model: App):
|
||||
"""Modify app model config"""
|
||||
# validate config
|
||||
|
||||
@ -9,7 +9,13 @@ from controllers.common.schema import query_params_from_model, register_response
|
||||
from controllers.console import console_ns
|
||||
from controllers.console.app.error import TracingConfigCheckError, TracingConfigIsExist, TracingConfigNotExist
|
||||
from controllers.console.app.wraps import get_app_model
|
||||
from controllers.console.wraps import account_initialization_required, setup_required
|
||||
from controllers.console.wraps import (
|
||||
RBACPermission,
|
||||
RBACResourceScope,
|
||||
account_initialization_required,
|
||||
rbac_permission_required,
|
||||
setup_required,
|
||||
)
|
||||
from fields.base import ResponseModel
|
||||
from libs.login import login_required
|
||||
from models import App
|
||||
@ -64,6 +70,7 @@ class TraceAppConfigApi(Resource):
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_MONITOR)
|
||||
@get_app_model
|
||||
def get(self, app_model: App):
|
||||
args = TraceProviderQuery.model_validate(request.args.to_dict(flat=True)) # type: ignore
|
||||
|
||||
6
api/controllers/console/app/permission_keys.py
Normal file
6
api/controllers/console/app/permission_keys.py
Normal file
@ -0,0 +1,6 @@
|
||||
from services.enterprise import rbac_service as enterprise_rbac_service
|
||||
|
||||
|
||||
def get_app_permission_keys(tenant_id: str, account_id: str | None, app_id: str) -> list[str]:
|
||||
permission_keys_map = enterprise_rbac_service.RBACService.AppPermissions.batch_get(tenant_id, account_id, [app_id])
|
||||
return permission_keys_map.get(app_id, [])
|
||||
@ -10,9 +10,12 @@ from controllers.common.schema import register_schema_models
|
||||
from controllers.console import console_ns
|
||||
from controllers.console.app.wraps import get_app_model
|
||||
from controllers.console.wraps import (
|
||||
RBACPermission,
|
||||
RBACResourceScope,
|
||||
account_initialization_required,
|
||||
edit_permission_required,
|
||||
is_admin_or_owner_required,
|
||||
rbac_permission_required,
|
||||
setup_required,
|
||||
with_current_user,
|
||||
)
|
||||
@ -85,9 +88,10 @@ class AppSite(Resource):
|
||||
@setup_required
|
||||
@login_required
|
||||
@edit_permission_required
|
||||
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_RELEASE_AND_VERSION)
|
||||
@account_initialization_required
|
||||
@get_app_model
|
||||
@with_current_user
|
||||
@get_app_model
|
||||
def post(self, current_user: Account, app_model: App):
|
||||
args = AppSiteUpdatePayload.model_validate(console_ns.payload or {})
|
||||
site = db.session.scalar(select(Site).where(Site.app_id == app_model.id).limit(1))
|
||||
@ -134,9 +138,10 @@ class AppSiteAccessTokenReset(Resource):
|
||||
@setup_required
|
||||
@login_required
|
||||
@is_admin_or_owner_required
|
||||
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_RELEASE_AND_VERSION)
|
||||
@account_initialization_required
|
||||
@get_app_model
|
||||
@with_current_user
|
||||
@get_app_model
|
||||
def post(self, current_user: Account, app_model: App):
|
||||
site = db.session.scalar(select(Site).where(Site.app_id == app_model.id).limit(1))
|
||||
|
||||
|
||||
@ -8,7 +8,14 @@ from pydantic import BaseModel, Field, field_validator
|
||||
from controllers.common.schema import query_params_from_model, register_response_schema_models, register_schema_models
|
||||
from controllers.console import console_ns
|
||||
from controllers.console.app.wraps import get_app_model
|
||||
from controllers.console.wraps import account_initialization_required, setup_required, with_current_user
|
||||
from controllers.console.wraps import (
|
||||
RBACPermission,
|
||||
RBACResourceScope,
|
||||
account_initialization_required,
|
||||
rbac_permission_required,
|
||||
setup_required,
|
||||
with_current_user,
|
||||
)
|
||||
from core.app.entities.app_invoke_entities import InvokeFrom
|
||||
from extensions.ext_database import db
|
||||
from fields.base import ResponseModel
|
||||
@ -131,11 +138,12 @@ class DailyMessageStatistic(Resource):
|
||||
"Daily message statistics retrieved successfully",
|
||||
console_ns.models[DailyMessageStatisticResponse.__name__],
|
||||
)
|
||||
@get_app_model
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@with_current_user
|
||||
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_MONITOR)
|
||||
@get_app_model
|
||||
def get(self, account: Account, app_model: App):
|
||||
args = StatisticTimeRangeQuery.model_validate(request.args.to_dict(flat=True))
|
||||
|
||||
@ -191,11 +199,12 @@ class DailyConversationStatistic(Resource):
|
||||
"Daily conversation statistics retrieved successfully",
|
||||
console_ns.models[DailyConversationStatisticResponse.__name__],
|
||||
)
|
||||
@get_app_model
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@with_current_user
|
||||
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_MONITOR)
|
||||
@get_app_model
|
||||
def get(self, account: Account, app_model: App):
|
||||
args = StatisticTimeRangeQuery.model_validate(request.args.to_dict(flat=True))
|
||||
|
||||
@ -250,11 +259,12 @@ class DailyTerminalsStatistic(Resource):
|
||||
"Daily terminal statistics retrieved successfully",
|
||||
console_ns.models[DailyTerminalStatisticResponse.__name__],
|
||||
)
|
||||
@get_app_model
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@with_current_user
|
||||
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_MONITOR)
|
||||
@get_app_model
|
||||
def get(self, account: Account, app_model: App):
|
||||
args = StatisticTimeRangeQuery.model_validate(request.args.to_dict(flat=True))
|
||||
|
||||
@ -310,11 +320,12 @@ class DailyTokenCostStatistic(Resource):
|
||||
"Daily token cost statistics retrieved successfully",
|
||||
console_ns.models[DailyTokenCostStatisticResponse.__name__],
|
||||
)
|
||||
@get_app_model
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@with_current_user
|
||||
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_MONITOR)
|
||||
@get_app_model
|
||||
def get(self, account: Account, app_model: App):
|
||||
args = StatisticTimeRangeQuery.model_validate(request.args.to_dict(flat=True))
|
||||
|
||||
@ -376,8 +387,9 @@ class AverageSessionInteractionStatistic(Resource):
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@get_app_model(mode=[AppMode.CHAT, AppMode.AGENT_CHAT, AppMode.ADVANCED_CHAT, AppMode.AGENT])
|
||||
@with_current_user
|
||||
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_MONITOR)
|
||||
@get_app_model(mode=[AppMode.CHAT, AppMode.AGENT_CHAT, AppMode.ADVANCED_CHAT, AppMode.AGENT])
|
||||
def get(self, account: Account, app_model: App):
|
||||
args = StatisticTimeRangeQuery.model_validate(request.args.to_dict(flat=True))
|
||||
|
||||
@ -452,11 +464,12 @@ class UserSatisfactionRateStatistic(Resource):
|
||||
"User satisfaction rate statistics retrieved successfully",
|
||||
console_ns.models[UserSatisfactionRateStatisticResponse.__name__],
|
||||
)
|
||||
@get_app_model
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@with_current_user
|
||||
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_MONITOR)
|
||||
@get_app_model
|
||||
def get(self, account: Account, app_model: App):
|
||||
args = StatisticTimeRangeQuery.model_validate(request.args.to_dict(flat=True))
|
||||
|
||||
@ -524,8 +537,9 @@ class AverageResponseTimeStatistic(Resource):
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@get_app_model(mode=AppMode.COMPLETION)
|
||||
@with_current_user
|
||||
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_MONITOR)
|
||||
@get_app_model(mode=AppMode.COMPLETION)
|
||||
def get(self, account: Account, app_model: App):
|
||||
args = StatisticTimeRangeQuery.model_validate(request.args.to_dict(flat=True))
|
||||
|
||||
@ -581,11 +595,12 @@ class TokensPerSecondStatistic(Resource):
|
||||
"Tokens per second statistics retrieved successfully",
|
||||
console_ns.models[TokensPerSecondStatisticResponse.__name__],
|
||||
)
|
||||
@get_app_model
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@with_current_user
|
||||
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_MONITOR)
|
||||
@get_app_model
|
||||
def get(self, account: Account, app_model: App):
|
||||
args = StatisticTimeRangeQuery.model_validate(request.args.to_dict(flat=True))
|
||||
|
||||
|
||||
@ -26,10 +26,14 @@ from controllers.console.app.error import (
|
||||
DraftWorkflowNotExist,
|
||||
DraftWorkflowNotSync,
|
||||
)
|
||||
from controllers.console.app.permission_keys import get_app_permission_keys
|
||||
from controllers.console.app.wraps import get_app_model
|
||||
from controllers.console.wraps import (
|
||||
RBACPermission,
|
||||
RBACResourceScope,
|
||||
account_initialization_required,
|
||||
edit_permission_required,
|
||||
rbac_permission_required,
|
||||
setup_required,
|
||||
with_current_tenant_id,
|
||||
with_current_user,
|
||||
@ -436,8 +440,9 @@ class DraftWorkflowApi(Resource):
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@get_app_model(mode=[AppMode.ADVANCED_CHAT, AppMode.WORKFLOW])
|
||||
@edit_permission_required
|
||||
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_VIEW_LAYOUT)
|
||||
@get_app_model(mode=[AppMode.ADVANCED_CHAT, AppMode.WORKFLOW])
|
||||
def get(self, app_model: App):
|
||||
"""
|
||||
Get draft workflow
|
||||
@ -483,6 +488,7 @@ class DraftWorkflowApi(Resource):
|
||||
@console_ns.response(403, "Permission denied")
|
||||
@with_current_user
|
||||
@edit_permission_required
|
||||
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_VIEW_LAYOUT)
|
||||
def post(self, current_user: Account, app_model: App):
|
||||
"""
|
||||
Sync draft workflow
|
||||
@ -548,6 +554,7 @@ class AdvancedChatDraftWorkflowRunApi(Resource):
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_TEST_AND_RUN)
|
||||
@get_app_model(mode=[AppMode.ADVANCED_CHAT])
|
||||
@with_current_user
|
||||
@edit_permission_required
|
||||
@ -597,6 +604,7 @@ class AdvancedChatDraftRunIterationNodeApi(Resource):
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_TEST_AND_RUN)
|
||||
@get_app_model(mode=[AppMode.ADVANCED_CHAT])
|
||||
@with_current_user
|
||||
@edit_permission_required
|
||||
@ -639,6 +647,7 @@ class WorkflowDraftRunIterationNodeApi(Resource):
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_TEST_AND_RUN)
|
||||
@get_app_model(mode=[AppMode.WORKFLOW])
|
||||
@with_current_user
|
||||
@edit_permission_required
|
||||
@ -677,6 +686,7 @@ class AdvancedChatDraftRunLoopNodeApi(Resource):
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_TEST_AND_RUN)
|
||||
@get_app_model(mode=[AppMode.ADVANCED_CHAT])
|
||||
@with_current_user
|
||||
@edit_permission_required
|
||||
@ -719,6 +729,7 @@ class WorkflowDraftRunLoopNodeApi(Resource):
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_TEST_AND_RUN)
|
||||
@get_app_model(mode=[AppMode.WORKFLOW])
|
||||
@with_current_user
|
||||
@edit_permission_required
|
||||
@ -793,6 +804,7 @@ class AdvancedChatDraftHumanInputFormPreviewApi(Resource):
|
||||
@get_app_model(mode=[AppMode.ADVANCED_CHAT])
|
||||
@with_current_user
|
||||
@edit_permission_required
|
||||
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_VIEW_LAYOUT)
|
||||
def post(self, current_user: Account, app_model: App, node_id: str):
|
||||
"""
|
||||
Preview human input form content and placeholders
|
||||
@ -824,6 +836,7 @@ class AdvancedChatDraftHumanInputFormRunApi(Resource):
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_TEST_AND_RUN)
|
||||
@get_app_model(mode=[AppMode.ADVANCED_CHAT])
|
||||
@with_current_user
|
||||
@edit_permission_required
|
||||
@ -857,6 +870,7 @@ class WorkflowDraftHumanInputFormPreviewApi(Resource):
|
||||
@get_app_model(mode=[AppMode.WORKFLOW])
|
||||
@with_current_user
|
||||
@edit_permission_required
|
||||
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_VIEW_LAYOUT)
|
||||
def post(self, current_user: Account, app_model: App, node_id: str):
|
||||
"""
|
||||
Preview human input form content and placeholders
|
||||
@ -888,6 +902,7 @@ class WorkflowDraftHumanInputFormRunApi(Resource):
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_TEST_AND_RUN)
|
||||
@get_app_model(mode=[AppMode.WORKFLOW])
|
||||
@with_current_user
|
||||
@edit_permission_required
|
||||
@ -921,6 +936,7 @@ class WorkflowDraftHumanInputDeliveryTestApi(Resource):
|
||||
@get_app_model(mode=[AppMode.WORKFLOW, AppMode.ADVANCED_CHAT])
|
||||
@with_current_user
|
||||
@edit_permission_required
|
||||
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_TEST_AND_RUN)
|
||||
def post(self, current_user: Account, app_model: App, node_id: str):
|
||||
"""
|
||||
Test human input delivery
|
||||
@ -952,6 +968,7 @@ class DraftWorkflowRunApi(Resource):
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_TEST_AND_RUN)
|
||||
@get_app_model(mode=[AppMode.WORKFLOW])
|
||||
@with_current_user
|
||||
@edit_permission_required
|
||||
@ -990,8 +1007,9 @@ class WorkflowTaskStopApi(Resource):
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@get_app_model(mode=[AppMode.ADVANCED_CHAT, AppMode.WORKFLOW])
|
||||
@edit_permission_required
|
||||
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_TEST_AND_RUN)
|
||||
@get_app_model(mode=[AppMode.ADVANCED_CHAT, AppMode.WORKFLOW])
|
||||
def post(self, app_model: App, task_id: str):
|
||||
"""
|
||||
Stop workflow task
|
||||
@ -1022,6 +1040,7 @@ class DraftWorkflowNodeRunApi(Resource):
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_TEST_AND_RUN)
|
||||
@get_app_model(mode=[AppMode.ADVANCED_CHAT, AppMode.WORKFLOW])
|
||||
@with_current_user
|
||||
@edit_permission_required
|
||||
@ -1072,8 +1091,9 @@ class PublishedWorkflowApi(Resource):
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@get_app_model(mode=[AppMode.ADVANCED_CHAT, AppMode.WORKFLOW])
|
||||
@edit_permission_required
|
||||
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_VIEW_LAYOUT)
|
||||
@get_app_model(mode=[AppMode.ADVANCED_CHAT, AppMode.WORKFLOW])
|
||||
def get(self, app_model: App):
|
||||
"""
|
||||
Get published workflow
|
||||
@ -1093,6 +1113,7 @@ class PublishedWorkflowApi(Resource):
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_RELEASE_AND_VERSION)
|
||||
@get_app_model(mode=[AppMode.ADVANCED_CHAT, AppMode.WORKFLOW])
|
||||
@with_current_user
|
||||
@edit_permission_required
|
||||
@ -1141,8 +1162,9 @@ class DefaultBlockConfigsApi(Resource):
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@get_app_model(mode=[AppMode.ADVANCED_CHAT, AppMode.WORKFLOW])
|
||||
@edit_permission_required
|
||||
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_VIEW_LAYOUT)
|
||||
@get_app_model(mode=[AppMode.ADVANCED_CHAT, AppMode.WORKFLOW])
|
||||
def get(self, app_model: App):
|
||||
"""
|
||||
Get default block config
|
||||
@ -1167,8 +1189,9 @@ class DefaultBlockConfigApi(Resource):
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@get_app_model(mode=[AppMode.ADVANCED_CHAT, AppMode.WORKFLOW])
|
||||
@edit_permission_required
|
||||
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_VIEW_LAYOUT)
|
||||
@get_app_model(mode=[AppMode.ADVANCED_CHAT, AppMode.WORKFLOW])
|
||||
def get(self, app_model: App, block_type: str):
|
||||
"""
|
||||
Get default block config
|
||||
@ -1205,14 +1228,15 @@ class ConvertToWorkflowApi(Resource):
|
||||
@account_initialization_required
|
||||
@get_app_model(mode=[AppMode.CHAT, AppMode.COMPLETION])
|
||||
@with_current_user
|
||||
@with_current_tenant_id
|
||||
@edit_permission_required
|
||||
def post(self, current_user: Account, app_model: App):
|
||||
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_EDIT)
|
||||
def post(self, current_tenant_id: str, current_user: Account, app_model: App):
|
||||
"""
|
||||
Convert basic mode of chatbot app to workflow mode
|
||||
Convert expert mode of chatbot app to workflow mode
|
||||
Convert Completion App to Workflow App
|
||||
"""
|
||||
|
||||
payload = console_ns.payload or {}
|
||||
args = ConvertToWorkflowPayload.model_validate(payload).model_dump(exclude_none=True)
|
||||
|
||||
@ -1223,6 +1247,7 @@ class ConvertToWorkflowApi(Resource):
|
||||
# return app id
|
||||
return {
|
||||
"new_app_id": new_app_model.id,
|
||||
"permission_keys": get_app_permission_keys(str(current_tenant_id), current_user.id, str(new_app_model.id)),
|
||||
}
|
||||
|
||||
|
||||
@ -1245,6 +1270,7 @@ class WorkflowFeaturesApi(Resource):
|
||||
@get_app_model(mode=[AppMode.ADVANCED_CHAT, AppMode.WORKFLOW])
|
||||
@with_current_user
|
||||
@edit_permission_required
|
||||
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_VIEW_LAYOUT)
|
||||
def post(self, current_user: Account, app_model: App):
|
||||
|
||||
args = WorkflowFeaturesPayload.model_validate(console_ns.payload or {})
|
||||
@ -1270,6 +1296,7 @@ class PublishedAllWorkflowApi(Resource):
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_VIEW_LAYOUT)
|
||||
@get_app_model(mode=[AppMode.ADVANCED_CHAT, AppMode.WORKFLOW])
|
||||
@with_current_user
|
||||
@edit_permission_required
|
||||
@ -1322,6 +1349,7 @@ class DraftWorkflowRestoreApi(Resource):
|
||||
@get_app_model(mode=[AppMode.ADVANCED_CHAT, AppMode.WORKFLOW])
|
||||
@with_current_user
|
||||
@edit_permission_required
|
||||
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_RELEASE_AND_VERSION)
|
||||
def post(self, current_user: Account, app_model: App, workflow_id: str):
|
||||
workflow_service = WorkflowService()
|
||||
|
||||
@ -1360,6 +1388,7 @@ class WorkflowByIdApi(Resource):
|
||||
@get_app_model(mode=[AppMode.ADVANCED_CHAT, AppMode.WORKFLOW])
|
||||
@with_current_user
|
||||
@edit_permission_required
|
||||
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_EDIT)
|
||||
def patch(self, current_user: Account, app_model: App, workflow_id: str):
|
||||
"""
|
||||
Update workflow attributes
|
||||
@ -1398,6 +1427,7 @@ class WorkflowByIdApi(Resource):
|
||||
@account_initialization_required
|
||||
@get_app_model(mode=[AppMode.ADVANCED_CHAT, AppMode.WORKFLOW])
|
||||
@edit_permission_required
|
||||
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_EDIT)
|
||||
@console_ns.response(204, "Workflow deleted successfully")
|
||||
def delete(self, app_model: App, workflow_id: str):
|
||||
"""
|
||||
@ -1436,6 +1466,7 @@ class DraftWorkflowNodeLastRunApi(Resource):
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_VIEW_LAYOUT)
|
||||
@get_app_model(mode=[AppMode.ADVANCED_CHAT, AppMode.WORKFLOW])
|
||||
def get(self, app_model: App, node_id: str):
|
||||
srv = WorkflowService()
|
||||
@ -1480,6 +1511,7 @@ class DraftWorkflowTriggerRunApi(Resource):
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_TEST_AND_RUN)
|
||||
@get_app_model(mode=[AppMode.WORKFLOW])
|
||||
@with_current_user
|
||||
@edit_permission_required
|
||||
@ -1548,6 +1580,7 @@ class DraftWorkflowTriggerNodeApi(Resource):
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_TEST_AND_RUN)
|
||||
@get_app_model(mode=[AppMode.WORKFLOW])
|
||||
@with_current_user
|
||||
@edit_permission_required
|
||||
@ -1631,6 +1664,7 @@ class DraftWorkflowTriggerRunAllApi(Resource):
|
||||
@get_app_model(mode=[AppMode.WORKFLOW])
|
||||
@with_current_user
|
||||
@edit_permission_required
|
||||
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_TEST_AND_RUN)
|
||||
def post(self, current_user: Account, app_model: App):
|
||||
"""
|
||||
Full workflow debug when the start node is a trigger
|
||||
|
||||
@ -10,7 +10,13 @@ from sqlalchemy.orm import sessionmaker
|
||||
from controllers.common.schema import query_params_from_model, register_schema_models
|
||||
from controllers.console import console_ns
|
||||
from controllers.console.app.wraps import get_app_model
|
||||
from controllers.console.wraps import account_initialization_required, setup_required
|
||||
from controllers.console.wraps import (
|
||||
RBACPermission,
|
||||
RBACResourceScope,
|
||||
account_initialization_required,
|
||||
rbac_permission_required,
|
||||
setup_required,
|
||||
)
|
||||
from extensions.ext_database import db
|
||||
from fields.base import ResponseModel
|
||||
from fields.end_user_fields import SimpleEndUser
|
||||
@ -175,6 +181,7 @@ class WorkflowAppLogApi(Resource):
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_MONITOR)
|
||||
@get_app_model(mode=[AppMode.WORKFLOW])
|
||||
def get(self, app_model: App):
|
||||
"""
|
||||
@ -218,6 +225,7 @@ class WorkflowArchivedLogApi(Resource):
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_MONITOR)
|
||||
@get_app_model(mode=[AppMode.WORKFLOW])
|
||||
def get(self, app_model: App):
|
||||
"""
|
||||
|
||||
@ -8,8 +8,11 @@ from controllers.common.schema import register_response_schema_models, register_
|
||||
from controllers.console import console_ns
|
||||
from controllers.console.app.wraps import get_app_model
|
||||
from controllers.console.wraps import (
|
||||
RBACPermission,
|
||||
RBACResourceScope,
|
||||
account_initialization_required,
|
||||
edit_permission_required,
|
||||
rbac_permission_required,
|
||||
setup_required,
|
||||
with_current_tenant_id,
|
||||
with_current_user,
|
||||
@ -218,8 +221,9 @@ class WorkflowCommentListApi(Resource):
|
||||
@login_required
|
||||
@setup_required
|
||||
@account_initialization_required
|
||||
@get_app_model()
|
||||
@with_current_tenant_id
|
||||
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_VIEW_LAYOUT)
|
||||
@get_app_model()
|
||||
def get(self, current_tenant_id: str, app_model: App):
|
||||
"""Get all comments for a workflow."""
|
||||
comments = WorkflowCommentService.get_comments(tenant_id=current_tenant_id, app_id=app_model.id)
|
||||
@ -234,10 +238,11 @@ class WorkflowCommentListApi(Resource):
|
||||
@login_required
|
||||
@setup_required
|
||||
@account_initialization_required
|
||||
@get_app_model()
|
||||
@edit_permission_required
|
||||
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_EDIT)
|
||||
@with_current_user
|
||||
@with_current_tenant_id
|
||||
@get_app_model()
|
||||
def post(self, current_tenant_id: str, current_user: Account, app_model: App):
|
||||
"""Create a new workflow comment."""
|
||||
payload = WorkflowCommentCreatePayload.model_validate(console_ns.payload or {})
|
||||
@ -266,8 +271,9 @@ class WorkflowCommentDetailApi(Resource):
|
||||
@login_required
|
||||
@setup_required
|
||||
@account_initialization_required
|
||||
@get_app_model()
|
||||
@with_current_tenant_id
|
||||
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_VIEW_LAYOUT)
|
||||
@get_app_model()
|
||||
def get(self, current_tenant_id: str, app_model: App, comment_id: str):
|
||||
"""Get a specific workflow comment."""
|
||||
comment = WorkflowCommentService.get_comment(
|
||||
@ -284,10 +290,11 @@ class WorkflowCommentDetailApi(Resource):
|
||||
@login_required
|
||||
@setup_required
|
||||
@account_initialization_required
|
||||
@get_app_model()
|
||||
@edit_permission_required
|
||||
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_EDIT)
|
||||
@with_current_user
|
||||
@with_current_tenant_id
|
||||
@get_app_model()
|
||||
def put(self, current_tenant_id: str, current_user: Account, app_model: App, comment_id: str):
|
||||
"""Update a workflow comment."""
|
||||
payload = WorkflowCommentUpdatePayload.model_validate(console_ns.payload or {})
|
||||
@ -312,10 +319,11 @@ class WorkflowCommentDetailApi(Resource):
|
||||
@login_required
|
||||
@setup_required
|
||||
@account_initialization_required
|
||||
@get_app_model()
|
||||
@edit_permission_required
|
||||
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_EDIT)
|
||||
@with_current_user
|
||||
@with_current_tenant_id
|
||||
@get_app_model()
|
||||
def delete(self, current_tenant_id: str, current_user: Account, app_model: App, comment_id: str):
|
||||
"""Delete a workflow comment."""
|
||||
WorkflowCommentService.delete_comment(
|
||||
@ -339,10 +347,11 @@ class WorkflowCommentResolveApi(Resource):
|
||||
@login_required
|
||||
@setup_required
|
||||
@account_initialization_required
|
||||
@get_app_model()
|
||||
@edit_permission_required
|
||||
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_EDIT)
|
||||
@with_current_user
|
||||
@with_current_tenant_id
|
||||
@get_app_model()
|
||||
def post(self, current_tenant_id: str, current_user: Account, app_model: App, comment_id: str):
|
||||
"""Resolve a workflow comment."""
|
||||
comment = WorkflowCommentService.resolve_comment(
|
||||
@ -367,10 +376,11 @@ class WorkflowCommentReplyApi(Resource):
|
||||
@login_required
|
||||
@setup_required
|
||||
@account_initialization_required
|
||||
@get_app_model()
|
||||
@edit_permission_required
|
||||
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_EDIT)
|
||||
@with_current_user
|
||||
@with_current_tenant_id
|
||||
@get_app_model()
|
||||
def post(self, current_tenant_id: str, current_user: Account, app_model: App, comment_id: str):
|
||||
"""Add a reply to a workflow comment."""
|
||||
# Validate comment access first
|
||||
@ -402,10 +412,11 @@ class WorkflowCommentReplyDetailApi(Resource):
|
||||
@login_required
|
||||
@setup_required
|
||||
@account_initialization_required
|
||||
@get_app_model()
|
||||
@edit_permission_required
|
||||
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_EDIT)
|
||||
@with_current_user
|
||||
@with_current_tenant_id
|
||||
@get_app_model()
|
||||
def put(self, current_tenant_id: str, current_user: Account, app_model: App, comment_id: str, reply_id: str):
|
||||
"""Update a comment reply."""
|
||||
# Validate comment access first
|
||||
@ -434,10 +445,11 @@ class WorkflowCommentReplyDetailApi(Resource):
|
||||
@login_required
|
||||
@setup_required
|
||||
@account_initialization_required
|
||||
@get_app_model()
|
||||
@edit_permission_required
|
||||
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_EDIT)
|
||||
@with_current_user
|
||||
@with_current_tenant_id
|
||||
@get_app_model()
|
||||
def delete(self, current_tenant_id: str, current_user: Account, app_model: App, comment_id: str, reply_id: str):
|
||||
"""Delete a comment reply."""
|
||||
# Validate comment access first
|
||||
@ -469,8 +481,9 @@ class WorkflowCommentMentionUsersApi(Resource):
|
||||
@login_required
|
||||
@setup_required
|
||||
@account_initialization_required
|
||||
@get_app_model()
|
||||
@with_current_user
|
||||
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_VIEW_LAYOUT)
|
||||
@get_app_model()
|
||||
def get(self, current_user: Account, app_model: App):
|
||||
"""Get all users in current tenant for mentions."""
|
||||
current_tenant = current_user.current_tenant # need the tenant object here
|
||||
|
||||
@ -18,8 +18,11 @@ from controllers.console.app.error import (
|
||||
)
|
||||
from controllers.console.app.wraps import get_app_model
|
||||
from controllers.console.wraps import (
|
||||
RBACPermission,
|
||||
RBACResourceScope,
|
||||
account_initialization_required,
|
||||
edit_permission_required,
|
||||
rbac_permission_required,
|
||||
setup_required,
|
||||
with_current_user,
|
||||
)
|
||||
@ -283,6 +286,7 @@ def _api_prerequisite[T, **P, R](
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@edit_permission_required
|
||||
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_VIEW_LAYOUT)
|
||||
@get_app_model(mode=[AppMode.ADVANCED_CHAT, AppMode.WORKFLOW])
|
||||
@with_current_user
|
||||
@wraps(f)
|
||||
@ -304,6 +308,7 @@ class WorkflowVariableCollectionApi(Resource):
|
||||
)
|
||||
@_api_prerequisite
|
||||
@marshal_with(workflow_draft_variable_list_without_value_model)
|
||||
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_VIEW_LAYOUT)
|
||||
def get(self, current_user: Account, app_model: App):
|
||||
"""
|
||||
Get draft workflow
|
||||
@ -368,6 +373,7 @@ class NodeVariableCollectionApi(Resource):
|
||||
@console_ns.response(200, "Node variables retrieved successfully", workflow_draft_variable_list_model)
|
||||
@_api_prerequisite
|
||||
@marshal_with(workflow_draft_variable_list_model)
|
||||
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_VIEW_LAYOUT)
|
||||
def get(self, current_user: Account, app_model: App, node_id: str):
|
||||
validate_node_id(node_id)
|
||||
with sessionmaker(bind=db.engine, expire_on_commit=False).begin() as session:
|
||||
@ -402,6 +408,7 @@ class VariableApi(Resource):
|
||||
@console_ns.response(404, "Variable not found")
|
||||
@_api_prerequisite
|
||||
@marshal_with(workflow_draft_variable_model)
|
||||
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_VIEW_LAYOUT)
|
||||
def get(self, current_user: Account, app_model: App, variable_id: UUID):
|
||||
draft_var_srv = WorkflowDraftVariableService(
|
||||
session=db.session(),
|
||||
@ -574,6 +581,7 @@ class ConversationVariableCollectionApi(Resource):
|
||||
@console_ns.response(404, "Draft workflow not found")
|
||||
@_api_prerequisite
|
||||
@marshal_with(workflow_draft_variable_list_model)
|
||||
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_VIEW_LAYOUT)
|
||||
def get(self, current_user: Account, app_model: App):
|
||||
# NOTE(QuantumGhost): Prefill conversation variables into the draft variables table
|
||||
# so their IDs can be returned to the caller.
|
||||
@ -599,8 +607,9 @@ class ConversationVariableCollectionApi(Resource):
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@edit_permission_required
|
||||
@get_app_model(mode=AppMode.ADVANCED_CHAT)
|
||||
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_EDIT)
|
||||
@with_current_user
|
||||
@get_app_model(mode=AppMode.ADVANCED_CHAT)
|
||||
def post(self, current_user: Account, app_model: App):
|
||||
payload = ConversationVariableUpdatePayload.model_validate(console_ns.payload or {})
|
||||
|
||||
@ -628,6 +637,7 @@ class SystemVariableCollectionApi(Resource):
|
||||
@console_ns.response(200, "System variables retrieved successfully", workflow_draft_variable_list_model)
|
||||
@_api_prerequisite
|
||||
@marshal_with(workflow_draft_variable_list_model)
|
||||
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_VIEW_LAYOUT)
|
||||
def get(self, current_user: Account, app_model: App):
|
||||
return _get_variable_list(app_model, SYSTEM_VARIABLE_NODE_ID, current_user.id)
|
||||
|
||||
@ -644,6 +654,7 @@ class EnvironmentVariableCollectionApi(Resource):
|
||||
)
|
||||
@console_ns.response(404, "Draft workflow not found")
|
||||
@_api_prerequisite
|
||||
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_VIEW_LAYOUT)
|
||||
def get(self, _current_user: Account, app_model: App):
|
||||
"""
|
||||
Get draft workflow
|
||||
@ -688,8 +699,9 @@ class EnvironmentVariableCollectionApi(Resource):
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@edit_permission_required
|
||||
@get_app_model(mode=[AppMode.ADVANCED_CHAT, AppMode.WORKFLOW])
|
||||
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_EDIT)
|
||||
@with_current_user
|
||||
@get_app_model(mode=[AppMode.ADVANCED_CHAT, AppMode.WORKFLOW])
|
||||
def post(self, current_user: Account, app_model: App):
|
||||
payload = EnvironmentVariableUpdatePayload.model_validate(console_ns.payload or {})
|
||||
|
||||
|
||||
@ -34,7 +34,13 @@ from controllers.common.fields import EventStreamResponse
|
||||
from controllers.common.schema import register_response_schema_models
|
||||
from controllers.console import console_ns
|
||||
from controllers.console.app.wraps import get_app_model
|
||||
from controllers.console.wraps import account_initialization_required, setup_required
|
||||
from controllers.console.wraps import (
|
||||
RBACPermission,
|
||||
RBACResourceScope,
|
||||
account_initialization_required,
|
||||
rbac_permission_required,
|
||||
setup_required,
|
||||
)
|
||||
from libs.exception import BaseHTTPException
|
||||
from libs.login import login_required
|
||||
from models import App, AppMode
|
||||
@ -146,6 +152,7 @@ class WorkflowDraftRunNodeOutputsApi(Resource):
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_VIEW_LAYOUT)
|
||||
@get_app_model(mode=[AppMode.ADVANCED_CHAT, AppMode.WORKFLOW])
|
||||
def get(self, app_model: App, run_id: UUID):
|
||||
return _serve_snapshot(app_model, run_id)
|
||||
@ -169,6 +176,7 @@ class WorkflowDraftRunNodeOutputDetailApi(Resource):
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_VIEW_LAYOUT)
|
||||
@get_app_model(mode=[AppMode.ADVANCED_CHAT, AppMode.WORKFLOW])
|
||||
def get(self, app_model: App, run_id: UUID, node_id: str):
|
||||
return _serve_node_detail(app_model, run_id, node_id)
|
||||
@ -195,6 +203,7 @@ class WorkflowDraftRunNodeOutputPreviewApi(Resource):
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_VIEW_LAYOUT)
|
||||
@get_app_model(mode=[AppMode.ADVANCED_CHAT, AppMode.WORKFLOW])
|
||||
def get(self, app_model: App, run_id: UUID, node_id: str, output_name: str):
|
||||
return _serve_output_preview(app_model, run_id, node_id, output_name)
|
||||
@ -338,6 +347,7 @@ class WorkflowDraftRunNodeOutputEventsApi(Resource):
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_VIEW_LAYOUT)
|
||||
@get_app_model(mode=[AppMode.ADVANCED_CHAT, AppMode.WORKFLOW])
|
||||
def get(self, app_model: App, run_id: UUID):
|
||||
return Response(
|
||||
@ -368,6 +378,7 @@ class WorkflowPublishedRunNodeOutputsApi(Resource):
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_VIEW_LAYOUT)
|
||||
@get_app_model(mode=[AppMode.ADVANCED_CHAT, AppMode.WORKFLOW])
|
||||
def get(self, app_model: App, run_id: UUID):
|
||||
return _serve_snapshot(app_model, run_id)
|
||||
@ -391,6 +402,7 @@ class WorkflowPublishedRunNodeOutputDetailApi(Resource):
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_VIEW_LAYOUT)
|
||||
@get_app_model(mode=[AppMode.ADVANCED_CHAT, AppMode.WORKFLOW])
|
||||
def get(self, app_model: App, run_id: UUID, node_id: str):
|
||||
return _serve_node_detail(app_model, run_id, node_id)
|
||||
@ -418,6 +430,7 @@ class WorkflowPublishedRunNodeOutputPreviewApi(Resource):
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_VIEW_LAYOUT)
|
||||
@get_app_model(mode=[AppMode.ADVANCED_CHAT, AppMode.WORKFLOW])
|
||||
def get(self, app_model: App, run_id: UUID, node_id: str, output_name: str):
|
||||
return _serve_output_preview(app_model, run_id, node_id, output_name)
|
||||
@ -439,6 +452,7 @@ class WorkflowPublishedRunNodeOutputEventsApi(Resource):
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_VIEW_LAYOUT)
|
||||
@get_app_model(mode=[AppMode.ADVANCED_CHAT, AppMode.WORKFLOW])
|
||||
def get(self, app_model: App, run_id: UUID):
|
||||
return Response(
|
||||
|
||||
@ -14,7 +14,10 @@ from controllers.common.schema import query_params_from_model, register_response
|
||||
from controllers.console import console_ns
|
||||
from controllers.console.app.wraps import get_app_model
|
||||
from controllers.console.wraps import (
|
||||
RBACPermission,
|
||||
RBACResourceScope,
|
||||
account_initialization_required,
|
||||
rbac_permission_required,
|
||||
setup_required,
|
||||
with_current_tenant_id,
|
||||
with_current_user,
|
||||
@ -155,6 +158,7 @@ class AdvancedChatAppWorkflowRunListApi(Resource):
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_CREATE_AND_MANAGEMENT)
|
||||
@get_app_model(mode=[AppMode.ADVANCED_CHAT])
|
||||
def get(self, app_model: App):
|
||||
"""
|
||||
@ -193,6 +197,7 @@ class WorkflowRunExportApi(Resource):
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_CREATE_AND_MANAGEMENT)
|
||||
@get_app_model()
|
||||
def get(self, app_model: App, run_id: UUID):
|
||||
tenant_id = app_model.tenant_id
|
||||
@ -251,6 +256,7 @@ class AdvancedChatAppWorkflowRunCountApi(Resource):
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_CREATE_AND_MANAGEMENT)
|
||||
@get_app_model(mode=[AppMode.ADVANCED_CHAT])
|
||||
def get(self, app_model: App):
|
||||
"""
|
||||
@ -291,6 +297,7 @@ class WorkflowRunListApi(Resource):
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_CREATE_AND_MANAGEMENT)
|
||||
@get_app_model(mode=[AppMode.ADVANCED_CHAT, AppMode.WORKFLOW])
|
||||
def get(self, app_model: App):
|
||||
"""
|
||||
@ -332,6 +339,7 @@ class WorkflowRunCountApi(Resource):
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_CREATE_AND_MANAGEMENT)
|
||||
@get_app_model(mode=[AppMode.ADVANCED_CHAT, AppMode.WORKFLOW])
|
||||
def get(self, app_model: App):
|
||||
"""
|
||||
@ -372,6 +380,7 @@ class WorkflowRunDetailApi(Resource):
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_CREATE_AND_MANAGEMENT)
|
||||
@get_app_model(mode=[AppMode.ADVANCED_CHAT, AppMode.WORKFLOW])
|
||||
def get(self, app_model: App, run_id: UUID):
|
||||
"""
|
||||
@ -401,8 +410,9 @@ class WorkflowRunNodeExecutionListApi(Resource):
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@get_app_model(mode=[AppMode.ADVANCED_CHAT, AppMode.WORKFLOW])
|
||||
@with_current_user
|
||||
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_CREATE_AND_MANAGEMENT)
|
||||
@get_app_model(mode=[AppMode.ADVANCED_CHAT, AppMode.WORKFLOW])
|
||||
def get(self, current_user: Account, app_model: App, run_id: UUID):
|
||||
"""
|
||||
Get workflow run node execution list
|
||||
|
||||
@ -6,7 +6,14 @@ from sqlalchemy.orm import sessionmaker
|
||||
from controllers.common.schema import query_params_from_model, register_response_schema_models, register_schema_models
|
||||
from controllers.console import console_ns
|
||||
from controllers.console.app.wraps import get_app_model
|
||||
from controllers.console.wraps import account_initialization_required, setup_required, with_current_user
|
||||
from controllers.console.wraps import (
|
||||
RBACPermission,
|
||||
RBACResourceScope,
|
||||
account_initialization_required,
|
||||
rbac_permission_required,
|
||||
setup_required,
|
||||
with_current_user,
|
||||
)
|
||||
from extensions.ext_database import db
|
||||
from fields.base import ResponseModel
|
||||
from libs.datetime_utils import parse_time_range
|
||||
@ -91,11 +98,12 @@ class WorkflowDailyRunsStatistic(Resource):
|
||||
"Daily runs statistics retrieved successfully",
|
||||
console_ns.models[WorkflowDailyRunsStatisticResponse.__name__],
|
||||
)
|
||||
@get_app_model
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@with_current_user
|
||||
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_MONITOR)
|
||||
@get_app_model
|
||||
def get(self, account: Account, app_model: App):
|
||||
args = WorkflowStatisticQuery.model_validate(request.args.to_dict(flat=True))
|
||||
|
||||
@ -134,11 +142,12 @@ class WorkflowDailyTerminalsStatistic(Resource):
|
||||
"Daily terminals statistics retrieved successfully",
|
||||
console_ns.models[WorkflowDailyTerminalsStatisticResponse.__name__],
|
||||
)
|
||||
@get_app_model
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@with_current_user
|
||||
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_MONITOR)
|
||||
@get_app_model
|
||||
def get(self, account: Account, app_model: App):
|
||||
args = WorkflowStatisticQuery.model_validate(request.args.to_dict(flat=True))
|
||||
|
||||
@ -177,11 +186,12 @@ class WorkflowDailyTokenCostStatistic(Resource):
|
||||
"Daily token cost statistics retrieved successfully",
|
||||
console_ns.models[WorkflowDailyTokenCostStatisticResponse.__name__],
|
||||
)
|
||||
@get_app_model
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@with_current_user
|
||||
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_MONITOR)
|
||||
@get_app_model
|
||||
def get(self, account: Account, app_model: App):
|
||||
args = WorkflowStatisticQuery.model_validate(request.args.to_dict(flat=True))
|
||||
|
||||
@ -223,8 +233,9 @@ class WorkflowAverageAppInteractionStatistic(Resource):
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@get_app_model(mode=[AppMode.WORKFLOW])
|
||||
@with_current_user
|
||||
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_MONITOR)
|
||||
@get_app_model(mode=[AppMode.WORKFLOW])
|
||||
def get(self, account: Account, app_model: App):
|
||||
args = WorkflowStatisticQuery.model_validate(request.args.to_dict(flat=True))
|
||||
|
||||
|
||||
@ -19,7 +19,15 @@ from models.trigger import AppTrigger, WorkflowWebhookTrigger
|
||||
|
||||
from .. import console_ns
|
||||
from ..app.wraps import get_app_model
|
||||
from ..wraps import account_initialization_required, edit_permission_required, setup_required, with_current_tenant_id
|
||||
from ..wraps import (
|
||||
RBACPermission,
|
||||
RBACResourceScope,
|
||||
account_initialization_required,
|
||||
edit_permission_required,
|
||||
rbac_permission_required,
|
||||
setup_required,
|
||||
with_current_tenant_id,
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@ -90,8 +98,9 @@ class WebhookTriggerApi(Resource):
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@get_app_model(mode=AppMode.WORKFLOW)
|
||||
@console_ns.response(200, "Success", console_ns.models[WebhookTriggerResponse.__name__])
|
||||
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_VIEW_LAYOUT)
|
||||
@get_app_model(mode=AppMode.WORKFLOW)
|
||||
def get(self, app_model: App):
|
||||
"""Get webhook trigger for a node"""
|
||||
args = Parser.model_validate(request.args.to_dict(flat=True))
|
||||
@ -122,9 +131,10 @@ class AppTriggersApi(Resource):
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@get_app_model(mode=AppMode.WORKFLOW)
|
||||
@console_ns.response(200, "Success", console_ns.models[WorkflowTriggerListResponse.__name__])
|
||||
@with_current_tenant_id
|
||||
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_VIEW_LAYOUT)
|
||||
@get_app_model(mode=AppMode.WORKFLOW)
|
||||
def get(self, current_tenant_id: str, app_model: App):
|
||||
"""Get app triggers list"""
|
||||
with sessionmaker(db.engine, expire_on_commit=False).begin() as session:
|
||||
@ -162,9 +172,10 @@ class AppTriggerEnableApi(Resource):
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@edit_permission_required
|
||||
@get_app_model(mode=AppMode.WORKFLOW)
|
||||
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_EDIT)
|
||||
@console_ns.response(200, "Success", console_ns.models[WorkflowTriggerResponse.__name__])
|
||||
@with_current_tenant_id
|
||||
@get_app_model(mode=AppMode.WORKFLOW)
|
||||
def post(self, current_tenant_id: str, app_model: App):
|
||||
"""Update app trigger (enable/disable)"""
|
||||
args = ParserEnable.model_validate(console_ns.payload)
|
||||
|
||||
@ -11,7 +11,15 @@ from services.auth.api_key_auth_service import ApiKeyAuthService
|
||||
|
||||
from .. import console_ns
|
||||
from ..auth.error import ApiKeyAuthFailedError
|
||||
from ..wraps import account_initialization_required, is_admin_or_owner_required, setup_required, with_current_tenant_id
|
||||
from ..wraps import (
|
||||
RBACPermission,
|
||||
RBACResourceScope,
|
||||
account_initialization_required,
|
||||
is_admin_or_owner_required,
|
||||
rbac_permission_required,
|
||||
setup_required,
|
||||
with_current_tenant_id,
|
||||
)
|
||||
|
||||
|
||||
class ApiKeyAuthBindingPayload(BaseModel):
|
||||
@ -75,6 +83,7 @@ class ApiKeyAuthDataSourceBinding(Resource):
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@is_admin_or_owner_required
|
||||
@rbac_permission_required(RBACResourceScope.WORKSPACE, RBACPermission.CREDENTIAL_MANAGE, resource_required=False)
|
||||
@console_ns.expect(console_ns.models[ApiKeyAuthBindingPayload.__name__])
|
||||
@with_current_tenant_id
|
||||
def post(self, current_tenant_id: str):
|
||||
@ -95,6 +104,7 @@ class ApiKeyAuthDataSourceBindingDelete(Resource):
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@is_admin_or_owner_required
|
||||
@rbac_permission_required(RBACResourceScope.WORKSPACE, RBACPermission.CREDENTIAL_MANAGE, resource_required=False)
|
||||
@console_ns.response(204, "Binding deleted successfully")
|
||||
@with_current_tenant_id
|
||||
def delete(self, current_tenant_id: str, binding_id: UUID):
|
||||
|
||||
@ -13,7 +13,14 @@ from libs.login import login_required
|
||||
from libs.oauth_data_source import NotionOAuth
|
||||
|
||||
from .. import console_ns
|
||||
from ..wraps import account_initialization_required, is_admin_or_owner_required, setup_required
|
||||
from ..wraps import (
|
||||
RBACPermission,
|
||||
RBACResourceScope,
|
||||
account_initialization_required,
|
||||
is_admin_or_owner_required,
|
||||
rbac_permission_required,
|
||||
setup_required,
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@ -75,6 +82,7 @@ class OAuthDataSource(Resource):
|
||||
@console_ns.response(400, "Invalid provider")
|
||||
@console_ns.response(403, "Admin privileges required")
|
||||
@is_admin_or_owner_required
|
||||
@rbac_permission_required(RBACResourceScope.WORKSPACE, RBACPermission.CREDENTIAL_MANAGE, resource_required=False)
|
||||
def get(self, provider: str):
|
||||
# The role of the current user in the table must be admin or owner
|
||||
OAUTH_DATASOURCE_PROVIDERS = get_oauth_providers()
|
||||
|
||||
@ -31,7 +31,15 @@ from services.datasource_provider_service import DatasourceProviderService
|
||||
from tasks.document_indexing_sync_task import document_indexing_sync_task
|
||||
|
||||
from .. import console_ns
|
||||
from ..wraps import account_initialization_required, setup_required, with_current_tenant_id, with_current_user
|
||||
from ..wraps import (
|
||||
RBACPermission,
|
||||
RBACResourceScope,
|
||||
account_initialization_required,
|
||||
rbac_permission_required,
|
||||
setup_required,
|
||||
with_current_tenant_id,
|
||||
with_current_user,
|
||||
)
|
||||
|
||||
|
||||
class NotionEstimatePayload(BaseModel):
|
||||
@ -390,6 +398,7 @@ class DataSourceNotionDatasetSyncApi(Resource):
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@console_ns.response(200, "Success", console_ns.models[SimpleResultResponse.__name__])
|
||||
@rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_CREATE_AND_MANAGEMENT)
|
||||
def get(self, dataset_id: UUID) -> tuple[dict[str, str], int]:
|
||||
dataset_id_str = str(dataset_id)
|
||||
dataset = DatasetService.get_dataset(dataset_id_str)
|
||||
@ -408,6 +417,7 @@ class DataSourceNotionDocumentSyncApi(Resource):
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@console_ns.response(200, "Success", console_ns.models[SimpleResultResponse.__name__])
|
||||
@rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_CREATE_AND_MANAGEMENT)
|
||||
def get(self, dataset_id: UUID, document_id: UUID) -> tuple[dict[str, str], int]:
|
||||
dataset_id_str = str(dataset_id)
|
||||
document_id_str = str(document_id)
|
||||
|
||||
@ -17,10 +17,13 @@ from controllers.console.apikey import ApiKeyItem, ApiKeyList
|
||||
from controllers.console.app.error import ProviderNotInitializeError
|
||||
from controllers.console.datasets.error import DatasetInUseError, DatasetNameDuplicateError, IndexingEstimateError
|
||||
from controllers.console.wraps import (
|
||||
RBACPermission,
|
||||
RBACResourceScope,
|
||||
account_initialization_required,
|
||||
cloud_edition_billing_rate_limit_check,
|
||||
enterprise_license_required,
|
||||
is_admin_or_owner_required,
|
||||
rbac_permission_required,
|
||||
setup_required,
|
||||
with_current_tenant_id,
|
||||
with_current_user,
|
||||
@ -46,9 +49,16 @@ from models.enums import ApiTokenType, SegmentStatus
|
||||
from models.provider_ids import ModelProviderID
|
||||
from services.api_token_service import ApiTokenCache
|
||||
from services.dataset_service import DatasetPermissionService, DatasetService, DocumentService
|
||||
from services.enterprise import rbac_service as enterprise_rbac_service
|
||||
|
||||
register_response_schema_models(console_ns, ApiBaseUrlResponse, SimpleResultResponse, UsageCheckResponse)
|
||||
|
||||
DATASET_LIST_PERMISSION_KEYS = frozenset({"dataset.preview", "dataset.acl.preview", "dataset.full_access"})
|
||||
|
||||
|
||||
def _has_dataset_list_permission(permission_keys: list[str]) -> bool:
|
||||
return any(permission_key in DATASET_LIST_PERMISSION_KEYS for permission_key in permission_keys)
|
||||
|
||||
|
||||
def _validate_indexing_technique(value: str | None) -> str | None:
|
||||
if value is None:
|
||||
@ -402,9 +412,49 @@ class DatasetListApi(Resource):
|
||||
if "tag_ids" in request.args:
|
||||
query_params["tag_ids"] = request.args.getlist("tag_ids")
|
||||
query = ConsoleDatasetListQuery.model_validate(query_params)
|
||||
# provider = request.args.get("provider", default="vendor")
|
||||
|
||||
permissions = enterprise_rbac_service.RBACService.MyPermissions.get(
|
||||
str(current_tenant_id),
|
||||
current_user.id,
|
||||
)
|
||||
|
||||
accessible_dataset_ids: list[str] | None = None
|
||||
include_own_datasets = False
|
||||
if dify_config.RBAC_ENABLED:
|
||||
whitelist_scope = enterprise_rbac_service.RBACService.DatasetAccess.whitelist_resources(
|
||||
str(current_tenant_id),
|
||||
current_user.id,
|
||||
)
|
||||
has_default_readonly = _has_dataset_list_permission(
|
||||
permissions.dataset.default_permission_keys
|
||||
) or _has_dataset_list_permission(permissions.workspace.permission_keys)
|
||||
permission_dataset_ids: set[str] | None = None
|
||||
if not has_default_readonly:
|
||||
permission_dataset_ids = {
|
||||
override.resource_id
|
||||
for override in permissions.dataset.overrides
|
||||
if _has_dataset_list_permission(override.permission_keys)
|
||||
}
|
||||
if getattr(whitelist_scope, "unrestricted", False):
|
||||
filtered_dataset_ids = permission_dataset_ids
|
||||
else:
|
||||
filtered_dataset_ids = set(whitelist_scope.resource_ids)
|
||||
if permission_dataset_ids is not None:
|
||||
filtered_dataset_ids |= permission_dataset_ids
|
||||
elif has_default_readonly:
|
||||
filtered_dataset_ids = None
|
||||
if filtered_dataset_ids is not None:
|
||||
accessible_dataset_ids = sorted(filtered_dataset_ids)
|
||||
include_own_datasets = "dataset.create_and_management" in permissions.workspace.permission_keys
|
||||
|
||||
if query.ids:
|
||||
datasets, total = DatasetService.get_datasets_by_ids(query.ids, current_tenant_id)
|
||||
datasets, total = DatasetService.get_datasets_by_ids(
|
||||
query.ids,
|
||||
current_tenant_id,
|
||||
user=current_user,
|
||||
accessible_dataset_ids=accessible_dataset_ids,
|
||||
include_own_datasets=include_own_datasets,
|
||||
)
|
||||
else:
|
||||
datasets, total = DatasetService.get_datasets(
|
||||
query.page,
|
||||
@ -415,8 +465,15 @@ class DatasetListApi(Resource):
|
||||
query.keyword,
|
||||
query.tag_ids,
|
||||
query.include_all,
|
||||
accessible_dataset_ids=accessible_dataset_ids,
|
||||
include_own_datasets=include_own_datasets,
|
||||
)
|
||||
|
||||
permission_keys_map = {}
|
||||
if datasets:
|
||||
dataset_ids = [str(dataset.id) for dataset in datasets]
|
||||
permission_keys_map = permissions.dataset.permission_keys_by_resource_ids(dataset_ids)
|
||||
|
||||
# check embedding setting
|
||||
provider_manager = create_plugin_provider_manager(tenant_id=current_tenant_id)
|
||||
configurations = provider_manager.get_configurations(tenant_id=current_tenant_id)
|
||||
@ -431,13 +488,13 @@ class DatasetListApi(Resource):
|
||||
dataset_ids = [item["id"] for item in data if item.get("permission") == "partial_members"]
|
||||
partial_members_map: dict[str, list[str]] = {}
|
||||
if dataset_ids:
|
||||
permissions = db.session.execute(
|
||||
partial_member_rows = db.session.execute(
|
||||
select(DatasetPermission.dataset_id, DatasetPermission.account_id).where(
|
||||
DatasetPermission.dataset_id.in_(dataset_ids)
|
||||
)
|
||||
).all()
|
||||
|
||||
for dataset_id, account_id in permissions:
|
||||
for dataset_id, account_id in partial_member_rows:
|
||||
partial_members_map.setdefault(dataset_id, []).append(account_id)
|
||||
|
||||
for item in data:
|
||||
@ -456,6 +513,7 @@ class DatasetListApi(Resource):
|
||||
item.update({"partial_member_list": partial_members_map.get(item["id"], [])})
|
||||
else:
|
||||
item.update({"partial_member_list": []})
|
||||
item["permission_keys"] = permission_keys_map.get(str(item["id"]), [])
|
||||
|
||||
response = {
|
||||
"data": data,
|
||||
@ -474,6 +532,9 @@ class DatasetListApi(Resource):
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@rbac_permission_required(
|
||||
RBACResourceScope.DATASET, RBACPermission.DATASET_CREATE_AND_MANAGEMENT, resource_required=False
|
||||
)
|
||||
@cloud_edition_billing_rate_limit_check("knowledge")
|
||||
@with_current_user
|
||||
@with_current_tenant_id
|
||||
@ -484,6 +545,11 @@ class DatasetListApi(Resource):
|
||||
if not current_user.is_dataset_editor:
|
||||
raise Forbidden()
|
||||
|
||||
if dify_config.RBAC_ENABLED:
|
||||
permission = DatasetPermissionEnum.ALL_TEAM
|
||||
else:
|
||||
permission = payload.permission or DatasetPermissionEnum.ONLY_ME
|
||||
|
||||
try:
|
||||
dataset = DatasetService.create_empty_dataset(
|
||||
tenant_id=current_tenant_id,
|
||||
@ -491,7 +557,7 @@ class DatasetListApi(Resource):
|
||||
description=payload.description,
|
||||
indexing_technique=payload.indexing_technique,
|
||||
account=current_user,
|
||||
permission=payload.permission or DatasetPermissionEnum.ONLY_ME,
|
||||
permission=permission,
|
||||
provider=payload.provider,
|
||||
external_knowledge_api_id=payload.external_knowledge_api_id,
|
||||
external_knowledge_id=payload.external_knowledge_id,
|
||||
@ -499,7 +565,17 @@ class DatasetListApi(Resource):
|
||||
except services.errors.dataset.DatasetNameDuplicateError:
|
||||
raise DatasetNameDuplicateError()
|
||||
|
||||
return dump_response(DatasetDetailResponse, dataset), 201
|
||||
permission_keys_map = enterprise_rbac_service.RBACService.DatasetPermissions.batch_get(
|
||||
str(current_tenant_id),
|
||||
current_user.id,
|
||||
[str(dataset.id)],
|
||||
)
|
||||
|
||||
item = DatasetDetailWithPartialMembersResponse.model_validate(dataset, from_attributes=True).model_dump(
|
||||
mode="json"
|
||||
)
|
||||
item["permission_keys"] = permission_keys_map.get(str(dataset.id), [])
|
||||
return item, 201
|
||||
|
||||
|
||||
@console_ns.route("/datasets/<uuid:dataset_id>")
|
||||
@ -517,6 +593,7 @@ class DatasetApi(Resource):
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_READONLY)
|
||||
@with_current_user
|
||||
@with_current_tenant_id
|
||||
def get(self, current_tenant_id: str, current_user: Account, dataset_id: UUID):
|
||||
@ -528,7 +605,14 @@ class DatasetApi(Resource):
|
||||
DatasetService.check_dataset_permission(dataset, current_user)
|
||||
except services.errors.account.NoPermissionError as e:
|
||||
raise Forbidden(str(e))
|
||||
permissions = enterprise_rbac_service.RBACService.MyPermissions.get(
|
||||
str(current_tenant_id),
|
||||
current_user.id,
|
||||
dataset_id=dataset_id_str,
|
||||
)
|
||||
permission_keys_map = permissions.dataset.permission_keys_by_resource_ids([dataset_id_str])
|
||||
data = dump_response(DatasetDetailResponse, dataset)
|
||||
data["permission_keys"] = permission_keys_map.get(dataset_id_str, [])
|
||||
if dataset.indexing_technique == IndexTechniqueType.HIGH_QUALITY:
|
||||
if dataset.embedding_model_provider:
|
||||
provider_id = ModelProviderID(dataset.embedding_model_provider)
|
||||
@ -574,6 +658,7 @@ class DatasetApi(Resource):
|
||||
@cloud_edition_billing_rate_limit_check("knowledge")
|
||||
@with_current_user
|
||||
@with_current_tenant_id
|
||||
@rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_EDIT)
|
||||
def patch(self, current_tenant_id: str, current_user: Account, dataset_id: UUID):
|
||||
dataset_id_str = str(dataset_id)
|
||||
dataset = DatasetService.get_dataset(dataset_id_str)
|
||||
@ -593,16 +678,23 @@ class DatasetApi(Resource):
|
||||
payload.is_multimodal = is_multimodal
|
||||
payload_data = payload.model_dump(exclude_unset=True)
|
||||
# The role of the current user in the ta table must be admin, owner, editor, or dataset_operator
|
||||
DatasetPermissionService.check_permission(
|
||||
current_user, dataset, payload.permission, payload.partial_member_list
|
||||
)
|
||||
if not dify_config.RBAC_ENABLED:
|
||||
DatasetPermissionService.check_permission(
|
||||
current_user, dataset, payload.permission, payload.partial_member_list
|
||||
)
|
||||
|
||||
dataset = DatasetService.update_dataset(dataset_id_str, payload_data, current_user)
|
||||
|
||||
if dataset is None:
|
||||
raise NotFound("Dataset not found.")
|
||||
|
||||
permission_keys_map = enterprise_rbac_service.RBACService.DatasetPermissions.batch_get(
|
||||
str(current_tenant_id),
|
||||
current_user.id,
|
||||
[dataset_id_str],
|
||||
)
|
||||
result_data = dump_response(DatasetDetailResponse, dataset)
|
||||
result_data["permission_keys"] = permission_keys_map.get(dataset_id_str, [])
|
||||
tenant_id = current_tenant_id
|
||||
|
||||
if payload.partial_member_list is not None and payload.permission == DatasetPermissionEnum.PARTIAL_TEAM:
|
||||
@ -622,6 +714,7 @@ class DatasetApi(Resource):
|
||||
@cloud_edition_billing_rate_limit_check("knowledge")
|
||||
@console_ns.response(204, "Dataset deleted successfully")
|
||||
@with_current_user
|
||||
@rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_EDIT)
|
||||
def delete(self, current_user: Account, dataset_id: UUID):
|
||||
dataset_id_str = str(dataset_id)
|
||||
|
||||
@ -651,6 +744,7 @@ class DatasetUseCheckApi(Resource):
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_READONLY)
|
||||
def get(self, dataset_id: UUID):
|
||||
dataset_id_str = str(dataset_id)
|
||||
|
||||
@ -672,6 +766,7 @@ class DatasetQueryApi(Resource):
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@with_current_user
|
||||
@rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_READONLY)
|
||||
def get(self, current_user: Account, dataset_id: UUID):
|
||||
dataset_id_str = str(dataset_id)
|
||||
dataset = DatasetService.get_dataset(dataset_id_str)
|
||||
@ -812,6 +907,7 @@ class DatasetRelatedAppListApi(Resource):
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@with_current_user
|
||||
@rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_READONLY)
|
||||
def get(self, current_user: Account, dataset_id: UUID):
|
||||
dataset_id_str = str(dataset_id)
|
||||
dataset = DatasetService.get_dataset(dataset_id_str)
|
||||
@ -848,6 +944,7 @@ class DatasetIndexingStatusApi(Resource):
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@with_current_tenant_id
|
||||
@rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_READONLY)
|
||||
def get(self, current_tenant_id: str, dataset_id: UUID):
|
||||
dataset_id_str = str(dataset_id)
|
||||
documents = db.session.scalars(
|
||||
@ -917,6 +1014,7 @@ class DatasetApiKeyApi(Resource):
|
||||
@setup_required
|
||||
@login_required
|
||||
@is_admin_or_owner_required
|
||||
@rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_API_KEY_MANAGE, resource_required=False)
|
||||
@account_initialization_required
|
||||
@with_current_tenant_id
|
||||
def post(self, current_tenant_id: str):
|
||||
@ -957,6 +1055,7 @@ class DatasetApiDeleteApi(Resource):
|
||||
@setup_required
|
||||
@login_required
|
||||
@is_admin_or_owner_required
|
||||
@rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_API_KEY_MANAGE, resource_required=False)
|
||||
@account_initialization_required
|
||||
@with_current_tenant_id
|
||||
def delete(self, current_tenant_id: str, api_key_id: UUID):
|
||||
@ -991,6 +1090,7 @@ class DatasetEnableApiApi(Resource):
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@console_ns.response(200, "Success", console_ns.models[SimpleResultResponse.__name__])
|
||||
@rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_EDIT)
|
||||
def post(self, dataset_id: UUID, status: str):
|
||||
dataset_id_str = str(dataset_id)
|
||||
|
||||
@ -1060,6 +1160,7 @@ class DatasetErrorDocs(Resource):
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_READONLY)
|
||||
def get(self, dataset_id: UUID):
|
||||
dataset_id_str = str(dataset_id)
|
||||
dataset = DatasetService.get_dataset(dataset_id_str)
|
||||
@ -1086,6 +1187,7 @@ class DatasetPermissionUserListApi(Resource):
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@with_current_user
|
||||
@rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_READONLY)
|
||||
def get(self, current_user: Account, dataset_id: UUID):
|
||||
dataset_id_str = str(dataset_id)
|
||||
dataset = DatasetService.get_dataset(dataset_id_str)
|
||||
@ -1115,6 +1217,7 @@ class DatasetAutoDisableLogApi(Resource):
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_READONLY)
|
||||
def get(self, dataset_id: UUID):
|
||||
dataset_id_str = str(dataset_id)
|
||||
dataset = DatasetService.get_dataset(dataset_id_str)
|
||||
|
||||
@ -19,6 +19,7 @@ from controllers.common.controller_schemas import DocumentBatchDownloadZipPayloa
|
||||
from controllers.common.fields import BinaryFileResponse, SimpleResultMessageResponse, SimpleResultResponse, UrlResponse
|
||||
from controllers.common.schema import register_response_schema_models, register_schema_models
|
||||
from controllers.console import console_ns
|
||||
from controllers.console.wraps import RBACPermission, RBACResourceScope, rbac_permission_required
|
||||
from core.errors.error import (
|
||||
LLMBadRequestError,
|
||||
ModelCurrentlyNotSupportError,
|
||||
@ -289,6 +290,7 @@ class DatasetDocumentListApi(Resource):
|
||||
@account_initialization_required
|
||||
@with_current_user
|
||||
@with_current_tenant_id
|
||||
@rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_CREATE_AND_MANAGEMENT)
|
||||
def get(self, current_tenant_id: str, current_user: Account, dataset_id: UUID):
|
||||
dataset_id_str = str(dataset_id)
|
||||
raw_args = request.args.to_dict()
|
||||
@ -415,6 +417,7 @@ class DatasetDocumentListApi(Resource):
|
||||
@console_ns.expect(console_ns.models[KnowledgeConfig.__name__])
|
||||
@console_ns.response(200, "Documents created successfully", console_ns.models[DatasetAndDocumentResponse.__name__])
|
||||
@with_current_user
|
||||
@rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_EDIT)
|
||||
def post(self, current_user: Account, dataset_id: UUID):
|
||||
dataset_id_str = str(dataset_id)
|
||||
|
||||
@ -458,6 +461,7 @@ class DatasetDocumentListApi(Resource):
|
||||
@account_initialization_required
|
||||
@cloud_edition_billing_rate_limit_check("knowledge")
|
||||
@console_ns.response(204, "Documents deleted successfully")
|
||||
@rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_EDIT)
|
||||
def delete(self, dataset_id: UUID):
|
||||
dataset_id_str = str(dataset_id)
|
||||
dataset = DatasetService.get_dataset(dataset_id_str)
|
||||
@ -555,6 +559,7 @@ class DocumentIndexingEstimateApi(DocumentResource):
|
||||
@account_initialization_required
|
||||
@with_current_user
|
||||
@with_current_tenant_id
|
||||
@rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_CREATE_AND_MANAGEMENT)
|
||||
def get(self, current_tenant_id: str, current_user: Account, dataset_id: UUID, document_id: UUID):
|
||||
dataset_id_str = str(dataset_id)
|
||||
document_id_str = str(document_id)
|
||||
@ -626,6 +631,7 @@ class DocumentBatchIndexingEstimateApi(DocumentResource):
|
||||
@account_initialization_required
|
||||
@with_current_user
|
||||
@with_current_tenant_id
|
||||
@rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_CREATE_AND_MANAGEMENT)
|
||||
def get(self, current_tenant_id: str, current_user: Account, dataset_id: UUID, batch: str):
|
||||
dataset_id_str = str(dataset_id)
|
||||
documents = self.get_batch_documents(dataset_id_str, batch, current_user)
|
||||
@ -726,6 +732,7 @@ class DocumentBatchIndexingStatusApi(DocumentResource):
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@with_current_user
|
||||
@rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_CREATE_AND_MANAGEMENT)
|
||||
def get(self, current_user: Account, dataset_id: UUID, batch: str):
|
||||
dataset_id_str = str(dataset_id)
|
||||
documents = self.get_batch_documents(dataset_id_str, batch, current_user)
|
||||
@ -783,6 +790,7 @@ class DocumentIndexingStatusApi(DocumentResource):
|
||||
@account_initialization_required
|
||||
@with_current_user
|
||||
@with_current_tenant_id
|
||||
@rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_CREATE_AND_MANAGEMENT)
|
||||
def get(self, current_tenant_id: str, current_user: Account, dataset_id: UUID, document_id: UUID):
|
||||
dataset_id_str = str(dataset_id)
|
||||
document_id_str = str(document_id)
|
||||
@ -846,6 +854,7 @@ class DocumentApi(DocumentResource):
|
||||
@account_initialization_required
|
||||
@with_current_user
|
||||
@with_current_tenant_id
|
||||
@rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_CREATE_AND_MANAGEMENT)
|
||||
def get(self, current_tenant_id: str, current_user: Account, dataset_id: UUID, document_id: UUID):
|
||||
dataset_id_str = str(dataset_id)
|
||||
document_id_str = str(document_id)
|
||||
@ -937,6 +946,7 @@ class DocumentApi(DocumentResource):
|
||||
@console_ns.response(204, "Document deleted successfully")
|
||||
@with_current_user
|
||||
@with_current_tenant_id
|
||||
@rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_EDIT)
|
||||
def delete(self, current_tenant_id: str, current_user: Account, dataset_id: UUID, document_id: UUID):
|
||||
dataset_id_str = str(dataset_id)
|
||||
document_id_str = str(document_id)
|
||||
@ -969,6 +979,7 @@ class DocumentDownloadApi(DocumentResource):
|
||||
@cloud_edition_billing_rate_limit_check("knowledge")
|
||||
@with_current_user
|
||||
@with_current_tenant_id
|
||||
@rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_DOCUMENT_DOWNLOAD)
|
||||
def get(self, current_tenant_id: str, current_user: Account, dataset_id: UUID, document_id: UUID) -> dict[str, Any]:
|
||||
# Reuse the shared permission/tenant checks implemented in DocumentResource.
|
||||
document = self.get_document(str(dataset_id), str(document_id), current_user, current_tenant_id)
|
||||
@ -989,6 +1000,7 @@ class DocumentBatchDownloadZipApi(DocumentResource):
|
||||
@console_ns.expect(console_ns.models[DocumentBatchDownloadZipPayload.__name__])
|
||||
@with_current_user
|
||||
@with_current_tenant_id
|
||||
@rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_EDIT)
|
||||
def post(self, current_tenant_id: str, current_user: Account, dataset_id: UUID):
|
||||
"""Stream a ZIP archive containing the requested uploaded documents."""
|
||||
# Parse and validate request payload.
|
||||
@ -1037,6 +1049,7 @@ class DocumentProcessingApi(DocumentResource):
|
||||
@cloud_edition_billing_rate_limit_check("knowledge")
|
||||
@with_current_user
|
||||
@with_current_tenant_id
|
||||
@rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_EDIT)
|
||||
def patch(
|
||||
self,
|
||||
current_tenant_id: str,
|
||||
@ -1093,6 +1106,7 @@ class DocumentMetadataApi(DocumentResource):
|
||||
@account_initialization_required
|
||||
@with_current_user
|
||||
@with_current_tenant_id
|
||||
@rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_EDIT)
|
||||
def put(self, current_tenant_id: str, current_user: Account, dataset_id: UUID, document_id: UUID):
|
||||
dataset_id_str = str(dataset_id)
|
||||
document_id_str = str(document_id)
|
||||
@ -1142,6 +1156,7 @@ class DocumentStatusApi(DocumentResource):
|
||||
@cloud_edition_billing_rate_limit_check("knowledge")
|
||||
@console_ns.response(200, "Success", console_ns.models[SimpleResultResponse.__name__])
|
||||
@with_current_user
|
||||
@rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_EDIT)
|
||||
def patch(
|
||||
self, current_user: Account, dataset_id: UUID, action: Literal["enable", "disable", "archive", "un_archive"]
|
||||
):
|
||||
@ -1181,6 +1196,7 @@ class DocumentPauseApi(DocumentResource):
|
||||
@account_initialization_required
|
||||
@cloud_edition_billing_rate_limit_check("knowledge")
|
||||
@console_ns.response(204, "Document paused successfully")
|
||||
@rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_EDIT)
|
||||
def patch(self, dataset_id: UUID, document_id: UUID):
|
||||
"""pause document."""
|
||||
dataset_id_str = str(dataset_id)
|
||||
@ -1216,6 +1232,7 @@ class DocumentRecoverApi(DocumentResource):
|
||||
@account_initialization_required
|
||||
@cloud_edition_billing_rate_limit_check("knowledge")
|
||||
@console_ns.response(204, "Document resumed successfully")
|
||||
@rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_EDIT)
|
||||
def patch(self, dataset_id: UUID, document_id: UUID):
|
||||
"""recover document."""
|
||||
dataset_id_str = str(dataset_id)
|
||||
@ -1249,6 +1266,7 @@ class DocumentRetryApi(DocumentResource):
|
||||
@cloud_edition_billing_rate_limit_check("knowledge")
|
||||
@console_ns.expect(console_ns.models[DocumentRetryPayload.__name__])
|
||||
@console_ns.response(204, "Documents retry started successfully")
|
||||
@rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_EDIT)
|
||||
def post(self, dataset_id: UUID):
|
||||
"""retry document."""
|
||||
payload = DocumentRetryPayload.model_validate(console_ns.payload or {})
|
||||
@ -1290,6 +1308,7 @@ class DocumentRenameApi(DocumentResource):
|
||||
@console_ns.response(200, "Document renamed successfully", console_ns.models[DocumentResponse.__name__])
|
||||
@console_ns.expect(console_ns.models[DocumentRenamePayload.__name__])
|
||||
@with_current_user
|
||||
@rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_EDIT)
|
||||
def post(self, current_user: Account, dataset_id: UUID, document_id: UUID):
|
||||
# The role of the current user in the ta table must be admin, owner, editor, or dataset_operator
|
||||
if not current_user.is_dataset_editor:
|
||||
@ -1315,6 +1334,7 @@ class WebsiteDocumentSyncApi(DocumentResource):
|
||||
@account_initialization_required
|
||||
@console_ns.response(200, "Success", console_ns.models[SimpleResultResponse.__name__])
|
||||
@with_current_tenant_id
|
||||
@rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_CREATE_AND_MANAGEMENT)
|
||||
def get(self, current_tenant_id: str, dataset_id: UUID, document_id: UUID):
|
||||
"""sync website document."""
|
||||
dataset_id_str = str(dataset_id)
|
||||
@ -1348,6 +1368,7 @@ class DocumentPipelineExecutionLogApi(DocumentResource):
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_CREATE_AND_MANAGEMENT)
|
||||
def get(self, dataset_id: UUID, document_id: UUID):
|
||||
dataset_id_str = str(dataset_id)
|
||||
document_id_str = str(document_id)
|
||||
@ -1398,6 +1419,7 @@ class DocumentGenerateSummaryApi(Resource):
|
||||
@account_initialization_required
|
||||
@cloud_edition_billing_rate_limit_check("knowledge")
|
||||
@with_current_user
|
||||
@rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_EDIT)
|
||||
def post(self, current_user: Account, dataset_id: UUID):
|
||||
"""
|
||||
Generate summary index for specified documents.
|
||||
@ -1491,6 +1513,7 @@ class DocumentSummaryStatusApi(DocumentResource):
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@with_current_user
|
||||
@rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_CREATE_AND_MANAGEMENT)
|
||||
def get(self, current_user: Account, dataset_id: UUID, document_id: UUID):
|
||||
"""
|
||||
Get summary index generation status for a document.
|
||||
|
||||
@ -28,10 +28,13 @@ from controllers.console.datasets.error import (
|
||||
InvalidActionError,
|
||||
)
|
||||
from controllers.console.wraps import (
|
||||
RBACPermission,
|
||||
RBACResourceScope,
|
||||
account_initialization_required,
|
||||
cloud_edition_billing_knowledge_limit_check,
|
||||
cloud_edition_billing_rate_limit_check,
|
||||
cloud_edition_billing_resource_check,
|
||||
rbac_permission_required,
|
||||
setup_required,
|
||||
with_current_tenant_id,
|
||||
with_current_user,
|
||||
@ -169,6 +172,7 @@ class DatasetDocumentSegmentListApi(Resource):
|
||||
@account_initialization_required
|
||||
@with_current_user
|
||||
@with_current_tenant_id
|
||||
@rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_READONLY)
|
||||
def get(self, current_tenant_id: str, current_user: Account, dataset_id: UUID, document_id: UUID):
|
||||
dataset_id_str = str(dataset_id)
|
||||
document_id_str = str(document_id)
|
||||
@ -278,6 +282,7 @@ class DatasetDocumentSegmentListApi(Resource):
|
||||
@console_ns.doc(params=query_params_from_model(SegmentIdListQuery))
|
||||
@console_ns.response(204, "Segments deleted successfully")
|
||||
@with_current_user
|
||||
@rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_EDIT)
|
||||
def delete(self, current_user: Account, dataset_id: UUID, document_id: UUID):
|
||||
# check dataset
|
||||
dataset_id_str = str(dataset_id)
|
||||
@ -316,6 +321,7 @@ class DatasetDocumentSegmentApi(Resource):
|
||||
@console_ns.response(200, "Success", console_ns.models[SimpleResultResponse.__name__])
|
||||
@with_current_user
|
||||
@with_current_tenant_id
|
||||
@rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_EDIT)
|
||||
def patch(
|
||||
self,
|
||||
current_tenant_id: str,
|
||||
@ -384,6 +390,7 @@ class DatasetDocumentSegmentAddApi(Resource):
|
||||
@console_ns.response(200, "Segment created successfully", console_ns.models[SegmentDetailResponse.__name__])
|
||||
@with_current_user
|
||||
@with_current_tenant_id
|
||||
@rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_EDIT)
|
||||
def post(self, current_tenant_id: str, current_user: Account, dataset_id: UUID, document_id: UUID):
|
||||
# check dataset
|
||||
dataset_id_str = str(dataset_id)
|
||||
@ -442,6 +449,7 @@ class DatasetDocumentSegmentUpdateApi(Resource):
|
||||
@console_ns.response(200, "Segment updated successfully", console_ns.models[SegmentDetailResponse.__name__])
|
||||
@with_current_user
|
||||
@with_current_tenant_id
|
||||
@rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_EDIT)
|
||||
def patch(
|
||||
self, current_tenant_id: str, current_user: Account, dataset_id: UUID, document_id: UUID, segment_id: UUID
|
||||
):
|
||||
@ -513,6 +521,7 @@ class DatasetDocumentSegmentUpdateApi(Resource):
|
||||
@console_ns.response(204, "Segment deleted successfully")
|
||||
@with_current_user
|
||||
@with_current_tenant_id
|
||||
@rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_EDIT)
|
||||
def delete(
|
||||
self, current_tenant_id: str, current_user: Account, dataset_id: UUID, document_id: UUID, segment_id: UUID
|
||||
):
|
||||
@ -563,6 +572,7 @@ class DatasetDocumentSegmentBatchImportApi(Resource):
|
||||
@console_ns.expect(console_ns.models[BatchImportPayload.__name__])
|
||||
@with_current_user
|
||||
@with_current_tenant_id
|
||||
@rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_EDIT)
|
||||
def post(self, current_tenant_id: str, current_user: Account, dataset_id: UUID, document_id: UUID):
|
||||
# check dataset
|
||||
dataset_id_str = str(dataset_id)
|
||||
@ -608,6 +618,7 @@ class DatasetDocumentSegmentBatchImportApi(Resource):
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_READONLY)
|
||||
def get(self, job_id=None, dataset_id: UUID | None = None, document_id: UUID | None = None):
|
||||
if job_id is None:
|
||||
raise NotFound("The job does not exist.")
|
||||
@ -634,6 +645,7 @@ class ChildChunkAddApi(Resource):
|
||||
@console_ns.response(200, "Child chunk created successfully", console_ns.models[ChildChunkDetailResponse.__name__])
|
||||
@with_current_user
|
||||
@with_current_tenant_id
|
||||
@rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_EDIT)
|
||||
def post(
|
||||
self, current_tenant_id: str, current_user: Account, dataset_id: UUID, document_id: UUID, segment_id: UUID
|
||||
):
|
||||
@ -693,6 +705,7 @@ class ChildChunkAddApi(Resource):
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@with_current_tenant_id
|
||||
@rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_READONLY)
|
||||
def get(self, current_tenant_id: str, dataset_id: UUID, document_id: UUID, segment_id: UUID):
|
||||
# check dataset
|
||||
dataset_id_str = str(dataset_id)
|
||||
@ -747,6 +760,7 @@ class ChildChunkAddApi(Resource):
|
||||
@console_ns.expect(console_ns.models[ChildChunkBatchUpdatePayload.__name__])
|
||||
@with_current_user
|
||||
@with_current_tenant_id
|
||||
@rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_EDIT)
|
||||
def patch(
|
||||
self, current_tenant_id: str, current_user: Account, dataset_id: UUID, document_id: UUID, segment_id: UUID
|
||||
):
|
||||
@ -799,6 +813,7 @@ class ChildChunkUpdateApi(Resource):
|
||||
@console_ns.response(204, "Child chunk deleted successfully")
|
||||
@with_current_user
|
||||
@with_current_tenant_id
|
||||
@rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_EDIT)
|
||||
def delete(
|
||||
self,
|
||||
current_tenant_id: str,
|
||||
@ -866,6 +881,7 @@ class ChildChunkUpdateApi(Resource):
|
||||
@console_ns.response(200, "Child chunk updated successfully", console_ns.models[ChildChunkDetailResponse.__name__])
|
||||
@with_current_user
|
||||
@with_current_tenant_id
|
||||
@rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_EDIT)
|
||||
def patch(
|
||||
self,
|
||||
current_tenant_id: str,
|
||||
|
||||
@ -17,8 +17,11 @@ from controllers.common.schema import (
|
||||
from controllers.console import console_ns
|
||||
from controllers.console.datasets.error import DatasetNameDuplicateError
|
||||
from controllers.console.wraps import (
|
||||
RBACPermission,
|
||||
RBACResourceScope,
|
||||
account_initialization_required,
|
||||
edit_permission_required,
|
||||
rbac_permission_required,
|
||||
setup_required,
|
||||
with_current_tenant_id,
|
||||
with_current_user,
|
||||
@ -40,6 +43,7 @@ from fields.dataset_fields import (
|
||||
from libs.login import login_required
|
||||
from models import Account
|
||||
from services.dataset_service import DatasetService
|
||||
from services.enterprise import rbac_service as enterprise_rbac_service
|
||||
from services.external_knowledge_service import ExternalDatasetService
|
||||
from services.hit_testing_service import HitTestingService
|
||||
from services.knowledge_service import BedrockRetrievalSetting, ExternalDatasetTestService
|
||||
@ -319,6 +323,7 @@ class ExternalDatasetCreateApi(Resource):
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@edit_permission_required
|
||||
@rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_EXTERNAL_CONNECT)
|
||||
@with_current_user
|
||||
@with_current_tenant_id
|
||||
def post(self, current_tenant_id: str, current_user: Account):
|
||||
@ -339,7 +344,16 @@ class ExternalDatasetCreateApi(Resource):
|
||||
except services.errors.dataset.DatasetNameDuplicateError:
|
||||
raise DatasetNameDuplicateError()
|
||||
|
||||
return marshal(dataset, dataset_detail_fields), 201
|
||||
item = marshal(dataset, dataset_detail_fields)
|
||||
dataset_id_str = item["id"]
|
||||
permission_keys_map = enterprise_rbac_service.RBACService.DatasetPermissions.batch_get(
|
||||
str(current_tenant_id),
|
||||
current_user.id,
|
||||
[dataset_id_str],
|
||||
)
|
||||
item["permission_keys"] = permission_keys_map.get(dataset_id_str, [])
|
||||
|
||||
return item, 201
|
||||
|
||||
|
||||
@console_ns.route("/datasets/<uuid:dataset_id>/external-hit-testing")
|
||||
@ -359,6 +373,7 @@ class ExternalKnowledgeHitTestingApi(Resource):
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@with_current_user
|
||||
@rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_PIPELINE_TEST)
|
||||
def post(self, current_user: Account, dataset_id: UUID):
|
||||
dataset_id_str = str(dataset_id)
|
||||
dataset = DatasetService.get_dataset(dataset_id_str)
|
||||
|
||||
@ -5,6 +5,7 @@ from uuid import UUID
|
||||
from flask_restx import Resource
|
||||
|
||||
from controllers.common.schema import register_response_schema_models, register_schema_models
|
||||
from controllers.console.wraps import RBACPermission, RBACResourceScope, rbac_permission_required
|
||||
from fields.hit_testing_fields import HitTestingResponse
|
||||
from libs.helper import dump_response
|
||||
from libs.login import login_required
|
||||
@ -43,6 +44,7 @@ class HitTestingApi(Resource, DatasetsHitTestingBase):
|
||||
@cloud_edition_billing_rate_limit_check("knowledge")
|
||||
@with_current_tenant_id
|
||||
@with_current_user
|
||||
@rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_PIPELINE_TEST)
|
||||
def post(self, current_user: Account, current_tenant_id: str, dataset_id: UUID) -> dict[str, object]:
|
||||
dataset_id_str = str(dataset_id)
|
||||
|
||||
|
||||
@ -8,8 +8,11 @@ from controllers.common.controller_schemas import MetadataUpdatePayload
|
||||
from controllers.common.schema import register_response_schema_models, register_schema_models
|
||||
from controllers.console import console_ns
|
||||
from controllers.console.wraps import (
|
||||
RBACPermission,
|
||||
RBACResourceScope,
|
||||
account_initialization_required,
|
||||
enterprise_license_required,
|
||||
rbac_permission_required,
|
||||
setup_required,
|
||||
with_current_tenant_id,
|
||||
with_current_user,
|
||||
@ -52,6 +55,7 @@ class DatasetMetadataCreateApi(Resource):
|
||||
@console_ns.expect(console_ns.models[MetadataArgs.__name__])
|
||||
@with_current_user
|
||||
@with_current_tenant_id
|
||||
@rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_EDIT)
|
||||
def post(self, current_tenant_id: str, current_user: Account, dataset_id: UUID):
|
||||
metadata_args = MetadataArgs.model_validate(console_ns.payload or {})
|
||||
|
||||
@ -71,6 +75,7 @@ class DatasetMetadataCreateApi(Resource):
|
||||
@console_ns.response(
|
||||
200, "Metadata retrieved successfully", console_ns.models[DatasetMetadataListResponse.__name__]
|
||||
)
|
||||
@rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_CREATE_AND_MANAGEMENT)
|
||||
def get(self, dataset_id: UUID):
|
||||
dataset_id_str = str(dataset_id)
|
||||
dataset = DatasetService.get_dataset(dataset_id_str)
|
||||
@ -90,6 +95,7 @@ class DatasetMetadataApi(Resource):
|
||||
@console_ns.expect(console_ns.models[MetadataUpdatePayload.__name__])
|
||||
@with_current_user
|
||||
@with_current_tenant_id
|
||||
@rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_EDIT)
|
||||
def patch(self, current_tenant_id: str, current_user: Account, dataset_id: UUID, metadata_id: UUID):
|
||||
payload = MetadataUpdatePayload.model_validate(console_ns.payload or {})
|
||||
name = payload.name
|
||||
@ -112,6 +118,7 @@ class DatasetMetadataApi(Resource):
|
||||
@enterprise_license_required
|
||||
@console_ns.response(204, "Metadata deleted successfully")
|
||||
@with_current_user
|
||||
@rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_EDIT)
|
||||
def delete(self, current_user: Account, dataset_id: UUID, metadata_id: UUID):
|
||||
dataset_id_str = str(dataset_id)
|
||||
metadata_id_str = str(metadata_id)
|
||||
@ -149,6 +156,7 @@ class DatasetMetadataBuiltInFieldActionApi(Resource):
|
||||
@enterprise_license_required
|
||||
@console_ns.response(204, "Action completed successfully")
|
||||
@with_current_user
|
||||
@rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_EDIT)
|
||||
def post(self, current_user: Account, dataset_id: UUID, action: Literal["enable", "disable"]):
|
||||
dataset_id_str = str(dataset_id)
|
||||
dataset = DatasetService.get_dataset(dataset_id_str)
|
||||
@ -177,6 +185,7 @@ class DocumentMetadataEditApi(Resource):
|
||||
"Documents metadata updated successfully",
|
||||
)
|
||||
@with_current_user
|
||||
@rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_EDIT)
|
||||
def post(self, current_user: Account, dataset_id: UUID):
|
||||
dataset_id_str = str(dataset_id)
|
||||
dataset = DatasetService.get_dataset(dataset_id_str)
|
||||
|
||||
@ -10,8 +10,11 @@ from controllers.common.fields import RedirectResponse, SimpleResultResponse
|
||||
from controllers.common.schema import query_params_from_model, register_response_schema_models, register_schema_models
|
||||
from controllers.console import console_ns
|
||||
from controllers.console.wraps import (
|
||||
RBACPermission,
|
||||
RBACResourceScope,
|
||||
account_initialization_required,
|
||||
edit_permission_required,
|
||||
rbac_permission_required,
|
||||
setup_required,
|
||||
with_current_tenant_id,
|
||||
with_current_user,
|
||||
@ -104,6 +107,7 @@ class DatasourcePluginOAuthAuthorizationUrl(Resource):
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@edit_permission_required
|
||||
@rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.CREDENTIAL_MANAGE, resource_required=False)
|
||||
@with_current_user
|
||||
@with_current_tenant_id
|
||||
def get(self, current_tenant_id: str, current_user: Account, provider_id: str):
|
||||
@ -218,6 +222,7 @@ class DatasourceAuth(Resource):
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@edit_permission_required
|
||||
@rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.CREDENTIAL_MANAGE, resource_required=False)
|
||||
@with_current_tenant_id
|
||||
def post(self, current_tenant_id: str, provider_id: str):
|
||||
payload = DatasourceCredentialPayload.model_validate(console_ns.payload or {})
|
||||
@ -262,6 +267,7 @@ class DatasourceAuthDeleteApi(Resource):
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@edit_permission_required
|
||||
@rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.CREDENTIAL_MANAGE, resource_required=False)
|
||||
@with_current_tenant_id
|
||||
def post(self, current_tenant_id: str, provider_id: str):
|
||||
datasource_provider_id = DatasourceProviderID(provider_id)
|
||||
@ -287,6 +293,7 @@ class DatasourceAuthUpdateApi(Resource):
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@edit_permission_required
|
||||
@rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.CREDENTIAL_MANAGE, resource_required=False)
|
||||
@with_current_tenant_id
|
||||
def post(self, current_tenant_id: str, provider_id: str):
|
||||
datasource_provider_id = DatasourceProviderID(provider_id)
|
||||
@ -338,6 +345,7 @@ class DatasourceAuthOauthCustomClient(Resource):
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@edit_permission_required
|
||||
@rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.CREDENTIAL_MANAGE, resource_required=False)
|
||||
@with_current_tenant_id
|
||||
def post(self, current_tenant_id: str, provider_id: str):
|
||||
payload = DatasourceCustomClientPayload.model_validate(console_ns.payload or {})
|
||||
@ -374,6 +382,7 @@ class DatasourceAuthDefaultApi(Resource):
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@edit_permission_required
|
||||
@rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.CREDENTIAL_MANAGE, resource_required=False)
|
||||
@with_current_tenant_id
|
||||
def post(self, current_tenant_id: str, provider_id: str):
|
||||
payload = DatasourceDefaultPayload.model_validate(console_ns.payload or {})
|
||||
@ -395,6 +404,7 @@ class DatasourceUpdateProviderNameApi(Resource):
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@edit_permission_required
|
||||
@rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.CREDENTIAL_MANAGE, resource_required=False)
|
||||
@with_current_tenant_id
|
||||
def post(self, current_tenant_id: str, provider_id: str):
|
||||
payload = DatasourceUpdateNamePayload.model_validate(console_ns.payload or {})
|
||||
|
||||
@ -13,8 +13,11 @@ from controllers.common.schema import (
|
||||
from controllers.console import console_ns
|
||||
from controllers.console.datasets.wraps import get_rag_pipeline
|
||||
from controllers.console.wraps import (
|
||||
RBACPermission,
|
||||
RBACResourceScope,
|
||||
account_initialization_required,
|
||||
edit_permission_required,
|
||||
rbac_permission_required,
|
||||
setup_required,
|
||||
with_current_user,
|
||||
)
|
||||
@ -78,6 +81,9 @@ class RagPipelineImportApi(Resource):
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@edit_permission_required
|
||||
@rbac_permission_required(
|
||||
RBACResourceScope.DATASET, RBACPermission.DATASET_CREATE_AND_MANAGEMENT, resource_required=False
|
||||
)
|
||||
@with_current_user
|
||||
def post(self, current_user: Account) -> JsonResponseWithStatus:
|
||||
# Check user role first
|
||||
@ -122,6 +128,9 @@ class RagPipelineImportConfirmApi(Resource):
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@edit_permission_required
|
||||
@rbac_permission_required(
|
||||
RBACResourceScope.DATASET, RBACPermission.DATASET_CREATE_AND_MANAGEMENT, resource_required=False
|
||||
)
|
||||
@with_current_user
|
||||
def post(self, current_user: Account, import_id: str) -> JsonResponseWithStatus:
|
||||
with Session(db.engine, expire_on_commit=False) as session:
|
||||
@ -151,6 +160,7 @@ class RagPipelineImportCheckDependenciesApi(Resource):
|
||||
@get_rag_pipeline
|
||||
@account_initialization_required
|
||||
@edit_permission_required
|
||||
@rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_READONLY)
|
||||
def get(self, pipeline: Pipeline) -> JsonResponseWithStatus:
|
||||
with Session(db.engine, expire_on_commit=False) as session:
|
||||
import_service = RagPipelineDslService(session)
|
||||
@ -168,6 +178,7 @@ class RagPipelineExportApi(Resource):
|
||||
@get_rag_pipeline
|
||||
@account_initialization_required
|
||||
@edit_permission_required
|
||||
@rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_IMPORT_EXPORT_DSL)
|
||||
def get(self, pipeline: Pipeline) -> JsonResponseWithStatus:
|
||||
# Add include_secret params
|
||||
query = IncludeSecretQuery.model_validate(request.args.to_dict())
|
||||
|
||||
@ -28,8 +28,11 @@ from controllers.console.app.workflow import (
|
||||
)
|
||||
from controllers.console.datasets.wraps import get_rag_pipeline
|
||||
from controllers.console.wraps import (
|
||||
RBACPermission,
|
||||
RBACResourceScope,
|
||||
account_initialization_required,
|
||||
edit_permission_required,
|
||||
rbac_permission_required,
|
||||
setup_required,
|
||||
with_current_tenant_id,
|
||||
with_current_user,
|
||||
@ -186,6 +189,7 @@ class DraftRagPipelineApi(Resource):
|
||||
@account_initialization_required
|
||||
@get_rag_pipeline
|
||||
@edit_permission_required
|
||||
@rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_EDIT)
|
||||
def get(self, pipeline: Pipeline):
|
||||
"""
|
||||
Get draft rag pipeline's workflow
|
||||
@ -206,6 +210,7 @@ class DraftRagPipelineApi(Resource):
|
||||
@with_current_user
|
||||
@get_rag_pipeline
|
||||
@edit_permission_required
|
||||
@rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_EDIT)
|
||||
@console_ns.expect(console_ns.models[DraftWorkflowSyncPayload.__name__])
|
||||
@console_ns.response(200, "Success", console_ns.models[RagPipelineWorkflowSyncResponse.__name__])
|
||||
def post(self, current_user: Account, pipeline: Pipeline):
|
||||
@ -266,6 +271,7 @@ class RagPipelineDraftRunIterationNodeApi(Resource):
|
||||
@with_current_user
|
||||
@get_rag_pipeline
|
||||
@edit_permission_required
|
||||
@rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_EDIT)
|
||||
def post(self, current_user: Account, pipeline: Pipeline, node_id: str):
|
||||
"""
|
||||
Run draft workflow iteration node
|
||||
@ -298,6 +304,7 @@ class RagPipelineDraftRunLoopNodeApi(Resource):
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@edit_permission_required
|
||||
@rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_EDIT)
|
||||
@with_current_user
|
||||
@get_rag_pipeline
|
||||
def post(self, current_user: Account, pipeline: Pipeline, node_id: str):
|
||||
@ -332,6 +339,7 @@ class DraftRagPipelineRunApi(Resource):
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@edit_permission_required
|
||||
@rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_EDIT)
|
||||
@with_current_user
|
||||
@get_rag_pipeline
|
||||
def post(self, current_user: Account, pipeline: Pipeline):
|
||||
@ -363,6 +371,7 @@ class PublishedRagPipelineRunApi(Resource):
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@edit_permission_required
|
||||
@rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_EDIT)
|
||||
@with_current_user
|
||||
@get_rag_pipeline
|
||||
def post(self, current_user: Account, pipeline: Pipeline):
|
||||
@ -395,6 +404,7 @@ class RagPipelinePublishedDatasourceNodeRunApi(Resource):
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@edit_permission_required
|
||||
@rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_EDIT)
|
||||
@with_current_user
|
||||
@get_rag_pipeline
|
||||
def post(self, current_user: Account, pipeline: Pipeline, node_id: str):
|
||||
@ -426,6 +436,7 @@ class RagPipelineDraftDatasourceNodeRunApi(Resource):
|
||||
@setup_required
|
||||
@login_required
|
||||
@edit_permission_required
|
||||
@rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_EDIT)
|
||||
@account_initialization_required
|
||||
@with_current_user
|
||||
@get_rag_pipeline
|
||||
@ -462,6 +473,7 @@ class RagPipelineDraftNodeRunApi(Resource):
|
||||
@setup_required
|
||||
@login_required
|
||||
@edit_permission_required
|
||||
@rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_EDIT)
|
||||
@account_initialization_required
|
||||
@with_current_user
|
||||
@get_rag_pipeline
|
||||
@ -491,6 +503,7 @@ class RagPipelineTaskStopApi(Resource):
|
||||
@setup_required
|
||||
@login_required
|
||||
@edit_permission_required
|
||||
@rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_EDIT)
|
||||
@account_initialization_required
|
||||
@with_current_user
|
||||
@get_rag_pipeline
|
||||
@ -514,6 +527,7 @@ class PublishedRagPipelineApi(Resource):
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@edit_permission_required
|
||||
@rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_EDIT)
|
||||
@get_rag_pipeline
|
||||
def get(self, pipeline: Pipeline):
|
||||
"""
|
||||
@ -537,6 +551,7 @@ class PublishedRagPipelineApi(Resource):
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@edit_permission_required
|
||||
@rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_EDIT)
|
||||
@with_current_user
|
||||
@get_rag_pipeline
|
||||
def post(self, current_user: Account, pipeline: Pipeline):
|
||||
@ -571,6 +586,7 @@ class DefaultRagPipelineBlockConfigsApi(Resource):
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@edit_permission_required
|
||||
@rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_EDIT)
|
||||
@get_rag_pipeline
|
||||
def get(self, pipeline: Pipeline):
|
||||
"""
|
||||
@ -593,6 +609,7 @@ class DefaultRagPipelineBlockConfigApi(Resource):
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@edit_permission_required
|
||||
@rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_EDIT)
|
||||
@get_rag_pipeline
|
||||
def get(self, pipeline: Pipeline, block_type: str):
|
||||
"""
|
||||
@ -625,6 +642,7 @@ class PublishedAllRagPipelineApi(Resource):
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@edit_permission_required
|
||||
@rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_EDIT)
|
||||
@with_current_user
|
||||
@get_rag_pipeline
|
||||
def get(self, current_user: Account, pipeline: Pipeline):
|
||||
@ -670,6 +688,7 @@ class RagPipelineDraftWorkflowRestoreApi(Resource):
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@edit_permission_required
|
||||
@rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_EDIT)
|
||||
@with_current_user
|
||||
@get_rag_pipeline
|
||||
def post(self, current_user: Account, pipeline: Pipeline, workflow_id: str):
|
||||
@ -704,6 +723,7 @@ class RagPipelineByIdApi(Resource):
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@edit_permission_required
|
||||
@rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_EDIT)
|
||||
@with_current_user
|
||||
@get_rag_pipeline
|
||||
@console_ns.expect(console_ns.models[WorkflowUpdatePayload.__name__])
|
||||
@ -739,6 +759,7 @@ class RagPipelineByIdApi(Resource):
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@edit_permission_required
|
||||
@rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_EDIT)
|
||||
@get_rag_pipeline
|
||||
def delete(self, pipeline: Pipeline, workflow_id: str):
|
||||
"""
|
||||
@ -775,6 +796,7 @@ class PublishedRagPipelineSecondStepApi(Resource):
|
||||
@account_initialization_required
|
||||
@get_rag_pipeline
|
||||
@edit_permission_required
|
||||
@rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_EDIT)
|
||||
def get(self, pipeline: Pipeline):
|
||||
"""
|
||||
Get second step parameters of rag pipeline
|
||||
@ -797,6 +819,7 @@ class PublishedRagPipelineFirstStepApi(Resource):
|
||||
@account_initialization_required
|
||||
@get_rag_pipeline
|
||||
@edit_permission_required
|
||||
@rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_EDIT)
|
||||
def get(self, pipeline: Pipeline):
|
||||
"""
|
||||
Get first step parameters of rag pipeline
|
||||
@ -819,6 +842,7 @@ class DraftRagPipelineFirstStepApi(Resource):
|
||||
@account_initialization_required
|
||||
@get_rag_pipeline
|
||||
@edit_permission_required
|
||||
@rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_EDIT)
|
||||
def get(self, pipeline: Pipeline):
|
||||
"""
|
||||
Get first step parameters of rag pipeline
|
||||
@ -841,6 +865,7 @@ class DraftRagPipelineSecondStepApi(Resource):
|
||||
@account_initialization_required
|
||||
@get_rag_pipeline
|
||||
@edit_permission_required
|
||||
@rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_EDIT)
|
||||
def get(self, pipeline: Pipeline):
|
||||
"""
|
||||
Get second step parameters of rag pipeline
|
||||
@ -1012,6 +1037,7 @@ class RagPipelineDatasourceVariableApi(Resource):
|
||||
@with_current_user
|
||||
@get_rag_pipeline
|
||||
@edit_permission_required
|
||||
@rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_EDIT)
|
||||
def post(self, current_user: Account, pipeline: Pipeline):
|
||||
"""
|
||||
Set datasource variables
|
||||
|
||||
@ -31,8 +31,11 @@ from controllers.console.snippets.payloads import (
|
||||
WorkflowRunQuery,
|
||||
)
|
||||
from controllers.console.wraps import (
|
||||
RBACPermission,
|
||||
RBACResourceScope,
|
||||
account_initialization_required,
|
||||
edit_permission_required,
|
||||
rbac_permission_required,
|
||||
setup_required,
|
||||
with_current_user,
|
||||
)
|
||||
@ -154,6 +157,7 @@ class SnippetDraftWorkflowApi(Resource):
|
||||
@account_initialization_required
|
||||
@get_snippet
|
||||
@edit_permission_required
|
||||
@rbac_permission_required(RBACResourceScope.WORKSPACE, RBACPermission.SNIPPETS_MANAGE, resource_required=False)
|
||||
def get(self, snippet: CustomizedSnippet):
|
||||
"""Get draft workflow for snippet."""
|
||||
snippet_service = _snippet_service()
|
||||
@ -181,6 +185,9 @@ class SnippetDraftWorkflowApi(Resource):
|
||||
@with_current_user
|
||||
@get_snippet
|
||||
@edit_permission_required
|
||||
@rbac_permission_required(
|
||||
RBACResourceScope.WORKSPACE, RBACPermission.SNIPPETS_CREATE_AND_MODIFY, resource_required=False
|
||||
)
|
||||
def post(self, current_user: Account, snippet: CustomizedSnippet):
|
||||
"""Sync draft workflow for snippet."""
|
||||
payload = SnippetDraftSyncPayload.model_validate(console_ns.payload or {})
|
||||
@ -219,6 +226,7 @@ class SnippetDraftConfigApi(Resource):
|
||||
@account_initialization_required
|
||||
@get_snippet
|
||||
@edit_permission_required
|
||||
@rbac_permission_required(RBACResourceScope.WORKSPACE, RBACPermission.SNIPPETS_MANAGE, resource_required=False)
|
||||
def get(self, snippet: CustomizedSnippet):
|
||||
"""Get snippet draft workflow configuration limits."""
|
||||
return {
|
||||
@ -240,6 +248,7 @@ class SnippetPublishedWorkflowApi(Resource):
|
||||
@account_initialization_required
|
||||
@get_snippet
|
||||
@edit_permission_required
|
||||
@rbac_permission_required(RBACResourceScope.WORKSPACE, RBACPermission.SNIPPETS_MANAGE, resource_required=False)
|
||||
def get(self, snippet: CustomizedSnippet):
|
||||
"""Get published workflow for snippet."""
|
||||
if not snippet.is_published:
|
||||
@ -265,6 +274,9 @@ class SnippetPublishedWorkflowApi(Resource):
|
||||
@with_current_user
|
||||
@get_snippet
|
||||
@edit_permission_required
|
||||
@rbac_permission_required(
|
||||
RBACResourceScope.WORKSPACE, RBACPermission.SNIPPETS_CREATE_AND_MODIFY, resource_required=False
|
||||
)
|
||||
def post(self, current_user: Account, snippet: CustomizedSnippet):
|
||||
"""Publish snippet workflow."""
|
||||
snippet_service = _snippet_service()
|
||||
@ -301,6 +313,7 @@ class SnippetDefaultBlockConfigsApi(Resource):
|
||||
@account_initialization_required
|
||||
@get_snippet
|
||||
@edit_permission_required
|
||||
@rbac_permission_required(RBACResourceScope.WORKSPACE, RBACPermission.SNIPPETS_MANAGE, resource_required=False)
|
||||
def get(self, snippet: CustomizedSnippet):
|
||||
"""Get default block configurations for snippet workflow."""
|
||||
snippet_service = _snippet_service()
|
||||
@ -323,6 +336,7 @@ class SnippetPublishedAllWorkflowApi(Resource):
|
||||
@account_initialization_required
|
||||
@get_snippet
|
||||
@edit_permission_required
|
||||
@rbac_permission_required(RBACResourceScope.WORKSPACE, RBACPermission.SNIPPETS_MANAGE, resource_required=False)
|
||||
def get(self, snippet: CustomizedSnippet):
|
||||
"""Get all published workflow versions for snippet."""
|
||||
args = SnippetWorkflowListQuery.model_validate(request.args.to_dict(flat=True))
|
||||
@ -361,6 +375,9 @@ class SnippetDraftWorkflowRestoreApi(Resource):
|
||||
@with_current_user
|
||||
@get_snippet
|
||||
@edit_permission_required
|
||||
@rbac_permission_required(
|
||||
RBACResourceScope.WORKSPACE, RBACPermission.SNIPPETS_CREATE_AND_MODIFY, resource_required=False
|
||||
)
|
||||
def post(self, current_user: Account, snippet: CustomizedSnippet, workflow_id: str):
|
||||
"""Restore a published snippet workflow version into the draft workflow."""
|
||||
snippet_service = _snippet_service()
|
||||
@ -486,6 +503,9 @@ class SnippetDraftNodeRunApi(Resource):
|
||||
@with_current_user
|
||||
@get_snippet
|
||||
@edit_permission_required
|
||||
@rbac_permission_required(
|
||||
RBACResourceScope.WORKSPACE, RBACPermission.SNIPPETS_CREATE_AND_MODIFY, resource_required=False
|
||||
)
|
||||
def post(self, current_user: Account, snippet: CustomizedSnippet, node_id: str):
|
||||
"""
|
||||
Run a single node in snippet draft workflow.
|
||||
@ -574,6 +594,9 @@ class SnippetDraftRunIterationNodeApi(Resource):
|
||||
@with_current_user
|
||||
@get_snippet
|
||||
@edit_permission_required
|
||||
@rbac_permission_required(
|
||||
RBACResourceScope.WORKSPACE, RBACPermission.SNIPPETS_CREATE_AND_MODIFY, resource_required=False
|
||||
)
|
||||
def post(self, current_user: Account, snippet: CustomizedSnippet, node_id: str):
|
||||
"""
|
||||
Run a draft workflow iteration node for snippet.
|
||||
@ -619,6 +642,9 @@ class SnippetDraftRunLoopNodeApi(Resource):
|
||||
@with_current_user
|
||||
@get_snippet
|
||||
@edit_permission_required
|
||||
@rbac_permission_required(
|
||||
RBACResourceScope.WORKSPACE, RBACPermission.SNIPPETS_CREATE_AND_MODIFY, resource_required=False
|
||||
)
|
||||
def post(self, current_user: Account, snippet: CustomizedSnippet, node_id: str):
|
||||
"""
|
||||
Run a draft workflow loop node for snippet.
|
||||
@ -662,6 +688,9 @@ class SnippetDraftWorkflowRunApi(Resource):
|
||||
@with_current_user
|
||||
@get_snippet
|
||||
@edit_permission_required
|
||||
@rbac_permission_required(
|
||||
RBACResourceScope.WORKSPACE, RBACPermission.SNIPPETS_CREATE_AND_MODIFY, resource_required=False
|
||||
)
|
||||
def post(self, current_user: Account, snippet: CustomizedSnippet):
|
||||
"""
|
||||
Run draft workflow for snippet.
|
||||
@ -700,6 +729,9 @@ class SnippetWorkflowTaskStopApi(Resource):
|
||||
@account_initialization_required
|
||||
@get_snippet
|
||||
@edit_permission_required
|
||||
@rbac_permission_required(
|
||||
RBACResourceScope.WORKSPACE, RBACPermission.SNIPPETS_CREATE_AND_MODIFY, resource_required=False
|
||||
)
|
||||
def post(self, snippet: CustomizedSnippet, task_id: str):
|
||||
"""
|
||||
Stop a running snippet workflow task.
|
||||
|
||||
@ -34,8 +34,11 @@ from controllers.console.app.workflow_draft_variable import (
|
||||
)
|
||||
from controllers.console.snippets.snippet_workflow import get_snippet
|
||||
from controllers.console.wraps import (
|
||||
RBACPermission,
|
||||
RBACResourceScope,
|
||||
account_initialization_required,
|
||||
edit_permission_required,
|
||||
rbac_permission_required,
|
||||
setup_required,
|
||||
with_current_user,
|
||||
)
|
||||
@ -102,6 +105,7 @@ class SnippetWorkflowVariableCollectionApi(Resource):
|
||||
)
|
||||
@_snippet_draft_var_prerequisite
|
||||
@marshal_with(workflow_draft_variable_list_without_value_model)
|
||||
@rbac_permission_required(RBACResourceScope.WORKSPACE, RBACPermission.SNIPPETS_MANAGE, resource_required=False)
|
||||
def get(self, current_user: Account, snippet: CustomizedSnippet) -> WorkflowDraftVariableList:
|
||||
args = WorkflowDraftVariableListQuery.model_validate(request.args.to_dict(flat=True)) # type: ignore
|
||||
|
||||
@ -125,6 +129,9 @@ class SnippetWorkflowVariableCollectionApi(Resource):
|
||||
@console_ns.doc(description="Delete all draft workflow variables for the current user (snippet scope)")
|
||||
@console_ns.response(204, "Workflow variables deleted successfully")
|
||||
@_snippet_draft_var_prerequisite
|
||||
@rbac_permission_required(
|
||||
RBACResourceScope.WORKSPACE, RBACPermission.SNIPPETS_CREATE_AND_MODIFY, resource_required=False
|
||||
)
|
||||
def delete(self, current_user: Account, snippet: CustomizedSnippet) -> Response:
|
||||
draft_var_srv = WorkflowDraftVariableService(session=db.session())
|
||||
draft_var_srv.delete_user_workflow_variables(snippet.id, user_id=current_user.id)
|
||||
|
||||
@ -15,8 +15,11 @@ from pydantic import BaseModel, Field
|
||||
from controllers.common.schema import query_params_from_model, register_schema_models
|
||||
from controllers.console import console_ns
|
||||
from controllers.console.wraps import (
|
||||
RBACPermission,
|
||||
RBACResourceScope,
|
||||
account_initialization_required,
|
||||
is_admin_or_owner_required,
|
||||
rbac_permission_required,
|
||||
setup_required,
|
||||
with_current_tenant_id,
|
||||
with_current_user_id,
|
||||
@ -166,6 +169,7 @@ class EndpointCollectionApi(Resource):
|
||||
@setup_required
|
||||
@login_required
|
||||
@is_admin_or_owner_required
|
||||
@rbac_permission_required(RBACResourceScope.WORKSPACE, RBACPermission.PLUGIN_MANAGE, resource_required=False)
|
||||
@account_initialization_required
|
||||
@with_current_user_id
|
||||
@with_current_tenant_id
|
||||
@ -194,6 +198,7 @@ class DeprecatedEndpointCreateApi(Resource):
|
||||
@setup_required
|
||||
@login_required
|
||||
@is_admin_or_owner_required
|
||||
@rbac_permission_required(RBACResourceScope.WORKSPACE, RBACPermission.PLUGIN_MANAGE, resource_required=False)
|
||||
@account_initialization_required
|
||||
@with_current_user_id
|
||||
@with_current_tenant_id
|
||||
@ -285,6 +290,7 @@ class EndpointItemApi(Resource):
|
||||
@setup_required
|
||||
@login_required
|
||||
@is_admin_or_owner_required
|
||||
@rbac_permission_required(RBACResourceScope.WORKSPACE, RBACPermission.PLUGIN_MANAGE, resource_required=False)
|
||||
@account_initialization_required
|
||||
@with_current_user_id
|
||||
@with_current_tenant_id
|
||||
@ -304,6 +310,7 @@ class EndpointItemApi(Resource):
|
||||
@setup_required
|
||||
@login_required
|
||||
@is_admin_or_owner_required
|
||||
@rbac_permission_required(RBACResourceScope.WORKSPACE, RBACPermission.PLUGIN_MANAGE, resource_required=False)
|
||||
@account_initialization_required
|
||||
@with_current_user_id
|
||||
@with_current_tenant_id
|
||||
@ -333,6 +340,7 @@ class DeprecatedEndpointDeleteApi(Resource):
|
||||
@setup_required
|
||||
@login_required
|
||||
@is_admin_or_owner_required
|
||||
@rbac_permission_required(RBACResourceScope.WORKSPACE, RBACPermission.PLUGIN_MANAGE, resource_required=False)
|
||||
@account_initialization_required
|
||||
@with_current_user_id
|
||||
@with_current_tenant_id
|
||||
@ -363,6 +371,7 @@ class DeprecatedEndpointUpdateApi(Resource):
|
||||
@setup_required
|
||||
@login_required
|
||||
@is_admin_or_owner_required
|
||||
@rbac_permission_required(RBACResourceScope.WORKSPACE, RBACPermission.PLUGIN_MANAGE, resource_required=False)
|
||||
@account_initialization_required
|
||||
@with_current_user_id
|
||||
@with_current_tenant_id
|
||||
@ -385,6 +394,7 @@ class EndpointEnableApi(Resource):
|
||||
@setup_required
|
||||
@login_required
|
||||
@is_admin_or_owner_required
|
||||
@rbac_permission_required(RBACResourceScope.WORKSPACE, RBACPermission.PLUGIN_MANAGE, resource_required=False)
|
||||
@account_initialization_required
|
||||
@with_current_user_id
|
||||
@with_current_tenant_id
|
||||
@ -412,6 +422,7 @@ class EndpointDisableApi(Resource):
|
||||
@setup_required
|
||||
@login_required
|
||||
@is_admin_or_owner_required
|
||||
@rbac_permission_required(RBACResourceScope.WORKSPACE, RBACPermission.PLUGIN_MANAGE, resource_required=False)
|
||||
@account_initialization_required
|
||||
@with_current_user_id
|
||||
@with_current_tenant_id
|
||||
|
||||
@ -32,16 +32,17 @@ from extensions.ext_redis import redis_client
|
||||
from fields.base import ResponseModel
|
||||
from fields.member_fields import AccountWithRole, AccountWithRoleList
|
||||
from libs.helper import extract_remote_ip
|
||||
from libs.login import login_required
|
||||
from libs.login import current_account_with_tenant, login_required
|
||||
from models.account import Account, TenantAccountJoin, TenantAccountRole
|
||||
from services.account_service import AccountService, RegisterService, TenantService
|
||||
from services.enterprise import rbac_service as enterprise_rbac_service
|
||||
from services.errors.account import AccountAlreadyInTenantError
|
||||
from services.feature_service import FeatureService
|
||||
|
||||
|
||||
class MemberInvitePayload(BaseModel):
|
||||
emails: list[str] = Field(default_factory=list)
|
||||
role: TenantAccountRole
|
||||
role: str
|
||||
language: str | None = None
|
||||
|
||||
|
||||
@ -107,6 +108,22 @@ def _is_role_enabled(role: TenantAccountRole | str, tenant_id: str) -> bool:
|
||||
return FeatureService.get_features(tenant_id=tenant_id, exclude_vector_space=True).dataset_operator_enabled
|
||||
|
||||
|
||||
def _serialize_member_roles(
|
||||
current_role: str | None, member_roles: list[enterprise_rbac_service.RBACRole]
|
||||
) -> list[dict[str, str]]:
|
||||
if dify_config.RBAC_ENABLED:
|
||||
return [{"id": role.id, "name": role.name} for role in member_roles]
|
||||
else:
|
||||
if current_role:
|
||||
return [{"id": current_role, "name": current_role}]
|
||||
return []
|
||||
|
||||
|
||||
def _normalize_enum_value(value: object) -> str:
|
||||
normalized = getattr(value, "value", value)
|
||||
return str(normalized) if normalized is not None else ""
|
||||
|
||||
|
||||
def _normalize_invitee_emails(emails: list[str]) -> list[str]:
|
||||
return list(dict.fromkeys(email.lower() for email in emails))
|
||||
|
||||
@ -164,11 +181,42 @@ class MemberListApi(Resource):
|
||||
@account_initialization_required
|
||||
@console_ns.response(200, "Success", console_ns.models[AccountWithRoleList.__name__])
|
||||
@with_current_user
|
||||
def get(self, current_user: Account):
|
||||
def get(self, current_user: Account | None = None):
|
||||
if current_user is None:
|
||||
current_user, _ = current_account_with_tenant()
|
||||
if not current_user.current_tenant:
|
||||
raise ValueError("No current tenant")
|
||||
members = TenantService.get_tenant_members(current_user.current_tenant)
|
||||
member_models = TypeAdapter(list[AccountWithRole]).validate_python(members, from_attributes=True)
|
||||
if dify_config.RBAC_ENABLED:
|
||||
member_ids = [member.id for member in members]
|
||||
member_roles = enterprise_rbac_service.RBACService.MemberRoles.batch_get(
|
||||
str(current_user.current_tenant.id),
|
||||
current_user.id,
|
||||
member_ids,
|
||||
)
|
||||
roles_map = {item.account_id: item.roles for item in member_roles}
|
||||
else:
|
||||
roles_map = {}
|
||||
|
||||
serialized_members = []
|
||||
for member in members:
|
||||
current_role = _normalize_enum_value(member.current_role)
|
||||
serialized_members.append(
|
||||
{
|
||||
"id": member.id,
|
||||
"name": member.name,
|
||||
"email": member.email,
|
||||
"avatar": member.avatar,
|
||||
"last_login_at": member.last_login_at,
|
||||
"last_active_at": member.last_active_at,
|
||||
"created_at": member.created_at,
|
||||
"role": current_role,
|
||||
"roles": _serialize_member_roles(current_role, roles_map.get(member.id, [])),
|
||||
"status": _normalize_enum_value(member.status),
|
||||
}
|
||||
)
|
||||
|
||||
member_models = TypeAdapter(list[AccountWithRole]).validate_python(serialized_members)
|
||||
response = AccountWithRoleList(accounts=member_models)
|
||||
return response.model_dump(mode="json"), 200
|
||||
|
||||
@ -190,8 +238,11 @@ class MemberInviteEmailApi(Resource):
|
||||
invitee_emails = _normalize_invitee_emails(args.emails)
|
||||
invitee_role = args.role
|
||||
interface_language = args.language
|
||||
if not TenantAccountRole.is_non_owner_role(invitee_role):
|
||||
return {"code": "invalid-role", "message": "Invalid role"}, 400
|
||||
if not dify_config.RBAC_ENABLED:
|
||||
if not TenantAccountRole.is_valid_role(invitee_role):
|
||||
return {"code": "invalid-role", "message": "Invalid role"}, 400
|
||||
if not TenantAccountRole.is_non_owner_role(TenantAccountRole(invitee_role)):
|
||||
return {"code": "invalid-role", "message": "Invalid role"}, 400
|
||||
inviter = current_user
|
||||
if not inviter.current_tenant:
|
||||
raise ValueError("No current tenant")
|
||||
@ -208,8 +259,9 @@ class MemberInviteEmailApi(Resource):
|
||||
|
||||
tenant_id = inviter.current_tenant.id
|
||||
with redis_client.lock(f"workspace_member_invite:{tenant_id}", timeout=60):
|
||||
new_member_count = _count_new_member_invites(tenant_id, invitee_emails)
|
||||
_check_member_invite_limits(tenant_id, new_member_count)
|
||||
if dify_config.ENTERPRISE_ENABLED is True or dify_config.BILLING_ENABLED is True:
|
||||
new_member_count = _count_new_member_invites(tenant_id, invitee_emails)
|
||||
_check_member_invite_limits(tenant_id, new_member_count)
|
||||
|
||||
for invitee_email in invitee_emails:
|
||||
try:
|
||||
|
||||
@ -9,8 +9,11 @@ from controllers.common.fields import BinaryFileResponse, SimpleResultResponse
|
||||
from controllers.common.schema import query_params_from_model, register_response_schema_models, register_schema_models
|
||||
from controllers.console import console_ns
|
||||
from controllers.console.wraps import (
|
||||
RBACPermission,
|
||||
RBACResourceScope,
|
||||
account_initialization_required,
|
||||
is_admin_or_owner_required,
|
||||
rbac_permission_required,
|
||||
setup_required,
|
||||
with_current_tenant_id,
|
||||
with_current_user,
|
||||
@ -166,6 +169,7 @@ class ModelProviderCredentialApi(Resource):
|
||||
@setup_required
|
||||
@login_required
|
||||
@is_admin_or_owner_required
|
||||
@rbac_permission_required(RBACResourceScope.WORKSPACE, RBACPermission.CREDENTIAL_MANAGE, resource_required=False)
|
||||
@account_initialization_required
|
||||
@with_current_tenant_id
|
||||
def post(self, current_tenant_id: str, provider: str):
|
||||
@ -191,6 +195,7 @@ class ModelProviderCredentialApi(Resource):
|
||||
@setup_required
|
||||
@login_required
|
||||
@is_admin_or_owner_required
|
||||
@rbac_permission_required(RBACResourceScope.WORKSPACE, RBACPermission.CREDENTIAL_MANAGE, resource_required=False)
|
||||
@account_initialization_required
|
||||
@with_current_tenant_id
|
||||
def put(self, current_tenant_id: str, provider: str):
|
||||
@ -217,6 +222,7 @@ class ModelProviderCredentialApi(Resource):
|
||||
@setup_required
|
||||
@login_required
|
||||
@is_admin_or_owner_required
|
||||
@rbac_permission_required(RBACResourceScope.WORKSPACE, RBACPermission.CREDENTIAL_MANAGE, resource_required=False)
|
||||
@account_initialization_required
|
||||
@with_current_tenant_id
|
||||
def delete(self, current_tenant_id: str, provider: str):
|
||||
@ -238,6 +244,7 @@ class ModelProviderCredentialSwitchApi(Resource):
|
||||
@setup_required
|
||||
@login_required
|
||||
@is_admin_or_owner_required
|
||||
@rbac_permission_required(RBACResourceScope.WORKSPACE, RBACPermission.CREDENTIAL_MANAGE, resource_required=False)
|
||||
@account_initialization_required
|
||||
@with_current_tenant_id
|
||||
def post(self, current_tenant_id: str, provider: str):
|
||||
@ -319,6 +326,7 @@ class PreferredProviderTypeUpdateApi(Resource):
|
||||
@setup_required
|
||||
@login_required
|
||||
@is_admin_or_owner_required
|
||||
@rbac_permission_required(RBACResourceScope.WORKSPACE, RBACPermission.CREDENTIAL_MANAGE, resource_required=False)
|
||||
@account_initialization_required
|
||||
@with_current_tenant_id
|
||||
def post(self, tenant_id: str, provider: str):
|
||||
|
||||
@ -14,8 +14,11 @@ from controllers.common.schema import (
|
||||
)
|
||||
from controllers.console import console_ns
|
||||
from controllers.console.wraps import (
|
||||
RBACPermission,
|
||||
RBACResourceScope,
|
||||
account_initialization_required,
|
||||
is_admin_or_owner_required,
|
||||
rbac_permission_required,
|
||||
setup_required,
|
||||
with_current_tenant_id,
|
||||
with_current_user,
|
||||
@ -217,6 +220,7 @@ class DefaultModelApi(Resource):
|
||||
@setup_required
|
||||
@login_required
|
||||
@is_admin_or_owner_required
|
||||
@rbac_permission_required(RBACResourceScope.WORKSPACE, RBACPermission.PLUGIN_PREFERENCES, resource_required=False)
|
||||
@account_initialization_required
|
||||
@with_current_tenant_id
|
||||
def post(self, tenant_id: str):
|
||||
@ -263,6 +267,7 @@ class ModelProviderModelApi(Resource):
|
||||
@setup_required
|
||||
@login_required
|
||||
@is_admin_or_owner_required
|
||||
@rbac_permission_required(RBACResourceScope.WORKSPACE, RBACPermission.PLUGIN_PREFERENCES, resource_required=False)
|
||||
@account_initialization_required
|
||||
@with_current_tenant_id
|
||||
def post(self, tenant_id: str, provider: str):
|
||||
@ -310,6 +315,7 @@ class ModelProviderModelApi(Resource):
|
||||
@setup_required
|
||||
@login_required
|
||||
@is_admin_or_owner_required
|
||||
@rbac_permission_required(RBACResourceScope.WORKSPACE, RBACPermission.PLUGIN_PREFERENCES, resource_required=False)
|
||||
@account_initialization_required
|
||||
@with_current_tenant_id
|
||||
def delete(self, tenant_id: str, provider: str):
|
||||
@ -389,6 +395,7 @@ class ModelProviderModelCredentialApi(Resource):
|
||||
@setup_required
|
||||
@login_required
|
||||
@is_admin_or_owner_required
|
||||
@rbac_permission_required(RBACResourceScope.WORKSPACE, RBACPermission.CREDENTIAL_MANAGE, resource_required=False)
|
||||
@account_initialization_required
|
||||
@with_current_tenant_id
|
||||
def post(self, tenant_id: str, provider: str):
|
||||
@ -421,6 +428,7 @@ class ModelProviderModelCredentialApi(Resource):
|
||||
@setup_required
|
||||
@login_required
|
||||
@is_admin_or_owner_required
|
||||
@rbac_permission_required(RBACResourceScope.WORKSPACE, RBACPermission.CREDENTIAL_MANAGE, resource_required=False)
|
||||
@account_initialization_required
|
||||
@with_current_tenant_id
|
||||
def put(self, current_tenant_id: str, provider: str):
|
||||
@ -448,6 +456,7 @@ class ModelProviderModelCredentialApi(Resource):
|
||||
@setup_required
|
||||
@login_required
|
||||
@is_admin_or_owner_required
|
||||
@rbac_permission_required(RBACResourceScope.WORKSPACE, RBACPermission.CREDENTIAL_MANAGE, resource_required=False)
|
||||
@account_initialization_required
|
||||
@with_current_tenant_id
|
||||
def delete(self, current_tenant_id: str, provider: str):
|
||||
@ -472,6 +481,7 @@ class ModelProviderModelCredentialSwitchApi(Resource):
|
||||
@setup_required
|
||||
@login_required
|
||||
@is_admin_or_owner_required
|
||||
@rbac_permission_required(RBACResourceScope.WORKSPACE, RBACPermission.CREDENTIAL_MANAGE, resource_required=False)
|
||||
@account_initialization_required
|
||||
@with_current_tenant_id
|
||||
def post(self, current_tenant_id: str, provider: str):
|
||||
@ -498,6 +508,7 @@ class ModelProviderModelEnableApi(Resource):
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@with_current_tenant_id
|
||||
@rbac_permission_required(RBACResourceScope.WORKSPACE, RBACPermission.PLUGIN_PREFERENCES, resource_required=False)
|
||||
def patch(self, tenant_id: str, provider: str):
|
||||
args = ParserDeleteModels.model_validate(console_ns.payload)
|
||||
|
||||
@ -519,6 +530,7 @@ class ModelProviderModelDisableApi(Resource):
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@with_current_tenant_id
|
||||
@rbac_permission_required(RBACResourceScope.WORKSPACE, RBACPermission.PLUGIN_PREFERENCES, resource_required=False)
|
||||
def patch(self, tenant_id: str, provider: str):
|
||||
args = ParserDeleteModels.model_validate(console_ns.payload)
|
||||
|
||||
|
||||
@ -20,8 +20,11 @@ from controllers.common.schema import (
|
||||
from controllers.console import console_ns
|
||||
from controllers.console.workspace import plugin_permission_required
|
||||
from controllers.console.wraps import (
|
||||
RBACPermission,
|
||||
RBACResourceScope,
|
||||
account_initialization_required,
|
||||
is_admin_or_owner_required,
|
||||
rbac_permission_required,
|
||||
setup_required,
|
||||
with_current_tenant_id,
|
||||
with_current_user,
|
||||
@ -975,6 +978,7 @@ class PluginFetchDynamicSelectOptionsApi(Resource):
|
||||
@setup_required
|
||||
@login_required
|
||||
@is_admin_or_owner_required
|
||||
@rbac_permission_required(RBACResourceScope.WORKSPACE, RBACPermission.PLUGIN_MANAGE, resource_required=False)
|
||||
@account_initialization_required
|
||||
@with_current_user
|
||||
@with_current_tenant_id
|
||||
@ -1005,6 +1009,7 @@ class PluginFetchDynamicSelectOptionsWithCredentialsApi(Resource):
|
||||
@setup_required
|
||||
@login_required
|
||||
@is_admin_or_owner_required
|
||||
@rbac_permission_required(RBACResourceScope.WORKSPACE, RBACPermission.CREDENTIAL_MANAGE, resource_required=False)
|
||||
@account_initialization_required
|
||||
@with_current_user
|
||||
@with_current_tenant_id
|
||||
|
||||
939
api/controllers/console/workspace/rbac.py
Normal file
939
api/controllers/console/workspace/rbac.py
Normal file
@ -0,0 +1,939 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from enum import StrEnum
|
||||
from typing import Any
|
||||
|
||||
from flask import request
|
||||
from flask_restx import Resource
|
||||
from pydantic import AliasChoices, BaseModel, ConfigDict, Field, ValidationError, field_validator
|
||||
from sqlalchemy import select
|
||||
from werkzeug.exceptions import NotFound
|
||||
|
||||
from configs import dify_config
|
||||
from controllers.common.schema import register_response_schema_models
|
||||
from controllers.console import console_ns
|
||||
from controllers.console.wraps import RBACPermission, RBACResourceScope, rbac_permission_required
|
||||
from core.db.session_factory import session_factory
|
||||
from libs.login import current_account_with_tenant, login_required
|
||||
from models import Account
|
||||
from services.enterprise import rbac_service as svc
|
||||
|
||||
|
||||
class _RBACRoleList(svc.Paginated[svc.RBACRole]):
|
||||
pass
|
||||
|
||||
|
||||
class _RBACRoleAccountList(svc.Paginated[svc.RBACRoleAccount]):
|
||||
pass
|
||||
|
||||
|
||||
class _AccessPolicyList(svc.Paginated[svc.AccessPolicy]):
|
||||
pass
|
||||
|
||||
|
||||
class _MembersInRoleList(svc.Paginated[svc.MembersInRole]):
|
||||
pass
|
||||
|
||||
|
||||
register_response_schema_models(
|
||||
console_ns,
|
||||
svc.PermissionCatalogResponse,
|
||||
svc.RBACRole,
|
||||
_RBACRoleList,
|
||||
_RBACRoleAccountList,
|
||||
_MembersInRoleList,
|
||||
svc.AccessPolicy,
|
||||
_AccessPolicyList,
|
||||
svc.AccessPolicyBindingState,
|
||||
svc.MyPermissionsResponse,
|
||||
svc.AppAccessMatrix,
|
||||
svc.DatasetAccessMatrix,
|
||||
svc.WorkspaceAccessMatrix,
|
||||
svc.ResourceWhitelist,
|
||||
svc.ResourceUserAccessPoliciesResponse,
|
||||
svc.ReplaceUserAccessPoliciesResponse,
|
||||
svc.RoleBindingsResponse,
|
||||
svc.MemberBindingsResponse,
|
||||
svc.MemberRolesResponse,
|
||||
svc.AccessMatrixItem,
|
||||
)
|
||||
|
||||
_LEGACY_ROLE_PERMISSION_KEYS: dict[str, list[str]] = {
|
||||
# This is a compatibility projection from the pre-RBAC workspace roles into
|
||||
# the 2.0 permission matrix documented in "权限整理2.0". It intentionally
|
||||
# models the product-facing role surface for the new RBAC UI instead of the
|
||||
# legacy backend's exact hard-authorization checks.
|
||||
"owner": [
|
||||
*svc._LEGACY_WORKSPACE_OWNER_KEYS,
|
||||
*svc._LEGACY_APP_OWNER_KEYS,
|
||||
*svc._LEGACY_DATASET_OWNER_KEYS,
|
||||
],
|
||||
"admin": [
|
||||
*svc._LEGACY_WORKSPACE_ADMIN_KEYS,
|
||||
*svc._LEGACY_APP_ADMIN_KEYS,
|
||||
*svc._LEGACY_DATASET_ADMIN_KEYS,
|
||||
],
|
||||
"editor": [
|
||||
*svc._LEGACY_WORKSPACE_EDITOR_KEYS,
|
||||
*svc._LEGACY_APP_EDITOR_KEYS,
|
||||
*svc._LEGACY_DATASET_EDITOR_KEYS,
|
||||
],
|
||||
"normal": [
|
||||
*svc._LEGACY_WORKSPACE_NORMAL_KEYS,
|
||||
*svc._LEGACY_APP_NORMAL_KEYS,
|
||||
],
|
||||
"dataset_operator": [
|
||||
*svc._LEGACY_WORKSPACE_DATASET_OPERATOR_KEYS,
|
||||
*svc._LEGACY_DATASET_DATASET_OPERATOR_KEYS,
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
def _current_ids() -> tuple[str, str]:
|
||||
"""Return ``(tenant_id, account_id)`` for the authenticated user, or
|
||||
raise a 404 when no tenant is associated with the session.
|
||||
"""
|
||||
|
||||
user, tenant_id = current_account_with_tenant()
|
||||
if not tenant_id:
|
||||
raise NotFound("Current workspace not found")
|
||||
return tenant_id, user.id
|
||||
|
||||
|
||||
def _payload(model: type[BaseModel]) -> Any:
|
||||
"""Validate the JSON body against ``model`` or raise ``ValidationError``.
|
||||
|
||||
``ValidationError`` bubbles up as HTTP 400 thanks to
|
||||
``controllers/common/helpers.py`` error handling.
|
||||
"""
|
||||
try:
|
||||
return model.model_validate(console_ns.payload or {})
|
||||
except ValidationError as exc:
|
||||
# Re-raise as-is so the upstream error handler renders a 400.
|
||||
raise exc
|
||||
|
||||
|
||||
def _dump(model: BaseModel) -> dict[str, Any]:
|
||||
return model.model_dump(mode="json")
|
||||
|
||||
|
||||
def _account_names_by_ids(account_ids: list[str]) -> dict[str, dict[str, str]]:
|
||||
ids = sorted({account_id.strip() for account_id in account_ids if account_id and account_id.strip()})
|
||||
if not ids:
|
||||
return {}
|
||||
|
||||
with session_factory.create_session() as session:
|
||||
rows = session.execute(
|
||||
select(Account.id, Account.name, Account.avatar, Account.email).where(Account.id.in_(ids))
|
||||
).all()
|
||||
|
||||
return {
|
||||
account_id: {
|
||||
"name": name or "",
|
||||
"avatar": avatar or "",
|
||||
"email": email or "",
|
||||
}
|
||||
for account_id, name, avatar, email in rows
|
||||
}
|
||||
|
||||
|
||||
def _hydrate_access_matrix_account_names(items: list[svc.AccessMatrixItem]) -> None:
|
||||
account_ids: list[str] = []
|
||||
for item in items:
|
||||
for account in item.accounts:
|
||||
account_id = account.account_id
|
||||
if account_id and not account.account_name:
|
||||
account_ids.append(account_id)
|
||||
|
||||
account_names = _account_names_by_ids(account_ids)
|
||||
if not account_names:
|
||||
return
|
||||
|
||||
for item in items:
|
||||
for account in item.accounts:
|
||||
account_id = str(account.account_id or "").strip()
|
||||
if account_id and not account.account_name:
|
||||
account.account_name = account_names.get(account_id, {}).get("name", "")
|
||||
account.avatar = account_names.get(account_id, {}).get("avatar", "")
|
||||
account.email = account_names.get(account_id, {}).get("email", "")
|
||||
|
||||
|
||||
def _hydrate_resource_user_account_names(items: list[svc.ResourceUserAccessPolicies]) -> None:
|
||||
account_names = _account_names_by_ids([item.account.account_id for item in items])
|
||||
for item in items:
|
||||
account_id = item.account.account_id
|
||||
if account_id and not item.account.account_name:
|
||||
item.account.account_name = account_names.get(account_id, {}).get("name", "")
|
||||
item.account.avatar = account_names.get(account_id, {}).get("avatar", "")
|
||||
item.account.email = account_names.get(account_id, {}).get("email", "")
|
||||
|
||||
|
||||
class _PaginationQuery(BaseModel):
|
||||
model_config = ConfigDict(extra="ignore")
|
||||
|
||||
page_number: int | None = Field(default=None, ge=1, validation_alias=AliasChoices("page", "page_number"))
|
||||
results_per_page: int | None = Field(
|
||||
default=None, ge=1, le=99999, validation_alias=AliasChoices("limit", "results_per_page")
|
||||
)
|
||||
reverse: bool | None = None
|
||||
|
||||
def to_inner_options(self) -> svc.ListOption:
|
||||
return svc.ListOption.model_validate(self.model_dump())
|
||||
|
||||
|
||||
class _RolesListQuery(_PaginationQuery):
|
||||
include_owner: int = Field(default=0, ge=0, le=1)
|
||||
|
||||
|
||||
class CopyRoleParam(BaseModel):
|
||||
copy_member: bool = True
|
||||
|
||||
|
||||
def _pagination_options() -> svc.ListOption:
|
||||
return _PaginationQuery.model_validate(request.args.to_dict(flat=True)).to_inner_options()
|
||||
|
||||
|
||||
def _legacy_workspace_roles(
|
||||
options: svc.ListOption | None = None, *, include_owner: int = 0
|
||||
) -> svc.Paginated[svc.RBACRole]:
|
||||
"""Return the built-in legacy workspace roles in the RBAC list shape.
|
||||
|
||||
This keeps the new `/rbac/roles` endpoint compatible with the original
|
||||
Dify role model when enterprise RBAC is disabled.
|
||||
"""
|
||||
|
||||
legacy_roles = [
|
||||
svc.RBACRole(
|
||||
id=role_name,
|
||||
tenant_id="",
|
||||
type=svc.RBACRoleType.WORKSPACE.value,
|
||||
category="global_system_default",
|
||||
name=role_name,
|
||||
description="",
|
||||
is_builtin=True,
|
||||
permission_keys=list(_LEGACY_ROLE_PERMISSION_KEYS[role_name]),
|
||||
role_tag="owner" if role_name == "owner" else "",
|
||||
)
|
||||
for role_name in ("owner", "admin", "editor", "normal", "dataset_operator")
|
||||
]
|
||||
|
||||
if not include_owner:
|
||||
legacy_roles = [r for r in legacy_roles if r.name != "owner"]
|
||||
|
||||
page_number = options.page_number if options and options.page_number is not None else 1
|
||||
results_per_page = (
|
||||
options.results_per_page if options and options.results_per_page is not None else len(legacy_roles)
|
||||
)
|
||||
reverse = options.reverse if options and options.reverse is not None else False
|
||||
|
||||
ordered_roles = list(reversed(legacy_roles)) if reverse else legacy_roles
|
||||
start = max(page_number - 1, 0) * results_per_page
|
||||
end = start + results_per_page
|
||||
paged_roles = ordered_roles[start:end]
|
||||
total_count = len(legacy_roles)
|
||||
total_pages = (total_count + results_per_page - 1) // results_per_page if results_per_page > 0 else 0
|
||||
|
||||
return svc.Paginated[svc.RBACRole](
|
||||
data=paged_roles,
|
||||
pagination=svc.Pagination(
|
||||
total_count=total_count,
|
||||
per_page=results_per_page,
|
||||
current_page=page_number,
|
||||
total_pages=total_pages,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Permission catalogs.
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@console_ns.route("/workspaces/current/rbac/role-permissions/catalog")
|
||||
class RBACWorkspaceCatalogApi(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.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.
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class _RoleUpsertRequest(BaseModel):
|
||||
"""Accepts the payload sent by the Create/Edit Role dialog."""
|
||||
|
||||
name: str
|
||||
description: str = ""
|
||||
permission_keys: list[str] = []
|
||||
|
||||
def to_mutation(self) -> svc.RoleMutation:
|
||||
return svc.RoleMutation(
|
||||
name=self.name,
|
||||
description=self.description,
|
||||
permission_keys=list(self.permission_keys),
|
||||
)
|
||||
|
||||
|
||||
@console_ns.route("/workspaces/current/rbac/roles")
|
||||
class RBACRolesApi(Resource):
|
||||
@login_required
|
||||
@rbac_permission_required(
|
||||
RBACResourceScope.WORKSPACE, RBACPermission.WORKSPACE_ROLE_MANAGE, resource_required=False
|
||||
)
|
||||
@console_ns.response(200, "Success", console_ns.models[_RBACRoleList.__name__])
|
||||
def get(self):
|
||||
tenant_id, account_id = _current_ids()
|
||||
query = _RolesListQuery.model_validate(request.args.to_dict(flat=True))
|
||||
options = query.to_inner_options()
|
||||
if not dify_config.RBAC_ENABLED:
|
||||
result = _legacy_workspace_roles(options, include_owner=query.include_owner)
|
||||
else:
|
||||
result = svc.RBACService.Roles.list(
|
||||
tenant_id, account_id, include_owner=query.include_owner, options=options
|
||||
)
|
||||
|
||||
return _dump(result)
|
||||
|
||||
@login_required
|
||||
@rbac_permission_required(
|
||||
RBACResourceScope.WORKSPACE, RBACPermission.WORKSPACE_ROLE_MANAGE, resource_required=False
|
||||
)
|
||||
@console_ns.response(201, "Role created", console_ns.models[svc.RBACRole.__name__])
|
||||
def post(self):
|
||||
tenant_id, account_id = _current_ids()
|
||||
request = _payload(_RoleUpsertRequest)
|
||||
role = svc.RBACService.Roles.create(tenant_id, account_id, request.to_mutation())
|
||||
return _dump(role), 201
|
||||
|
||||
|
||||
@console_ns.route("/workspaces/current/rbac/roles/<uuid:role_id>")
|
||||
class RBACRoleItemApi(Resource):
|
||||
@login_required
|
||||
@rbac_permission_required(
|
||||
RBACResourceScope.WORKSPACE, RBACPermission.WORKSPACE_ROLE_MANAGE, resource_required=False
|
||||
)
|
||||
@console_ns.response(200, "Success", console_ns.models[svc.RBACRole.__name__])
|
||||
def get(self, role_id):
|
||||
tenant_id, account_id = _current_ids()
|
||||
return _dump(svc.RBACService.Roles.get(tenant_id, account_id, str(role_id)))
|
||||
|
||||
@login_required
|
||||
@rbac_permission_required(
|
||||
RBACResourceScope.WORKSPACE, RBACPermission.WORKSPACE_ROLE_MANAGE, resource_required=False
|
||||
)
|
||||
@console_ns.response(200, "Success", console_ns.models[svc.RBACRole.__name__])
|
||||
def put(self, role_id):
|
||||
tenant_id, account_id = _current_ids()
|
||||
request = _payload(_RoleUpsertRequest)
|
||||
role = svc.RBACService.Roles.update(tenant_id, account_id, str(role_id), request.to_mutation())
|
||||
return _dump(role)
|
||||
|
||||
@login_required
|
||||
@rbac_permission_required(
|
||||
RBACResourceScope.WORKSPACE, RBACPermission.WORKSPACE_ROLE_MANAGE, resource_required=False
|
||||
)
|
||||
@console_ns.response(200, "Success", console_ns.models[svc.RBACRole.__name__])
|
||||
def delete(self, role_id):
|
||||
tenant_id, account_id = _current_ids()
|
||||
svc.RBACService.Roles.delete(tenant_id, account_id, str(role_id))
|
||||
return {"result": "success"}
|
||||
|
||||
|
||||
@console_ns.route("/workspaces/current/rbac/roles/<uuid:role_id>/copy")
|
||||
class RBACRoleCopyApi(Resource):
|
||||
@login_required
|
||||
@rbac_permission_required(
|
||||
RBACResourceScope.WORKSPACE, RBACPermission.WORKSPACE_ROLE_MANAGE, resource_required=False
|
||||
)
|
||||
@console_ns.response(201, "Role copied", console_ns.models[svc.RBACRole.__name__])
|
||||
def post(self, role_id):
|
||||
tenant_id, account_id = _current_ids()
|
||||
request = _payload(CopyRoleParam)
|
||||
role = svc.RBACService.Roles.copy(tenant_id, account_id, str(role_id), copy_member=request.copy_member)
|
||||
return _dump(role), 201
|
||||
|
||||
|
||||
@console_ns.route("/workspaces/current/rbac/roles/<uuid:role_id>/members")
|
||||
class RBACRoleMembersApi(Resource):
|
||||
@login_required
|
||||
@rbac_permission_required(
|
||||
RBACResourceScope.WORKSPACE, RBACPermission.WORKSPACE_ROLE_MANAGE, resource_required=False
|
||||
)
|
||||
@console_ns.response(200, "Success", console_ns.models[_RBACRoleAccountList.__name__])
|
||||
def get(self, role_id):
|
||||
tenant_id, account_id = _current_ids()
|
||||
return _dump(
|
||||
svc.RBACService.Roles.members(
|
||||
tenant_id,
|
||||
account_id,
|
||||
str(role_id),
|
||||
options=_pagination_options(),
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Access policies (tenant-level permission sets).
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class _AccessPolicyCreateRequest(BaseModel):
|
||||
name: str
|
||||
resource_type: svc.RBACResourceType
|
||||
description: str = ""
|
||||
permission_keys: list[str] = []
|
||||
|
||||
|
||||
class _AccessPolicyUpdateRequest(BaseModel):
|
||||
name: str
|
||||
description: str = ""
|
||||
permission_keys: list[str] = []
|
||||
|
||||
|
||||
@console_ns.route("/workspaces/current/rbac/access-policies")
|
||||
class RBACAccessPoliciesApi(Resource):
|
||||
@login_required
|
||||
@rbac_permission_required(
|
||||
RBACResourceScope.WORKSPACE, RBACPermission.WORKSPACE_ROLE_MANAGE, resource_required=False
|
||||
)
|
||||
@console_ns.response(200, "Success", console_ns.models[_AccessPolicyList.__name__])
|
||||
def get(self):
|
||||
tenant_id, account_id = _current_ids()
|
||||
# `resource_type` is exposed as a query argument so the UI can show
|
||||
# only app-scoped or only dataset-scoped permission sets.
|
||||
resource_type = request.args.get("resource_type") or None
|
||||
return _dump(
|
||||
svc.RBACService.AccessPolicies.list(
|
||||
tenant_id,
|
||||
account_id,
|
||||
resource_type=resource_type,
|
||||
options=_pagination_options(),
|
||||
)
|
||||
)
|
||||
|
||||
@login_required
|
||||
@rbac_permission_required(
|
||||
RBACResourceScope.WORKSPACE, RBACPermission.WORKSPACE_ROLE_MANAGE, resource_required=False
|
||||
)
|
||||
@console_ns.response(201, "Policy created", console_ns.models[svc.AccessPolicy.__name__])
|
||||
def post(self):
|
||||
tenant_id, account_id = _current_ids()
|
||||
request = _payload(_AccessPolicyCreateRequest)
|
||||
policy = svc.RBACService.AccessPolicies.create(
|
||||
tenant_id,
|
||||
account_id,
|
||||
svc.AccessPolicyCreate(
|
||||
name=request.name,
|
||||
resource_type=request.resource_type,
|
||||
description=request.description,
|
||||
permission_keys=list(request.permission_keys),
|
||||
),
|
||||
)
|
||||
return _dump(policy), 201
|
||||
|
||||
|
||||
@console_ns.route("/workspaces/current/rbac/access-policies/<uuid:policy_id>")
|
||||
class RBACAccessPolicyItemApi(Resource):
|
||||
@login_required
|
||||
@rbac_permission_required(
|
||||
RBACResourceScope.WORKSPACE, RBACPermission.WORKSPACE_ROLE_MANAGE, resource_required=False
|
||||
)
|
||||
@console_ns.response(200, "Success", console_ns.models[svc.AccessPolicy.__name__])
|
||||
def get(self, policy_id):
|
||||
tenant_id, account_id = _current_ids()
|
||||
return _dump(svc.RBACService.AccessPolicies.get(tenant_id, account_id, str(policy_id)))
|
||||
|
||||
@login_required
|
||||
@rbac_permission_required(
|
||||
RBACResourceScope.WORKSPACE, RBACPermission.WORKSPACE_ROLE_MANAGE, resource_required=False
|
||||
)
|
||||
@console_ns.response(200, "Success", console_ns.models[svc.AccessPolicy.__name__])
|
||||
def put(self, policy_id):
|
||||
tenant_id, account_id = _current_ids()
|
||||
request = _payload(_AccessPolicyUpdateRequest)
|
||||
policy = svc.RBACService.AccessPolicies.update(
|
||||
tenant_id,
|
||||
account_id,
|
||||
str(policy_id),
|
||||
svc.AccessPolicyUpdate(
|
||||
name=request.name,
|
||||
description=request.description,
|
||||
permission_keys=list(request.permission_keys),
|
||||
),
|
||||
)
|
||||
return _dump(policy)
|
||||
|
||||
@login_required
|
||||
@rbac_permission_required(
|
||||
RBACResourceScope.WORKSPACE, RBACPermission.WORKSPACE_ROLE_MANAGE, resource_required=False
|
||||
)
|
||||
@console_ns.response(200, "Success", console_ns.models[svc.AccessPolicy.__name__])
|
||||
def delete(self, policy_id):
|
||||
tenant_id, account_id = _current_ids()
|
||||
svc.RBACService.AccessPolicies.delete(tenant_id, account_id, str(policy_id))
|
||||
return {"result": "success"}
|
||||
|
||||
|
||||
@console_ns.route("/workspaces/current/rbac/access-policies/<uuid:policy_id>/copy")
|
||||
class RBACAccessPolicyCopyApi(Resource):
|
||||
@login_required
|
||||
@rbac_permission_required(
|
||||
RBACResourceScope.WORKSPACE, RBACPermission.WORKSPACE_ROLE_MANAGE, resource_required=False
|
||||
)
|
||||
@console_ns.response(201, "Policy copied", console_ns.models[svc.AccessPolicy.__name__])
|
||||
def post(self, policy_id):
|
||||
tenant_id, account_id = _current_ids()
|
||||
policy = svc.RBACService.AccessPolicies.copy(tenant_id, account_id, str(policy_id))
|
||||
return _dump(policy), 201
|
||||
|
||||
|
||||
@console_ns.route("/workspaces/current/rbac/access-policy-bindings/<uuid:binding_id>/lock")
|
||||
class RBACAccessPolicyBindingLockApi(Resource):
|
||||
@login_required
|
||||
@rbac_permission_required(
|
||||
RBACResourceScope.WORKSPACE, RBACPermission.WORKSPACE_ROLE_MANAGE, resource_required=False
|
||||
)
|
||||
@console_ns.response(200, "Success", console_ns.models[svc.AccessPolicyBindingState.__name__])
|
||||
def put(self, binding_id):
|
||||
tenant_id, account_id = _current_ids()
|
||||
return _dump(svc.RBACService.AccessPolicyBindings.lock(tenant_id, account_id, str(binding_id)))
|
||||
|
||||
|
||||
@console_ns.route("/workspaces/current/rbac/access-policy-bindings/<uuid:binding_id>/unlock")
|
||||
class RBACAccessPolicyBindingUnlockApi(Resource):
|
||||
@login_required
|
||||
@rbac_permission_required(
|
||||
RBACResourceScope.WORKSPACE, RBACPermission.WORKSPACE_ROLE_MANAGE, resource_required=False
|
||||
)
|
||||
@console_ns.response(200, "Success", console_ns.models[svc.AccessPolicyBindingState.__name__])
|
||||
def put(self, binding_id):
|
||||
tenant_id, account_id = _current_ids()
|
||||
return _dump(svc.RBACService.AccessPolicyBindings.unlock(tenant_id, account_id, str(binding_id)))
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Per-app access (App Access Config).
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class _AccessScope(StrEnum):
|
||||
ALL = "all"
|
||||
SPECIFIC = "specific"
|
||||
ONLY_ME = "only_me"
|
||||
|
||||
|
||||
class _ResourceAccessScopeRequest(BaseModel):
|
||||
scope: _AccessScope
|
||||
|
||||
|
||||
class _ReplaceBindingsRequest(BaseModel):
|
||||
role_ids: list[str] = Field(default_factory=list)
|
||||
account_ids: list[str] = Field(default_factory=list)
|
||||
|
||||
@field_validator("role_ids", "account_ids", mode="before")
|
||||
@classmethod
|
||||
def _coerce_bindings(cls, value: Any) -> list[str]:
|
||||
if value is None:
|
||||
return []
|
||||
return value
|
||||
|
||||
|
||||
class _DeleteMemberBindingsRequest(BaseModel):
|
||||
account_ids: list[str] = Field(default_factory=list)
|
||||
|
||||
@field_validator("account_ids", mode="before")
|
||||
@classmethod
|
||||
def _coerce_account_ids(cls, value: Any) -> list[str]:
|
||||
if value is None:
|
||||
return []
|
||||
return value
|
||||
|
||||
|
||||
@console_ns.route("/workspaces/current/rbac/my-permissions")
|
||||
class RBACMyPermissionsApi(Resource):
|
||||
@login_required
|
||||
@console_ns.response(200, "Success", console_ns.models[svc.MyPermissionsResponse.__name__])
|
||||
def get(self):
|
||||
tenant_id, account_id = _current_ids()
|
||||
return _dump(
|
||||
svc.RBACService.MyPermissions.get(
|
||||
tenant_id,
|
||||
account_id,
|
||||
app_id=request.args.get("app_id") or None,
|
||||
dataset_id=request.args.get("dataset_id") or None,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@console_ns.route("/workspaces/current/rbac/apps/<uuid:app_id>/access-policy")
|
||||
class RBACAppMatrixApi(Resource):
|
||||
@login_required
|
||||
@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.response(200, "Success", console_ns.models[svc.ResourceWhitelist.__name__])
|
||||
def put(self, app_id):
|
||||
tenant_id, account_id = _current_ids()
|
||||
request = _payload(_ResourceAccessScopeRequest)
|
||||
return _dump(
|
||||
svc.RBACService.AppAccess.replace_whitelist(
|
||||
tenant_id,
|
||||
account_id,
|
||||
str(app_id),
|
||||
svc.ReplaceMemberBindings(scope=request.scope.value),
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@console_ns.route("/workspaces/current/rbac/apps/<uuid:app_id>/user-access-policies")
|
||||
class RBACAppUserAccessPoliciesApi(Resource):
|
||||
@login_required
|
||||
@console_ns.response(200, "Success", console_ns.models[svc.ResourceUserAccessPoliciesResponse.__name__])
|
||||
def get(self, app_id):
|
||||
tenant_id, account_id = _current_ids()
|
||||
result = svc.RBACService.AppAccess.user_access_policies(tenant_id, account_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.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,
|
||||
str(app_id),
|
||||
str(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.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.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")
|
||||
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.response(200, "Success", console_ns.models[svc.ResourceWhitelist.__name__])
|
||||
def put(self, dataset_id):
|
||||
tenant_id, account_id = _current_ids()
|
||||
request = _payload(_ResourceAccessScopeRequest)
|
||||
return _dump(
|
||||
svc.RBACService.DatasetAccess.replace_whitelist(
|
||||
tenant_id,
|
||||
account_id,
|
||||
str(dataset_id),
|
||||
svc.ReplaceMemberBindings(scope=request.scope.value),
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@console_ns.route("/workspaces/current/rbac/datasets/<uuid:dataset_id>/user-access-policies")
|
||||
class RBACDatasetUserAccessPoliciesApi(Resource):
|
||||
@login_required
|
||||
@console_ns.response(200, "Success", console_ns.models[svc.ResourceUserAccessPoliciesResponse.__name__])
|
||||
def get(self, dataset_id):
|
||||
tenant_id, account_id = _current_ids()
|
||||
result = svc.RBACService.DatasetAccess.user_access_policies(tenant_id, account_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.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.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"}
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Workspace-level access (Settings > Access Rules).
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@console_ns.route("/workspaces/current/rbac/workspace/apps/access-policy")
|
||||
class RBACWorkspaceAppMatrixApi(Resource):
|
||||
@login_required
|
||||
@console_ns.response(200, "Success", console_ns.models[svc.WorkspaceAccessMatrix.__name__])
|
||||
def get(self):
|
||||
tenant_id, account_id = _current_ids()
|
||||
options = _pagination_options()
|
||||
result = svc.RBACService.WorkspaceAccess.app_matrix(tenant_id, account_id, options=options)
|
||||
_hydrate_access_matrix_account_names(result.items)
|
||||
return _dump(result)
|
||||
|
||||
|
||||
@console_ns.route("/workspaces/current/rbac/workspace/apps/access-policies/<uuid:policy_id>/role-bindings")
|
||||
class RBACWorkspaceAppRoleBindingsApi(Resource):
|
||||
@login_required
|
||||
@console_ns.response(200, "Success", console_ns.models[svc.RoleBindingsResponse.__name__])
|
||||
def get(self, policy_id):
|
||||
tenant_id, account_id = _current_ids()
|
||||
return _dump(svc.RBACService.WorkspaceAccess.list_app_role_bindings(tenant_id, account_id, str(policy_id)))
|
||||
|
||||
|
||||
@console_ns.route("/workspaces/current/rbac/workspace/apps/access-policies/<uuid:policy_id>/bindings")
|
||||
class RBACWorkspaceAppBindingsApi(Resource):
|
||||
@login_required
|
||||
@console_ns.response(200, "Success", console_ns.models[svc.AccessMatrixItem.__name__])
|
||||
def put(self, policy_id):
|
||||
tenant_id, account_id = _current_ids()
|
||||
request = _payload(_ReplaceBindingsRequest)
|
||||
return _dump(
|
||||
svc.RBACService.WorkspaceAccess.replace_app_bindings(
|
||||
tenant_id,
|
||||
account_id,
|
||||
str(policy_id),
|
||||
svc.ReplaceBindings(role_ids=list(request.role_ids), account_ids=list(request.account_ids)),
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@console_ns.route("/workspaces/current/rbac/workspace/apps/access-policies/<uuid:policy_id>/member-bindings")
|
||||
class RBACWorkspaceAppMemberBindingsApi(Resource):
|
||||
@login_required
|
||||
@console_ns.response(200, "Success", console_ns.models[svc.MemberBindingsResponse.__name__])
|
||||
def get(self, policy_id):
|
||||
tenant_id, account_id = _current_ids()
|
||||
return _dump(svc.RBACService.WorkspaceAccess.list_app_member_bindings(tenant_id, account_id, str(policy_id)))
|
||||
|
||||
|
||||
@console_ns.route("/workspaces/current/rbac/workspace/datasets/access-policy")
|
||||
class RBACWorkspaceDatasetMatrixApi(Resource):
|
||||
@login_required
|
||||
@console_ns.response(200, "Success", console_ns.models[svc.WorkspaceAccessMatrix.__name__])
|
||||
def get(self):
|
||||
tenant_id, account_id = _current_ids()
|
||||
options = _pagination_options()
|
||||
result = svc.RBACService.WorkspaceAccess.dataset_matrix(tenant_id, account_id, options=options)
|
||||
_hydrate_access_matrix_account_names(result.items)
|
||||
return _dump(result)
|
||||
|
||||
|
||||
@console_ns.route("/workspaces/current/rbac/workspace/datasets/access-policies/<uuid:policy_id>/role-bindings")
|
||||
class RBACWorkspaceDatasetRoleBindingsApi(Resource):
|
||||
@login_required
|
||||
@console_ns.response(200, "Success", console_ns.models[svc.RoleBindingsResponse.__name__])
|
||||
def get(self, policy_id):
|
||||
tenant_id, account_id = _current_ids()
|
||||
return _dump(svc.RBACService.WorkspaceAccess.list_dataset_role_bindings(tenant_id, account_id, str(policy_id)))
|
||||
|
||||
|
||||
@console_ns.route("/workspaces/current/rbac/workspace/datasets/access-policies/<uuid:policy_id>/bindings")
|
||||
class RBACWorkspaceDatasetBindingsApi(Resource):
|
||||
@login_required
|
||||
@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,
|
||||
account_id,
|
||||
str(policy_id),
|
||||
svc.ReplaceBindings(role_ids=list(request.role_ids), account_ids=list(request.account_ids)),
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@console_ns.route("/workspaces/current/rbac/workspace/datasets/access-policies/<uuid:policy_id>/member-bindings")
|
||||
class RBACWorkspaceDatasetMemberBindingsApi(Resource):
|
||||
@login_required
|
||||
@console_ns.response(200, "Success", console_ns.models[svc.MemberBindingsResponse.__name__])
|
||||
def get(self, policy_id):
|
||||
tenant_id, account_id = _current_ids()
|
||||
return _dump(
|
||||
svc.RBACService.WorkspaceAccess.list_dataset_member_bindings(tenant_id, account_id, str(policy_id))
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Member ↔ role bindings (Settings > Members > Assign roles).
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class _ReplaceMemberRolesRequest(BaseModel):
|
||||
role_ids: list[str] = []
|
||||
|
||||
@field_validator("role_ids", mode="before")
|
||||
@classmethod
|
||||
def _coerce_role_ids(cls, value: Any) -> list[str]:
|
||||
if value is None:
|
||||
return []
|
||||
return value
|
||||
|
||||
|
||||
@console_ns.route("/workspaces/current/rbac/members/<uuid:member_id>/rbac-roles")
|
||||
class RBACMemberRolesApi(Resource):
|
||||
@login_required
|
||||
@console_ns.response(200, "Success", console_ns.models[svc.MemberRolesResponse.__name__])
|
||||
def get(self, member_id):
|
||||
tenant_id, account_id = _current_ids()
|
||||
return _dump(svc.RBACService.MemberRoles.get(tenant_id, account_id, str(member_id)))
|
||||
|
||||
@login_required
|
||||
@console_ns.response(200, "Success", console_ns.models[svc.MemberRolesResponse.__name__])
|
||||
def put(self, member_id):
|
||||
tenant_id, account_id = _current_ids()
|
||||
request = _payload(_ReplaceMemberRolesRequest)
|
||||
return _dump(
|
||||
svc.RBACService.MemberRoles.replace(
|
||||
tenant_id,
|
||||
account_id,
|
||||
str(member_id),
|
||||
role_ids=list(request.role_ids),
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@console_ns.route("/workspaces/current/rbac/roles/<uuid:role_id>/members")
|
||||
class ListMembersByRole(Resource):
|
||||
@login_required
|
||||
@console_ns.response(200, "Success", console_ns.models[_MembersInRoleList.__name__])
|
||||
def get(self, role_id):
|
||||
tenant_id, account_id = _current_ids()
|
||||
return _dump(
|
||||
svc.RBACService.Roles.list_members_by_role(tenant_id, role_id=role_id, options=_pagination_options())
|
||||
)
|
||||
@ -21,8 +21,11 @@ from controllers.console.snippets.payloads import (
|
||||
UpdateSnippetPayload,
|
||||
)
|
||||
from controllers.console.wraps import (
|
||||
RBACPermission,
|
||||
RBACResourceScope,
|
||||
account_initialization_required,
|
||||
edit_permission_required,
|
||||
rbac_permission_required,
|
||||
setup_required,
|
||||
with_current_tenant_id,
|
||||
with_current_user,
|
||||
@ -151,6 +154,9 @@ class CustomizedSnippetsApi(Resource):
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@edit_permission_required
|
||||
@rbac_permission_required(
|
||||
RBACResourceScope.WORKSPACE, RBACPermission.SNIPPETS_CREATE_AND_MODIFY, resource_required=False
|
||||
)
|
||||
@with_current_user
|
||||
@with_current_tenant_id
|
||||
def post(self, current_tenant_id: str, current_user: Account):
|
||||
@ -213,6 +219,9 @@ class CustomizedSnippetDetailApi(Resource):
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@edit_permission_required
|
||||
@rbac_permission_required(
|
||||
RBACResourceScope.WORKSPACE, RBACPermission.SNIPPETS_CREATE_AND_MODIFY, resource_required=False
|
||||
)
|
||||
@with_current_user
|
||||
@with_current_tenant_id
|
||||
def patch(self, current_tenant_id: str, current_user: Account, snippet_id: str):
|
||||
@ -257,6 +266,7 @@ class CustomizedSnippetDetailApi(Resource):
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@edit_permission_required
|
||||
@rbac_permission_required(RBACResourceScope.WORKSPACE, RBACPermission.SNIPPETS_MANAGE, resource_required=False)
|
||||
@with_current_tenant_id
|
||||
def delete(self, current_tenant_id: str, snippet_id: str):
|
||||
"""Delete customized snippet."""
|
||||
@ -292,6 +302,9 @@ class CustomizedSnippetExportApi(Resource):
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@edit_permission_required
|
||||
@rbac_permission_required(
|
||||
RBACResourceScope.WORKSPACE, RBACPermission.SNIPPETS_CREATE_AND_MODIFY, resource_required=False
|
||||
)
|
||||
@with_current_tenant_id
|
||||
def get(self, current_tenant_id: str, snippet_id: str):
|
||||
"""Export snippet as DSL."""
|
||||
@ -337,6 +350,9 @@ class CustomizedSnippetImportApi(Resource):
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@edit_permission_required
|
||||
@rbac_permission_required(
|
||||
RBACResourceScope.WORKSPACE, RBACPermission.SNIPPETS_CREATE_AND_MODIFY, resource_required=False
|
||||
)
|
||||
@with_current_user
|
||||
def post(self, current_user: Account):
|
||||
"""Import snippet from DSL."""
|
||||
@ -375,6 +391,9 @@ class CustomizedSnippetImportConfirmApi(Resource):
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@edit_permission_required
|
||||
@rbac_permission_required(
|
||||
RBACResourceScope.WORKSPACE, RBACPermission.SNIPPETS_CREATE_AND_MODIFY, resource_required=False
|
||||
)
|
||||
@with_current_user
|
||||
def post(self, current_user: Account, import_id: str):
|
||||
"""Confirm a pending snippet import."""
|
||||
@ -403,6 +422,9 @@ class CustomizedSnippetCheckDependenciesApi(Resource):
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@edit_permission_required
|
||||
@rbac_permission_required(
|
||||
RBACResourceScope.WORKSPACE, RBACPermission.SNIPPETS_CREATE_AND_MODIFY, resource_required=False
|
||||
)
|
||||
@with_current_tenant_id
|
||||
def get(self, current_tenant_id: str, snippet_id: str):
|
||||
"""Check dependencies for a snippet."""
|
||||
@ -433,6 +455,9 @@ class CustomizedSnippetUseCountIncrementApi(Resource):
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@edit_permission_required
|
||||
@rbac_permission_required(
|
||||
RBACResourceScope.WORKSPACE, RBACPermission.SNIPPETS_CREATE_AND_MODIFY, resource_required=False
|
||||
)
|
||||
@with_current_tenant_id
|
||||
def post(self, current_tenant_id: str, snippet_id: str):
|
||||
"""Increment snippet use count when it is inserted into a workflow."""
|
||||
|
||||
@ -14,9 +14,12 @@ from controllers.common.fields import BinaryFileResponse, RedirectResponse, Simp
|
||||
from controllers.common.schema import query_params_from_model, register_response_schema_models, register_schema_models
|
||||
from controllers.console import console_ns
|
||||
from controllers.console.wraps import (
|
||||
RBACPermission,
|
||||
RBACResourceScope,
|
||||
account_initialization_required,
|
||||
enterprise_license_required,
|
||||
is_admin_or_owner_required,
|
||||
rbac_permission_required,
|
||||
setup_required,
|
||||
with_current_tenant_id,
|
||||
with_current_user,
|
||||
@ -369,6 +372,7 @@ class ToolBuiltinProviderDeleteApi(Resource):
|
||||
@setup_required
|
||||
@login_required
|
||||
@is_admin_or_owner_required
|
||||
@rbac_permission_required(RBACResourceScope.WORKSPACE, RBACPermission.CREDENTIAL_MANAGE, resource_required=False)
|
||||
@account_initialization_required
|
||||
@with_current_tenant_id
|
||||
def post(self, tenant_id: str, provider: str):
|
||||
@ -411,6 +415,7 @@ class ToolBuiltinProviderUpdateApi(Resource):
|
||||
@setup_required
|
||||
@login_required
|
||||
@is_admin_or_owner_required
|
||||
@rbac_permission_required(RBACResourceScope.WORKSPACE, RBACPermission.CREDENTIAL_MANAGE, resource_required=False)
|
||||
@account_initialization_required
|
||||
@with_current_user
|
||||
@with_current_tenant_id
|
||||
@ -471,6 +476,7 @@ class ToolApiProviderAddApi(Resource):
|
||||
@setup_required
|
||||
@login_required
|
||||
@is_admin_or_owner_required
|
||||
@rbac_permission_required(RBACResourceScope.WORKSPACE, RBACPermission.TOOL_MANAGE, resource_required=False)
|
||||
@account_initialization_required
|
||||
@with_current_user
|
||||
@with_current_tenant_id
|
||||
@ -540,6 +546,7 @@ class ToolApiProviderUpdateApi(Resource):
|
||||
@setup_required
|
||||
@login_required
|
||||
@is_admin_or_owner_required
|
||||
@rbac_permission_required(RBACResourceScope.WORKSPACE, RBACPermission.TOOL_MANAGE, resource_required=False)
|
||||
@account_initialization_required
|
||||
@with_current_user
|
||||
@with_current_tenant_id
|
||||
@ -568,6 +575,7 @@ class ToolApiProviderDeleteApi(Resource):
|
||||
@setup_required
|
||||
@login_required
|
||||
@is_admin_or_owner_required
|
||||
@rbac_permission_required(RBACResourceScope.WORKSPACE, RBACPermission.TOOL_MANAGE, resource_required=False)
|
||||
@account_initialization_required
|
||||
@with_current_user
|
||||
@with_current_tenant_id
|
||||
@ -659,6 +667,7 @@ class ToolWorkflowProviderCreateApi(Resource):
|
||||
@setup_required
|
||||
@login_required
|
||||
@is_admin_or_owner_required
|
||||
@rbac_permission_required(RBACResourceScope.WORKSPACE, RBACPermission.TOOL_MANAGE, resource_required=False)
|
||||
@account_initialization_required
|
||||
@with_current_user
|
||||
@with_current_tenant_id
|
||||
@ -686,6 +695,7 @@ class ToolWorkflowProviderUpdateApi(Resource):
|
||||
@setup_required
|
||||
@login_required
|
||||
@is_admin_or_owner_required
|
||||
@rbac_permission_required(RBACResourceScope.WORKSPACE, RBACPermission.TOOL_MANAGE, resource_required=False)
|
||||
@account_initialization_required
|
||||
@with_current_user
|
||||
@with_current_tenant_id
|
||||
@ -713,6 +723,7 @@ class ToolWorkflowProviderDeleteApi(Resource):
|
||||
@setup_required
|
||||
@login_required
|
||||
@is_admin_or_owner_required
|
||||
@rbac_permission_required(RBACResourceScope.WORKSPACE, RBACPermission.TOOL_MANAGE, resource_required=False)
|
||||
@account_initialization_required
|
||||
@with_current_user
|
||||
@with_current_tenant_id
|
||||
@ -858,6 +869,7 @@ class ToolPluginOAuthApi(Resource):
|
||||
@setup_required
|
||||
@login_required
|
||||
@is_admin_or_owner_required
|
||||
@rbac_permission_required(RBACResourceScope.WORKSPACE, RBACPermission.CREDENTIAL_MANAGE, resource_required=False)
|
||||
@account_initialization_required
|
||||
@with_current_user
|
||||
@with_current_tenant_id
|
||||
@ -959,6 +971,7 @@ class ToolBuiltinProviderSetDefaultApi(Resource):
|
||||
@setup_required
|
||||
@login_required
|
||||
@is_admin_or_owner_required
|
||||
@rbac_permission_required(RBACResourceScope.WORKSPACE, RBACPermission.CREDENTIAL_MANAGE, resource_required=False)
|
||||
@account_initialization_required
|
||||
@with_current_tenant_id
|
||||
def post(self, current_tenant_id: str, provider: str):
|
||||
@ -975,6 +988,7 @@ class ToolOAuthCustomClient(Resource):
|
||||
@setup_required
|
||||
@login_required
|
||||
@is_admin_or_owner_required
|
||||
@rbac_permission_required(RBACResourceScope.WORKSPACE, RBACPermission.CREDENTIAL_MANAGE, resource_required=False)
|
||||
@account_initialization_required
|
||||
@with_current_tenant_id
|
||||
def post(self, tenant_id: str, provider: str):
|
||||
|
||||
@ -27,9 +27,12 @@ from services.trigger.trigger_subscription_operator_service import TriggerSubscr
|
||||
|
||||
from .. import console_ns
|
||||
from ..wraps import (
|
||||
RBACPermission,
|
||||
RBACResourceScope,
|
||||
account_initialization_required,
|
||||
edit_permission_required,
|
||||
is_admin_or_owner_required,
|
||||
rbac_permission_required,
|
||||
setup_required,
|
||||
with_current_tenant_id,
|
||||
with_current_user,
|
||||
@ -143,6 +146,7 @@ class TriggerSubscriptionListApi(Resource):
|
||||
@setup_required
|
||||
@login_required
|
||||
@edit_permission_required
|
||||
@rbac_permission_required(RBACResourceScope.WORKSPACE, RBACPermission.PLUGIN_PREFERENCES, resource_required=False)
|
||||
@account_initialization_required
|
||||
@with_current_user
|
||||
@with_current_tenant_id
|
||||
@ -172,6 +176,7 @@ class TriggerSubscriptionBuilderCreateApi(Resource):
|
||||
@setup_required
|
||||
@login_required
|
||||
@edit_permission_required
|
||||
@rbac_permission_required(RBACResourceScope.WORKSPACE, RBACPermission.PLUGIN_PREFERENCES, resource_required=False)
|
||||
@account_initialization_required
|
||||
@with_current_user
|
||||
@with_current_tenant_id
|
||||
@ -201,6 +206,7 @@ class TriggerSubscriptionBuilderGetApi(Resource):
|
||||
@setup_required
|
||||
@login_required
|
||||
@edit_permission_required
|
||||
@rbac_permission_required(RBACResourceScope.WORKSPACE, RBACPermission.PLUGIN_PREFERENCES, resource_required=False)
|
||||
@account_initialization_required
|
||||
def get(self, provider: str, subscription_builder_id: str):
|
||||
"""Get a subscription instance for a trigger provider"""
|
||||
@ -218,6 +224,7 @@ class TriggerSubscriptionBuilderVerifyApi(Resource):
|
||||
@setup_required
|
||||
@login_required
|
||||
@edit_permission_required
|
||||
@rbac_permission_required(RBACResourceScope.WORKSPACE, RBACPermission.CREDENTIAL_MANAGE, resource_required=False)
|
||||
@account_initialization_required
|
||||
@with_current_user
|
||||
@with_current_tenant_id
|
||||
@ -250,6 +257,7 @@ class TriggerSubscriptionBuilderUpdateApi(Resource):
|
||||
@setup_required
|
||||
@login_required
|
||||
@edit_permission_required
|
||||
@rbac_permission_required(RBACResourceScope.WORKSPACE, RBACPermission.CREDENTIAL_MANAGE, resource_required=False)
|
||||
@account_initialization_required
|
||||
@with_current_tenant_id
|
||||
def post(self, tenant_id: str, provider: str, subscription_builder_id: str):
|
||||
@ -282,6 +290,7 @@ class TriggerSubscriptionBuilderLogsApi(Resource):
|
||||
@setup_required
|
||||
@login_required
|
||||
@edit_permission_required
|
||||
@rbac_permission_required(RBACResourceScope.WORKSPACE, RBACPermission.PLUGIN_PREFERENCES, resource_required=False)
|
||||
@account_initialization_required
|
||||
def get(self, provider: str, subscription_builder_id: str):
|
||||
"""Get the request logs for a subscription instance for a trigger provider"""
|
||||
@ -302,6 +311,7 @@ class TriggerSubscriptionBuilderBuildApi(Resource):
|
||||
@setup_required
|
||||
@login_required
|
||||
@edit_permission_required
|
||||
@rbac_permission_required(RBACResourceScope.WORKSPACE, RBACPermission.PLUGIN_PREFERENCES, resource_required=False)
|
||||
@account_initialization_required
|
||||
@with_current_user
|
||||
@with_current_tenant_id
|
||||
@ -336,6 +346,7 @@ class TriggerSubscriptionUpdateApi(Resource):
|
||||
@setup_required
|
||||
@login_required
|
||||
@edit_permission_required
|
||||
@rbac_permission_required(RBACResourceScope.WORKSPACE, RBACPermission.PLUGIN_PREFERENCES, resource_required=False)
|
||||
@account_initialization_required
|
||||
@with_current_tenant_id
|
||||
def post(self, tenant_id: str, subscription_id: str):
|
||||
@ -393,6 +404,7 @@ class TriggerSubscriptionDeleteApi(Resource):
|
||||
@setup_required
|
||||
@login_required
|
||||
@is_admin_or_owner_required
|
||||
@rbac_permission_required(RBACResourceScope.WORKSPACE, RBACPermission.PLUGIN_PREFERENCES, resource_required=False)
|
||||
@account_initialization_required
|
||||
@with_current_tenant_id
|
||||
def post(self, tenant_id: str, subscription_id: str):
|
||||
@ -581,6 +593,7 @@ class TriggerOAuthClientManageApi(Resource):
|
||||
@setup_required
|
||||
@login_required
|
||||
@is_admin_or_owner_required
|
||||
@rbac_permission_required(RBACResourceScope.WORKSPACE, RBACPermission.PLUGIN_PREFERENCES, resource_required=False)
|
||||
@account_initialization_required
|
||||
@with_current_tenant_id
|
||||
def get(self, tenant_id: str, provider: str):
|
||||
@ -626,6 +639,7 @@ class TriggerOAuthClientManageApi(Resource):
|
||||
@setup_required
|
||||
@login_required
|
||||
@is_admin_or_owner_required
|
||||
@rbac_permission_required(RBACResourceScope.WORKSPACE, RBACPermission.PLUGIN_PREFERENCES, resource_required=False)
|
||||
@account_initialization_required
|
||||
@with_current_tenant_id
|
||||
def post(self, tenant_id: str, provider: str):
|
||||
@ -650,6 +664,7 @@ class TriggerOAuthClientManageApi(Resource):
|
||||
@setup_required
|
||||
@login_required
|
||||
@is_admin_or_owner_required
|
||||
@rbac_permission_required(RBACResourceScope.WORKSPACE, RBACPermission.PLUGIN_PREFERENCES, resource_required=False)
|
||||
@account_initialization_required
|
||||
@console_ns.response(200, "Success", console_ns.models[SimpleResultResponse.__name__])
|
||||
@with_current_tenant_id
|
||||
@ -678,6 +693,7 @@ class TriggerSubscriptionVerifyApi(Resource):
|
||||
@setup_required
|
||||
@login_required
|
||||
@edit_permission_required
|
||||
@rbac_permission_required(RBACResourceScope.WORKSPACE, RBACPermission.PLUGIN_PREFERENCES, resource_required=False)
|
||||
@account_initialization_required
|
||||
@with_current_user
|
||||
@with_current_tenant_id
|
||||
|
||||
@ -9,9 +9,14 @@ from typing import Any, Concatenate, overload
|
||||
from flask import abort, request
|
||||
from pydantic import BaseModel, ValidationError
|
||||
from sqlalchemy import select
|
||||
from werkzeug.exceptions import UnprocessableEntity
|
||||
from werkzeug.exceptions import Forbidden, UnprocessableEntity
|
||||
|
||||
from configs import dify_config
|
||||
from controllers.common.wraps import (
|
||||
RBACPermission,
|
||||
RBACResourceScope,
|
||||
rbac_permission_required,
|
||||
)
|
||||
from controllers.console.auth.error import AuthenticationFailedError, EmailCodeError
|
||||
from controllers.console.workspace.error import AccountNotInitializedError
|
||||
from enums.cloud_plan import CloudPlan
|
||||
@ -28,6 +33,10 @@ from services.operation_service import OperationService, UtmInfo
|
||||
|
||||
from .error import NotInitValidateError, NotSetupError, UnauthorizedAndForceLogout
|
||||
|
||||
# Re-exported so controllers can import the RBAC enums and decorator alongside
|
||||
# other console wraps from this module.
|
||||
__all__ = ["RBACPermission", "RBACResourceScope", "rbac_permission_required"]
|
||||
|
||||
# Field names for decryption
|
||||
FIELD_NAME_PASSWORD = "password"
|
||||
FIELD_NAME_CODE = "code"
|
||||
@ -335,15 +344,15 @@ def knowledge_pipeline_publish_enabled[**P, R](view: Callable[P, R]) -> Callable
|
||||
def edit_permission_required[**P, R](f: Callable[P, R]) -> Callable[P, R]:
|
||||
@wraps(f)
|
||||
def decorated_function(*args: P.args, **kwargs: P.kwargs):
|
||||
from werkzeug.exceptions import Forbidden
|
||||
|
||||
from libs.login import current_user
|
||||
|
||||
user = current_user._get_current_object() # type: ignore
|
||||
if not isinstance(user, Account):
|
||||
raise Forbidden()
|
||||
if not current_user.has_edit_permission:
|
||||
raise Forbidden()
|
||||
if not dify_config.RBAC_ENABLED:
|
||||
user = current_user._get_current_object() # type: ignore
|
||||
if not isinstance(user, Account):
|
||||
raise Forbidden()
|
||||
if not current_user.has_edit_permission:
|
||||
raise Forbidden()
|
||||
return f(*args, **kwargs)
|
||||
|
||||
return decorated_function
|
||||
@ -352,13 +361,13 @@ def edit_permission_required[**P, R](f: Callable[P, R]) -> Callable[P, R]:
|
||||
def is_admin_or_owner_required[**P, R](f: Callable[P, R]) -> Callable[P, R]:
|
||||
@wraps(f)
|
||||
def decorated_function(*args: P.args, **kwargs: P.kwargs):
|
||||
from werkzeug.exceptions import Forbidden
|
||||
|
||||
from libs.login import current_user
|
||||
|
||||
user = current_user._get_current_object()
|
||||
if not isinstance(user, Account) or not user.is_admin_or_owner:
|
||||
raise Forbidden()
|
||||
if not dify_config.RBAC_ENABLED:
|
||||
user = current_user._get_current_object()
|
||||
if not isinstance(user, Account) or not user.is_admin_or_owner:
|
||||
raise Forbidden()
|
||||
return f(*args, **kwargs)
|
||||
|
||||
return decorated_function
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
from typing import Annotated, Literal, override
|
||||
from typing import Annotated, Any, Literal, override
|
||||
from uuid import UUID
|
||||
|
||||
from flask import request
|
||||
@ -15,6 +15,7 @@ from pydantic import (
|
||||
from werkzeug.exceptions import Forbidden, NotFound
|
||||
|
||||
import services
|
||||
from configs import dify_config
|
||||
from controllers.common.fields import SimpleResultResponse
|
||||
from controllers.common.schema import (
|
||||
query_params_from_model,
|
||||
@ -84,6 +85,32 @@ PartialMemberList = Annotated[
|
||||
]
|
||||
|
||||
|
||||
_SERVICE_DATASET_DETAIL_EXCLUDE = {"permission_keys"}
|
||||
_SERVICE_DATASET_LIST_EXCLUDE = {"data": {"__all__": _SERVICE_DATASET_DETAIL_EXCLUDE}}
|
||||
|
||||
|
||||
def _dump_service_dataset_detail(dataset: Any) -> dict[str, Any]:
|
||||
return DatasetDetailResponse.model_validate(dataset, from_attributes=True).model_dump(
|
||||
mode="json",
|
||||
exclude=_SERVICE_DATASET_DETAIL_EXCLUDE,
|
||||
)
|
||||
|
||||
|
||||
def _dump_service_dataset_list(response: dict[str, Any]) -> dict[str, Any]:
|
||||
return DatasetListResponse.model_validate(response).model_dump(
|
||||
mode="json",
|
||||
exclude=_SERVICE_DATASET_LIST_EXCLUDE,
|
||||
)
|
||||
|
||||
|
||||
def _dump_service_dataset_with_partial_members(data: dict[str, Any]) -> dict[str, Any]:
|
||||
exclude: set[str] = set(_SERVICE_DATASET_DETAIL_EXCLUDE)
|
||||
if "partial_member_list" not in data:
|
||||
exclude.add("partial_member_list")
|
||||
|
||||
return DatasetDetailWithPartialMembersResponse.model_validate(data).model_dump(mode="json", exclude=exclude)
|
||||
|
||||
|
||||
class DatasetCreatePayload(BaseModel):
|
||||
name: str = Field(..., min_length=1, max_length=40, description="Name of the knowledge base.")
|
||||
description: str = Field(default="", description="Description of the knowledge base.", max_length=400)
|
||||
@ -383,7 +410,14 @@ class DatasetListApi(DatasetApiResource):
|
||||
# provider = request.args.get("provider", default="vendor")
|
||||
|
||||
datasets, total = DatasetService.get_datasets(
|
||||
query.page, query.limit, tenant_id, current_user, query.keyword, query.tag_ids, query.include_all
|
||||
query.page,
|
||||
query.limit,
|
||||
db.session,
|
||||
tenant_id,
|
||||
current_user,
|
||||
query.keyword,
|
||||
query.tag_ids,
|
||||
query.include_all,
|
||||
)
|
||||
# check embedding setting
|
||||
assert isinstance(current_user, Account)
|
||||
@ -398,7 +432,7 @@ class DatasetListApi(DatasetApiResource):
|
||||
for embedding_model in embedding_models:
|
||||
model_names.append(f"{embedding_model.model}:{embedding_model.provider.provider}")
|
||||
|
||||
data = [dump_response(DatasetDetailResponse, dataset) for dataset in datasets]
|
||||
data = [_dump_service_dataset_detail(dataset) for dataset in datasets]
|
||||
for item in data:
|
||||
if item["indexing_technique"] == IndexTechniqueType.HIGH_QUALITY and item["embedding_model_provider"]:
|
||||
item["embedding_model_provider"] = str(ModelProviderID(item["embedding_model_provider"]))
|
||||
@ -416,7 +450,7 @@ class DatasetListApi(DatasetApiResource):
|
||||
"total": total,
|
||||
"page": query.page,
|
||||
}
|
||||
return dump_response(DatasetListResponse, response), 200
|
||||
return _dump_service_dataset_list(response), 200
|
||||
|
||||
@service_api_ns.doc(
|
||||
summary="Create an Empty Knowledge Base",
|
||||
@ -489,7 +523,7 @@ class DatasetListApi(DatasetApiResource):
|
||||
except services.errors.dataset.DatasetNameDuplicateError:
|
||||
raise DatasetNameDuplicateError()
|
||||
|
||||
return dump_response(DatasetDetailResponse, dataset), 200
|
||||
return _dump_service_dataset_detail(dataset), 200
|
||||
|
||||
|
||||
@service_api_ns.route("/datasets/<uuid:dataset_id>")
|
||||
@ -534,7 +568,7 @@ class DatasetApi(DatasetApiResource):
|
||||
DatasetService.check_dataset_permission(dataset, current_user)
|
||||
except services.errors.account.NoPermissionError as e:
|
||||
raise Forbidden(str(e))
|
||||
data = dump_response(DatasetDetailResponse, dataset)
|
||||
data = _dump_service_dataset_detail(dataset)
|
||||
# check embedding setting
|
||||
assert isinstance(current_user, Account)
|
||||
cid = current_user.current_tenant_id
|
||||
@ -566,13 +600,7 @@ class DatasetApi(DatasetApiResource):
|
||||
part_users_list = DatasetPermissionService.get_dataset_partial_member_list(dataset_id_str)
|
||||
data.update({"partial_member_list": part_users_list})
|
||||
|
||||
return (
|
||||
DatasetDetailWithPartialMembersResponse.model_validate(data).model_dump(
|
||||
mode="json",
|
||||
exclude={"partial_member_list"} if "partial_member_list" not in data else set(),
|
||||
),
|
||||
200,
|
||||
)
|
||||
return _dump_service_dataset_with_partial_members(data), 200
|
||||
|
||||
@service_api_ns.doc(
|
||||
summary="Update Knowledge Base",
|
||||
@ -641,20 +669,21 @@ class DatasetApi(DatasetApiResource):
|
||||
retrieval_model.reranking_model.reranking_model_name,
|
||||
)
|
||||
|
||||
# The role of the current user in the ta table must be admin, owner, editor, or dataset_operator
|
||||
DatasetPermissionService.check_permission(
|
||||
current_user,
|
||||
dataset,
|
||||
str(payload.permission) if payload.permission else None,
|
||||
payload.partial_member_list,
|
||||
)
|
||||
if not dify_config.RBAC_ENABLED:
|
||||
# The role of the current user in the ta table must be admin, owner, editor, or dataset_operator
|
||||
DatasetPermissionService.check_permission(
|
||||
current_user,
|
||||
dataset,
|
||||
str(payload.permission) if payload.permission else None,
|
||||
payload.partial_member_list,
|
||||
)
|
||||
|
||||
dataset = DatasetService.update_dataset(dataset_id_str, update_data, current_user)
|
||||
|
||||
if dataset is None:
|
||||
raise NotFound("Dataset not found.")
|
||||
|
||||
result_data = dump_response(DatasetDetailResponse, dataset)
|
||||
result_data = _dump_service_dataset_detail(dataset)
|
||||
assert isinstance(current_user, Account)
|
||||
tenant_id = current_user.current_tenant_id
|
||||
|
||||
@ -667,7 +696,7 @@ class DatasetApi(DatasetApiResource):
|
||||
partial_member_list = DatasetPermissionService.get_dataset_partial_member_list(dataset_id_str)
|
||||
result_data.update({"partial_member_list": partial_member_list})
|
||||
|
||||
return DatasetDetailWithPartialMembersResponse.model_validate(result_data).model_dump(mode="json"), 200
|
||||
return _dump_service_dataset_with_partial_members(result_data), 200
|
||||
|
||||
@service_api_ns.doc(
|
||||
summary="Delete Knowledge Base",
|
||||
|
||||
@ -25,6 +25,7 @@ class RBACPermission(StrEnum):
|
||||
APP_CREATE_AND_MANAGEMENT = "app_create_and_management"
|
||||
APP_RELEASE_AND_VERSION = "app_release_and_version"
|
||||
APP_IMPORT_EXPORT_DSL = "app_import_export_dsl"
|
||||
APP_EDIT = "app_edit"
|
||||
APP_MONITOR = "app_monitor"
|
||||
APP_DELETE = "app_delete"
|
||||
|
||||
@ -33,5 +34,22 @@ class RBACPermission(StrEnum):
|
||||
DATASET_CREATE_AND_MANAGEMENT = "dataset_create_and_management"
|
||||
DATASET_PIPELINE_TEST = "dataset_pipeline_test"
|
||||
DATASET_DOCUMENT_DOWNLOAD = "dataset_document_download"
|
||||
DATASET_API_KEY_MANAGE = "dataset_api_key_manage"
|
||||
DATASET_EXTERNAL_CONNECT = "dataset_external_connect"
|
||||
DATASET_IMPORT_EXPORT_DSL = "dataset_import_export_dsl"
|
||||
|
||||
WORKSPACE_ROLE_MANAGE = "workspace_role_manage"
|
||||
|
||||
SNIPPETS_CREATE_AND_MODIFY = "snippets_create_and_modify"
|
||||
SNIPPETS_MANAGE = "snippets_management"
|
||||
|
||||
PLUGIN_INSTALL = "plugin_install"
|
||||
PLUGIN_PREFERENCES = "plugin_preferences"
|
||||
PLUGIN_MANAGE = "plugin_manage"
|
||||
PLUGIN_DEBUG = "plugin_debug"
|
||||
|
||||
CREDENTIAL_USE = "credential_use"
|
||||
CREDENTIAL_MANAGE = "credential_manage"
|
||||
|
||||
TOOL_MANAGE = "tool_manage"
|
||||
MCP_MANAGE = "mcp_manage"
|
||||
|
||||
@ -26,6 +26,7 @@ def init_app(app: DifyApp):
|
||||
install_plugins,
|
||||
install_rag_pipeline_plugins,
|
||||
migrate_data_for_plugin,
|
||||
migrate_member_roles_to_rbac,
|
||||
migrate_oss,
|
||||
migration_data_wizard,
|
||||
old_metadata_migration,
|
||||
@ -54,6 +55,7 @@ def init_app(app: DifyApp):
|
||||
upgrade_db,
|
||||
fix_app_site_missing,
|
||||
migrate_data_for_plugin,
|
||||
migrate_member_roles_to_rbac,
|
||||
backfill_plugin_auto_upgrade,
|
||||
extract_plugins,
|
||||
extract_unique_plugins,
|
||||
|
||||
@ -100,6 +100,7 @@ app_detail_fields = {
|
||||
"updated_at": TimestampField,
|
||||
"access_mode": fields.String,
|
||||
"tags": fields.List(fields.Nested(tag_fields)),
|
||||
"permission_keys": fields.List(fields.String()),
|
||||
}
|
||||
|
||||
prompt_config_fields = {
|
||||
@ -137,6 +138,7 @@ app_partial_fields = {
|
||||
"create_user_name": fields.String,
|
||||
"author_name": fields.String,
|
||||
"has_draft_trigger": fields.Boolean,
|
||||
"permission_keys": fields.List(fields.String()),
|
||||
}
|
||||
|
||||
|
||||
@ -217,6 +219,7 @@ app_detail_fields_with_site = {
|
||||
"deleted_tools": fields.List(fields.Nested(deleted_tool_fields)),
|
||||
"access_mode": fields.String,
|
||||
"tags": fields.List(fields.Nested(tag_fields)),
|
||||
"permission_keys": fields.List(fields.String()),
|
||||
"site": fields.Nested(site_fields),
|
||||
}
|
||||
|
||||
|
||||
@ -15,6 +15,7 @@ dataset_fields = {
|
||||
"indexing_technique": fields.String,
|
||||
"created_by": fields.String,
|
||||
"created_at": TimestampField,
|
||||
"permission_keys": fields.List(fields.String()),
|
||||
}
|
||||
|
||||
|
||||
@ -143,6 +144,7 @@ dataset_detail_fields = {
|
||||
"total_available_documents": fields.Integer,
|
||||
"enable_api": fields.Boolean,
|
||||
"is_multimodal": fields.Boolean,
|
||||
"permission_keys": fields.List(fields.String()),
|
||||
}
|
||||
|
||||
|
||||
@ -267,6 +269,8 @@ class DatasetDetailResponse(ResponseModel):
|
||||
total_available_documents: int
|
||||
enable_api: bool
|
||||
is_multimodal: bool
|
||||
permission_keys: list[str] = Field(default_factory=list)
|
||||
maintainer: str | None = None
|
||||
|
||||
@field_validator("created_at", "updated_at", mode="before")
|
||||
@classmethod
|
||||
|
||||
@ -3,7 +3,7 @@ from __future__ import annotations
|
||||
from datetime import datetime
|
||||
|
||||
from flask_restx import fields
|
||||
from pydantic import computed_field, field_validator
|
||||
from pydantic import Field, computed_field, field_validator
|
||||
|
||||
from fields.base import ResponseModel
|
||||
from libs.helper import build_avatar_url, to_timestamp
|
||||
@ -56,6 +56,7 @@ class AccountWithRole(_AccountAvatar):
|
||||
last_active_at: int | None = None
|
||||
created_at: int | None = None
|
||||
role: str
|
||||
roles: list[dict[str, str]] = Field(default_factory=list)
|
||||
status: str
|
||||
|
||||
@field_validator("last_login_at", "last_active_at", "created_at", mode="before")
|
||||
|
||||
@ -490,7 +490,8 @@ def check_workspace_membership(
|
||||
account_id: uuid.UUID | str,
|
||||
tenant_id: str,
|
||||
token_hash: str,
|
||||
membership_cache: dict[str, bool],
|
||||
membership_cache: dict[str, bool] | None = None,
|
||||
cached_verdicts: dict[str, bool] | None = None,
|
||||
) -> None:
|
||||
"""Layer-0 enforcement core. Raises `Forbidden` on deny, returns on allow.
|
||||
|
||||
@ -499,7 +500,8 @@ def check_workspace_membership(
|
||||
short-circuiting on EE / SSO subjects before invoking — this function
|
||||
runs the membership + active-status checks unconditionally.
|
||||
"""
|
||||
cached = membership_cache.get(tenant_id)
|
||||
cache = membership_cache if membership_cache is not None else cached_verdicts or {}
|
||||
cached = cache.get(tenant_id)
|
||||
if cached is True:
|
||||
return
|
||||
if cached is False:
|
||||
|
||||
@ -0,0 +1,41 @@
|
||||
"""add resource maintainers
|
||||
|
||||
Revision ID: a7c4e9d2f681
|
||||
Revises: 9f4b7c2d1a80
|
||||
Create Date: 2026-06-15 12:00:00.000000
|
||||
|
||||
"""
|
||||
|
||||
import sqlalchemy as sa
|
||||
from alembic import op
|
||||
|
||||
import models.types
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = "a7c4e9d2f681"
|
||||
down_revision = "d2f1a4b8c3e0"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
with op.batch_alter_table("apps", schema=None) as batch_op:
|
||||
batch_op.add_column(sa.Column("maintainer", models.types.StringUUID(), nullable=True))
|
||||
batch_op.create_index("app_tenant_maintainer_idx", ["tenant_id", "maintainer"], unique=False)
|
||||
|
||||
with op.batch_alter_table("datasets", schema=None) as batch_op:
|
||||
batch_op.add_column(sa.Column("maintainer", models.types.StringUUID(), nullable=True))
|
||||
batch_op.create_index("dataset_tenant_maintainer_idx", ["tenant_id", "maintainer"], unique=False)
|
||||
|
||||
op.execute(sa.text("UPDATE apps SET maintainer = created_by WHERE maintainer IS NULL"))
|
||||
op.execute(sa.text("UPDATE datasets SET maintainer = created_by WHERE maintainer IS NULL"))
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
with op.batch_alter_table("datasets", schema=None) as batch_op:
|
||||
batch_op.drop_index("dataset_tenant_maintainer_idx")
|
||||
batch_op.drop_column("maintainer")
|
||||
|
||||
with op.batch_alter_table("apps", schema=None) as batch_op:
|
||||
batch_op.drop_index("app_tenant_maintainer_idx")
|
||||
batch_op.drop_column("maintainer")
|
||||
@ -11,6 +11,8 @@ from sqlalchemy import DateTime, String, func, select
|
||||
from sqlalchemy.orm import Mapped, Session, mapped_column
|
||||
from typing_extensions import deprecated
|
||||
|
||||
from configs import dify_config
|
||||
|
||||
from .base import TypeBase
|
||||
from .engine import db
|
||||
from .types import EnumText, LongText, StringUUID
|
||||
@ -187,10 +189,14 @@ class Account(UserMixin, TypeBase):
|
||||
# check current_user.current_tenant.current_role in ['admin', 'owner']
|
||||
@property
|
||||
def is_admin_or_owner(self):
|
||||
if dify_config.RBAC_ENABLED:
|
||||
return True
|
||||
return TenantAccountRole.is_privileged_role(self.role)
|
||||
|
||||
@property
|
||||
def is_admin(self):
|
||||
if dify_config.RBAC_ENABLED:
|
||||
return True
|
||||
return TenantAccountRole.is_admin_role(self.role)
|
||||
|
||||
@property
|
||||
@ -216,14 +222,20 @@ class Account(UserMixin, TypeBase):
|
||||
- `ADMIN`
|
||||
- `EDITOR`
|
||||
"""
|
||||
if dify_config.RBAC_ENABLED:
|
||||
return True
|
||||
return TenantAccountRole.is_editing_role(self.role)
|
||||
|
||||
@property
|
||||
def is_dataset_editor(self):
|
||||
if dify_config.RBAC_ENABLED:
|
||||
return True
|
||||
return TenantAccountRole.is_dataset_edit_role(self.role)
|
||||
|
||||
@property
|
||||
def is_dataset_operator(self):
|
||||
if dify_config.RBAC_ENABLED:
|
||||
return True
|
||||
return self.role == TenantAccountRole.DATASET_OPERATOR
|
||||
|
||||
|
||||
|
||||
@ -167,6 +167,7 @@ class Dataset(Base):
|
||||
__table_args__ = (
|
||||
sa.PrimaryKeyConstraint("id", name="dataset_pkey"),
|
||||
sa.Index("dataset_tenant_idx", "tenant_id"),
|
||||
sa.Index("dataset_tenant_maintainer_idx", "tenant_id", "maintainer"),
|
||||
adjusted_json_index("retrieval_model_idx", "retrieval_model"),
|
||||
)
|
||||
|
||||
@ -188,6 +189,7 @@ class Dataset(Base):
|
||||
indexing_technique: Mapped[IndexTechniqueType | None] = mapped_column(EnumText(IndexTechniqueType, length=255))
|
||||
index_struct = mapped_column(LongText, nullable=True)
|
||||
created_by = mapped_column(StringUUID, nullable=False)
|
||||
maintainer: Mapped[str | None] = mapped_column(StringUUID, nullable=True)
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime, nullable=False, server_default=func.current_timestamp())
|
||||
updated_by = mapped_column(StringUUID, nullable=True)
|
||||
updated_at = mapped_column(
|
||||
|
||||
@ -395,7 +395,11 @@ class IconType(StrEnum):
|
||||
|
||||
class App(Base):
|
||||
__tablename__ = "apps"
|
||||
__table_args__ = (sa.PrimaryKeyConstraint("id", name="app_pkey"), sa.Index("app_tenant_id_idx", "tenant_id"))
|
||||
__table_args__ = (
|
||||
sa.PrimaryKeyConstraint("id", name="app_pkey"),
|
||||
sa.Index("app_tenant_id_idx", "tenant_id"),
|
||||
sa.Index("app_tenant_maintainer_idx", "tenant_id", "maintainer"),
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
# Response-only attributes attached by app list/detail enrichers.
|
||||
@ -426,6 +430,7 @@ class App(Base):
|
||||
tracing = mapped_column(LongText, nullable=True)
|
||||
max_active_requests: Mapped[int | None]
|
||||
created_by = mapped_column(StringUUID, nullable=True)
|
||||
maintainer: Mapped[str | None] = mapped_column(StringUUID, nullable=True)
|
||||
created_at = mapped_column(sa.DateTime, nullable=False, server_default=func.current_timestamp())
|
||||
updated_by = mapped_column(StringUUID, nullable=True)
|
||||
updated_at: Mapped[datetime] = mapped_column(
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -805,6 +805,7 @@ Liveness payload for `GET /openapi/v1/_health` — no auth required.
|
||||
| error | string | | No |
|
||||
| id | string | | Yes |
|
||||
| imported_dsl_version | string | | No |
|
||||
| permission_keys | [ string ] | | No |
|
||||
| status | [ImportStatus](#importstatus) | | Yes |
|
||||
|
||||
#### ImportStatus
|
||||
|
||||
@ -2701,8 +2701,10 @@ Enum class for custom configuration status.
|
||||
| indexing_technique | string | | Yes |
|
||||
| is_multimodal | boolean | | Yes |
|
||||
| is_published | boolean | | Yes |
|
||||
| maintainer | string | | No |
|
||||
| name | string | | Yes |
|
||||
| permission | string | | Yes |
|
||||
| permission_keys | [ string ] | | No |
|
||||
| pipeline_id | string | | Yes |
|
||||
| provider | string | | Yes |
|
||||
| retrieval_model_dict | [DatasetRetrievalModelResponse](#datasetretrievalmodelresponse) | | Yes |
|
||||
@ -2741,9 +2743,11 @@ Enum class for custom configuration status.
|
||||
| indexing_technique | string | | Yes |
|
||||
| is_multimodal | boolean | | Yes |
|
||||
| is_published | boolean | | Yes |
|
||||
| maintainer | string | | No |
|
||||
| name | string | | Yes |
|
||||
| partial_member_list | [ string ] | | No |
|
||||
| permission | string | | Yes |
|
||||
| permission_keys | [ string ] | | No |
|
||||
| pipeline_id | string | | Yes |
|
||||
| provider | string | | Yes |
|
||||
| retrieval_model_dict | [DatasetRetrievalModelResponse](#datasetretrievalmodelresponse) | | Yes |
|
||||
|
||||
@ -1613,6 +1613,7 @@ Default configuration for form inputs.
|
||||
| max_plugin_package_size | integer, <br>**Default:** 15728640 | | Yes |
|
||||
| plugin_installation_permission | [PluginInstallationPermissionModel](#plugininstallationpermissionmodel) | | Yes |
|
||||
| plugin_manager | [PluginManagerModel](#pluginmanagermodel) | | Yes |
|
||||
| rbac_enabled | boolean | | Yes |
|
||||
| sso_enforced_for_signin | boolean | | Yes |
|
||||
| sso_enforced_for_signin_protocol | string | | Yes |
|
||||
| webapp_auth | [WebAppAuthModel](#webappauthmodel) | | Yes |
|
||||
|
||||
@ -10,23 +10,11 @@ from typing import Any, NotRequired, TypedDict, cast
|
||||
from pydantic import BaseModel, TypeAdapter, ValidationError
|
||||
from sqlalchemy import Row, delete, func, select, update
|
||||
from sqlalchemy.orm import Session, scoped_session
|
||||
|
||||
from core.db.session_factory import session_factory
|
||||
|
||||
|
||||
class InvitationData(TypedDict):
|
||||
account_id: str
|
||||
email: str
|
||||
workspace_id: str
|
||||
role: NotRequired[str]
|
||||
requires_setup: NotRequired[bool]
|
||||
|
||||
|
||||
_invitation_adapter: TypeAdapter[InvitationData] = TypeAdapter(InvitationData)
|
||||
from werkzeug.exceptions import Unauthorized
|
||||
|
||||
from configs import dify_config
|
||||
from constants.languages import get_valid_language, language_timezone_mapping
|
||||
from core.db.session_factory import session_factory
|
||||
from events.tenant_event import tenant_was_created
|
||||
from extensions.ext_database import db
|
||||
from extensions.ext_redis import redis_client, redis_fallback
|
||||
@ -47,8 +35,10 @@ from models.account import (
|
||||
TenantPluginAutoUpgradeStrategy,
|
||||
TenantStatus,
|
||||
)
|
||||
from models.model import DifySetup
|
||||
from models.dataset import Dataset
|
||||
from models.model import App, DifySetup
|
||||
from services.billing_service import BillingService
|
||||
from services.enterprise.rbac_service import ListOption, RBACService
|
||||
from services.entities.auth_entities import (
|
||||
ChangeEmailNewEmailToken,
|
||||
ChangeEmailOldEmailToken,
|
||||
@ -92,6 +82,17 @@ from tasks.mail_reset_password_task import (
|
||||
send_reset_password_mail_task_when_account_not_exist,
|
||||
)
|
||||
|
||||
|
||||
class InvitationData(TypedDict):
|
||||
account_id: str
|
||||
email: str
|
||||
workspace_id: str
|
||||
role: NotRequired[str]
|
||||
requires_setup: NotRequired[bool]
|
||||
|
||||
|
||||
_invitation_adapter: TypeAdapter[InvitationData] = TypeAdapter(InvitationData)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
_change_email_token_adapter: TypeAdapter[ChangeEmailTokenData] = TypeAdapter(ChangeEmailTokenData)
|
||||
@ -149,6 +150,67 @@ class AccountService:
|
||||
OWNER_TRANSFER_MAX_ERROR_LIMITS = 5
|
||||
EMAIL_REGISTER_MAX_ERROR_LIMITS = 5
|
||||
|
||||
@staticmethod
|
||||
def _resolve_legacy_role_id(tenant_id: str, account_id: str, role: TenantAccountRole) -> str:
|
||||
"""Resolve a legacy workspace role to the corresponding RBAC role id.
|
||||
|
||||
Looks up the builtin RBAC role whose tag matches the legacy role name
|
||||
(e.g. ``TenantAccountRole.ADMIN`` → builtin role with tag ``"admin"``).
|
||||
"""
|
||||
options = ListOption(page_number=1, results_per_page=100)
|
||||
roles = RBACService.Roles.list(tenant_id, account_id, options=options).data
|
||||
|
||||
expected_tag = {
|
||||
TenantAccountRole.OWNER: "owner",
|
||||
TenantAccountRole.ADMIN: "admin",
|
||||
TenantAccountRole.EDITOR: "editor",
|
||||
TenantAccountRole.NORMAL: "normal",
|
||||
TenantAccountRole.DATASET_OPERATOR: "dataset_operator",
|
||||
}[role]
|
||||
for rbac_role in roles:
|
||||
if (
|
||||
rbac_role.is_builtin
|
||||
and rbac_role.category == "global_system_default"
|
||||
and rbac_role.role_tag == expected_tag
|
||||
):
|
||||
return str(rbac_role.id)
|
||||
|
||||
raise ValueError(f"Builtin RBAC role not found for {role.value} in tenant {tenant_id}")
|
||||
|
||||
@staticmethod
|
||||
def get_workspace_permission_keys(tenant_id: str, account_id: str) -> set[str]:
|
||||
permissions = RBACService.MyPermissions.get(tenant_id, account_id)
|
||||
return set(getattr(getattr(permissions, "workspace", None), "permission_keys", []) or [])
|
||||
|
||||
@staticmethod
|
||||
def get_rbac_workspace_owner_account_id(tenant_id: str, actor_account_id: str) -> str:
|
||||
"""Return the account id bound to the workspace owner RBAC role."""
|
||||
owner_role_id = AccountService._resolve_legacy_role_id(
|
||||
tenant_id=tenant_id,
|
||||
account_id=actor_account_id,
|
||||
role=TenantAccountRole.OWNER,
|
||||
)
|
||||
owner_members = RBACService.Roles.members(
|
||||
tenant_id=tenant_id,
|
||||
account_id=actor_account_id,
|
||||
role_id=owner_role_id,
|
||||
options=ListOption(page_number=1, results_per_page=1),
|
||||
).data
|
||||
if not owner_members:
|
||||
raise ValueError(f"Workspace RBAC owner not found for tenant {tenant_id}.")
|
||||
return owner_members[0].account_id
|
||||
|
||||
@staticmethod
|
||||
def is_rbac_workspace_owner(tenant_id: str, actor_account_id: str, member_account_id: str) -> bool:
|
||||
roles = RBACService.MemberRoles.get(
|
||||
tenant_id=tenant_id,
|
||||
account_id=actor_account_id,
|
||||
member_account_id=member_account_id,
|
||||
).roles
|
||||
return any(
|
||||
role.is_builtin and role.category == "global_system_default" and role.role_tag == "owner" for role in roles
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _get_refresh_token_key(refresh_token: str) -> str:
|
||||
return f"{REFRESH_TOKEN_PREFIX}{refresh_token}"
|
||||
@ -1219,6 +1281,14 @@ class TenantService:
|
||||
else:
|
||||
tenant = TenantService.create_tenant(name=f"{account.name}'s Workspace", is_setup=is_setup)
|
||||
TenantService.create_tenant_member(tenant, account, role="owner")
|
||||
if dify_config.RBAC_ENABLED:
|
||||
owner_role_id = AccountService._resolve_legacy_role_id(str(tenant.id), account.id, TenantAccountRole.OWNER)
|
||||
RBACService.MemberRoles.replace(
|
||||
tenant_id=str(tenant.id),
|
||||
account_id=account.id,
|
||||
member_account_id=account.id,
|
||||
role_ids=[owner_role_id],
|
||||
)
|
||||
account.current_tenant = tenant
|
||||
db.session.commit()
|
||||
tenant_was_created.send(tenant)
|
||||
@ -1347,6 +1417,7 @@ class TenantService:
|
||||
"""
|
||||
if not account_id:
|
||||
return None
|
||||
|
||||
role = session.execute(
|
||||
select(TenantAccountJoin.role).where(
|
||||
TenantAccountJoin.tenant_id == tenant_id,
|
||||
@ -1533,11 +1604,6 @@ class TenantService:
|
||||
@staticmethod
|
||||
def check_member_permission(tenant: Tenant, operator: Account, member: Account | None, action: str):
|
||||
"""Check member permission"""
|
||||
perms = {
|
||||
"add": [TenantAccountRole.OWNER, TenantAccountRole.ADMIN],
|
||||
"remove": [TenantAccountRole.OWNER, TenantAccountRole.ADMIN],
|
||||
"update": [TenantAccountRole.OWNER, TenantAccountRole.ADMIN],
|
||||
}
|
||||
if action not in {"add", "remove", "update"}:
|
||||
raise InvalidActionError("Invalid action.")
|
||||
|
||||
@ -1545,6 +1611,31 @@ class TenantService:
|
||||
if operator.id == member.id:
|
||||
raise CannotOperateSelfError("Cannot operate self.")
|
||||
|
||||
if dify_config.RBAC_ENABLED:
|
||||
workspace_permission_keys = AccountService.get_workspace_permission_keys(
|
||||
str(tenant.id),
|
||||
str(operator.id),
|
||||
)
|
||||
required_permission_key = (
|
||||
"workspace.member.manage" if action in {"add", "remove"} else "workspace.role.manage"
|
||||
)
|
||||
if required_permission_key not in workspace_permission_keys:
|
||||
raise NoPermissionError(f"No permission to {action} member.")
|
||||
|
||||
if (
|
||||
action == "remove"
|
||||
and member
|
||||
and AccountService.is_rbac_workspace_owner(str(tenant.id), str(operator.id), str(member.id))
|
||||
):
|
||||
raise NoPermissionError(f"No permission to {action} member.")
|
||||
return
|
||||
|
||||
perms = {
|
||||
"add": [TenantAccountRole.OWNER, TenantAccountRole.ADMIN],
|
||||
"remove": [TenantAccountRole.OWNER, TenantAccountRole.ADMIN],
|
||||
"update": [TenantAccountRole.OWNER, TenantAccountRole.ADMIN],
|
||||
}
|
||||
|
||||
ta_operator = db.session.scalar(
|
||||
select(TenantAccountJoin)
|
||||
.where(TenantAccountJoin.tenant_id == tenant.id, TenantAccountJoin.account_id == operator.id)
|
||||
@ -1567,6 +1658,8 @@ class TenantService:
|
||||
def remove_member_from_tenant(tenant: Tenant, account: Account, operator: Account):
|
||||
"""Remove member from tenant.
|
||||
|
||||
Apps and datasets maintained by the removed member are reassigned to
|
||||
the workspace owner without changing their immutable creator records.
|
||||
If the removed member has ``AccountStatus.PENDING`` (invited but never
|
||||
activated) and no remaining workspace memberships, the orphaned account
|
||||
record is deleted as well.
|
||||
@ -1589,6 +1682,37 @@ class TenantService:
|
||||
account_id = account.id
|
||||
account_email = account.email
|
||||
|
||||
owner_id: str | None
|
||||
if dify_config.RBAC_ENABLED:
|
||||
owner_id = AccountService.get_rbac_workspace_owner_account_id(str(tenant.id), str(operator.id))
|
||||
else:
|
||||
owner_id = db.session.scalar(
|
||||
select(TenantAccountJoin.account_id)
|
||||
.where(
|
||||
TenantAccountJoin.tenant_id == tenant.id,
|
||||
TenantAccountJoin.role == TenantAccountRole.OWNER,
|
||||
)
|
||||
.limit(1)
|
||||
)
|
||||
if owner_id is None:
|
||||
raise ValueError(f"Workspace owner not found for tenant {tenant.id}.")
|
||||
|
||||
db.session.execute(
|
||||
update(App)
|
||||
.where(
|
||||
App.tenant_id == tenant.id,
|
||||
App.maintainer == account_id,
|
||||
)
|
||||
.values(maintainer=owner_id)
|
||||
)
|
||||
db.session.execute(
|
||||
update(Dataset)
|
||||
.where(
|
||||
Dataset.tenant_id == tenant.id,
|
||||
Dataset.maintainer == account_id,
|
||||
)
|
||||
.values(maintainer=owner_id)
|
||||
)
|
||||
db.session.delete(ta)
|
||||
|
||||
# Clean up orphaned pending accounts (invited but never activated)
|
||||
@ -1660,11 +1784,37 @@ class TenantService:
|
||||
.where(TenantAccountJoin.tenant_id == tenant.id, TenantAccountJoin.role == "owner")
|
||||
.limit(1)
|
||||
)
|
||||
if current_owner_join:
|
||||
current_owner_join.role = TenantAccountRole.ADMIN
|
||||
if not dify_config.RBAC_ENABLED:
|
||||
if current_owner_join:
|
||||
current_owner_join.role = TenantAccountRole.ADMIN
|
||||
elif current_owner_join:
|
||||
admin_role_id = AccountService._resolve_legacy_role_id(
|
||||
tenant_id=str(tenant.id),
|
||||
account_id=operator.id,
|
||||
role=TenantAccountRole.ADMIN,
|
||||
)
|
||||
RBACService.MemberRoles.replace(
|
||||
tenant_id=str(tenant.id),
|
||||
account_id=operator.id,
|
||||
member_account_id=str(current_owner_join.account_id),
|
||||
role_ids=[admin_role_id],
|
||||
)
|
||||
|
||||
# Update the role of the target member
|
||||
target_member_join.role = new_tenant_role
|
||||
if dify_config.RBAC_ENABLED:
|
||||
resolved_role_id = AccountService._resolve_legacy_role_id(
|
||||
tenant_id=str(tenant.id),
|
||||
account_id=operator.id,
|
||||
role=TenantAccountRole.OWNER,
|
||||
)
|
||||
RBACService.MemberRoles.replace(
|
||||
tenant_id=str(tenant.id),
|
||||
account_id=operator.id,
|
||||
member_account_id=member.id,
|
||||
role_ids=[resolved_role_id],
|
||||
)
|
||||
else:
|
||||
target_member_join.role = new_tenant_role
|
||||
db.session.commit()
|
||||
|
||||
@staticmethod
|
||||
@ -1798,6 +1948,7 @@ class RegisterService:
|
||||
raise ValueError("Inviter is required")
|
||||
|
||||
normalized_email = email.lower()
|
||||
tenant_join_role = TenantAccountRole.NORMAL.value if dify_config.RBAC_ENABLED else role
|
||||
|
||||
"""Invite new member"""
|
||||
# Check workspace permission for member invitations
|
||||
@ -1819,8 +1970,7 @@ class RegisterService:
|
||||
status=AccountStatus.PENDING,
|
||||
is_setup=True,
|
||||
)
|
||||
# Create new tenant member for invited tenant
|
||||
TenantService.create_tenant_member(tenant, account, role)
|
||||
TenantService.create_tenant_member(tenant, account, tenant_join_role)
|
||||
TenantService.switch_tenant(account, tenant.id)
|
||||
requires_setup = True
|
||||
else:
|
||||
@ -1832,12 +1982,29 @@ class RegisterService:
|
||||
)
|
||||
requires_setup = account.status == AccountStatus.PENDING
|
||||
|
||||
if not ta and account.status == AccountStatus.PENDING:
|
||||
TenantService.create_tenant_member(tenant, account, role)
|
||||
if not ta and (account.status == AccountStatus.PENDING or dify_config.RBAC_ENABLED):
|
||||
TenantService.create_tenant_member(tenant, account, tenant_join_role)
|
||||
|
||||
# Support resend invitation email when the account is pending status
|
||||
if ta and account.status != AccountStatus.PENDING:
|
||||
raise AccountAlreadyInTenantError("Account already in tenant.")
|
||||
if account.status != AccountStatus.PENDING:
|
||||
if dify_config.RBAC_ENABLED and not ta:
|
||||
RBACService.MemberRoles.replace(
|
||||
tenant_id=str(tenant.id),
|
||||
account_id=inviter.id,
|
||||
member_account_id=account.id,
|
||||
role_ids=[role],
|
||||
)
|
||||
if ta or dify_config.RBAC_ENABLED:
|
||||
raise AccountAlreadyInTenantError("Account already in tenant.")
|
||||
|
||||
# Assign RBAC role if RBAC is enabled
|
||||
if dify_config.RBAC_ENABLED:
|
||||
RBACService.MemberRoles.replace(
|
||||
tenant_id=str(tenant.id),
|
||||
account_id=inviter.id,
|
||||
member_account_id=account.id,
|
||||
role_ids=[role],
|
||||
)
|
||||
|
||||
token = cls.generate_invite_token(tenant, account, role, requires_setup=requires_setup)
|
||||
language = account.interface_language or "en-US"
|
||||
|
||||
@ -11,7 +11,7 @@ import yaml
|
||||
from Crypto.Cipher import AES
|
||||
from Crypto.Util.Padding import pad, unpad
|
||||
from packaging.version import parse as parse_version
|
||||
from pydantic import BaseModel
|
||||
from pydantic import BaseModel, Field
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
@ -60,6 +60,7 @@ class Import(BaseModel):
|
||||
status: ImportStatus
|
||||
app_id: str | None = None
|
||||
app_mode: str | None = None
|
||||
permission_keys: list[str] = Field(default_factory=list)
|
||||
current_dsl_version: str = CURRENT_DSL_VERSION
|
||||
imported_dsl_version: str = ""
|
||||
error: str = ""
|
||||
@ -433,6 +434,7 @@ class AppDslService:
|
||||
app.enable_api = True
|
||||
app.use_icon_as_answer_icon = app_data.get("use_icon_as_answer_icon", False)
|
||||
app.created_by = account.id
|
||||
app.maintainer = account.id
|
||||
app.updated_by = account.id
|
||||
|
||||
self._session.add(app)
|
||||
|
||||
@ -7,7 +7,7 @@ from typing import Any, Literal, NotRequired, TypedDict, cast, override
|
||||
import sqlalchemy as sa
|
||||
from flask_sqlalchemy.pagination import Pagination
|
||||
from pydantic import BaseModel, Field
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy import ColumnElement, select
|
||||
from sqlalchemy.orm import Session, scoped_session
|
||||
|
||||
from configs import dify_config
|
||||
@ -28,6 +28,7 @@ from models.agent import Agent, AgentIconType, AgentScope, AgentSource, AgentSta
|
||||
from models.model import App, AppMode, AppModelConfig, IconType, Site
|
||||
from models.tools import ApiToolProvider
|
||||
from services.billing_service import BillingService
|
||||
from services.enterprise import rbac_service as enterprise_rbac_service
|
||||
from services.enterprise.enterprise_service import EnterpriseService
|
||||
from services.feature_service import FeatureService
|
||||
from services.openapi.visibility import apply_openapi_gate, is_openapi_visible
|
||||
@ -48,6 +49,8 @@ class AppListBaseParams(BaseModel):
|
||||
tag_ids: list[str] | None = None
|
||||
creator_ids: list[str] | None = None
|
||||
is_created_by_me: bool | None = None
|
||||
accessible_app_ids: list[str] | None = None
|
||||
include_own_apps: bool = False
|
||||
|
||||
|
||||
class AppListParams(AppListBaseParams):
|
||||
@ -106,6 +109,11 @@ class AppService:
|
||||
|
||||
if params.is_created_by_me:
|
||||
filters.append(App.created_by == user_id)
|
||||
elif params.accessible_app_ids is not None:
|
||||
accessible_filter: ColumnElement[bool] = App.id.in_(params.accessible_app_ids)
|
||||
if params.include_own_apps:
|
||||
accessible_filter = sa.or_(App.maintainer == user_id, accessible_filter)
|
||||
filters.append(accessible_filter)
|
||||
if params.creator_ids:
|
||||
filters.append(App.created_by.in_(params.creator_ids))
|
||||
if params.name:
|
||||
@ -375,6 +383,7 @@ class AppService:
|
||||
app.api_rpm = params.api_rpm
|
||||
app.max_active_requests = params.max_active_requests
|
||||
app.created_by = account.id
|
||||
app.maintainer = account.id
|
||||
app.updated_by = account.id
|
||||
|
||||
db.session.add(app)
|
||||
@ -426,6 +435,12 @@ class AppService:
|
||||
db.session.commit()
|
||||
|
||||
app_was_created.send(app, account=account)
|
||||
enterprise_rbac_service.try_sync_creator_access_policy_member_bindings(
|
||||
tenant_id,
|
||||
account.id,
|
||||
enterprise_rbac_service.RBACResourceType.APP,
|
||||
app.id,
|
||||
)
|
||||
|
||||
if FeatureService.get_system_features().webapp_auth.enabled:
|
||||
# update web app setting as private
|
||||
|
||||
@ -12,7 +12,7 @@ from typing import Annotated, Any, Literal, TypedDict, cast
|
||||
import sqlalchemy as sa
|
||||
from pydantic import BaseModel, ConfigDict, Field, ValidationError, field_validator
|
||||
from redis.exceptions import LockNotOwnedError
|
||||
from sqlalchemy import delete, exists, func, select, update
|
||||
from sqlalchemy import ColumnElement, delete, exists, func, select, update
|
||||
from sqlalchemy.orm import Session, scoped_session, sessionmaker
|
||||
from werkzeug.exceptions import Forbidden, NotFound
|
||||
|
||||
@ -67,6 +67,7 @@ from models.source import DataSourceOauthBinding
|
||||
from models.workflow import Workflow
|
||||
from services.document_indexing_proxy.document_indexing_task_proxy import DocumentIndexingTaskProxy
|
||||
from services.document_indexing_proxy.duplicate_document_indexing_task_proxy import DuplicateDocumentIndexingTaskProxy
|
||||
from services.enterprise import rbac_service as enterprise_rbac_service
|
||||
from services.entities.knowledge_entities.knowledge_entities import (
|
||||
ChildChunkUpdateArgs,
|
||||
KnowledgeConfig,
|
||||
@ -234,12 +235,37 @@ class _EstimateArgs(BaseModel):
|
||||
|
||||
|
||||
class DatasetService:
|
||||
@staticmethod
|
||||
def _can_manage_all_datasets(tenant_id: str, account_id: str) -> bool:
|
||||
if not dify_config.RBAC_ENABLED:
|
||||
return False
|
||||
|
||||
permissions = enterprise_rbac_service.RBACService.MyPermissions.get(tenant_id, account_id)
|
||||
workspace_permission_keys = getattr(getattr(permissions, "workspace", None), "permission_keys", []) or []
|
||||
return "dataset.create_and_management" in workspace_permission_keys
|
||||
|
||||
@staticmethod
|
||||
def get_datasets(
|
||||
page, per_page, session: scoped_session, tenant_id=None, user=None, search=None, tag_ids=None, include_all=False
|
||||
page,
|
||||
per_page,
|
||||
session: scoped_session | Session | None = None,
|
||||
tenant_id=None,
|
||||
user=None,
|
||||
search=None,
|
||||
tag_ids=None,
|
||||
include_all=False,
|
||||
accessible_dataset_ids: list[str] | None = None,
|
||||
include_own_datasets: bool = False,
|
||||
):
|
||||
session = session or db.session
|
||||
query = select(Dataset).where(Dataset.tenant_id == tenant_id).order_by(Dataset.created_at.desc(), Dataset.id)
|
||||
|
||||
if dify_config.RBAC_ENABLED and accessible_dataset_ids is not None:
|
||||
accessible_filter: ColumnElement[bool] = Dataset.id.in_(accessible_dataset_ids)
|
||||
if include_own_datasets and user:
|
||||
accessible_filter = sa.or_(Dataset.maintainer == user.id, accessible_filter)
|
||||
query = query.where(accessible_filter)
|
||||
|
||||
if user:
|
||||
# get permitted dataset ids
|
||||
dataset_permission = db.session.scalars(
|
||||
@ -248,8 +274,7 @@ class DatasetService:
|
||||
)
|
||||
).all()
|
||||
permitted_dataset_ids = {dp.dataset_id for dp in dataset_permission} if dataset_permission else None
|
||||
|
||||
if user.current_role == TenantAccountRole.DATASET_OPERATOR:
|
||||
if not dify_config.RBAC_ENABLED and user.current_role == TenantAccountRole.DATASET_OPERATOR:
|
||||
# only show datasets that the user has permission to access
|
||||
# Check if permitted_dataset_ids is not empty to avoid WHERE false condition
|
||||
if permitted_dataset_ids and len(permitted_dataset_ids) > 0:
|
||||
@ -257,34 +282,50 @@ class DatasetService:
|
||||
else:
|
||||
return [], 0
|
||||
else:
|
||||
if user.current_role != TenantAccountRole.OWNER or not include_all:
|
||||
# show all datasets that the user has permission to access
|
||||
# Check if permitted_dataset_ids is not empty to avoid WHERE false condition
|
||||
if permitted_dataset_ids and len(permitted_dataset_ids) > 0:
|
||||
query = query.where(
|
||||
sa.or_(
|
||||
Dataset.permission == DatasetPermissionEnum.ALL_TEAM,
|
||||
sa.and_(
|
||||
Dataset.permission == DatasetPermissionEnum.ONLY_ME, Dataset.created_by == user.id
|
||||
),
|
||||
sa.and_(
|
||||
Dataset.permission == DatasetPermissionEnum.PARTIAL_TEAM,
|
||||
Dataset.id.in_(permitted_dataset_ids),
|
||||
),
|
||||
)
|
||||
)
|
||||
if dify_config.RBAC_ENABLED:
|
||||
can_manage_all_datasets = DatasetService._can_manage_all_datasets(str(tenant_id), str(user.id))
|
||||
should_show_all_datasets = include_all and can_manage_all_datasets
|
||||
else:
|
||||
should_show_all_datasets = user.current_role == TenantAccountRole.OWNER and include_all
|
||||
|
||||
if not should_show_all_datasets:
|
||||
if dify_config.RBAC_ENABLED:
|
||||
# RBAC mode: show all datasets. Permission control is enforced
|
||||
# via permission_keys on each item and @rbac_permission_required decorators.
|
||||
pass
|
||||
else:
|
||||
query = query.where(
|
||||
sa.or_(
|
||||
Dataset.permission == DatasetPermissionEnum.ALL_TEAM,
|
||||
sa.and_(
|
||||
Dataset.permission == DatasetPermissionEnum.ONLY_ME, Dataset.created_by == user.id
|
||||
),
|
||||
# Keep legacy visibility rules when RBAC is disabled.
|
||||
if permitted_dataset_ids and len(permitted_dataset_ids) > 0:
|
||||
query = query.where(
|
||||
sa.or_(
|
||||
Dataset.permission == DatasetPermissionEnum.ALL_TEAM,
|
||||
sa.and_(
|
||||
Dataset.permission == DatasetPermissionEnum.ONLY_ME,
|
||||
Dataset.maintainer == user.id,
|
||||
),
|
||||
sa.and_(
|
||||
Dataset.permission == DatasetPermissionEnum.PARTIAL_TEAM,
|
||||
Dataset.id.in_(permitted_dataset_ids),
|
||||
),
|
||||
)
|
||||
)
|
||||
else:
|
||||
query = query.where(
|
||||
sa.or_(
|
||||
Dataset.permission == DatasetPermissionEnum.ALL_TEAM,
|
||||
sa.and_(
|
||||
Dataset.permission == DatasetPermissionEnum.ONLY_ME,
|
||||
Dataset.maintainer == user.id,
|
||||
),
|
||||
)
|
||||
)
|
||||
)
|
||||
else:
|
||||
# if no user, only show datasets that are shared with all team members
|
||||
query = query.where(Dataset.permission == DatasetPermissionEnum.ALL_TEAM)
|
||||
if dify_config.RBAC_ENABLED:
|
||||
# Without an account we cannot resolve RBAC resource visibility.
|
||||
query = query.where(sa.false())
|
||||
else:
|
||||
# if no user, only show datasets that are shared with all team members
|
||||
query = query.where(Dataset.permission == DatasetPermissionEnum.ALL_TEAM)
|
||||
|
||||
if search:
|
||||
escaped_search = helper.escape_like_pattern(search)
|
||||
@ -329,12 +370,28 @@ class DatasetService:
|
||||
return {"mode": mode, "rules": rules}
|
||||
|
||||
@staticmethod
|
||||
def get_datasets_by_ids(ids, tenant_id):
|
||||
def get_datasets_by_ids(
|
||||
ids,
|
||||
tenant_id,
|
||||
user=None,
|
||||
accessible_dataset_ids: list[str] | None = None,
|
||||
include_own_datasets: bool = False,
|
||||
):
|
||||
# Check if ids is not empty to avoid WHERE false condition
|
||||
if not ids or len(ids) == 0:
|
||||
return [], 0
|
||||
stmt = select(Dataset).where(Dataset.id.in_(ids), Dataset.tenant_id == tenant_id)
|
||||
|
||||
if dify_config.RBAC_ENABLED and accessible_dataset_ids is not None:
|
||||
requested_dataset_ids = set(ids)
|
||||
accessible_dataset_ids = [
|
||||
dataset_id for dataset_id in accessible_dataset_ids if dataset_id in requested_dataset_ids
|
||||
]
|
||||
accessible_filter: ColumnElement[bool] = Dataset.id.in_(accessible_dataset_ids)
|
||||
if include_own_datasets and user:
|
||||
accessible_filter = sa.or_(Dataset.maintainer == user.id, accessible_filter)
|
||||
stmt = stmt.where(accessible_filter)
|
||||
|
||||
datasets = db.paginate(select=stmt, page=1, per_page=len(ids), max_per_page=len(ids), error_out=False)
|
||||
|
||||
return datasets.items, datasets.total
|
||||
@ -392,6 +449,7 @@ class DatasetService:
|
||||
# dataset = Dataset(name=name, provider=provider, config=config)
|
||||
dataset.description = description
|
||||
dataset.created_by = account.id
|
||||
dataset.maintainer = account.id
|
||||
dataset.updated_by = account.id
|
||||
dataset.tenant_id = tenant_id
|
||||
dataset.embedding_model_provider = embedding_model.provider if embedding_model else None
|
||||
@ -422,6 +480,12 @@ class DatasetService:
|
||||
db.session.add(external_knowledge_binding)
|
||||
|
||||
db.session.commit()
|
||||
enterprise_rbac_service.try_sync_creator_access_policy_member_bindings(
|
||||
tenant_id,
|
||||
account.id,
|
||||
enterprise_rbac_service.RBACResourceType.DATASET,
|
||||
dataset.id,
|
||||
)
|
||||
return dataset
|
||||
|
||||
@staticmethod
|
||||
@ -467,6 +531,7 @@ class DatasetService:
|
||||
runtime_mode=DatasetRuntimeMode.RAG_PIPELINE,
|
||||
icon_info=rag_pipeline_dataset_create_entity.icon_info.model_dump(),
|
||||
created_by=current_user.id,
|
||||
maintainer=current_user.id,
|
||||
pipeline_id=pipeline.id,
|
||||
)
|
||||
db.session.add(dataset)
|
||||
@ -1265,12 +1330,12 @@ class DatasetService:
|
||||
logger.debug("User %s does not have permission to access dataset %s", user.id, dataset.id)
|
||||
raise NoPermissionError("You do not have permission to access this dataset.")
|
||||
if user.current_role != TenantAccountRole.OWNER:
|
||||
if dataset.permission == DatasetPermissionEnum.ONLY_ME and dataset.created_by != user.id:
|
||||
if dataset.permission == DatasetPermissionEnum.ONLY_ME and dataset.maintainer != user.id:
|
||||
logger.debug("User %s does not have permission to access dataset %s", user.id, dataset.id)
|
||||
raise NoPermissionError("You do not have permission to access this dataset.")
|
||||
if dataset.permission == DatasetPermissionEnum.PARTIAL_TEAM:
|
||||
# For partial team permission, user needs explicit permission or be the creator
|
||||
if dataset.created_by != user.id:
|
||||
# For partial team permission, user needs explicit permission or be the maintainer.
|
||||
if dataset.maintainer != user.id:
|
||||
user_permission = db.session.scalar(
|
||||
select(DatasetPermission)
|
||||
.where(DatasetPermission.dataset_id == dataset.id, DatasetPermission.account_id == user.id)
|
||||
@ -1290,7 +1355,7 @@ class DatasetService:
|
||||
|
||||
if user.current_role != TenantAccountRole.OWNER:
|
||||
if dataset.permission == DatasetPermissionEnum.ONLY_ME:
|
||||
if dataset.created_by != user.id:
|
||||
if dataset.maintainer != user.id:
|
||||
raise NoPermissionError("You do not have permission to access this dataset.")
|
||||
|
||||
elif dataset.permission == DatasetPermissionEnum.PARTIAL_TEAM:
|
||||
@ -2864,6 +2929,7 @@ class DocumentService:
|
||||
data_source_type=knowledge_config.data_source.info_list.data_source_type,
|
||||
indexing_technique=IndexTechniqueType(knowledge_config.indexing_technique),
|
||||
created_by=account.id,
|
||||
maintainer=account.id,
|
||||
embedding_model=knowledge_config.embedding_model,
|
||||
embedding_model_provider=knowledge_config.embedding_model_provider,
|
||||
collection_binding_id=dataset_collection_binding_id,
|
||||
|
||||
@ -36,6 +36,11 @@ class MCPIdentityRefreshError(MCPTokenError):
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Headers recognised by dify-enterprise's /inner/api/rbac/* endpoints.
|
||||
# Keep in sync with pkg/enterprise/service/rbac_inner_handlers.go.
|
||||
INNER_TENANT_ID_HEADER = "X-Inner-Tenant-Id"
|
||||
INNER_ACCOUNT_ID_HEADER = "X-Inner-Account-Id"
|
||||
|
||||
|
||||
class BaseRequest:
|
||||
proxies: Mapping[str, str] | None = {
|
||||
@ -69,8 +74,16 @@ class BaseRequest:
|
||||
*,
|
||||
timeout: float | httpx.Timeout | None = None,
|
||||
raise_for_status: bool = False,
|
||||
extra_headers: Mapping[str, str] | None = None,
|
||||
) -> Any:
|
||||
headers = {"Content-Type": "application/json", cls.secret_key_header: cls.secret_key}
|
||||
if extra_headers:
|
||||
# Explicitly ignore empty values so callers can pass optional
|
||||
# headers (e.g. `X-Inner-Account-Id`) without having to branch.
|
||||
for key, value in extra_headers.items():
|
||||
if value is None or value == "":
|
||||
continue
|
||||
headers[key] = value
|
||||
url = f"{cls.base_url}{endpoint}"
|
||||
mounts = cls._build_mounts()
|
||||
|
||||
@ -139,9 +152,60 @@ class BaseRequest:
|
||||
|
||||
class EnterpriseRequest(BaseRequest):
|
||||
base_url = os.environ.get("ENTERPRISE_API_URL", "ENTERPRISE_API_URL")
|
||||
rbac_base_url = os.environ.get("ENTERPRISE_RBAC_API_URL", base_url)
|
||||
secret_key = os.environ.get("ENTERPRISE_API_SECRET_KEY", "ENTERPRISE_API_SECRET_KEY")
|
||||
secret_key_header = "Enterprise-Api-Secret-Key"
|
||||
|
||||
@classmethod
|
||||
def send_inner_rbac_request(
|
||||
cls,
|
||||
method: str,
|
||||
endpoint: str,
|
||||
*,
|
||||
tenant_id: str,
|
||||
account_id: str | None = None,
|
||||
json: Any | None = None,
|
||||
params: Mapping[str, Any] | None = None,
|
||||
timeout: float | httpx.Timeout | None = None,
|
||||
) -> Any:
|
||||
"""Call an /inner/api/rbac/* endpoint on dify-enterprise.
|
||||
|
||||
Inner RBAC endpoints require three headers on top of the standard
|
||||
Enterprise-Api-Secret-Key: the tenant the call targets and (optionally)
|
||||
the account acting on behalf of the workspace. This helper centralises
|
||||
both the assertions and the header wiring so callers only have to
|
||||
supply business payload.
|
||||
"""
|
||||
if not tenant_id:
|
||||
raise ValueError("tenant_id must be provided for inner RBAC requests")
|
||||
|
||||
inner_headers: dict[str, str] = {INNER_TENANT_ID_HEADER: tenant_id}
|
||||
if account_id:
|
||||
inner_headers[INNER_ACCOUNT_ID_HEADER] = account_id
|
||||
url = f"{cls.rbac_base_url}{endpoint}"
|
||||
mounts = cls._build_mounts()
|
||||
|
||||
try:
|
||||
traceparent = generate_traceparent_header()
|
||||
if traceparent:
|
||||
inner_headers = dict(inner_headers)
|
||||
inner_headers["traceparent"] = traceparent
|
||||
except Exception:
|
||||
logger.debug("Failed to generate traceparent header", exc_info=True)
|
||||
|
||||
with httpx.Client(mounts=mounts) as client:
|
||||
request_kwargs: dict[str, Any] = {
|
||||
"json": json,
|
||||
"params": params,
|
||||
"headers": {"Content-Type": "application/json", cls.secret_key_header: cls.secret_key, **inner_headers},
|
||||
}
|
||||
if timeout is not None:
|
||||
request_kwargs["timeout"] = timeout
|
||||
response = client.request(method, url, **request_kwargs)
|
||||
if not response.is_success:
|
||||
cls._handle_error_response(response)
|
||||
return response.json()
|
||||
|
||||
|
||||
class EnterprisePluginManagerRequest(BaseRequest):
|
||||
base_url = os.environ.get("ENTERPRISE_PLUGIN_MANAGER_API_URL", "ENTERPRISE_PLUGIN_MANAGER_API_URL")
|
||||
|
||||
1713
api/services/enterprise/rbac_service.py
Normal file
1713
api/services/enterprise/rbac_service.py
Normal file
File diff suppressed because it is too large
Load Diff
@ -280,6 +280,7 @@ class ExternalDatasetService:
|
||||
provider="external",
|
||||
retrieval_model=args.get("external_retrieval_model"),
|
||||
created_by=user_id,
|
||||
maintainer=user_id,
|
||||
)
|
||||
|
||||
db.session.add(dataset)
|
||||
|
||||
@ -181,6 +181,7 @@ class SystemFeatureModel(FeatureResponseModel):
|
||||
enable_creators_platform: bool = False
|
||||
enable_trial_app: bool = False
|
||||
enable_explore_banner: bool = False
|
||||
rbac_enabled: bool = False
|
||||
|
||||
|
||||
class FeatureService:
|
||||
@ -247,6 +248,7 @@ class FeatureService:
|
||||
@classmethod
|
||||
def get_system_features(cls, is_authenticated: bool = False) -> SystemFeatureModel:
|
||||
system_features = SystemFeatureModel()
|
||||
system_features.rbac_enabled = dify_config.RBAC_ENABLED
|
||||
|
||||
cls._fulfill_system_params_from_env(system_features)
|
||||
|
||||
|
||||
@ -283,6 +283,7 @@ class RagPipelineDslService:
|
||||
},
|
||||
indexing_technique=IndexTechniqueType(knowledge_configuration.indexing_technique),
|
||||
created_by=account.id,
|
||||
maintainer=account.id,
|
||||
retrieval_model=knowledge_configuration.retrieval_model.model_dump(),
|
||||
runtime_mode=DatasetRuntimeMode.RAG_PIPELINE,
|
||||
chunk_structure=knowledge_configuration.chunk_structure,
|
||||
@ -415,6 +416,7 @@ class RagPipelineDslService:
|
||||
},
|
||||
indexing_technique=IndexTechniqueType(knowledge_configuration.indexing_technique),
|
||||
created_by=account.id,
|
||||
maintainer=account.id,
|
||||
retrieval_model=knowledge_configuration.retrieval_model.model_dump(),
|
||||
runtime_mode=DatasetRuntimeMode.RAG_PIPELINE,
|
||||
chunk_structure=knowledge_configuration.chunk_structure,
|
||||
|
||||
@ -55,7 +55,12 @@ class TagService:
|
||||
|
||||
@staticmethod
|
||||
def get_target_ids_by_tag_ids(
|
||||
tag_type: str, current_tenant_id: str, tag_ids: list[str], session: scoped_session, *, match_all: bool = False
|
||||
tag_type: str,
|
||||
current_tenant_id: str,
|
||||
tag_ids: list[str],
|
||||
session: scoped_session | Session,
|
||||
*,
|
||||
match_all: bool = False,
|
||||
):
|
||||
"""
|
||||
Return target IDs bound to tags for the given tenant and resource type.
|
||||
|
||||
@ -95,6 +95,7 @@ class WorkflowConverter:
|
||||
new_app.is_demo = False
|
||||
new_app.is_public = app_model.is_public
|
||||
new_app.created_by = account.id
|
||||
new_app.maintainer = account.id
|
||||
new_app.updated_by = account.id
|
||||
db.session.add(new_app)
|
||||
db.session.flush()
|
||||
|
||||
@ -20,13 +20,13 @@ from unittest.mock import ANY, Mock, PropertyMock, patch
|
||||
|
||||
import pytest
|
||||
from flask import Flask
|
||||
from sqlalchemy.orm import Session
|
||||
from sqlalchemy.orm import Session, scoped_session
|
||||
from werkzeug.exceptions import Forbidden, NotFound
|
||||
|
||||
|
||||
class SessionMatcher:
|
||||
def __eq__(self, other):
|
||||
return isinstance(other, Session)
|
||||
return isinstance(other, (Session, scoped_session))
|
||||
|
||||
|
||||
import services
|
||||
@ -366,6 +366,7 @@ DATASET_DETAIL_KEYS = {
|
||||
"total_available_documents",
|
||||
"enable_api",
|
||||
"is_multimodal",
|
||||
"maintainer",
|
||||
}
|
||||
|
||||
|
||||
@ -468,6 +469,7 @@ class TestDatasetListApiGet:
|
||||
mock_dataset_svc.get_datasets.assert_called_once_with(
|
||||
1,
|
||||
20,
|
||||
SessionMatcher(),
|
||||
mock_tenant.id,
|
||||
mock_current_user,
|
||||
None,
|
||||
|
||||
@ -9,7 +9,7 @@ from werkzeug.exceptions import Unauthorized
|
||||
|
||||
from configs import dify_config
|
||||
from controllers.console.error import AccountNotFound, NotAllowedCreateWorkspace
|
||||
from models import AccountStatus, TenantAccountJoin, TenantStatus
|
||||
from models import AccountStatus, App, Dataset, TenantAccountJoin, TenantStatus
|
||||
from services.account_service import AccountService, RegisterService, TenantService, TokenPair
|
||||
from services.errors.account import (
|
||||
AccountAlreadyInTenantError,
|
||||
@ -1799,8 +1799,32 @@ class TestTenantService:
|
||||
TenantService.create_tenant_member(tenant, owner_account, role="owner")
|
||||
TenantService.create_tenant_member(tenant, member_account, role="normal")
|
||||
|
||||
app = App(
|
||||
tenant_id=tenant.id,
|
||||
name="Member app",
|
||||
mode="chat",
|
||||
enable_site=True,
|
||||
enable_api=True,
|
||||
created_by=member_account.id,
|
||||
maintainer=member_account.id,
|
||||
)
|
||||
dataset = Dataset(
|
||||
tenant_id=tenant.id,
|
||||
name="Member dataset",
|
||||
created_by=member_account.id,
|
||||
maintainer=member_account.id,
|
||||
)
|
||||
db_session_with_containers.add_all([app, dataset])
|
||||
db_session_with_containers.commit()
|
||||
|
||||
# Remove member
|
||||
with patch("services.enterprise.account_deletion_sync.sync_workspace_member_removal") as mock_sync:
|
||||
with (
|
||||
patch("services.enterprise.account_deletion_sync.sync_workspace_member_removal") as mock_sync,
|
||||
patch(
|
||||
"services.account_service.AccountService.get_rbac_workspace_owner_account_id",
|
||||
return_value=owner_account.id,
|
||||
),
|
||||
):
|
||||
mock_sync.return_value = True
|
||||
|
||||
TenantService.remove_member_from_tenant(tenant, member_account, owner_account)
|
||||
@ -1819,6 +1843,12 @@ class TestTenantService:
|
||||
.first()
|
||||
)
|
||||
assert member_join is None
|
||||
db_session_with_containers.refresh(app)
|
||||
db_session_with_containers.refresh(dataset)
|
||||
assert app.created_by == member_account.id
|
||||
assert app.maintainer == owner_account.id
|
||||
assert dataset.created_by == member_account.id
|
||||
assert dataset.maintainer == owner_account.id
|
||||
|
||||
def test_remove_member_from_tenant_operate_self(
|
||||
self, db_session_with_containers: Session, mock_external_service_dependencies
|
||||
|
||||
@ -96,6 +96,7 @@ class TestAppService:
|
||||
assert app.api_rph == app_params.api_rph
|
||||
assert app.api_rpm == app_params.api_rpm
|
||||
assert app.created_by == account.id
|
||||
assert app.maintainer == account.id
|
||||
assert app.updated_by == account.id
|
||||
assert app.status == "normal"
|
||||
assert app.enable_site is True
|
||||
|
||||
@ -73,6 +73,7 @@ class DatasetPermissionTestDataFactory:
|
||||
data_source_type=DataSourceType.UPLOAD_FILE,
|
||||
indexing_technique=IndexTechniqueType.HIGH_QUALITY,
|
||||
created_by=created_by,
|
||||
maintainer=created_by,
|
||||
permission=permission,
|
||||
provider="vendor",
|
||||
retrieval_model={"top_k": 2},
|
||||
|
||||
@ -441,6 +441,7 @@ class TestDatasetServiceCreateRagPipelineDataset:
|
||||
assert created_dataset.name == entity.name
|
||||
assert created_dataset.runtime_mode == DatasetRuntimeMode.RAG_PIPELINE
|
||||
assert created_dataset.created_by == account.id
|
||||
assert created_dataset.maintainer == account.id
|
||||
assert created_dataset.permission == DatasetPermissionEnum.ONLY_ME
|
||||
assert created_pipeline is not None
|
||||
assert created_pipeline.name == entity.name
|
||||
|
||||
@ -30,11 +30,13 @@ class DocumentServiceIntegrationFactory:
|
||||
created_by: str | None = None,
|
||||
name: str | None = None,
|
||||
) -> Dataset:
|
||||
resolved_created_by = created_by or str(uuid4())
|
||||
dataset = Dataset(
|
||||
tenant_id=tenant_id or str(uuid4()),
|
||||
name=name or f"dataset-{uuid4()}",
|
||||
data_source_type=DataSourceType.UPLOAD_FILE,
|
||||
created_by=created_by or str(uuid4()),
|
||||
created_by=resolved_created_by,
|
||||
maintainer=resolved_created_by,
|
||||
)
|
||||
db_session_with_containers.add(dataset)
|
||||
db_session_with_containers.commit()
|
||||
|
||||
@ -100,6 +100,7 @@ class DatasetPermissionIntegrationFactory:
|
||||
data_source_type=DataSourceType.UPLOAD_FILE,
|
||||
indexing_technique=indexing_technique,
|
||||
created_by=created_by,
|
||||
maintainer=created_by,
|
||||
provider="vendor",
|
||||
permission=permission,
|
||||
retrieval_model={"top_k": 2},
|
||||
|
||||
@ -106,6 +106,7 @@ class DatasetRetrievalTestDataFactory:
|
||||
data_source_type=DataSourceType.UPLOAD_FILE,
|
||||
indexing_technique=IndexTechniqueType.HIGH_QUALITY,
|
||||
created_by=created_by,
|
||||
maintainer=created_by,
|
||||
permission=permission,
|
||||
provider="vendor",
|
||||
retrieval_model={"top_k": 2},
|
||||
|
||||
@ -13,13 +13,28 @@ from controllers.console.app import app_import as app_import_module
|
||||
from services.app_dsl_service import ImportStatus
|
||||
|
||||
|
||||
def _unwrap(func):
|
||||
bound_self = getattr(func, "__self__", None)
|
||||
while hasattr(func, "__wrapped__"):
|
||||
func = func.__wrapped__
|
||||
if bound_self is not None:
|
||||
return func.__get__(bound_self, bound_self.__class__)
|
||||
return func
|
||||
|
||||
|
||||
class _Result:
|
||||
def __init__(self, status: ImportStatus, app_id: str | None = "app-1"):
|
||||
def __init__(
|
||||
self,
|
||||
status: ImportStatus,
|
||||
app_id: str | None = "app-1",
|
||||
permission_keys: list[str] | None = None,
|
||||
):
|
||||
self.status = status
|
||||
self.app_id = app_id
|
||||
self.permission_keys = permission_keys or []
|
||||
|
||||
def model_dump(self, mode: str = "json"):
|
||||
return {"status": self.status, "app_id": self.app_id}
|
||||
return {"status": self.status, "app_id": self.app_id, "permission_keys": self.permission_keys}
|
||||
|
||||
|
||||
def _install_features(monkeypatch: pytest.MonkeyPatch, enabled: bool) -> None:
|
||||
@ -107,6 +122,72 @@ class TestAppImportApi:
|
||||
assert status == 200
|
||||
assert response["status"] == ImportStatus.COMPLETED
|
||||
|
||||
def test_import_post_attaches_permission_keys_when_creating_new_app_and_rbac_enabled(
|
||||
self, api, app: Flask, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
method = _unwrap(api.post)
|
||||
|
||||
_install_features(monkeypatch, enabled=False)
|
||||
session = _mock_session(monkeypatch)
|
||||
monkeypatch.setattr(
|
||||
app_import_module,
|
||||
"current_account_with_tenant",
|
||||
lambda: (SimpleNamespace(id="u1"), "tenant-1"),
|
||||
)
|
||||
monkeypatch.setattr(app_import_module.dify_config, "RBAC_ENABLED", True)
|
||||
monkeypatch.setattr(
|
||||
app_import_module.AppDslService,
|
||||
"import_app",
|
||||
lambda *_args, **_kwargs: _Result(ImportStatus.COMPLETED, app_id="app-123"),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
app_import_module,
|
||||
"get_app_permission_keys",
|
||||
lambda tenant_id, account_id, app_id: ["app.acl.view_layout", "app.acl.edit"],
|
||||
)
|
||||
|
||||
with app.test_request_context("/console/api/apps/imports", method="POST", json={"mode": "yaml-content"}):
|
||||
response, status = method()
|
||||
|
||||
session.commit.assert_called_once_with()
|
||||
assert status == 200
|
||||
assert response["permission_keys"] == ["app.acl.view_layout", "app.acl.edit"]
|
||||
|
||||
def test_import_post_does_not_attach_permission_keys_when_overwriting_existing_app(
|
||||
self, api, app: Flask, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
method = _unwrap(api.post)
|
||||
|
||||
_install_features(monkeypatch, enabled=False)
|
||||
session = _mock_session(monkeypatch)
|
||||
monkeypatch.setattr(
|
||||
app_import_module,
|
||||
"current_account_with_tenant",
|
||||
lambda: (SimpleNamespace(id="u1"), "tenant-1"),
|
||||
)
|
||||
monkeypatch.setattr(app_import_module.dify_config, "RBAC_ENABLED", True)
|
||||
monkeypatch.setattr(
|
||||
app_import_module.AppDslService,
|
||||
"import_app",
|
||||
lambda *_args, **_kwargs: _Result(ImportStatus.COMPLETED, app_id="app-123"),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
app_import_module,
|
||||
"get_app_permission_keys",
|
||||
lambda *_args, **_kwargs: ["app.acl.view_layout", "app.acl.edit"],
|
||||
)
|
||||
|
||||
with app.test_request_context(
|
||||
"/console/api/apps/imports",
|
||||
method="POST",
|
||||
json={"mode": "yaml-content", "app_id": "existing-app"},
|
||||
):
|
||||
response, status = method()
|
||||
|
||||
session.commit.assert_called_once_with()
|
||||
assert status == 200
|
||||
assert response["permission_keys"] == []
|
||||
|
||||
|
||||
class TestAppImportConfirmApi:
|
||||
@pytest.fixture
|
||||
@ -132,3 +213,79 @@ class TestAppImportConfirmApi:
|
||||
session.commit.assert_not_called()
|
||||
assert status == 400
|
||||
assert response["status"] == ImportStatus.FAILED
|
||||
|
||||
def test_import_confirm_attaches_permission_keys_when_creating_new_app_and_rbac_enabled(
|
||||
self, api, app: Flask, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
method = _unwrap(api.post)
|
||||
|
||||
session = _mock_session(monkeypatch)
|
||||
monkeypatch.setattr(
|
||||
app_import_module,
|
||||
"current_account_with_tenant",
|
||||
lambda: (SimpleNamespace(id="u1"), "tenant-1"),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
app_import_module.redis_client,
|
||||
"get",
|
||||
lambda *_args, **_kwargs: (
|
||||
b'{"import_mode":"yaml-content","yaml_content":"app: {}","app_id":null,'
|
||||
b'"name":null,"description":null,"icon_type":null,"icon":null,"icon_background":null}'
|
||||
),
|
||||
)
|
||||
monkeypatch.setattr(app_import_module.dify_config, "RBAC_ENABLED", True)
|
||||
monkeypatch.setattr(
|
||||
app_import_module.AppDslService,
|
||||
"confirm_import",
|
||||
lambda *_args, **_kwargs: _Result(ImportStatus.COMPLETED, app_id="app-456"),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
app_import_module,
|
||||
"get_app_permission_keys",
|
||||
lambda tenant_id, account_id, app_id: ["app.acl.view_layout", "app.acl.edit"],
|
||||
)
|
||||
|
||||
with app.test_request_context("/console/api/apps/imports/import-1/confirm", method="POST"):
|
||||
response, status = method(import_id="import-1")
|
||||
|
||||
session.commit.assert_called_once_with()
|
||||
assert status == 200
|
||||
assert response["permission_keys"] == ["app.acl.view_layout", "app.acl.edit"]
|
||||
|
||||
def test_import_confirm_does_not_attach_permission_keys_when_overwriting_existing_app(
|
||||
self, api, app: Flask, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
method = _unwrap(api.post)
|
||||
|
||||
session = _mock_session(monkeypatch)
|
||||
monkeypatch.setattr(
|
||||
app_import_module,
|
||||
"current_account_with_tenant",
|
||||
lambda: (SimpleNamespace(id="u1"), "tenant-1"),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
app_import_module.redis_client,
|
||||
"get",
|
||||
lambda *_args, **_kwargs: (
|
||||
b'{"import_mode":"yaml-content","yaml_content":"app: {}","app_id":"existing-app",'
|
||||
b'"name":null,"description":null,"icon_type":null,"icon":null,"icon_background":null}'
|
||||
),
|
||||
)
|
||||
monkeypatch.setattr(app_import_module.dify_config, "RBAC_ENABLED", True)
|
||||
monkeypatch.setattr(
|
||||
app_import_module.AppDslService,
|
||||
"confirm_import",
|
||||
lambda *_args, **_kwargs: _Result(ImportStatus.COMPLETED, app_id="app-456"),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
app_import_module,
|
||||
"get_app_permission_keys",
|
||||
lambda *_args, **_kwargs: ["app.acl.view_layout", "app.acl.edit"],
|
||||
)
|
||||
|
||||
with app.test_request_context("/console/api/apps/imports/import-1/confirm", method="POST"):
|
||||
response, status = method(import_id="import-1")
|
||||
|
||||
session.commit.assert_called_once_with()
|
||||
assert status == 200
|
||||
assert response["permission_keys"] == []
|
||||
|
||||
@ -14,6 +14,8 @@ from flask.views import MethodView
|
||||
from pydantic import ValidationError
|
||||
from werkzeug.datastructures import MultiDict
|
||||
|
||||
from configs import dify_config
|
||||
|
||||
# kombu references MethodView as a global when importing celery/kombu pools.
|
||||
if not hasattr(builtins, "MethodView"):
|
||||
builtins.MethodView = MethodView # type: ignore[attr-defined]
|
||||
@ -351,6 +353,7 @@ def test_app_partial_serialization_uses_aliases(app_models):
|
||||
create_user_name="Creator",
|
||||
author_name="Author",
|
||||
has_draft_trigger=True,
|
||||
permission_keys=["app.acl.view_layout"],
|
||||
role="Should stay agent-only",
|
||||
)
|
||||
|
||||
@ -364,6 +367,7 @@ def test_app_partial_serialization_uses_aliases(app_models):
|
||||
assert serialized["model_config"]["model"] == {"provider": "openai", "name": "gpt-4o"}
|
||||
assert serialized["workflow"]["id"] == "wf-1"
|
||||
assert serialized["tags"][0]["name"] == "Utilities"
|
||||
assert serialized["permission_keys"] == ["app.acl.view_layout"]
|
||||
assert "role" not in serialized
|
||||
|
||||
|
||||
@ -402,6 +406,7 @@ def test_app_detail_with_site_includes_nested_serialization(app_models):
|
||||
updated_at=timestamp,
|
||||
access_mode="public",
|
||||
tags=[SimpleNamespace(id="tag-2", name="Prod", type="app")],
|
||||
permission_keys=["app.acl.view_layout", "app.acl.edit"],
|
||||
api_base_url="https://api.example.com/v1",
|
||||
max_active_requests=5,
|
||||
deleted_tools=[{"type": "api", "tool_name": "search", "provider_id": "prov"}],
|
||||
@ -417,6 +422,7 @@ def test_app_detail_with_site_includes_nested_serialization(app_models):
|
||||
assert serialized["deleted_tools"][0]["tool_name"] == "search"
|
||||
assert serialized["site"]["icon_url"] == "signed:site-icon"
|
||||
assert serialized["site"]["created_at"] == int(timestamp.timestamp())
|
||||
assert serialized["permission_keys"] == ["app.acl.view_layout", "app.acl.edit"]
|
||||
assert serialized["bound_agent_id"] == "agent-1"
|
||||
assert "role" not in serialized
|
||||
|
||||
@ -432,6 +438,7 @@ def test_app_pagination_aliases_per_page_and_has_next(app_models):
|
||||
icon="first-icon",
|
||||
created_at=_ts(15),
|
||||
updated_at=_ts(15),
|
||||
permission_keys=["app.acl.edit"],
|
||||
)
|
||||
item_two = SimpleNamespace(
|
||||
id="app-11",
|
||||
@ -493,6 +500,20 @@ def test_app_list_uses_injected_session_for_draft_workflows(
|
||||
"FeatureService",
|
||||
SimpleNamespace(get_system_features=lambda: SimpleNamespace(webapp_auth=SimpleNamespace(enabled=False))),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
app_module.enterprise_rbac_service.RBACService.MyPermissions,
|
||||
"get",
|
||||
lambda tenant_id, account_id: app_module.enterprise_rbac_service.MyPermissionsResponse(
|
||||
app=app_module.enterprise_rbac_service.ResourcePermissionSnapshot(
|
||||
overrides=[
|
||||
app_module.enterprise_rbac_service.ResourcePermissionKeys(
|
||||
resource_id="app-1",
|
||||
permission_keys=["app.acl.edit"],
|
||||
)
|
||||
]
|
||||
)
|
||||
),
|
||||
)
|
||||
monkeypatch.setattr(app_module, "db", SimpleNamespace(session=scoped_session))
|
||||
|
||||
with app.test_request_context("/console/api/apps?page=1&limit=20", method="GET"):
|
||||
@ -502,3 +523,351 @@ def test_app_list_uses_injected_session_for_draft_workflows(
|
||||
assert response["data"][0]["has_draft_trigger"] is True
|
||||
session.execute.assert_called_once()
|
||||
scoped_session.execute.assert_not_called()
|
||||
assert response["data"][0]["permission_keys"] == ["app.acl.edit"]
|
||||
|
||||
|
||||
def test_app_create_api_attaches_permission_keys(app, app_module):
|
||||
method = app_module.AppListApi.post
|
||||
while hasattr(method, "__wrapped__"):
|
||||
method = method.__wrapped__
|
||||
|
||||
app_obj = SimpleNamespace(
|
||||
id="app-new",
|
||||
name="Created App",
|
||||
description="Summary",
|
||||
mode_compatible_with_agent="advanced-chat",
|
||||
enable_site=True,
|
||||
enable_api=True,
|
||||
permission_keys=[],
|
||||
)
|
||||
|
||||
with app.test_request_context("/apps", method="POST", json={}):
|
||||
with pytest.MonkeyPatch.context() as monkeypatch:
|
||||
app_module.console_ns.payload = {
|
||||
"name": "Created App",
|
||||
"description": "Summary",
|
||||
"mode": "advanced-chat",
|
||||
}
|
||||
monkeypatch.setattr(
|
||||
app_module,
|
||||
"AppService",
|
||||
lambda: SimpleNamespace(create_app=lambda tenant_id, params, user: app_obj),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
app_module.enterprise_rbac_service.RBACService.AppPermissions,
|
||||
"batch_get",
|
||||
lambda tenant_id, account_id, app_ids: {"app-new": ["app.acl.view_layout", "app.acl.edit"]},
|
||||
)
|
||||
|
||||
resp, status = method(app_module.AppListApi(), "tenant-1", SimpleNamespace(id="acct-1"))
|
||||
|
||||
assert status == 201
|
||||
assert resp["permission_keys"] == ["app.acl.view_layout", "app.acl.edit"]
|
||||
|
||||
|
||||
def test_app_list_api_attaches_permission_keys(app, app_module):
|
||||
method = app_module.AppListApi.get
|
||||
while hasattr(method, "__wrapped__"):
|
||||
method = method.__wrapped__
|
||||
|
||||
app_obj = SimpleNamespace(
|
||||
id="app-1",
|
||||
name="List App",
|
||||
desc_or_prompt="Summary",
|
||||
mode_compatible_with_agent="chat",
|
||||
mode="chat",
|
||||
created_at=_ts(15),
|
||||
updated_at=_ts(15),
|
||||
permission_keys=[],
|
||||
)
|
||||
pagination = SimpleNamespace(page=1, per_page=20, total=1, has_next=False, items=[app_obj])
|
||||
get_paginate_apps = MagicMock(return_value=pagination)
|
||||
|
||||
with app.test_request_context("/apps"):
|
||||
with pytest.MonkeyPatch.context() as monkeypatch:
|
||||
monkeypatch.setattr(dify_config, "RBAC_ENABLED", True)
|
||||
monkeypatch.setattr(
|
||||
app_module.AppService,
|
||||
"get_paginate_apps",
|
||||
get_paginate_apps,
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
app_module.FeatureService,
|
||||
"get_system_features",
|
||||
lambda: SimpleNamespace(webapp_auth=SimpleNamespace(enabled=False)),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
app_module.enterprise_rbac_service.RBACService.MyPermissions,
|
||||
"get",
|
||||
lambda tenant_id, account_id: app_module.enterprise_rbac_service.MyPermissionsResponse(
|
||||
app=app_module.enterprise_rbac_service.ResourcePermissionSnapshot(
|
||||
default_permission_keys=["app.preview", "app.acl.view_layout"],
|
||||
overrides=[
|
||||
app_module.enterprise_rbac_service.ResourcePermissionKeys(
|
||||
resource_id="app-1",
|
||||
permission_keys=["app.acl.view_layout", "app.acl.edit"],
|
||||
)
|
||||
],
|
||||
)
|
||||
),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
app_module.enterprise_rbac_service.RBACService.AppAccess,
|
||||
"whitelist_resources",
|
||||
lambda tenant_id, account_id: SimpleNamespace(unrestricted=True, resource_ids=[]),
|
||||
)
|
||||
|
||||
session = MagicMock()
|
||||
session.execute.return_value.scalars.return_value.all.return_value = []
|
||||
resp, status = method(app_module.AppListApi(), "tenant-1", "acct-1", session)
|
||||
|
||||
assert status == 200
|
||||
params = get_paginate_apps.call_args.args[2]
|
||||
assert params.accessible_app_ids is None
|
||||
assert params.is_created_by_me is None
|
||||
assert resp["data"][0]["permission_keys"] == ["app.acl.view_layout", "app.acl.edit"]
|
||||
|
||||
|
||||
def test_app_list_api_limits_to_apps_created_by_current_user_without_view_permission(app, app_module):
|
||||
method = app_module.AppListApi.get
|
||||
while hasattr(method, "__wrapped__"):
|
||||
method = method.__wrapped__
|
||||
|
||||
pagination = SimpleNamespace(page=1, per_page=20, total=0, has_next=False, items=[])
|
||||
get_paginate_apps = MagicMock(return_value=pagination)
|
||||
|
||||
with app.test_request_context("/apps"):
|
||||
with pytest.MonkeyPatch.context() as monkeypatch:
|
||||
monkeypatch.setattr(app_module.AppService, "get_paginate_apps", get_paginate_apps)
|
||||
monkeypatch.setattr(app_module.dify_config, "RBAC_ENABLED", True)
|
||||
monkeypatch.setattr(
|
||||
app_module.enterprise_rbac_service.RBACService.MyPermissions,
|
||||
"get",
|
||||
lambda tenant_id, account_id: app_module.enterprise_rbac_service.MyPermissionsResponse(
|
||||
workspace=app_module.enterprise_rbac_service.WorkspacePermissionSnapshot(
|
||||
permission_keys=["app.create_and_management"]
|
||||
)
|
||||
),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
app_module.enterprise_rbac_service.RBACService.AppAccess,
|
||||
"whitelist_resources",
|
||||
lambda tenant_id, account_id: SimpleNamespace(resource_ids=["app-shared", "app-not-permitted"]),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
app_module.FeatureService,
|
||||
"get_system_features",
|
||||
lambda: SimpleNamespace(webapp_auth=SimpleNamespace(enabled=False)),
|
||||
)
|
||||
|
||||
session = MagicMock()
|
||||
resp, status = method(app_module.AppListApi(), "tenant-1", "acct-1", session)
|
||||
|
||||
assert status == 200
|
||||
assert resp["data"] == []
|
||||
params = get_paginate_apps.call_args.args[2]
|
||||
assert params.accessible_app_ids == ["app-not-permitted", "app-shared"]
|
||||
assert params.include_own_apps is True
|
||||
assert params.is_created_by_me is None
|
||||
|
||||
|
||||
def test_app_list_api_limits_to_preview_overrides_without_manage_own_permission(app, app_module):
|
||||
method = app_module.AppListApi.get
|
||||
while hasattr(method, "__wrapped__"):
|
||||
method = method.__wrapped__
|
||||
|
||||
pagination = SimpleNamespace(page=1, per_page=20, total=0, has_next=False, items=[])
|
||||
get_paginate_apps = MagicMock(return_value=pagination)
|
||||
|
||||
with app.test_request_context("/apps"):
|
||||
with pytest.MonkeyPatch.context() as monkeypatch:
|
||||
monkeypatch.setattr(app_module.AppService, "get_paginate_apps", get_paginate_apps)
|
||||
monkeypatch.setattr(app_module.dify_config, "RBAC_ENABLED", True)
|
||||
monkeypatch.setattr(
|
||||
app_module.enterprise_rbac_service.RBACService.MyPermissions,
|
||||
"get",
|
||||
lambda tenant_id, account_id: app_module.enterprise_rbac_service.MyPermissionsResponse(
|
||||
app=app_module.enterprise_rbac_service.ResourcePermissionSnapshot(
|
||||
overrides=[
|
||||
app_module.enterprise_rbac_service.ResourcePermissionKeys(
|
||||
resource_id="app-acl-shared",
|
||||
permission_keys=["app.acl.preview"],
|
||||
),
|
||||
app_module.enterprise_rbac_service.ResourcePermissionKeys(
|
||||
resource_id="app-full",
|
||||
permission_keys=["app.full_access"],
|
||||
),
|
||||
app_module.enterprise_rbac_service.ResourcePermissionKeys(
|
||||
resource_id="app-shared",
|
||||
permission_keys=["app.preview"],
|
||||
),
|
||||
]
|
||||
)
|
||||
),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
app_module.enterprise_rbac_service.RBACService.AppAccess,
|
||||
"whitelist_resources",
|
||||
lambda tenant_id, account_id: SimpleNamespace(
|
||||
resource_ids=["app-shared", "app-acl-shared", "app-full", "app-whitelist-only"]
|
||||
),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
app_module.FeatureService,
|
||||
"get_system_features",
|
||||
lambda: SimpleNamespace(webapp_auth=SimpleNamespace(enabled=False)),
|
||||
)
|
||||
|
||||
session = MagicMock()
|
||||
method(app_module.AppListApi(), "tenant-1", "acct-1", session)
|
||||
|
||||
params = get_paginate_apps.call_args.args[2]
|
||||
assert params.accessible_app_ids == ["app-acl-shared", "app-full", "app-shared", "app-whitelist-only"]
|
||||
assert params.include_own_apps is False
|
||||
assert params.is_created_by_me is None
|
||||
|
||||
|
||||
def test_app_list_api_returns_no_apps_without_workspace_or_resource_view_permission(app, app_module):
|
||||
method = app_module.AppListApi.get
|
||||
while hasattr(method, "__wrapped__"):
|
||||
method = method.__wrapped__
|
||||
|
||||
pagination = SimpleNamespace(page=1, per_page=20, total=0, has_next=False, items=[])
|
||||
get_paginate_apps = MagicMock(return_value=pagination)
|
||||
|
||||
with app.test_request_context("/apps"):
|
||||
with pytest.MonkeyPatch.context() as monkeypatch:
|
||||
monkeypatch.setattr(app_module.AppService, "get_paginate_apps", get_paginate_apps)
|
||||
monkeypatch.setattr(app_module.dify_config, "RBAC_ENABLED", True)
|
||||
monkeypatch.setattr(
|
||||
app_module.enterprise_rbac_service.RBACService.MyPermissions,
|
||||
"get",
|
||||
lambda tenant_id, account_id: app_module.enterprise_rbac_service.MyPermissionsResponse(),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
app_module.enterprise_rbac_service.RBACService.AppAccess,
|
||||
"whitelist_resources",
|
||||
lambda tenant_id, account_id: SimpleNamespace(resource_ids=["app-not-permitted"]),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
app_module.FeatureService,
|
||||
"get_system_features",
|
||||
lambda: SimpleNamespace(webapp_auth=SimpleNamespace(enabled=False)),
|
||||
)
|
||||
|
||||
session = MagicMock()
|
||||
method(app_module.AppListApi(), "tenant-1", "acct-1", session)
|
||||
|
||||
params = get_paginate_apps.call_args.args[2]
|
||||
assert params.accessible_app_ids == ["app-not-permitted"]
|
||||
assert params.include_own_apps is False
|
||||
assert params.is_created_by_me is None
|
||||
|
||||
|
||||
def test_app_detail_api_attaches_current_user_permission_keys(app, app_module):
|
||||
method = app_module.AppApi.get
|
||||
while hasattr(method, "__wrapped__"):
|
||||
method = method.__wrapped__
|
||||
|
||||
app_obj = SimpleNamespace(
|
||||
id="app-1",
|
||||
name="Detail App",
|
||||
description="Summary",
|
||||
mode_compatible_with_agent="chat",
|
||||
enable_site=True,
|
||||
enable_api=True,
|
||||
permission_keys=[],
|
||||
)
|
||||
|
||||
with app.test_request_context("/apps/app-1"):
|
||||
with pytest.MonkeyPatch.context() as monkeypatch:
|
||||
monkeypatch.setattr(dify_config, "RBAC_ENABLED", True)
|
||||
monkeypatch.setattr(app_module, "AppService", lambda: SimpleNamespace(get_app=lambda app_model: app_obj))
|
||||
monkeypatch.setattr(
|
||||
app_module.FeatureService,
|
||||
"get_system_features",
|
||||
lambda: SimpleNamespace(webapp_auth=SimpleNamespace(enabled=False)),
|
||||
)
|
||||
get_permissions = MagicMock(
|
||||
return_value=app_module.enterprise_rbac_service.MyPermissionsResponse(
|
||||
app=app_module.enterprise_rbac_service.ResourcePermissionSnapshot(
|
||||
overrides=[
|
||||
app_module.enterprise_rbac_service.ResourcePermissionKeys(
|
||||
resource_id="app-1",
|
||||
permission_keys=["app.acl.view_layout", "app.acl.edit", "app.acl.monitor"],
|
||||
)
|
||||
]
|
||||
)
|
||||
)
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
app_module.enterprise_rbac_service.RBACService.MyPermissions,
|
||||
"get",
|
||||
get_permissions,
|
||||
)
|
||||
|
||||
resp = method(app_module.AppApi(), "tenant-1", SimpleNamespace(id="acct-1"), app_model=app_obj)
|
||||
|
||||
get_permissions.assert_called_once_with("tenant-1", "acct-1", app_id="app-1")
|
||||
assert resp["permission_keys"] == ["app.acl.view_layout", "app.acl.edit", "app.acl.monitor"]
|
||||
|
||||
|
||||
def test_app_copy_api_attaches_permission_keys(app, app_module):
|
||||
method = app_module.AppCopyApi.post
|
||||
while hasattr(method, "__wrapped__"):
|
||||
method = method.__wrapped__
|
||||
|
||||
app_obj = SimpleNamespace(
|
||||
id="app-new",
|
||||
name="Copied App",
|
||||
description="Summary",
|
||||
mode_compatible_with_agent="workflow",
|
||||
enable_site=True,
|
||||
enable_api=True,
|
||||
permission_keys=[],
|
||||
)
|
||||
|
||||
import_result = SimpleNamespace(status=app_module.ImportStatus.COMPLETED, app_id="app-new")
|
||||
fake_session = MagicMock()
|
||||
fake_session.__enter__.return_value = fake_session
|
||||
fake_session.__exit__.return_value = None
|
||||
fake_session.scalar.return_value = app_obj
|
||||
|
||||
with app.test_request_context("/apps/app-original/copy", method="POST", json={}):
|
||||
with pytest.MonkeyPatch.context() as monkeypatch:
|
||||
monkeypatch.setattr(dify_config, "RBAC_ENABLED", True)
|
||||
monkeypatch.setattr(
|
||||
app_module,
|
||||
"AppDslService",
|
||||
lambda *_args, **_kwargs: SimpleNamespace(
|
||||
export_dsl=lambda **_kwargs: "dsl",
|
||||
import_app=lambda **_kwargs: import_result,
|
||||
),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
app_module.FeatureService,
|
||||
"get_system_features",
|
||||
lambda: SimpleNamespace(webapp_auth=SimpleNamespace(enabled=False)),
|
||||
)
|
||||
monkeypatch.setattr(app_module, "db", SimpleNamespace(engine=object()))
|
||||
monkeypatch.setattr(
|
||||
app_module,
|
||||
"Session",
|
||||
lambda *_args, **_kwargs: fake_session,
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
app_module.enterprise_rbac_service.RBACService.AppPermissions,
|
||||
"batch_get",
|
||||
lambda tenant_id, account_id, app_ids: {"app-new": ["app.acl.view_layout", "app.acl.edit"]},
|
||||
)
|
||||
|
||||
resp, status = method(
|
||||
app_module.AppCopyApi(),
|
||||
"tenant-1",
|
||||
SimpleNamespace(id="acct-1"),
|
||||
app_model=SimpleNamespace(id="app-original"),
|
||||
)
|
||||
|
||||
assert status == 201
|
||||
assert fake_session.scalar.called
|
||||
assert resp["permission_keys"] == ["app.acl.view_layout", "app.acl.edit"]
|
||||
|
||||
@ -0,0 +1,55 @@
|
||||
"""Unit tests for convert-to-workflow endpoint."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from types import SimpleNamespace
|
||||
|
||||
import pytest
|
||||
from flask import Flask
|
||||
|
||||
from controllers.console.app import workflow as workflow_module
|
||||
|
||||
|
||||
def _unwrap(func):
|
||||
bound_self = getattr(func, "__self__", None)
|
||||
while hasattr(func, "__wrapped__"):
|
||||
func = func.__wrapped__
|
||||
if bound_self is not None:
|
||||
return func.__get__(bound_self, bound_self.__class__)
|
||||
return func
|
||||
|
||||
|
||||
class TestConvertToWorkflowApi:
|
||||
@pytest.fixture
|
||||
def api(self):
|
||||
return workflow_module.ConvertToWorkflowApi()
|
||||
|
||||
def test_convert_to_workflow_attaches_permission_keys_when_rbac_enabled(
|
||||
self, api, app: Flask, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
method = _unwrap(api.post)
|
||||
|
||||
monkeypatch.setattr(
|
||||
workflow_module,
|
||||
"WorkflowService",
|
||||
lambda: SimpleNamespace(convert_to_workflow=lambda **_kwargs: SimpleNamespace(id="new-app-1")),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
workflow_module,
|
||||
"get_app_permission_keys",
|
||||
lambda tenant_id, account_id, app_id: ["app.acl.view_layout", "app.acl.edit"],
|
||||
)
|
||||
|
||||
with app.test_request_context(
|
||||
"/console/api/apps/app-1/convert-to-workflow",
|
||||
method="POST",
|
||||
json={},
|
||||
):
|
||||
response = method(
|
||||
current_tenant_id="tenant-1",
|
||||
current_user=SimpleNamespace(id="u1"),
|
||||
app_model=SimpleNamespace(id="app-1"),
|
||||
)
|
||||
|
||||
assert response["new_app_id"] == "new-app-1"
|
||||
assert response["permission_keys"] == ["app.acl.view_layout", "app.acl.edit"]
|
||||
@ -2,6 +2,7 @@ import datetime
|
||||
import json
|
||||
from contextlib import ExitStack
|
||||
from inspect import unwrap
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import MagicMock, PropertyMock, patch
|
||||
|
||||
import pytest
|
||||
@ -39,6 +40,7 @@ from models.dataset import Dataset, DatasetQuery, Document
|
||||
from models.enums import CreatorUserRole, DataSourceType, DocumentCreatedFrom, IndexingStatus
|
||||
from models.model import ApiToken, App, AppMode, IconType, UploadFile
|
||||
from services.dataset_service import DatasetPermissionService, DatasetService
|
||||
from services.enterprise import rbac_service as enterprise_rbac_service
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
@ -211,6 +213,201 @@ class TestDatasetList:
|
||||
assert status == 200
|
||||
assert resp["total"] == 2
|
||||
|
||||
def test_get_attaches_current_user_permission_keys(self, app: Flask):
|
||||
api = DatasetListApi()
|
||||
method = unwrap(api.get)
|
||||
current_user = self._mock_user()
|
||||
dataset = make_dataset(id="dataset-1")
|
||||
permissions = enterprise_rbac_service.MyPermissionsResponse(
|
||||
dataset=enterprise_rbac_service.ResourcePermissionSnapshot(
|
||||
default_permission_keys=["dataset.acl.readonly"],
|
||||
overrides=[
|
||||
enterprise_rbac_service.ResourcePermissionKeys(
|
||||
resource_id="dataset-1",
|
||||
permission_keys=["dataset.acl.readonly", "dataset.acl.edit"],
|
||||
)
|
||||
],
|
||||
)
|
||||
)
|
||||
|
||||
with app.test_request_context("/datasets"):
|
||||
with (
|
||||
patch.object(DatasetService, "get_datasets", return_value=([dataset], 1)),
|
||||
patch.object(
|
||||
ProviderManager,
|
||||
"get_configurations",
|
||||
return_value=MagicMock(get_models=lambda **_: []),
|
||||
),
|
||||
patch(
|
||||
"controllers.console.datasets.datasets.enterprise_rbac_service.RBACService.MyPermissions.get",
|
||||
return_value=permissions,
|
||||
) as get_permissions,
|
||||
):
|
||||
resp, status = method(api, "tenant-1", current_user)
|
||||
|
||||
get_permissions.assert_called_once_with("tenant-1", current_user.id)
|
||||
assert status == 200
|
||||
assert resp["data"][0]["permission_keys"] == ["dataset.acl.readonly", "dataset.acl.edit"]
|
||||
|
||||
def test_get_limits_to_own_datasets_without_default_read_permission(self, app: Flask):
|
||||
api = DatasetListApi()
|
||||
method = unwrap(api.get)
|
||||
current_user = self._mock_user()
|
||||
permissions = enterprise_rbac_service.MyPermissionsResponse(
|
||||
workspace=enterprise_rbac_service.WorkspacePermissionSnapshot(
|
||||
permission_keys=["dataset.create_and_management"]
|
||||
)
|
||||
)
|
||||
|
||||
with app.test_request_context("/datasets"):
|
||||
with (
|
||||
patch("controllers.console.datasets.datasets.dify_config.RBAC_ENABLED", True),
|
||||
patch.object(DatasetService, "get_datasets", return_value=([], 0)) as get_datasets,
|
||||
patch(
|
||||
"controllers.console.datasets.datasets.enterprise_rbac_service.RBACService.MyPermissions.get",
|
||||
return_value=permissions,
|
||||
),
|
||||
patch(
|
||||
"controllers.console.datasets.datasets.enterprise_rbac_service.RBACService.DatasetAccess.whitelist_resources",
|
||||
return_value=SimpleNamespace(resource_ids=[]),
|
||||
),
|
||||
patch.object(
|
||||
ProviderManager,
|
||||
"get_configurations",
|
||||
return_value=MagicMock(get_models=lambda **_: []),
|
||||
),
|
||||
):
|
||||
method(api, "tenant-1", current_user)
|
||||
|
||||
assert get_datasets.call_args.kwargs["accessible_dataset_ids"] == []
|
||||
assert get_datasets.call_args.kwargs["include_own_datasets"] is True
|
||||
|
||||
def test_get_workspace_owner_bypasses_dataset_whitelist(self, app: Flask):
|
||||
api = DatasetListApi()
|
||||
method = unwrap(api.get)
|
||||
current_user = self._mock_user()
|
||||
permissions = enterprise_rbac_service.MyPermissionsResponse(
|
||||
dataset=enterprise_rbac_service.ResourcePermissionSnapshot(default_permission_keys=["dataset.preview"])
|
||||
)
|
||||
|
||||
with app.test_request_context("/datasets"):
|
||||
with (
|
||||
patch("controllers.console.datasets.datasets.dify_config.RBAC_ENABLED", True),
|
||||
patch.object(DatasetService, "get_datasets", return_value=([], 0)) as get_datasets,
|
||||
patch(
|
||||
"controllers.console.datasets.datasets.enterprise_rbac_service.RBACService.MyPermissions.get",
|
||||
return_value=permissions,
|
||||
),
|
||||
patch(
|
||||
"controllers.console.datasets.datasets.enterprise_rbac_service.RBACService.DatasetAccess.whitelist_resources",
|
||||
return_value=SimpleNamespace(unrestricted=True, resource_ids=[]),
|
||||
),
|
||||
patch.object(
|
||||
ProviderManager,
|
||||
"get_configurations",
|
||||
return_value=MagicMock(get_models=lambda **_: []),
|
||||
),
|
||||
):
|
||||
method(api, "tenant-1", current_user)
|
||||
|
||||
assert get_datasets.call_args.kwargs["accessible_dataset_ids"] is None
|
||||
|
||||
def test_get_limits_to_dataset_read_overrides(self, app: Flask):
|
||||
api = DatasetListApi()
|
||||
method = unwrap(api.get)
|
||||
current_user = self._mock_user()
|
||||
permissions = enterprise_rbac_service.MyPermissionsResponse(
|
||||
dataset=enterprise_rbac_service.ResourcePermissionSnapshot(
|
||||
overrides=[
|
||||
enterprise_rbac_service.ResourcePermissionKeys(
|
||||
resource_id="dataset-acl-shared",
|
||||
permission_keys=["dataset.acl.preview"],
|
||||
),
|
||||
enterprise_rbac_service.ResourcePermissionKeys(
|
||||
resource_id="dataset-full",
|
||||
permission_keys=["dataset.full_access"],
|
||||
),
|
||||
enterprise_rbac_service.ResourcePermissionKeys(
|
||||
resource_id="dataset-shared",
|
||||
permission_keys=["dataset.preview"],
|
||||
),
|
||||
enterprise_rbac_service.ResourcePermissionKeys(
|
||||
resource_id="dataset-hidden",
|
||||
permission_keys=[],
|
||||
),
|
||||
]
|
||||
)
|
||||
)
|
||||
|
||||
with app.test_request_context("/datasets"):
|
||||
with (
|
||||
patch("controllers.console.datasets.datasets.dify_config.RBAC_ENABLED", True),
|
||||
patch.object(DatasetService, "get_datasets", return_value=([], 0)) as get_datasets,
|
||||
patch(
|
||||
"controllers.console.datasets.datasets.enterprise_rbac_service.RBACService.MyPermissions.get",
|
||||
return_value=permissions,
|
||||
),
|
||||
patch(
|
||||
"controllers.console.datasets.datasets.enterprise_rbac_service.RBACService.DatasetAccess.whitelist_resources",
|
||||
return_value=SimpleNamespace(
|
||||
resource_ids=[
|
||||
"dataset-shared",
|
||||
"dataset-acl-shared",
|
||||
"dataset-full",
|
||||
"dataset-whitelist-only",
|
||||
]
|
||||
),
|
||||
),
|
||||
patch.object(
|
||||
ProviderManager,
|
||||
"get_configurations",
|
||||
return_value=MagicMock(get_models=lambda **_: []),
|
||||
),
|
||||
):
|
||||
method(api, "tenant-1", current_user)
|
||||
|
||||
assert get_datasets.call_args.kwargs["accessible_dataset_ids"] == [
|
||||
"dataset-acl-shared",
|
||||
"dataset-full",
|
||||
"dataset-shared",
|
||||
"dataset-whitelist-only",
|
||||
]
|
||||
assert get_datasets.call_args.kwargs["include_own_datasets"] is False
|
||||
|
||||
def test_get_with_ids_applies_dataset_visibility(self, app: Flask):
|
||||
api = DatasetListApi()
|
||||
method = unwrap(api.get)
|
||||
current_user = self._mock_user()
|
||||
permissions = enterprise_rbac_service.MyPermissionsResponse()
|
||||
|
||||
with app.test_request_context("/datasets?ids=dataset-1"):
|
||||
with (
|
||||
patch("controllers.console.datasets.datasets.dify_config.RBAC_ENABLED", True),
|
||||
patch.object(DatasetService, "get_datasets_by_ids", return_value=([], 0)) as get_datasets_by_ids,
|
||||
patch(
|
||||
"controllers.console.datasets.datasets.enterprise_rbac_service.RBACService.MyPermissions.get",
|
||||
return_value=permissions,
|
||||
),
|
||||
patch(
|
||||
"controllers.console.datasets.datasets.enterprise_rbac_service.RBACService.DatasetAccess.whitelist_resources",
|
||||
return_value=SimpleNamespace(resource_ids=[]),
|
||||
),
|
||||
patch.object(
|
||||
ProviderManager,
|
||||
"get_configurations",
|
||||
return_value=MagicMock(get_models=lambda **_: []),
|
||||
),
|
||||
):
|
||||
method(api, "tenant-1", current_user)
|
||||
|
||||
get_datasets_by_ids.assert_called_once_with(
|
||||
["dataset-1"],
|
||||
"tenant-1",
|
||||
user=current_user,
|
||||
accessible_dataset_ids=[],
|
||||
include_own_datasets=False,
|
||||
)
|
||||
|
||||
def test_get_with_tag_ids(self, app: Flask):
|
||||
api = DatasetListApi()
|
||||
method = unwrap(api.get)
|
||||
@ -504,6 +701,51 @@ class TestDatasetApiGet:
|
||||
assert status == 200
|
||||
assert data["embedding_available"] is True
|
||||
|
||||
def test_get_attaches_permission_keys_when_rbac_enabled(self, app: Flask):
|
||||
api = DatasetApi()
|
||||
method = unwrap(api.get)
|
||||
|
||||
dataset_id = "123e4567-e89b-12d3-a456-426614174000"
|
||||
user = MagicMock(id="account-1")
|
||||
tenant_id = "tenant-1"
|
||||
dataset = make_dataset(id=dataset_id)
|
||||
|
||||
with (
|
||||
app.test_request_context(f"/datasets/{dataset_id}"),
|
||||
patch("controllers.console.datasets.datasets.dify_config.RBAC_ENABLED", True),
|
||||
patch.object(
|
||||
DatasetService,
|
||||
"get_dataset",
|
||||
return_value=dataset,
|
||||
),
|
||||
patch.object(
|
||||
DatasetService,
|
||||
"check_dataset_permission",
|
||||
return_value=None,
|
||||
),
|
||||
patch(
|
||||
"controllers.console.datasets.datasets.enterprise_rbac_service.RBACService.MyPermissions.get",
|
||||
return_value=enterprise_rbac_service.MyPermissionsResponse(
|
||||
dataset=enterprise_rbac_service.ResourcePermissionSnapshot(
|
||||
overrides=[
|
||||
enterprise_rbac_service.ResourcePermissionKeys(
|
||||
resource_id=dataset_id,
|
||||
permission_keys=["dataset.acl.readonly", "dataset.acl.edit"],
|
||||
)
|
||||
]
|
||||
)
|
||||
),
|
||||
) as get_permissions,
|
||||
patch("controllers.console.datasets.datasets.create_plugin_provider_manager") as provider_manager_mock,
|
||||
):
|
||||
provider_manager_mock.return_value.get_configurations.return_value.get_models.return_value = []
|
||||
|
||||
data, status = method(api, tenant_id, user, dataset_id)
|
||||
|
||||
get_permissions.assert_called_once_with(tenant_id, user.id, dataset_id=dataset_id)
|
||||
assert status == 200
|
||||
assert data["permission_keys"] == ["dataset.acl.readonly", "dataset.acl.edit"]
|
||||
|
||||
def test_get_uses_default_external_retrieval_model(self, app: Flask):
|
||||
api = DatasetApi()
|
||||
method = unwrap(api.get)
|
||||
|
||||
@ -171,6 +171,7 @@ class TestExternalDatasetCreateApi:
|
||||
dataset.external_retrieval_model = None
|
||||
dataset.doc_metadata = []
|
||||
dataset.icon_info = None
|
||||
dataset.permission_keys = []
|
||||
|
||||
dataset.summary_index_setting = MagicMock()
|
||||
dataset.summary_index_setting.enable = False
|
||||
|
||||
@ -49,6 +49,7 @@ class TestMemberInviteEmailApi:
|
||||
inviter = SimpleNamespace(email="Owner@Example.com", current_tenant=tenant, status="active")
|
||||
|
||||
with (
|
||||
patch("controllers.console.workspace.members.dify_config.RBAC_ENABLED", False),
|
||||
patch("controllers.console.workspace.members.dify_config.CONSOLE_WEB_URL", "https://console.example.com"),
|
||||
patch("controllers.console.workspace.members._count_new_member_invites", return_value=1),
|
||||
patch("controllers.console.workspace.members.dify_config.ENTERPRISE_ENABLED", False),
|
||||
@ -76,3 +77,116 @@ class TestMemberInviteEmailApi:
|
||||
assert call_args.kwargs["role"] == TenantAccountRole.EDITOR
|
||||
assert call_args.kwargs["inviter"] == account
|
||||
mock_csrf.assert_called_once_with(ANY, account.id)
|
||||
|
||||
@patch("controllers.console.workspace.members.FeatureService.get_features")
|
||||
@patch("controllers.console.workspace.members.RegisterService.invite_new_member")
|
||||
@patch("controllers.console.workspace.members.current_account_with_tenant")
|
||||
@patch("controllers.console.wraps.db")
|
||||
@patch("libs.login.check_csrf_token", return_value=None)
|
||||
def test_invite_rbac_enabled_accepts_rbac_role_id(
|
||||
self,
|
||||
mock_csrf,
|
||||
mock_db,
|
||||
mock_current_account,
|
||||
mock_invite_member,
|
||||
mock_get_features,
|
||||
app,
|
||||
):
|
||||
"""When RBAC is enabled, any non-empty role string should be accepted."""
|
||||
mock_get_features.return_value = _build_feature_flags()
|
||||
mock_invite_member.return_value = "rbac-token"
|
||||
|
||||
tenant = SimpleNamespace(id="tenant-1", name="Test Tenant")
|
||||
inviter = SimpleNamespace(email="inviter@example.com", current_tenant=tenant, status="active")
|
||||
mock_current_account.return_value = (inviter, tenant.id)
|
||||
|
||||
with patch("controllers.console.workspace.members.dify_config") as mock_config:
|
||||
mock_config.RBAC_ENABLED = True
|
||||
mock_config.CONSOLE_WEB_URL = "https://console.example.com"
|
||||
with app.test_request_context(
|
||||
"/workspaces/current/members/invite-email",
|
||||
method="POST",
|
||||
json={"emails": ["user@example.com"], "role": "rbac-role-id-abc", "language": "en-US"},
|
||||
):
|
||||
account = Account(name="tester", email="tester@example.com")
|
||||
account._current_tenant = tenant
|
||||
g._login_user = account
|
||||
g._current_tenant = tenant
|
||||
response, status_code = MemberInviteEmailApi().post()
|
||||
|
||||
assert status_code == 201
|
||||
mock_invite_member.assert_called_once()
|
||||
call_args = mock_invite_member.call_args
|
||||
assert call_args.kwargs["role"] == "rbac-role-id-abc"
|
||||
|
||||
@patch("controllers.console.workspace.members.FeatureService.get_features")
|
||||
@patch("controllers.console.workspace.members.current_account_with_tenant")
|
||||
@patch("controllers.console.wraps.db")
|
||||
@patch("libs.login.check_csrf_token", return_value=None)
|
||||
def test_invite_rbac_disabled_rejects_invalid_role(
|
||||
self,
|
||||
mock_csrf,
|
||||
mock_db,
|
||||
mock_current_account,
|
||||
mock_get_features,
|
||||
app,
|
||||
):
|
||||
"""When RBAC is disabled, an invalid role string should be rejected."""
|
||||
mock_get_features.return_value = _build_feature_flags()
|
||||
|
||||
tenant = SimpleNamespace(id="tenant-1", name="Test Tenant")
|
||||
inviter = SimpleNamespace(email="inviter@example.com", current_tenant=tenant, status="active")
|
||||
mock_current_account.return_value = (inviter, tenant.id)
|
||||
|
||||
with patch("controllers.console.workspace.members.dify_config") as mock_config:
|
||||
mock_config.RBAC_ENABLED = False
|
||||
mock_config.CONSOLE_WEB_URL = "https://console.example.com"
|
||||
with app.test_request_context(
|
||||
"/workspaces/current/members/invite-email",
|
||||
method="POST",
|
||||
json={"emails": ["user@example.com"], "role": "invalid-role", "language": "en-US"},
|
||||
):
|
||||
account = Account(name="tester", email="tester@example.com")
|
||||
account._current_tenant = tenant
|
||||
g._login_user = account
|
||||
g._current_tenant = tenant
|
||||
response, status_code = MemberInviteEmailApi().post()
|
||||
|
||||
assert status_code == 400
|
||||
assert response["code"] == "invalid-role"
|
||||
|
||||
@patch("controllers.console.workspace.members.FeatureService.get_features")
|
||||
@patch("controllers.console.workspace.members.current_account_with_tenant")
|
||||
@patch("controllers.console.wraps.db")
|
||||
@patch("libs.login.check_csrf_token", return_value=None)
|
||||
def test_invite_rbac_disabled_rejects_owner_role(
|
||||
self,
|
||||
mock_csrf,
|
||||
mock_db,
|
||||
mock_current_account,
|
||||
mock_get_features,
|
||||
app,
|
||||
):
|
||||
"""When RBAC is disabled, owner role should be rejected for invite."""
|
||||
mock_get_features.return_value = _build_feature_flags()
|
||||
|
||||
tenant = SimpleNamespace(id="tenant-1", name="Test Tenant")
|
||||
inviter = SimpleNamespace(email="inviter@example.com", current_tenant=tenant, status="active")
|
||||
mock_current_account.return_value = (inviter, tenant.id)
|
||||
|
||||
with patch("controllers.console.workspace.members.dify_config") as mock_config:
|
||||
mock_config.RBAC_ENABLED = False
|
||||
mock_config.CONSOLE_WEB_URL = "https://console.example.com"
|
||||
with app.test_request_context(
|
||||
"/workspaces/current/members/invite-email",
|
||||
method="POST",
|
||||
json={"emails": ["user@example.com"], "role": "owner", "language": "en-US"},
|
||||
):
|
||||
account = Account(name="tester", email="tester@example.com")
|
||||
account._current_tenant = tenant
|
||||
g._login_user = account
|
||||
g._current_tenant = tenant
|
||||
response, status_code = MemberInviteEmailApi().post()
|
||||
|
||||
assert status_code == 400
|
||||
assert response["code"] == "invalid-role"
|
||||
|
||||
@ -2,24 +2,29 @@ from typing import override
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
from flask import Flask
|
||||
from flask import Flask, request
|
||||
from flask_login import LoginManager, UserMixin
|
||||
from pydantic import BaseModel
|
||||
from werkzeug.exceptions import HTTPException
|
||||
|
||||
from controllers.common.wraps import _extract_resource_id
|
||||
from controllers.console.error import NotInitValidateError, NotSetupError, UnauthorizedAndForceLogout
|
||||
from controllers.console.workspace.error import AccountNotInitializedError
|
||||
from controllers.console.wraps import (
|
||||
RBACPermission,
|
||||
RBACResourceScope,
|
||||
account_initialization_required,
|
||||
cloud_edition_billing_enabled,
|
||||
cloud_edition_billing_rate_limit_check,
|
||||
cloud_edition_billing_resource_check,
|
||||
cloud_utm_record,
|
||||
enterprise_license_required,
|
||||
is_admin_or_owner_required,
|
||||
model_validate,
|
||||
only_edition_cloud,
|
||||
only_edition_enterprise,
|
||||
only_edition_self_hosted,
|
||||
rbac_permission_required,
|
||||
setup_required,
|
||||
with_current_tenant_id,
|
||||
with_current_user,
|
||||
@ -165,6 +170,165 @@ class TestCurrentContextInjection:
|
||||
assert Handler().get() == ("user-99", "tenant-456")
|
||||
|
||||
|
||||
class TestRbacPermissionRequired:
|
||||
"""Test enterprise RBAC decorator."""
|
||||
|
||||
def test_resource_scoped_check_uses_resource_id(self):
|
||||
current_user = make_account("account-1")
|
||||
|
||||
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_DELETE)
|
||||
def protected_view(**kwargs):
|
||||
return "ok"
|
||||
|
||||
with (
|
||||
patch("controllers.common.wraps.dify_config.RBAC_ENABLED", True),
|
||||
patch("controllers.common.wraps.current_account_with_tenant", return_value=(current_user, "tenant-1")),
|
||||
patch("controllers.common.wraps._extract_resource_id", return_value="app-123") as mock_extract,
|
||||
patch("controllers.common.wraps._is_resource_owned_by_current_user", return_value=False) as mock_owned,
|
||||
patch("controllers.common.wraps.RBACService.CheckAccess.check", return_value=True) as mock_check,
|
||||
):
|
||||
assert protected_view(app_id="app-123") == "ok"
|
||||
|
||||
mock_extract.assert_called_once_with("app", {"app_id": "app-123"})
|
||||
mock_owned.assert_called_once_with("tenant-1", "account-1", "app", "app-123")
|
||||
mock_check.assert_called_once_with(
|
||||
"tenant-1",
|
||||
"account-1",
|
||||
scene="app_delete",
|
||||
resource_type="app",
|
||||
resource_id="app-123",
|
||||
)
|
||||
|
||||
def test_workspace_scoped_check_skips_resource_id_extraction(self):
|
||||
current_user = make_account("account-2")
|
||||
|
||||
@rbac_permission_required(
|
||||
RBACResourceScope.DATASET, RBACPermission.DATASET_CREATE_AND_MANAGEMENT, resource_required=False
|
||||
)
|
||||
def protected_view():
|
||||
return "ok"
|
||||
|
||||
with (
|
||||
patch("controllers.common.wraps.dify_config.RBAC_ENABLED", True),
|
||||
patch("controllers.common.wraps.current_account_with_tenant", return_value=(current_user, "tenant-2")),
|
||||
patch("controllers.common.wraps._extract_resource_id") as mock_extract,
|
||||
patch("controllers.common.wraps._is_resource_owned_by_current_user", return_value=False) as mock_owned,
|
||||
patch("controllers.common.wraps.RBACService.CheckAccess.check", return_value=True) as mock_check,
|
||||
):
|
||||
assert protected_view() == "ok"
|
||||
|
||||
mock_extract.assert_not_called()
|
||||
mock_owned.assert_not_called()
|
||||
mock_check.assert_called_once_with(
|
||||
"tenant-2",
|
||||
"account-2",
|
||||
scene="dataset_create_and_management",
|
||||
resource_type="dataset",
|
||||
resource_id=None,
|
||||
)
|
||||
|
||||
def test_workspace_scene_omits_resource_type(self):
|
||||
current_user = make_account("account-3")
|
||||
|
||||
@rbac_permission_required(
|
||||
RBACResourceScope.WORKSPACE, RBACPermission.WORKSPACE_ROLE_MANAGE, resource_required=False
|
||||
)
|
||||
def protected_view():
|
||||
return "ok"
|
||||
|
||||
with (
|
||||
patch("controllers.common.wraps.dify_config.RBAC_ENABLED", True),
|
||||
patch("controllers.common.wraps.current_account_with_tenant", return_value=(current_user, "tenant-3")),
|
||||
patch("controllers.common.wraps.RBACService.CheckAccess.check", return_value=True) as mock_check,
|
||||
):
|
||||
assert protected_view() == "ok"
|
||||
|
||||
mock_check.assert_called_once_with(
|
||||
"tenant-3",
|
||||
"account-3",
|
||||
scene="workspace_role_manage",
|
||||
resource_type=None,
|
||||
resource_id=None,
|
||||
)
|
||||
|
||||
def test_resource_owned_app_skips_rbac_check(self):
|
||||
current_user = make_account("account-4")
|
||||
|
||||
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_DELETE)
|
||||
def protected_view(**kwargs):
|
||||
return "ok"
|
||||
|
||||
with (
|
||||
patch("controllers.common.wraps.dify_config.RBAC_ENABLED", True),
|
||||
patch("controllers.common.wraps.current_account_with_tenant", return_value=(current_user, "tenant-4")),
|
||||
patch("controllers.common.wraps._extract_resource_id", return_value="app-123"),
|
||||
patch("controllers.common.wraps._is_resource_owned_by_current_user", return_value=True) as mock_owned,
|
||||
patch("controllers.common.wraps.RBACService.CheckAccess.check") as mock_check,
|
||||
):
|
||||
assert protected_view(app_id="app-123") == "ok"
|
||||
|
||||
mock_owned.assert_called_once_with("tenant-4", "account-4", "app", "app-123")
|
||||
mock_check.assert_not_called()
|
||||
|
||||
def test_resource_owned_dataset_skips_rbac_check(self):
|
||||
current_user = make_account("account-5")
|
||||
|
||||
@rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_EDIT)
|
||||
def protected_view(**kwargs):
|
||||
return "ok"
|
||||
|
||||
with (
|
||||
patch("controllers.common.wraps.dify_config.RBAC_ENABLED", True),
|
||||
patch("controllers.common.wraps.current_account_with_tenant", return_value=(current_user, "tenant-5")),
|
||||
patch("controllers.common.wraps._extract_resource_id", return_value="dataset-123"),
|
||||
patch("controllers.common.wraps._is_resource_owned_by_current_user", return_value=True) as mock_owned,
|
||||
patch("controllers.common.wraps.RBACService.CheckAccess.check") as mock_check,
|
||||
):
|
||||
assert protected_view(dataset_id="dataset-123") == "ok"
|
||||
|
||||
mock_owned.assert_called_once_with("tenant-5", "account-5", "dataset", "dataset-123")
|
||||
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", {"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") == "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) == "app-1"
|
||||
|
||||
with app.test_request_context("/agent/agent-1/features"):
|
||||
request.view_args = {"agent_id": "agent-1"}
|
||||
assert _extract_resource_id(RBACResourceScope.APP) == "agent-1"
|
||||
|
||||
with app.test_request_context("/datasets/dataset-1/api-keys"):
|
||||
request.view_args = {"resource_id": "dataset-1"}
|
||||
assert _extract_resource_id(RBACResourceScope.DATASET) == "dataset-1"
|
||||
|
||||
def test_legacy_admin_decorator_noops_when_rbac_enabled(self):
|
||||
@is_admin_or_owner_required
|
||||
def protected_view():
|
||||
return "ok"
|
||||
|
||||
with patch("controllers.console.wraps.dify_config.RBAC_ENABLED", True):
|
||||
assert protected_view() == "ok"
|
||||
|
||||
|
||||
class TestModelValidationInjection:
|
||||
"""Test request model validation decorator."""
|
||||
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
from contextlib import nullcontext
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
@ -43,8 +44,8 @@ class TestMemberListApi:
|
||||
member.name = "Member"
|
||||
member.email = "member@test.com"
|
||||
member.avatar = "avatar.png"
|
||||
member.role = "admin"
|
||||
member.status = "active"
|
||||
member.current_role = SimpleNamespace(value="admin")
|
||||
member.status = SimpleNamespace(value="active")
|
||||
members = [member]
|
||||
|
||||
with (
|
||||
@ -55,6 +56,53 @@ class TestMemberListApi:
|
||||
|
||||
assert status == 200
|
||||
assert len(result["accounts"]) == 1
|
||||
assert result["accounts"][0]["role"] == "admin"
|
||||
assert result["accounts"][0]["roles"] == [{"id": "admin", "name": "admin"}]
|
||||
|
||||
def test_get_with_rbac_enabled_fetches_roles_in_batch(self, app):
|
||||
api = MemberListApi()
|
||||
method = unwrap(api.get)
|
||||
|
||||
tenant = MagicMock(id="tenant-1")
|
||||
user = MagicMock(id="acct-1", current_tenant=tenant)
|
||||
member = SimpleNamespace(
|
||||
id="m1",
|
||||
name="Member",
|
||||
email="member@test.com",
|
||||
avatar=None,
|
||||
last_login_at=1,
|
||||
last_active_at=2,
|
||||
created_at=3,
|
||||
current_role=SimpleNamespace(value="editor"),
|
||||
status=SimpleNamespace(value="active"),
|
||||
)
|
||||
role_item = SimpleNamespace(
|
||||
account_id="m1",
|
||||
roles=[
|
||||
SimpleNamespace(id="workspace.owner", name="Owner"),
|
||||
SimpleNamespace(id="workspace.editor", name="Editor"),
|
||||
],
|
||||
)
|
||||
|
||||
with (
|
||||
app.test_request_context("/"),
|
||||
patch("controllers.console.workspace.members.current_account_with_tenant", return_value=(user, "tenant-1")),
|
||||
patch("controllers.console.workspace.members.dify_config.RBAC_ENABLED", True),
|
||||
patch("controllers.console.workspace.members.TenantService.get_tenant_members", return_value=[member]),
|
||||
patch(
|
||||
"controllers.console.workspace.members.enterprise_rbac_service.RBACService.MemberRoles.batch_get",
|
||||
return_value=[role_item],
|
||||
) as mock_batch_get,
|
||||
):
|
||||
result, status = method(api)
|
||||
|
||||
assert status == 200
|
||||
assert result["accounts"][0]["role"] == "editor"
|
||||
assert result["accounts"][0]["roles"] == [
|
||||
{"id": "workspace.owner", "name": "Owner"},
|
||||
{"id": "workspace.editor", "name": "Editor"},
|
||||
]
|
||||
mock_batch_get.assert_called_once_with("tenant-1", "acct-1", ["m1"])
|
||||
|
||||
def test_get_no_tenant(self, app: Flask):
|
||||
api = MemberListApi()
|
||||
|
||||
503
api/tests/unit_tests/controllers/console/workspace/test_rbac.py
Normal file
503
api/tests/unit_tests/controllers/console/workspace/test_rbac.py
Normal file
@ -0,0 +1,503 @@
|
||||
"""Controller tests for ``controllers.console.workspace.rbac``.
|
||||
|
||||
The controllers here are thin: almost every non-trivial behaviour lives in
|
||||
``services.enterprise.rbac_service`` (covered by its own suite). These tests
|
||||
therefore focus on the Flask-layer concerns the service layer cannot exercise:
|
||||
|
||||
* ``_current_ids`` raises 404 when the session has no tenant.
|
||||
* The pydantic request models accept / reject bodies as expected.
|
||||
|
||||
We explicitly avoid "happy-path" integration tests through the full
|
||||
decorator stack — those belong in e2e tests where a real Dify session is
|
||||
available — to keep this suite fast and resilient to ancillary auth wiring
|
||||
changes.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import inspect
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
from flask import Flask
|
||||
from pydantic import ValidationError
|
||||
from werkzeug.exceptions import Forbidden, NotFound
|
||||
|
||||
from controllers.console.workspace import rbac as rbac_mod
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def app():
|
||||
flask_app = Flask(__name__)
|
||||
flask_app.config["TESTING"] = True
|
||||
return flask_app
|
||||
|
||||
|
||||
def _enabled(enabled: bool):
|
||||
return patch("controllers.console.workspace.rbac.dify_config.ENTERPRISE_ENABLED", enabled)
|
||||
|
||||
|
||||
class TestCurrentIds:
|
||||
def test_rejects_missing_tenant(self):
|
||||
with patch("controllers.console.workspace.rbac.current_account_with_tenant") as mock_user:
|
||||
mock_user.return_value = (SimpleNamespace(id="acct-1"), None)
|
||||
with pytest.raises(NotFound):
|
||||
rbac_mod._current_ids()
|
||||
|
||||
def test_returns_tuple(self):
|
||||
with patch("controllers.console.workspace.rbac.current_account_with_tenant") as mock_user:
|
||||
mock_user.return_value = (SimpleNamespace(id="acct-1"), "tenant-1")
|
||||
assert rbac_mod._current_ids() == ("tenant-1", "acct-1")
|
||||
|
||||
|
||||
class TestAccessMatrixAccountNames:
|
||||
def test_hydrates_missing_account_names(self):
|
||||
items = [
|
||||
rbac_mod.svc.AccessMatrixItem(
|
||||
accounts=[
|
||||
{"account_id": "acct-1", "account_name": "Alice", "binding_id": "binding-1"},
|
||||
{"account_id": "acct-2", "account_name": "", "binding_id": "binding-2"},
|
||||
]
|
||||
)
|
||||
]
|
||||
|
||||
with patch(
|
||||
"controllers.console.workspace.rbac._account_names_by_ids",
|
||||
return_value={"acct-2": {"name": "Bob", "avatar": "ava"}},
|
||||
) as mock_names:
|
||||
rbac_mod._hydrate_access_matrix_account_names(items)
|
||||
|
||||
mock_names.assert_called_once_with(["acct-2"])
|
||||
assert items[0].accounts[0].account_id == "acct-1"
|
||||
assert items[0].accounts[0].account_name == "Alice"
|
||||
assert items[0].accounts[1].account_id == "acct-2"
|
||||
assert items[0].accounts[1].account_name == "Bob"
|
||||
assert items[0].accounts[1].avatar == "ava"
|
||||
|
||||
def test_hydrates_resource_user_account_names(self):
|
||||
items = [
|
||||
rbac_mod.svc.ResourceUserAccessPolicies(
|
||||
account={"account_id": "acct-1", "account_name": ""},
|
||||
roles=[],
|
||||
access_policies=[],
|
||||
)
|
||||
]
|
||||
|
||||
with patch(
|
||||
"controllers.console.workspace.rbac._account_names_by_ids",
|
||||
return_value={"acct-1": {"name": "Alice", "avatar": ""}},
|
||||
):
|
||||
rbac_mod._hydrate_resource_user_account_names(items)
|
||||
|
||||
assert items[0].account.account_name == "Alice"
|
||||
|
||||
|
||||
class TestPydanticModels:
|
||||
"""The internal `_…Request` models are the contract between the browser
|
||||
and the controllers. We only check non-obvious branches (enum parsing,
|
||||
missing required fields) — trivial `str` fields are not worth asserting.
|
||||
"""
|
||||
|
||||
def test_role_upsert_requires_name(self):
|
||||
with pytest.raises(ValidationError):
|
||||
rbac_mod._RoleUpsertRequest.model_validate({})
|
||||
|
||||
def test_role_upsert_to_mutation_preserves_fields(self):
|
||||
payload = rbac_mod._RoleUpsertRequest.model_validate(
|
||||
{
|
||||
"name": "Owner",
|
||||
"description": "full access",
|
||||
"permission_keys": ["workspace.member.manage"],
|
||||
}
|
||||
)
|
||||
mutation = payload.to_mutation()
|
||||
assert mutation.description == "full access"
|
||||
assert mutation.permission_keys == ["workspace.member.manage"]
|
||||
|
||||
def test_access_policy_create_parses_resource_type_enum(self):
|
||||
parsed = rbac_mod._AccessPolicyCreateRequest.model_validate(
|
||||
{
|
||||
"name": "Full access",
|
||||
"resource_type": "app",
|
||||
"description": "",
|
||||
"permission_keys": [],
|
||||
}
|
||||
)
|
||||
assert parsed.resource_type is rbac_mod.svc.RBACResourceType.APP
|
||||
|
||||
def test_access_policy_create_rejects_unknown_resource_type(self):
|
||||
with pytest.raises(ValidationError):
|
||||
rbac_mod._AccessPolicyCreateRequest.model_validate({"name": "bad", "resource_type": "unknown"})
|
||||
|
||||
def test_resource_access_scope_requires_scope(self):
|
||||
with pytest.raises(ValidationError):
|
||||
rbac_mod._ResourceAccessScopeRequest.model_validate({})
|
||||
|
||||
def test_resource_access_scope_defaults_empty_account_ids(self):
|
||||
parsed = rbac_mod._ResourceAccessScopeRequest.model_validate({"scope": "specific"})
|
||||
assert parsed.scope is rbac_mod._AccessScope.SPECIFIC
|
||||
|
||||
def test_resource_access_scope_coerce_null_account_ids(self):
|
||||
rbac_mod._ResourceAccessScopeRequest.model_validate({"scope": "all"})
|
||||
|
||||
def test_resource_access_scope_rejects_unknown_scope(self):
|
||||
with pytest.raises(ValidationError):
|
||||
rbac_mod._ResourceAccessScopeRequest.model_validate({"scope": "team"})
|
||||
|
||||
def test_replace_bindings_keeps_role_binding_contract(self):
|
||||
parsed = rbac_mod._ReplaceBindingsRequest.model_validate({"role_ids": None})
|
||||
assert parsed.role_ids == []
|
||||
|
||||
def test_replace_member_roles_coerce_null_list(self):
|
||||
parsed = rbac_mod._ReplaceMemberRolesRequest.model_validate({"role_ids": None})
|
||||
assert parsed.role_ids == []
|
||||
|
||||
def test_pagination_query_accepts_page_and_limit_aliases(self):
|
||||
parsed = rbac_mod._PaginationQuery.model_validate({"page": 3, "limit": 25, "reverse": True})
|
||||
assert parsed.page_number == 3
|
||||
assert parsed.results_per_page == 25
|
||||
assert parsed.reverse is True
|
||||
|
||||
def test_pagination_query_accepts_legacy_inner_names(self):
|
||||
parsed = rbac_mod._PaginationQuery.model_validate({"page_number": 4, "results_per_page": 30, "reverse": False})
|
||||
assert parsed.page_number == 4
|
||||
assert parsed.results_per_page == 30
|
||||
assert parsed.reverse is False
|
||||
|
||||
|
||||
class TestPaginationMapping:
|
||||
def test_roles_get_returns_legacy_compatible_roles_when_rbac_disabled(self, app):
|
||||
with (
|
||||
app.test_request_context("/workspaces/current/rbac/roles?page=1&limit=2&include_owner=1"),
|
||||
patch("controllers.console.workspace.rbac.dify_config.RBAC_ENABLED", False),
|
||||
patch("controllers.console.workspace.rbac._current_ids", return_value=("tenant-1", "acct-1")),
|
||||
patch("controllers.console.workspace.rbac.svc.RBACService.Roles.list") as mock_list,
|
||||
):
|
||||
response = inspect.unwrap(rbac_mod.RBACRolesApi.get)(rbac_mod.RBACRolesApi())
|
||||
|
||||
assert response["data"] == [
|
||||
{
|
||||
"id": "owner",
|
||||
"tenant_id": "",
|
||||
"type": "workspace",
|
||||
"category": "global_system_default",
|
||||
"name": "owner",
|
||||
"description": "",
|
||||
"is_builtin": True,
|
||||
"permission_keys": list(rbac_mod._LEGACY_ROLE_PERMISSION_KEYS["owner"]),
|
||||
"role_tag": "owner",
|
||||
},
|
||||
{
|
||||
"id": "admin",
|
||||
"tenant_id": "",
|
||||
"type": "workspace",
|
||||
"category": "global_system_default",
|
||||
"name": "admin",
|
||||
"description": "",
|
||||
"is_builtin": True,
|
||||
"permission_keys": list(rbac_mod._LEGACY_ROLE_PERMISSION_KEYS["admin"]),
|
||||
"role_tag": "",
|
||||
},
|
||||
]
|
||||
assert response["pagination"] == {
|
||||
"total_count": 5,
|
||||
"per_page": 2,
|
||||
"current_page": 1,
|
||||
"total_pages": 3,
|
||||
}
|
||||
mock_list.assert_not_called()
|
||||
|
||||
def test_roles_get_filters_out_owner_when_include_owner_is_zero(self, app):
|
||||
with (
|
||||
app.test_request_context("/workspaces/current/rbac/roles?include_owner=0"),
|
||||
patch("controllers.console.workspace.rbac.dify_config.RBAC_ENABLED", False),
|
||||
patch("controllers.console.workspace.rbac._current_ids", return_value=("tenant-1", "acct-1")),
|
||||
patch("controllers.console.workspace.rbac.svc.RBACService.Roles.list"),
|
||||
):
|
||||
response = inspect.unwrap(rbac_mod.RBACRolesApi.get)(rbac_mod.RBACRolesApi())
|
||||
|
||||
names = [r["name"] for r in response["data"]]
|
||||
assert "owner" not in names
|
||||
|
||||
def test_roles_get_keeps_owner_when_include_owner_is_one(self, app):
|
||||
with (
|
||||
app.test_request_context("/workspaces/current/rbac/roles?include_owner=1"),
|
||||
patch("controllers.console.workspace.rbac.dify_config.RBAC_ENABLED", False),
|
||||
patch("controllers.console.workspace.rbac._current_ids", return_value=("tenant-1", "acct-1")),
|
||||
patch("controllers.console.workspace.rbac.svc.RBACService.Roles.list"),
|
||||
):
|
||||
response = inspect.unwrap(rbac_mod.RBACRolesApi.get)(rbac_mod.RBACRolesApi())
|
||||
|
||||
names = [r["name"] for r in response["data"]]
|
||||
assert "owner" in names
|
||||
|
||||
def test_roles_get_filters_out_owner_by_default(self, app):
|
||||
with (
|
||||
app.test_request_context("/workspaces/current/rbac/roles"),
|
||||
patch("controllers.console.workspace.rbac.dify_config.RBAC_ENABLED", False),
|
||||
patch("controllers.console.workspace.rbac._current_ids", return_value=("tenant-1", "acct-1")),
|
||||
patch("controllers.console.workspace.rbac.svc.RBACService.Roles.list"),
|
||||
):
|
||||
response = inspect.unwrap(rbac_mod.RBACRolesApi.get)(rbac_mod.RBACRolesApi())
|
||||
|
||||
names = [r["name"] for r in response["data"]]
|
||||
assert "owner" not in names
|
||||
|
||||
def test_roles_get_forwards_outer_pagination_params(self, app):
|
||||
with (
|
||||
app.test_request_context("/workspaces/current/rbac/roles?page=2&limit=50&reverse=true&include_owner=1"),
|
||||
patch("controllers.console.workspace.rbac.dify_config.RBAC_ENABLED", True),
|
||||
patch("controllers.console.workspace.rbac._current_ids", return_value=("tenant-1", "acct-1")),
|
||||
patch("controllers.console.workspace.rbac.svc.RBACService.Roles.list") as mock_list,
|
||||
patch("controllers.console.workspace.rbac._dump", return_value={}),
|
||||
):
|
||||
inspect.unwrap(rbac_mod.RBACRolesApi.get)(rbac_mod.RBACRolesApi())
|
||||
|
||||
_, kwargs = mock_list.call_args
|
||||
options = kwargs["options"]
|
||||
assert options.page_number == 2
|
||||
assert options.results_per_page == 50
|
||||
assert options.reverse is True
|
||||
|
||||
|
||||
class TestResourceAccessScopeBindings:
|
||||
def test_app_user_access_policy_assignment_forwards_ids(self, app):
|
||||
with (
|
||||
app.test_request_context(
|
||||
"/workspaces/current/rbac/apps/app-1/users/acct-target/access-policies",
|
||||
method="PUT",
|
||||
json={"access_policy_ids": ["policy-1", "policy-2"]},
|
||||
),
|
||||
patch("controllers.console.workspace.rbac._current_ids", return_value=("tenant-1", "acct-actor")),
|
||||
patch(
|
||||
"controllers.console.workspace.rbac.svc.RBACService.AppAccess.replace_user_access_policies"
|
||||
) as mock_replace,
|
||||
patch("controllers.console.workspace.rbac._dump", return_value={}),
|
||||
):
|
||||
inspect.unwrap(rbac_mod.RBACAppUserAccessPolicyAssignmentApi.put)(
|
||||
rbac_mod.RBACAppUserAccessPolicyAssignmentApi(),
|
||||
"app-1",
|
||||
"acct-target",
|
||||
)
|
||||
|
||||
tenant_id, actor_id, app_id, target_id, payload = mock_replace.call_args.args
|
||||
assert (tenant_id, actor_id, app_id, target_id) == (
|
||||
"tenant-1",
|
||||
"acct-actor",
|
||||
"app-1",
|
||||
"acct-target",
|
||||
)
|
||||
assert payload.access_policy_ids == ["policy-1", "policy-2"]
|
||||
|
||||
def test_app_member_bindings_delete_forwards_account_ids(self, app):
|
||||
with (
|
||||
app.test_request_context(
|
||||
"/workspaces/current/rbac/apps/app-1/access-policies/policy-1/member-bindings",
|
||||
method="DELETE",
|
||||
json={"account_ids": ["acct-2", "acct-3"]},
|
||||
),
|
||||
patch("controllers.console.workspace.rbac._current_ids", return_value=("tenant-1", "acct-actor")),
|
||||
patch("controllers.console.workspace.rbac.svc.RBACService.AppAccess.delete_member_bindings") as mock_delete,
|
||||
):
|
||||
response = inspect.unwrap(rbac_mod.RBACAppMemberBindingsApi.delete)(
|
||||
rbac_mod.RBACAppMemberBindingsApi(),
|
||||
"app-1",
|
||||
"policy-1",
|
||||
)
|
||||
|
||||
assert response == {"result": "success"}
|
||||
tenant_id, actor_id, app_id, policy_id, payload = mock_delete.call_args.args
|
||||
assert (tenant_id, actor_id, app_id, policy_id) == ("tenant-1", "acct-actor", "app-1", "policy-1")
|
||||
assert payload.account_ids == ["acct-2", "acct-3"]
|
||||
|
||||
def test_dataset_member_bindings_delete_forwards_account_ids(self, app):
|
||||
with (
|
||||
app.test_request_context(
|
||||
"/workspaces/current/rbac/datasets/dataset-1/access-policies/policy-1/member-bindings",
|
||||
method="DELETE",
|
||||
json={"account_ids": ["acct-2"]},
|
||||
),
|
||||
patch("controllers.console.workspace.rbac._current_ids", return_value=("tenant-1", "acct-actor")),
|
||||
patch(
|
||||
"controllers.console.workspace.rbac.svc.RBACService.DatasetAccess.delete_member_bindings"
|
||||
) as mock_delete,
|
||||
):
|
||||
response = inspect.unwrap(rbac_mod.RBACDatasetMemberBindingsApi.delete)(
|
||||
rbac_mod.RBACDatasetMemberBindingsApi(),
|
||||
"dataset-1",
|
||||
"policy-1",
|
||||
)
|
||||
|
||||
assert response == {"result": "success"}
|
||||
tenant_id, actor_id, dataset_id, policy_id, payload = mock_delete.call_args.args
|
||||
assert (tenant_id, actor_id, dataset_id, policy_id) == ("tenant-1", "acct-actor", "dataset-1", "policy-1")
|
||||
assert payload.account_ids == ["acct-2"]
|
||||
|
||||
|
||||
class TestPaginationForwarding:
|
||||
def test_role_members_get_forwards_outer_pagination_params(self, app):
|
||||
with (
|
||||
app.test_request_context("/workspaces/current/rbac/roles/role-1/members?page=2&limit=50&reverse=true"),
|
||||
patch("controllers.console.workspace.rbac._current_ids", return_value=("tenant-1", "acct-1")),
|
||||
patch("controllers.console.workspace.rbac.svc.RBACService.Roles.members") as mock_members,
|
||||
patch("controllers.console.workspace.rbac._dump", return_value={}),
|
||||
):
|
||||
inspect.unwrap(rbac_mod.RBACRoleMembersApi.get)(rbac_mod.RBACRoleMembersApi(), "role-1")
|
||||
|
||||
_, _, role_id = mock_members.call_args.args
|
||||
_, kwargs = mock_members.call_args
|
||||
assert role_id == "role-1"
|
||||
options = kwargs["options"]
|
||||
assert options.page_number == 2
|
||||
assert options.results_per_page == 50
|
||||
assert options.reverse is True
|
||||
|
||||
def test_access_policies_get_forwards_outer_pagination_params(self, app):
|
||||
with (
|
||||
app.test_request_context(
|
||||
"/workspaces/current/rbac/access-policies?resource_type=app&page=3&limit=25&reverse=false"
|
||||
),
|
||||
_enabled(True),
|
||||
patch("controllers.console.workspace.rbac._current_ids", return_value=("tenant-1", "acct-1")),
|
||||
patch("controllers.console.workspace.rbac.svc.RBACService.AccessPolicies.list") as mock_list,
|
||||
patch("controllers.console.workspace.rbac._dump", return_value={}),
|
||||
):
|
||||
inspect.unwrap(rbac_mod.RBACAccessPoliciesApi.get)(rbac_mod.RBACAccessPoliciesApi())
|
||||
|
||||
_, kwargs = mock_list.call_args
|
||||
assert kwargs["resource_type"] == "app"
|
||||
options = kwargs["options"]
|
||||
assert options.page_number == 3
|
||||
assert options.results_per_page == 25
|
||||
assert options.reverse is False
|
||||
|
||||
def test_workspace_app_matrix_forwards_outer_pagination_params(self, app):
|
||||
with (
|
||||
app.test_request_context("/workspaces/current/rbac/workspace/apps/access-policy?page=4&limit=10"),
|
||||
_enabled(True),
|
||||
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"
|
||||
),
|
||||
_enabled(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
|
||||
|
||||
|
||||
class TestAccessPolicyBindingLockUnlock:
|
||||
def test_lock_forwards_binding_id(self, app):
|
||||
with (
|
||||
app.test_request_context("/workspaces/current/rbac/access-policy-bindings/binding-1/lock", method="PUT"),
|
||||
_enabled(True),
|
||||
patch("controllers.console.workspace.rbac._current_ids", return_value=("tenant-1", "acct-1")),
|
||||
patch("controllers.console.workspace.rbac.svc.RBACService.AccessPolicyBindings.lock") as mock_lock,
|
||||
patch("controllers.console.workspace.rbac._dump", return_value={}),
|
||||
):
|
||||
inspect.unwrap(rbac_mod.RBACAccessPolicyBindingLockApi.put)(
|
||||
rbac_mod.RBACAccessPolicyBindingLockApi(), "binding-1"
|
||||
)
|
||||
|
||||
mock_lock.assert_called_once_with("tenant-1", "acct-1", "binding-1")
|
||||
|
||||
def test_unlock_forwards_binding_id(self, app):
|
||||
with (
|
||||
app.test_request_context("/workspaces/current/rbac/access-policy-bindings/binding-1/unlock", method="PUT"),
|
||||
_enabled(True),
|
||||
patch("controllers.console.workspace.rbac._current_ids", return_value=("tenant-1", "acct-1")),
|
||||
patch("controllers.console.workspace.rbac.svc.RBACService.AccessPolicyBindings.unlock") as mock_unlock,
|
||||
patch("controllers.console.workspace.rbac._dump", return_value={}),
|
||||
):
|
||||
inspect.unwrap(rbac_mod.RBACAccessPolicyBindingUnlockApi.put)(
|
||||
rbac_mod.RBACAccessPolicyBindingUnlockApi(), "binding-1"
|
||||
)
|
||||
|
||||
mock_unlock.assert_called_once_with("tenant-1", "acct-1", "binding-1")
|
||||
|
||||
|
||||
class TestRoleCopy:
|
||||
def test_role_copy_forwards_path_id(self, app):
|
||||
with (
|
||||
app.test_request_context("/workspaces/current/rbac/roles/role-1/copy", method="POST", json={}),
|
||||
_enabled(True),
|
||||
patch("controllers.console.workspace.rbac._current_ids", return_value=("tenant-1", "acct-1")),
|
||||
patch("controllers.console.workspace.rbac.svc.RBACService.Roles.copy") as mock_copy,
|
||||
patch("controllers.console.workspace.rbac._dump", return_value={}),
|
||||
):
|
||||
inspect.unwrap(rbac_mod.RBACRoleCopyApi.post)(rbac_mod.RBACRoleCopyApi(), "role-1")
|
||||
|
||||
mock_copy.assert_called_once_with("tenant-1", "acct-1", "role-1", copy_member=True)
|
||||
|
||||
|
||||
class TestWorkspaceRbacGuards:
|
||||
def test_role_create_requires_workspace_role_manage(self, app):
|
||||
with (
|
||||
app.test_request_context(
|
||||
"/workspaces/current/rbac/roles",
|
||||
method="POST",
|
||||
json={"name": "test_role", "permission_keys": []},
|
||||
),
|
||||
patch("libs.login.dify_config.LOGIN_DISABLED", True),
|
||||
patch("controllers.console.wraps.dify_config.RBAC_ENABLED", True),
|
||||
patch(
|
||||
"controllers.common.wraps.current_account_with_tenant",
|
||||
return_value=(SimpleNamespace(id="acct-1"), "tenant-1"),
|
||||
),
|
||||
patch("controllers.common.wraps.RBACService.CheckAccess.check", return_value=False),
|
||||
patch("controllers.console.workspace.rbac.svc.RBACService.Roles.create") as mock_create,
|
||||
):
|
||||
with pytest.raises(Forbidden):
|
||||
rbac_mod.RBACRolesApi().post()
|
||||
|
||||
mock_create.assert_not_called()
|
||||
|
||||
def test_access_policy_create_requires_workspace_role_manage(self, app):
|
||||
with (
|
||||
app.test_request_context(
|
||||
"/workspaces/current/rbac/access-policies",
|
||||
method="POST",
|
||||
json={"name": "full_access", "resource_type": "app", "permission_keys": []},
|
||||
),
|
||||
patch("libs.login.dify_config.LOGIN_DISABLED", True),
|
||||
patch("controllers.console.wraps.dify_config.RBAC_ENABLED", True),
|
||||
patch(
|
||||
"controllers.common.wraps.current_account_with_tenant",
|
||||
return_value=(SimpleNamespace(id="acct-1"), "tenant-1"),
|
||||
),
|
||||
patch("controllers.common.wraps.RBACService.CheckAccess.check", return_value=False),
|
||||
patch("controllers.console.workspace.rbac.svc.RBACService.AccessPolicies.create") as mock_create,
|
||||
):
|
||||
with pytest.raises(Forbidden):
|
||||
rbac_mod.RBACAccessPoliciesApi().post()
|
||||
|
||||
mock_create.assert_not_called()
|
||||
|
||||
|
||||
class TestDumpHelper:
|
||||
def test_dump_returns_plain_dict(self):
|
||||
role = rbac_mod.svc.RBACRole(id="role-1", type="workspace", name="Owner")
|
||||
dumped = rbac_mod._dump(role)
|
||||
assert isinstance(dumped, dict)
|
||||
assert "role_id" not in dumped
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user