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

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
This commit is contained in:
Xiyuan Chenandautofix-ci[bot] authored and GitHub committed 2026-09-07 04:39:43 +00:00
1 parent 60e983c493
commit 86134ea99e
152 files changed
+5360 -3405

No files matched your search

+4 -1
View File
@@ -44,6 +44,7 @@ from services.agent.workspace_service import AgentWorkspaceNotFoundError, AgentW
from services.app_service import AppService, CreateAppParams
from services.enterprise.enterprise_service import EnterpriseService
from services.entities.agent_entities import RosterAgentCreatePayload, RosterAgentUpdatePayload
from services.rbac_agent_access_service import initialize_agent_rbac_access
from services.system_feature_service import SystemFeatureService
from tasks.collect_agent_resources_task import enqueue_agent_resource_collection
@@ -314,11 +315,13 @@ class AgentRosterService:
source=source,
)
self._session.commit()
return agent
except IntegrityError as exc:
self._session.rollback()
raise AgentNameConflictError() from exc
initialize_agent_rbac_access(tenant_id=tenant_id, agent_id=agent.id, creator_account_id=account_id)
return agent
def _create_roster_agent_in_transaction(
self,
*,
+14 -6
View File
@@ -44,6 +44,7 @@ from graphon.nodes.question_classifier.entities import QuestionClassifierNodeDat
from graphon.nodes.tool.entities import ToolNodeData
from libs.datetime_utils import naive_utc_now
from models import Account, App, AppMode
from models.agent import AgentScope
from models.model import AppModelConfig, AppModelConfigDict, IconType, load_annotation_reply_config
from models.workflow import Workflow
from services.agent.dsl_service import AgentDslService, AgentPackage
@@ -456,20 +457,27 @@ class AppDslService:
raise NoPermissionError("You do not have permission to overwrite this app")
return app
@staticmethod
def _ensure_agent_manage_permission(account: Account) -> None:
"""Importing an Agent DSL creates a roster Agent, which requires ``agent.manage``."""
def _ensure_agent_import_permission(self, account: Account, *, app: App | None) -> None:
if not dify_config.RBAC_ENABLED:
return
if account.current_tenant_id is None:
raise ValueError("Current tenant is not set")
binding = (
app.agent_app_binding_with_session(session=self._session, include_archived=True)
if app is not None
else None
)
if binding is not None and binding.scope == AgentScope.WORKFLOW_ONLY:
raise NoPermissionError("Agent DSL import permission is required to import an Agent App")
allowed = RBACService.CheckAccess.check(
account.current_tenant_id,
account.id,
scene=RBACPermission.AGENT_MANAGE,
scene=RBACPermission.AGENT_IMPORT_EXPORT_DSL,
resource_type=RBACResourceScope.AGENT if binding is not None else None,
resource_id=str(binding.id) if binding is not None else None,
)
if not allowed:
raise NoPermissionError("Agent management permission is required to import an Agent App")
raise NoPermissionError("Agent DSL import permission is required to import an Agent App")
def _create_or_update_app(
self,
@@ -492,7 +500,7 @@ class AppDslService:
raise ValueError("loss app mode")
app_mode = AppMode(app_mode)
if app_mode == AppMode.AGENT:
self._ensure_agent_manage_permission(account)
self._ensure_agent_import_permission(account, app=app)
# Set icon type
icon_type_value = icon_type or app_data.get("icon_type")
+44 -7
View File
@@ -1,6 +1,6 @@
import json
import logging
from collections.abc import Sequence
from collections.abc import Callable, Sequence
from dataclasses import dataclass
from datetime import datetime
from typing import Any, Literal, NotRequired, TypedDict, cast, override
@@ -50,6 +50,7 @@ from services.billing_service import BillingService
from services.enterprise import rbac_service as enterprise_rbac_service
from services.enterprise.enterprise_service import EnterpriseService
from services.openapi.visibility import apply_openapi_gate, is_openapi_visible
from services.rbac_agent_access_service import initialize_agent_rbac_access
from services.system_feature_service import SystemFeatureService
from services.tag_service import TagService
from tasks.collect_agent_resources_task import enqueue_agent_resource_collection
@@ -74,6 +75,38 @@ RECENT_APP_MODES: tuple[RecentAppMode, ...] = (
)
@dataclass(frozen=True)
class _CreatedApp:
tenant_id: str
creator_account_id: str
app_id: str
backing_agent_id: str | None
def _initialize_created_app_access(created: _CreatedApp) -> None:
enterprise_rbac_service.try_sync_creator_access_policy_member_bindings(
created.tenant_id,
created.creator_account_id,
enterprise_rbac_service.RBACResourceType.APP,
created.app_id,
)
def _initialize_created_agent_access(created: _CreatedApp) -> None:
if created.backing_agent_id is None:
raise ValueError(f"agent app {created.app_id} was created without a backing agent")
initialize_agent_rbac_access(
tenant_id=created.tenant_id,
agent_id=created.backing_agent_id,
creator_account_id=created.creator_account_id,
)
_CREATED_APP_ACCESS_INITIALIZERS: dict[AppMode, Callable[[_CreatedApp], None]] = {
AppMode.AGENT: _initialize_created_agent_access,
}
class AppListBaseParams(BaseModel):
page: int = Field(default=1, ge=1)
limit: int = Field(default=20, ge=1, le=100)
@@ -670,12 +703,13 @@ class AppService:
# Created in the same transaction so the App and its backing Agent persist
# atomically; the Agent Soul (model/prompt/tools) is configured afterward
# in the Composer.
backing_agent: Agent | None = None
if app_mode == AppMode.AGENT:
from services.agent.roster_service import AgentRosterService
icon_type = AgentIconType(params.icon_type) if params.icon_type else None
try:
AgentRosterService(session).create_backing_agent_for_app(
backing_agent = AgentRosterService(session).create_backing_agent_for_app(
tenant_id=tenant_id,
account_id=account.id,
app_id=app.id,
@@ -696,11 +730,14 @@ class AppService:
session.commit()
app_was_created.send(app, account=account, session=session)
session.commit()
enterprise_rbac_service.try_sync_creator_access_policy_member_bindings(
tenant_id,
account.id,
enterprise_rbac_service.RBACResourceType.APP,
app.id,
initialize_access = _CREATED_APP_ACCESS_INITIALIZERS.get(app_mode, _initialize_created_app_access)
initialize_access(
_CreatedApp(
tenant_id=tenant_id,
creator_account_id=account.id,
app_id=app.id,
backing_agent_id=backing_agent.id if backing_agent else None,
)
)
if SystemFeatureService.is_webapp_auth_enabled():
File diff suppressed because it is too large. Load diff
+35
View File
@@ -0,0 +1,35 @@
import logging
from configs import dify_config
from services.enterprise import rbac_service as enterprise_rbac_service
from tasks.initialize_created_app_rbac_access_task import initialize_created_app_rbac_access_task
logger = logging.getLogger(__name__)
def initialize_agent_rbac_access(*, tenant_id: str, agent_id: str, creator_account_id: str) -> None:
if not dify_config.RBAC_ENABLED:
return
try:
initialize_created_app_rbac_access_task.delay(tenant_id, creator_account_id, agent_id=agent_id)
enterprise_rbac_service.RBACService.AccessPolicies.sync_creator_access_policy_member_bindings(
tenant_id,
creator_account_id,
resource_type=enterprise_rbac_service.RBACResourceType.AGENT,
resource_id=agent_id,
)
enterprise_rbac_service.RBACService.AgentAccess.replace_whitelist(
tenant_id,
creator_account_id,
agent_id,
enterprise_rbac_service.ReplaceMemberBindings(automatic_include_workspace_members=True),
)
except Exception:
logger.warning(
"Failed to initialize RBAC access for agent: tenant_id=%s agent_id=%s account_id=%s",
tenant_id,
agent_id,
creator_account_id,
exc_info=True,
)
+40
View File
@@ -0,0 +1,40 @@
from __future__ import annotations
from typing import TYPE_CHECKING
from sqlalchemy import select
from extensions.ext_database import db
from models.dataset import Dataset
from models.model import App
if TYPE_CHECKING:
from models.agent import Agent
class RBACResourceService:
@staticmethod
def get_app_agent_binding(tenant_id: str, app_id: str) -> Agent | None:
app_model = db.session.scalar(select(App).where(App.id == app_id, App.tenant_id == tenant_id))
if app_model is None:
return None
return app_model.agent_app_binding_with_session(session=db.session, include_archived=True)
@staticmethod
def get_app_maintainer(tenant_id: str, app_id: str) -> str | None:
return db.session.scalar(
select(App.maintainer).where(App.id == app_id, App.tenant_id == tenant_id, App.status == "normal")
)
@staticmethod
def get_dataset_maintainer(tenant_id: str, dataset_id: str) -> str | None:
return db.session.scalar(
select(Dataset.maintainer).where(Dataset.id == dataset_id, Dataset.tenant_id == tenant_id)
)
@staticmethod
def get_dataset_id_by_pipeline(tenant_id: str, pipeline_id: str) -> str | None:
dataset_id = db.session.scalar(
select(Dataset.id).where(Dataset.pipeline_id == pipeline_id, Dataset.tenant_id == tenant_id)
)
return None if dataset_id is None else str(dataset_id)