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
@@ -0,0 +1,278 @@
|
||||
"""Unit tests for the agent-flavoured RBAC inner-API client.
|
||||
|
||||
`RBACService.AgentAccess`, the agent methods on `RBACService.WorkspaceAccess` and
|
||||
`RBACService.Catalog.agent` are all built from one generic resource-access client.
|
||||
These tests monkeypatch `_inner_call` and assert the HTTP method, the exact endpoint,
|
||||
the query/body keys and the returned model for every operation.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable, Iterator
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from services.enterprise import rbac_service as svc
|
||||
|
||||
MODULE = "services.enterprise.rbac_service"
|
||||
|
||||
TENANT = "tenant-1"
|
||||
ACTOR = "acct-1"
|
||||
AGENT = "agent-1"
|
||||
POLICY = "policy-1"
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def inner_call() -> Iterator[MagicMock]:
|
||||
with patch(f"{MODULE}._inner_call") as call:
|
||||
call.return_value = {"automatic_include_workspace_members": True}
|
||||
yield call
|
||||
|
||||
|
||||
def _last(call: MagicMock) -> SimpleNamespace:
|
||||
call.assert_called_once()
|
||||
args, kwargs = call.call_args
|
||||
return SimpleNamespace(
|
||||
method=args[0],
|
||||
endpoint=args[1],
|
||||
tenant_id=kwargs.get("tenant_id"),
|
||||
account_id=kwargs.get("account_id"),
|
||||
json=kwargs.get("json"),
|
||||
params=kwargs.get("params"),
|
||||
)
|
||||
|
||||
|
||||
_AGENT_CASES: list[tuple[str, str, str, Callable[[], object]]] = [
|
||||
(
|
||||
"whitelist_resources",
|
||||
"GET",
|
||||
"/rbac/agents/whitelist/resources",
|
||||
lambda: svc.RBACService.AgentAccess.whitelist_resources(TENANT, ACTOR),
|
||||
),
|
||||
(
|
||||
"user_access_policies",
|
||||
"GET",
|
||||
"/rbac/agents/user-access-policies",
|
||||
lambda: svc.RBACService.AgentAccess.user_access_policies(TENANT, ACTOR, agent_id=AGENT),
|
||||
),
|
||||
(
|
||||
"replace_user_access_policies",
|
||||
"PUT",
|
||||
"/rbac/agents/user-access-policies",
|
||||
lambda: svc.RBACService.AgentAccess.replace_user_access_policies(
|
||||
TENANT,
|
||||
ACTOR,
|
||||
agent_id=AGENT,
|
||||
target_account_id="member-1",
|
||||
payload=svc.ReplaceUserAccessPolicies(access_policy_ids=[POLICY], account_ids=["member-1"]),
|
||||
),
|
||||
),
|
||||
(
|
||||
"whitelist",
|
||||
"GET",
|
||||
"/rbac/agents/whitelist",
|
||||
lambda: svc.RBACService.AgentAccess.whitelist(TENANT, ACTOR, agent_id=AGENT),
|
||||
),
|
||||
(
|
||||
"whitelist_config",
|
||||
"GET",
|
||||
"/rbac/agents/whitelist",
|
||||
lambda: svc.RBACService.AgentAccess.whitelist_config(TENANT, ACTOR, agent_id=AGENT),
|
||||
),
|
||||
(
|
||||
"legacy_whitelist_config",
|
||||
"GET",
|
||||
"/rbac/agents/whitelist",
|
||||
lambda: svc.RBACService.AgentAccess.legacy_whitelist_config(TENANT, ACTOR, agent_id=AGENT),
|
||||
),
|
||||
(
|
||||
"replace_whitelist",
|
||||
"PUT",
|
||||
"/rbac/agents/whitelist",
|
||||
lambda: svc.RBACService.AgentAccess.replace_whitelist(
|
||||
TENANT,
|
||||
ACTOR,
|
||||
agent_id=AGENT,
|
||||
payload=svc.ReplaceMemberBindings(automatic_include_workspace_members=True),
|
||||
),
|
||||
),
|
||||
(
|
||||
"append_whitelist_members_batch",
|
||||
"POST",
|
||||
"/rbac/agents/whitelist/members/batch",
|
||||
lambda: svc.RBACService.AgentAccess.append_whitelist_members_batch(
|
||||
tenant_id=TENANT,
|
||||
account_id=ACTOR,
|
||||
data=[svc.AppendAgentWhitelistMembersBatchItem(agent_id=AGENT, account_ids=["member-1"], policy_id=POLICY)],
|
||||
),
|
||||
),
|
||||
(
|
||||
"matrix",
|
||||
"GET",
|
||||
"/rbac/agents/access-policy",
|
||||
lambda: svc.RBACService.AgentAccess.matrix(TENANT, ACTOR, agent_id=AGENT),
|
||||
),
|
||||
(
|
||||
"list_role_bindings",
|
||||
"GET",
|
||||
"/rbac/agents/access-policy/role-bindings",
|
||||
lambda: svc.RBACService.AgentAccess.list_role_bindings(TENANT, ACTOR, agent_id=AGENT, policy_id=POLICY),
|
||||
),
|
||||
(
|
||||
"replace_role_bindings",
|
||||
"PUT",
|
||||
"/rbac/agents/access-policy/role-bindings",
|
||||
lambda: svc.RBACService.AgentAccess.replace_role_bindings(
|
||||
TENANT, ACTOR, agent_id=AGENT, policy_id=POLICY, payload=svc.ReplaceRoleBindings(role_ids=["role-1"])
|
||||
),
|
||||
),
|
||||
(
|
||||
"list_member_bindings",
|
||||
"GET",
|
||||
"/rbac/agents/access-policy/member-bindings",
|
||||
lambda: svc.RBACService.AgentAccess.list_member_bindings(TENANT, ACTOR, agent_id=AGENT, policy_id=POLICY),
|
||||
),
|
||||
(
|
||||
"delete_member_bindings",
|
||||
"DELETE",
|
||||
"/rbac/agents/access-policy/member-bindings",
|
||||
lambda: svc.RBACService.AgentAccess.delete_member_bindings(
|
||||
TENANT, ACTOR, agent_id=AGENT, policy_id=POLICY, payload=svc.DeleteMemberBindings(account_ids=["member-1"])
|
||||
),
|
||||
),
|
||||
(
|
||||
"replace_bindings",
|
||||
"PUT",
|
||||
"/rbac/agents/access-policy/bindings",
|
||||
lambda: svc.RBACService.AgentAccess.replace_bindings(
|
||||
TENANT,
|
||||
ACTOR,
|
||||
agent_id=AGENT,
|
||||
policy_id=POLICY,
|
||||
payload=svc.ReplaceBindings(role_ids=["role-1"], account_ids=["member-1"]),
|
||||
),
|
||||
),
|
||||
(
|
||||
"workspace.agent_matrix",
|
||||
"GET",
|
||||
"/rbac/workspace/agents/access-policy",
|
||||
lambda: svc.RBACService.WorkspaceAccess.agent_matrix(TENANT, ACTOR),
|
||||
),
|
||||
(
|
||||
"workspace.list_agent_role_bindings",
|
||||
"GET",
|
||||
"/rbac/workspace/agents/access-policy/role-bindings",
|
||||
lambda: svc.RBACService.WorkspaceAccess.list_agent_role_bindings(TENANT, ACTOR, POLICY),
|
||||
),
|
||||
(
|
||||
"workspace.replace_agent_role_bindings",
|
||||
"PUT",
|
||||
"/rbac/workspace/agents/access-policy/role-bindings",
|
||||
lambda: svc.RBACService.WorkspaceAccess.replace_agent_role_bindings(
|
||||
TENANT, ACTOR, POLICY, svc.ReplaceRoleBindings(role_ids=["role-1"])
|
||||
),
|
||||
),
|
||||
(
|
||||
"workspace.list_agent_member_bindings",
|
||||
"GET",
|
||||
"/rbac/workspace/agents/access-policy/member-bindings",
|
||||
lambda: svc.RBACService.WorkspaceAccess.list_agent_member_bindings(TENANT, ACTOR, POLICY),
|
||||
),
|
||||
(
|
||||
"workspace.replace_agent_bindings",
|
||||
"PUT",
|
||||
"/rbac/workspace/agents/access-policy/bindings",
|
||||
lambda: svc.RBACService.WorkspaceAccess.replace_agent_bindings(
|
||||
TENANT, ACTOR, POLICY, svc.ReplaceBindings(role_ids=["role-1"], account_ids=["member-1"])
|
||||
),
|
||||
),
|
||||
(
|
||||
"catalog.agent",
|
||||
"GET",
|
||||
"/rbac/role-permissions/catalog/agent",
|
||||
lambda: svc.RBACService.Catalog.agent(TENANT, account_id=ACTOR),
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("method", "endpoint", "invoke"),
|
||||
[(case[1], case[2], case[3]) for case in _AGENT_CASES],
|
||||
ids=[case[0] for case in _AGENT_CASES],
|
||||
)
|
||||
def test_agent_operations_hit_the_agent_route(
|
||||
inner_call: MagicMock, method: str, endpoint: str, invoke: Callable[[], object]
|
||||
) -> None:
|
||||
invoke()
|
||||
|
||||
call = _last(inner_call)
|
||||
assert call.method == method
|
||||
assert call.endpoint == endpoint
|
||||
assert "/apps/" not in call.endpoint
|
||||
assert "/datasets/" not in call.endpoint
|
||||
assert call.tenant_id == TENANT
|
||||
assert call.account_id == ACTOR
|
||||
params = call.params or {}
|
||||
assert "app_id" not in params
|
||||
assert "dataset_id" not in params
|
||||
if AGENT in params.values():
|
||||
assert params.get("agent_id") == AGENT
|
||||
|
||||
|
||||
_PARITY_CASES: list[tuple[str, Callable[[], object], Callable[[], object]]] = [
|
||||
(
|
||||
"whitelist_resources",
|
||||
lambda: svc.RBACService.AppAccess.whitelist_resources(TENANT, ACTOR),
|
||||
lambda: svc.RBACService.AgentAccess.whitelist_resources(TENANT, ACTOR),
|
||||
),
|
||||
(
|
||||
"whitelist",
|
||||
lambda: svc.RBACService.AppAccess.whitelist(TENANT, ACTOR, "res-1"),
|
||||
lambda: svc.RBACService.AgentAccess.whitelist(TENANT, ACTOR, agent_id="res-1"),
|
||||
),
|
||||
(
|
||||
"user_access_policies",
|
||||
lambda: svc.RBACService.AppAccess.user_access_policies(TENANT, ACTOR, "res-1"),
|
||||
lambda: svc.RBACService.AgentAccess.user_access_policies(TENANT, ACTOR, agent_id="res-1"),
|
||||
),
|
||||
(
|
||||
"matrix",
|
||||
lambda: svc.RBACService.AppAccess.matrix(TENANT, ACTOR, "res-1"),
|
||||
lambda: svc.RBACService.AgentAccess.matrix(TENANT, ACTOR, agent_id="res-1"),
|
||||
),
|
||||
(
|
||||
"list_role_bindings",
|
||||
lambda: svc.RBACService.AppAccess.list_role_bindings(TENANT, ACTOR, "res-1", POLICY),
|
||||
lambda: svc.RBACService.AgentAccess.list_role_bindings(TENANT, ACTOR, agent_id="res-1", policy_id=POLICY),
|
||||
),
|
||||
(
|
||||
"list_member_bindings",
|
||||
lambda: svc.RBACService.AppAccess.list_member_bindings(TENANT, ACTOR, "res-1", POLICY),
|
||||
lambda: svc.RBACService.AgentAccess.list_member_bindings(TENANT, ACTOR, agent_id="res-1", policy_id=POLICY),
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("app_call", "agent_call"),
|
||||
[(case[1], case[2]) for case in _PARITY_CASES],
|
||||
ids=[case[0] for case in _PARITY_CASES],
|
||||
)
|
||||
def test_same_path_and_param_shape_modulo_segment(
|
||||
inner_call: MagicMock,
|
||||
app_call: Callable[[], object],
|
||||
agent_call: Callable[[], object],
|
||||
) -> None:
|
||||
app_call()
|
||||
app = _last(inner_call)
|
||||
inner_call.reset_mock()
|
||||
agent_call()
|
||||
agent = _last(inner_call)
|
||||
|
||||
assert app.endpoint.replace("/apps/", "/agents/") == agent.endpoint
|
||||
assert app.method == agent.method
|
||||
|
||||
app_params = {("agent_id" if k == "app_id" else k): v for k, v in (app.params or {}).items()}
|
||||
assert app_params == (agent.params or {})
|
||||
@@ -748,37 +748,42 @@ class TestMyPermissions:
|
||||
assert out.workspace.permission_keys == ["workspace.member.manage"]
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("role", "workspace_keys", "app_keys", "dataset_keys"),
|
||||
("role", "workspace_keys", "app_keys", "dataset_keys", "agent_keys"),
|
||||
[
|
||||
(
|
||||
"owner",
|
||||
svc._LEGACY_WORKSPACE_OWNER_KEYS,
|
||||
svc._LEGACY_APP_OWNER_KEYS,
|
||||
svc._LEGACY_DATASET_OWNER_KEYS,
|
||||
svc._LEGACY_AGENT_FULL_ACCESS_KEYS,
|
||||
),
|
||||
(
|
||||
"admin",
|
||||
svc._LEGACY_WORKSPACE_ADMIN_KEYS,
|
||||
svc._LEGACY_APP_ADMIN_KEYS,
|
||||
svc._LEGACY_DATASET_ADMIN_KEYS,
|
||||
svc._LEGACY_AGENT_FULL_ACCESS_KEYS,
|
||||
),
|
||||
(
|
||||
"editor",
|
||||
svc._LEGACY_WORKSPACE_EDITOR_KEYS,
|
||||
svc._LEGACY_APP_EDITOR_KEYS,
|
||||
svc._LEGACY_DATASET_EDITOR_KEYS,
|
||||
svc._LEGACY_AGENT_FULL_ACCESS_KEYS,
|
||||
),
|
||||
(
|
||||
"normal",
|
||||
svc._LEGACY_WORKSPACE_NORMAL_KEYS,
|
||||
svc._LEGACY_APP_NORMAL_KEYS,
|
||||
[],
|
||||
svc._LEGACY_AGENT_PREVIEW_KEYS,
|
||||
),
|
||||
(
|
||||
"dataset_operator",
|
||||
svc._LEGACY_WORKSPACE_DATASET_OPERATOR_KEYS,
|
||||
svc._LEGACY_APP_DATASET_OPERATOR_KEYS,
|
||||
svc._LEGACY_DATASET_DATASET_OPERATOR_KEYS,
|
||||
svc._LEGACY_AGENT_PREVIEW_KEYS,
|
||||
),
|
||||
],
|
||||
)
|
||||
@@ -789,6 +794,7 @@ class TestMyPermissions:
|
||||
workspace_keys: list[str],
|
||||
app_keys: list[str],
|
||||
dataset_keys: list[str],
|
||||
agent_keys: list[str],
|
||||
sqlite_session: Session,
|
||||
config_overrides,
|
||||
):
|
||||
@@ -804,8 +810,10 @@ class TestMyPermissions:
|
||||
assert len(out.workspace.permission_keys) == len(set(out.workspace.permission_keys))
|
||||
assert out.app.default_permission_keys == app_keys
|
||||
assert out.dataset.default_permission_keys == dataset_keys
|
||||
assert out.agent.default_permission_keys == agent_keys
|
||||
assert out.app.overrides == []
|
||||
assert out.dataset.overrides == []
|
||||
assert out.agent.overrides == []
|
||||
if role == "owner":
|
||||
assert "snippets.management" in out.workspace.permission_keys
|
||||
assert "app.acl.preview" in out.workspace.permission_keys
|
||||
@@ -859,6 +867,7 @@ class TestMyPermissions:
|
||||
assert out.workspace.permission_keys == []
|
||||
assert out.app.default_permission_keys == []
|
||||
assert out.dataset.default_permission_keys == []
|
||||
assert out.agent.default_permission_keys == []
|
||||
|
||||
def test_get_with_single_resource_filters(self, mock_send: MagicMock, sqlite_session: Session):
|
||||
mock_send.return_value = {
|
||||
@@ -878,6 +887,27 @@ class TestMyPermissions:
|
||||
assert call.params == {"app_id": "app-1"}
|
||||
assert out.app.overrides[0].resource_id == "app-1"
|
||||
|
||||
def test_get_forwards_agent_id_and_parses_agent_snapshot(self, mock_send: MagicMock, sqlite_session: Session):
|
||||
mock_send.return_value = {
|
||||
"workspace": {"permission_keys": []},
|
||||
"app": {"default_permission_keys": [], "overrides": []},
|
||||
"dataset": {"default_permission_keys": [], "overrides": []},
|
||||
"agent": {
|
||||
"default_permission_keys": ["agent.acl.preview"],
|
||||
"overrides": [{"resource_id": "agent-1", "permission_keys": ["agent.acl.edit"]}],
|
||||
},
|
||||
}
|
||||
|
||||
out = svc.RBACService.MyPermissions.get("tenant-1", "acct-1", agent_id="agent-1", session=sqlite_session)
|
||||
|
||||
call = _call_args(mock_send)
|
||||
assert call.method == "GET"
|
||||
assert call.endpoint == "/rbac/my-permissions"
|
||||
assert call.params == {"agent_id": "agent-1"}
|
||||
assert out.agent.default_permission_keys == ["agent.acl.preview"]
|
||||
assert out.agent.overrides[0].resource_id == "agent-1"
|
||||
assert out.agent.overrides[0].permission_keys == ["agent.acl.edit"]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("sqlite_session", [(TenantAccountJoin,)], indirect=True)
|
||||
class TestMemberRoles:
|
||||
@@ -924,6 +954,7 @@ class TestMemberRoles:
|
||||
*svc._LEGACY_WORKSPACE_EDITOR_KEYS,
|
||||
*svc._LEGACY_APP_EDITOR_KEYS,
|
||||
*svc._LEGACY_DATASET_EDITOR_KEYS,
|
||||
*svc._LEGACY_AGENT_FULL_ACCESS_KEYS,
|
||||
]
|
||||
)
|
||||
)
|
||||
@@ -1126,9 +1157,8 @@ class TestListOption:
|
||||
}
|
||||
|
||||
|
||||
class TestLegacyAgentManageKey:
|
||||
def test_legacy_agent_manage_key_membership(self):
|
||||
# Preserve Agent access for every legacy role while external RBAC is disabled.
|
||||
class TestLegacyAgentKeys:
|
||||
def test_legacy_workspace_keys_no_longer_carry_agent_manage(self):
|
||||
for keys in (
|
||||
svc._LEGACY_WORKSPACE_OWNER_KEYS,
|
||||
svc._LEGACY_WORKSPACE_ADMIN_KEYS,
|
||||
@@ -1136,4 +1166,5 @@ class TestLegacyAgentManageKey:
|
||||
svc._LEGACY_WORKSPACE_NORMAL_KEYS,
|
||||
svc._LEGACY_WORKSPACE_DATASET_OPERATOR_KEYS,
|
||||
):
|
||||
assert "agent.manage" in keys
|
||||
assert "agent.manage" not in keys
|
||||
assert {"agent.acl.preview", "agent.acl.access_point_view"} <= set(keys)
|
||||
@@ -542,38 +542,25 @@ def test_export_dsl_loads_model_config_and_annotation_reply_with_request_session
|
||||
load_annotation_reply_config.assert_called_once_with(service_session, app_id)
|
||||
|
||||
|
||||
def test_ensure_agent_manage_permission_noops_when_rbac_disabled(
|
||||
monkeypatch: pytest.MonkeyPatch, config_overrides: Callable[..., None]
|
||||
) -> None:
|
||||
config_overrides(RBAC_ENABLED=False)
|
||||
check = Mock()
|
||||
monkeypatch.setattr("services.app_dsl_service.RBACService.CheckAccess.check", check)
|
||||
|
||||
AppDslService._ensure_agent_manage_permission(_account())
|
||||
|
||||
check.assert_not_called()
|
||||
|
||||
|
||||
def test_ensure_agent_manage_permission_allows_agent_manager(
|
||||
monkeypatch: pytest.MonkeyPatch, config_overrides: Callable[..., None]
|
||||
def test_agent_import_of_new_agent_checks_function_scope(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
unbound_session: Session,
|
||||
config_overrides: Callable[..., None],
|
||||
) -> None:
|
||||
config_overrides(RBAC_ENABLED=True)
|
||||
check = Mock(return_value=True)
|
||||
monkeypatch.setattr("services.app_dsl_service.RBACService.CheckAccess.check", check)
|
||||
account = _account()
|
||||
|
||||
AppDslService._ensure_agent_manage_permission(_account())
|
||||
AppDslService(session=unbound_session)._ensure_agent_import_permission(account, app=None)
|
||||
|
||||
check.assert_called_once_with("tenant-1", "account-1", scene=RBACPermission.AGENT_MANAGE)
|
||||
|
||||
|
||||
def test_ensure_agent_manage_permission_rejects_without_agent_manage(
|
||||
monkeypatch: pytest.MonkeyPatch, config_overrides: Callable[..., None]
|
||||
) -> None:
|
||||
config_overrides(RBAC_ENABLED=True)
|
||||
monkeypatch.setattr("services.app_dsl_service.RBACService.CheckAccess.check", Mock(return_value=False))
|
||||
|
||||
with pytest.raises(NoPermissionError):
|
||||
AppDslService._ensure_agent_manage_permission(_account())
|
||||
check.assert_called_once_with(
|
||||
account.current_tenant_id,
|
||||
account.id,
|
||||
scene=RBACPermission.AGENT_IMPORT_EXPORT_DSL,
|
||||
resource_type=None,
|
||||
resource_id=None,
|
||||
)
|
||||
|
||||
|
||||
def test_create_or_update_app_gates_agent_mode_before_creation(
|
||||
|
||||
@@ -27,6 +27,7 @@ from models.model import App, AppMode, AppModelConfig, IconType
|
||||
from models.workflow import Workflow, WorkflowType
|
||||
from services.agent.errors import AgentAccessNotReadyError, AgentNameConflictError
|
||||
from services.app_service import AppListParams, AppService, CreateAppParams
|
||||
from services.enterprise import rbac_service as enterprise_rbac_service
|
||||
|
||||
|
||||
def _persist_account(session: Session) -> Account:
|
||||
@@ -222,6 +223,63 @@ class TestCreateAppTransactionBoundary:
|
||||
)
|
||||
|
||||
|
||||
class TestCreateAppRBACAccessInitialization:
|
||||
"""`create_app` bootstraps RBAC access per app mode: agents get the agent flavour."""
|
||||
|
||||
@staticmethod
|
||||
def _create(session: Session, account: Account, mode: AppMode) -> App:
|
||||
with (
|
||||
patch("services.app_service.app_was_created.send"),
|
||||
patch(
|
||||
"services.app_service.SystemFeatureService.is_webapp_auth_enabled",
|
||||
return_value=False,
|
||||
),
|
||||
):
|
||||
return AppService().create_app(
|
||||
account.current_tenant_id,
|
||||
CreateAppParams(name=f"RBAC {mode.value}", mode=mode.value),
|
||||
account,
|
||||
session=session,
|
||||
)
|
||||
|
||||
def test_agent_app_initializes_agent_access_and_skips_the_app_creator_sync(
|
||||
self, sqlite_session: Session, config_overrides: Callable[..., None]
|
||||
) -> None:
|
||||
config_overrides(DEPLOYMENT_EDITION=DeploymentEdition.COMMUNITY, RBAC_ENABLED=True)
|
||||
account = _persist_account(sqlite_session)
|
||||
|
||||
with (
|
||||
patch(
|
||||
"services.rbac_agent_access_service.enterprise_rbac_service.RBACService.AgentAccess.replace_whitelist"
|
||||
) as replace_whitelist,
|
||||
patch("services.rbac_agent_access_service.initialize_created_app_rbac_access_task.delay") as seed_task,
|
||||
patch(
|
||||
"services.rbac_agent_access_service.enterprise_rbac_service.RBACService.AccessPolicies"
|
||||
".sync_creator_access_policy_member_bindings"
|
||||
) as creator_sync,
|
||||
patch(
|
||||
"services.app_service.enterprise_rbac_service.try_sync_creator_access_policy_member_bindings"
|
||||
) as app_creator_sync,
|
||||
):
|
||||
app = self._create(sqlite_session, account, AppMode.AGENT)
|
||||
|
||||
agent = sqlite_session.scalars(select(Agent).where(Agent.app_id == app.id)).one()
|
||||
replace_whitelist.assert_called_once_with(
|
||||
account.current_tenant_id,
|
||||
account.id,
|
||||
agent.id,
|
||||
enterprise_rbac_service.ReplaceMemberBindings(automatic_include_workspace_members=True),
|
||||
)
|
||||
seed_task.assert_called_once_with(account.current_tenant_id, account.id, agent_id=agent.id)
|
||||
creator_sync.assert_called_once_with(
|
||||
account.current_tenant_id,
|
||||
account.id,
|
||||
resource_type=enterprise_rbac_service.RBACResourceType.AGENT,
|
||||
resource_id=agent.id,
|
||||
)
|
||||
app_creator_sync.assert_not_called()
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"update_status",
|
||||
[AppService.update_app_site_status, AppService.update_app_api_status],
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
"""Unit tests for the RBAC bootstrap applied to a newly created agent."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable, Iterator
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from services import rbac_agent_access_service
|
||||
from services.enterprise import rbac_service as enterprise_rbac_service
|
||||
|
||||
MODULE = "services.rbac_agent_access_service"
|
||||
|
||||
TENANT = "tenant-1"
|
||||
AGENT = "agent-1"
|
||||
CREATOR = "account-1"
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def rbac_calls(config_overrides: Callable[..., None]) -> Iterator[MagicMock]:
|
||||
config_overrides(RBAC_ENABLED=True)
|
||||
recorder = MagicMock()
|
||||
with (
|
||||
patch(f"{MODULE}.enterprise_rbac_service.RBACService.AgentAccess.replace_whitelist") as replace_whitelist,
|
||||
patch(f"{MODULE}.initialize_created_app_rbac_access_task.delay") as seed_task,
|
||||
patch(
|
||||
f"{MODULE}.enterprise_rbac_service.RBACService.AccessPolicies.sync_creator_access_policy_member_bindings"
|
||||
) as creator_sync,
|
||||
):
|
||||
recorder.attach_mock(replace_whitelist, "replace_whitelist")
|
||||
recorder.attach_mock(seed_task, "seed_task")
|
||||
recorder.attach_mock(creator_sync, "creator_sync")
|
||||
yield recorder
|
||||
|
||||
|
||||
def test_initialize_agent_rbac_access_seeds_scope_members_and_creator_policy(
|
||||
rbac_calls: MagicMock,
|
||||
) -> None:
|
||||
rbac_agent_access_service.initialize_agent_rbac_access(tenant_id=TENANT, agent_id=AGENT, creator_account_id=CREATOR)
|
||||
|
||||
rbac_calls.replace_whitelist.assert_called_once_with(
|
||||
TENANT,
|
||||
CREATOR,
|
||||
AGENT,
|
||||
enterprise_rbac_service.ReplaceMemberBindings(automatic_include_workspace_members=True),
|
||||
)
|
||||
rbac_calls.seed_task.assert_called_once_with(TENANT, CREATOR, agent_id=AGENT)
|
||||
rbac_calls.creator_sync.assert_called_once_with(
|
||||
TENANT,
|
||||
CREATOR,
|
||||
resource_type=enterprise_rbac_service.RBACResourceType.AGENT,
|
||||
resource_id=AGENT,
|
||||
)
|
||||
|
||||
assert [call[0] for call in rbac_calls.mock_calls] == ["seed_task", "creator_sync", "replace_whitelist"]
|
||||
Reference in new issue
Block a user