mirror of
https://github.com/langgenius/dify.git
synced 2026-10-04 13:47:55 +08:00
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:
1 parent
60e983c493
commit
86134ea99e
152 files changed
+5360
-3405
No files matched your search
@@ -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,
|
||||
*,
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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
@@ -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,
|
||||
)
|
||||
@@ -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)
|
||||
Reference in new issue
Block a user