feat: add tenant-scoped rule and permission management

This commit is contained in:
wren
2026-05-21 22:03:08 +08:00
parent a2c2bf1969
commit 1f1bccf3b3
193 changed files with 64463 additions and 1771 deletions
+1
View File
@@ -0,0 +1 @@
"""Release acceptance tests."""
+479
View File
@@ -0,0 +1,479 @@
from __future__ import annotations
import os
import time
from dataclasses import dataclass
from typing import Any, Callable
import pytest
from .helpers import ReleaseApiClient, ReleaseTestConfig
@dataclass(frozen=True)
class TenantSeed:
tenant_code: str
tenant_name: str
@dataclass(frozen=True)
class SeededUser:
user_id: int
sub: str
username: str
nickname: str
role_key: str
tenant_code: str
token: str
@dataclass(frozen=True)
class DocumentTypeSeed:
type_id: int
type_code: str
type_name: str
@dataclass(frozen=True)
class ReleaseDocumentSeed:
document_id: int
file_id: int | None
type_id: int
type_code: str
tenant_code: str
tenant_name: str
file_name: str
@dataclass(frozen=True)
class ReleaseDatasetSeed:
dataset_id: int
dataset_name: str
tenant_code: str
tenant_name: str
app_id: int | None
app_name: str | None
def _env(name: str, default: str) -> str:
return str(os.getenv(name, default)).strip()
@pytest.fixture(scope="session")
def release_config() -> ReleaseTestConfig:
return ReleaseTestConfig(
base_url=_env("LEAUDIT_TEST_BASE_URL", "http://127.0.0.1:8096").rstrip("/"),
admin_username=_env("LEAUDIT_TEST_ADMIN_USERNAME", "000"),
admin_password=_env("LEAUDIT_TEST_ADMIN_PASSWORD", "admin06111"),
timeout_seconds=float(_env("LEAUDIT_TEST_TIMEOUT_SECONDS", "30")),
tenant_a_code=_env("LEAUDIT_TEST_TENANT_A_CODE", "PTA01"),
tenant_a_name=_env("LEAUDIT_TEST_TENANT_A_NAME", "Pytest租户A"),
tenant_b_code=_env("LEAUDIT_TEST_TENANT_B_CODE", "PTB01"),
tenant_b_name=_env("LEAUDIT_TEST_TENANT_B_NAME", "Pytest租户B"),
module_name=_env("LEAUDIT_TEST_MODULE_NAME", f"Pytest发布验收入口模块-{time.time_ns()}"),
module_path=_env("LEAUDIT_TEST_MODULE_PATH", "/documents"),
)
@pytest.fixture(scope="session")
def anonymous_client(release_config: ReleaseTestConfig) -> ReleaseApiClient:
client = ReleaseApiClient(release_config)
yield client
client.close()
@pytest.fixture(scope="session")
def admin_auth(release_config: ReleaseTestConfig, anonymous_client: ReleaseApiClient) -> dict[str, Any]:
return anonymous_client.login_password(release_config.admin_username, release_config.admin_password)
@pytest.fixture(scope="session")
def admin_client(release_config: ReleaseTestConfig, admin_auth: dict[str, Any]) -> ReleaseApiClient:
client = ReleaseApiClient(release_config, token=str(admin_auth["access_token"]))
yield client
client.close()
@pytest.fixture(scope="session")
def role_map(admin_client: ReleaseApiClient) -> dict[str, int]:
response = admin_client.get("/api/v3/rbac/roles?page=1&page_size=200")
items = ReleaseApiClient.json_data(response)["items"]
mapping = {str(item["role_key"]): int(item["id"]) for item in items}
for role_key in ("super_admin", "provincial_admin", "admin", "common"):
assert role_key in mapping, f"缺少角色种子: {role_key}"
return mapping
def _ensure_tenant(admin_client: ReleaseApiClient, tenant_code: str, tenant_name: str) -> TenantSeed:
detail_response = admin_client.get(f"/api/v3/tenants/{tenant_code}", expected_status=None)
body = detail_response.json()
desired_payload = {
"tenant_name": tenant_name,
"tenant_short_name": tenant_name,
"tenant_type": "CUSTOM",
"parent_tenant_code": None,
"display_order": 500,
"is_public": False,
"can_host_entry_module": True,
"can_host_documents": True,
"can_host_rag": True,
"can_host_templates": True,
"feature_keys": ["home.entry_module", "documents.upload", "rag.dataset"],
"alias_values": [tenant_name],
"ext": {"created_by": "pytest-release"},
}
if detail_response.status_code == 404:
create_payload = {
"tenant_code": tenant_code,
"is_enabled": True,
**desired_payload,
}
admin_client.post("/api/v3/tenants", json=create_payload, expected_status=200)
else:
assert detail_response.status_code == 200, body
admin_client.put(f"/api/v3/tenants/{tenant_code}", json=desired_payload, expected_status=200)
admin_client.patch(f"/api/v3/tenants/{tenant_code}/status", json={"is_enabled": True}, expected_status=200)
return TenantSeed(tenant_code=tenant_code, tenant_name=tenant_name)
@pytest.fixture(scope="session")
def tenant_a(admin_client: ReleaseApiClient, release_config: ReleaseTestConfig) -> TenantSeed:
return _ensure_tenant(admin_client, release_config.tenant_a_code, release_config.tenant_a_name)
@pytest.fixture(scope="session")
def tenant_b(admin_client: ReleaseApiClient, release_config: ReleaseTestConfig) -> TenantSeed:
return _ensure_tenant(admin_client, release_config.tenant_b_code, release_config.tenant_b_name)
def _ensure_user(
*,
anonymous_client: ReleaseApiClient,
admin_client: ReleaseApiClient,
role_map: dict[str, int],
sub: str,
username: str,
nickname: str,
role_key: str,
tenant: TenantSeed,
) -> SeededUser:
login_data = anonymous_client.login_oauth(
sub=sub,
username=username,
nickname=nickname,
ou_id=f"pytest-{tenant.tenant_code.lower()}",
ou_name=f"{tenant.tenant_name}测试组织",
area=tenant.tenant_name,
)
user_info = login_data["user_info"]
user_id = int(user_info["user_id"])
admin_client.post(
f"/api/v3/rbac/users/{user_id}/roles",
json={"role_ids": [role_map[role_key]]},
expected_status=200,
)
admin_client.put(
f"/api/v3/rbac/users/{user_id}/tenant",
json={"tenant_code": tenant.tenant_code},
expected_status=200,
)
refreshed = anonymous_client.login_oauth(
sub=sub,
username=username,
nickname=nickname,
ou_id=f"pytest-{tenant.tenant_code.lower()}",
ou_name=f"{tenant.tenant_name}测试组织",
area=tenant.tenant_name,
)
refreshed_user = refreshed["user_info"]
assert str(refreshed_user.get("tenant_code") or "") == tenant.tenant_code
return SeededUser(
user_id=user_id,
sub=sub,
username=username,
nickname=nickname,
role_key=role_key,
tenant_code=tenant.tenant_code,
token=str(refreshed["access_token"]),
)
@pytest.fixture(scope="session")
def tenant_admin_user(
anonymous_client: ReleaseApiClient,
admin_client: ReleaseApiClient,
role_map: dict[str, int],
tenant_a: TenantSeed,
) -> SeededUser:
return _ensure_user(
anonymous_client=anonymous_client,
admin_client=admin_client,
role_map=role_map,
sub="pytest-admin-pta01",
username="pytest_admin_pta01",
nickname="Pytest租户A管理员",
role_key="admin",
tenant=tenant_a,
)
@pytest.fixture(scope="session")
def tenant_admin_user_b(
anonymous_client: ReleaseApiClient,
admin_client: ReleaseApiClient,
role_map: dict[str, int],
tenant_b: TenantSeed,
) -> SeededUser:
return _ensure_user(
anonymous_client=anonymous_client,
admin_client=admin_client,
role_map=role_map,
sub="pytest-admin-ptb01",
username="pytest_admin_ptb01",
nickname="Pytest租户B管理员",
role_key="admin",
tenant=tenant_b,
)
@pytest.fixture(scope="session")
def tenant_common_user_a(
anonymous_client: ReleaseApiClient,
admin_client: ReleaseApiClient,
role_map: dict[str, int],
tenant_a: TenantSeed,
) -> SeededUser:
return _ensure_user(
anonymous_client=anonymous_client,
admin_client=admin_client,
role_map=role_map,
sub="pytest-common-pta01",
username="pytest_common_pta01",
nickname="Pytest租户A普通用户",
role_key="common",
tenant=tenant_a,
)
@pytest.fixture(scope="session")
def tenant_common_user_b(
anonymous_client: ReleaseApiClient,
admin_client: ReleaseApiClient,
role_map: dict[str, int],
tenant_b: TenantSeed,
) -> SeededUser:
return _ensure_user(
anonymous_client=anonymous_client,
admin_client=admin_client,
role_map=role_map,
sub="pytest-common-ptb01",
username="pytest_common_ptb01",
nickname="Pytest租户B普通用户",
role_key="common",
tenant=tenant_b,
)
@pytest.fixture
def tenant_admin_api(release_config: ReleaseTestConfig, tenant_admin_user: SeededUser) -> ReleaseApiClient:
client = ReleaseApiClient(release_config, token=tenant_admin_user.token)
yield client
client.close()
@pytest.fixture
def tenant_admin_api_b(release_config: ReleaseTestConfig, tenant_admin_user_b: SeededUser) -> ReleaseApiClient:
client = ReleaseApiClient(release_config, token=tenant_admin_user_b.token)
yield client
client.close()
@pytest.fixture
def common_api_a(release_config: ReleaseTestConfig, tenant_common_user_a: SeededUser) -> ReleaseApiClient:
client = ReleaseApiClient(release_config, token=tenant_common_user_a.token)
yield client
client.close()
@pytest.fixture
def common_api_b(release_config: ReleaseTestConfig, tenant_common_user_b: SeededUser) -> ReleaseApiClient:
client = ReleaseApiClient(release_config, token=tenant_common_user_b.token)
yield client
client.close()
def _find_module_by_name(admin_client: ReleaseApiClient, module_name: str) -> dict[str, Any] | None:
response = admin_client.get(f"/api/v3/entry-modules?page=1&page_size=200&name={module_name}".replace("page_size", "page_size"), expected_status=None)
items = ReleaseApiClient.json_data(response)["items"]
for item in items:
if str(item.get("name") or "") == module_name:
return item
return None
@pytest.fixture
def release_entry_module(
admin_client: ReleaseApiClient,
release_config: ReleaseTestConfig,
tenant_b: TenantSeed,
) -> dict[str, Any]:
module_name = release_config.module_name
existing = _find_module_by_name(admin_client, module_name)
payload = {
"name": module_name,
"description": "pytest release acceptance only",
"path": release_config.module_path,
"route_path": release_config.module_path,
"tenants": [
{
"tenant_code": tenant_b.tenant_code,
"tenant_name": tenant_b.tenant_name,
"enabled": True,
"sort_order": 1,
}
],
}
created = False
if existing:
module_id = int(existing["id"])
response = admin_client.put(f"/api/v3/entry-modules/{module_id}", json=payload, expected_status=200)
else:
response = admin_client.post("/api/v3/entry-modules", json=payload, expected_status=None)
if response.status_code == 200:
created = True
else:
retried = False
for attempt in range(1, 4):
module_name = f"{release_config.module_name}-{attempt}"
payload["name"] = module_name
existing = _find_module_by_name(admin_client, module_name)
if existing:
module_id = int(existing["id"])
response = admin_client.put(f"/api/v3/entry-modules/{module_id}", json=payload, expected_status=200)
retried = True
break
response = admin_client.post("/api/v3/entry-modules", json=payload, expected_status=None)
if response.status_code == 200:
created = True
retried = True
break
assert retried, response.text
module = ReleaseApiClient.json_data(response)
try:
yield module
finally:
if created:
admin_client.delete(f"/api/v3/entry-modules/{int(module['id'])}", expected_status=200)
@pytest.fixture(scope="session")
def release_document_type(admin_client: ReleaseApiClient) -> DocumentTypeSeed:
response = admin_client.get("/api/document-types")
items = ReleaseApiClient.json_data(response)
assert isinstance(items, list), items
for item in items:
type_id = int(item.get("id") or 0)
type_code = str(item.get("code") or "").strip()
type_name = str(item.get("name") or "").strip()
if type_id > 0 and type_code:
return DocumentTypeSeed(type_id=type_id, type_code=type_code, type_name=type_name or type_code)
pytest.skip("当前环境没有可用于发布验收的文档类型")
@pytest.fixture
def make_release_document(
admin_client: ReleaseApiClient,
release_document_type: DocumentTypeSeed,
) -> Callable[..., ReleaseDocumentSeed]:
created_document_ids: list[int] = []
def _make_document(
*,
client: ReleaseApiClient,
tenant: TenantSeed,
file_name: str,
content: bytes | None = None,
content_type: str = "text/plain",
) -> ReleaseDocumentSeed:
payload_bytes = content or f"pytest release document for {tenant.tenant_code} / {file_name}\n".encode("utf-8")
response = client.post(
"/api/upload",
data={
"typeId": str(release_document_type.type_id),
"typeCode": release_document_type.type_code,
"tenant_code": tenant.tenant_code,
"region": tenant.tenant_name,
"fileRole": "primary",
"autoRun": "false",
"speed": "normal",
},
files={
"file": (file_name, payload_bytes, content_type),
},
expected_status=200,
)
data = ReleaseApiClient.json_data(response)
document_id = int(data["documentId"])
created_document_ids.append(document_id)
return ReleaseDocumentSeed(
document_id=document_id,
file_id=int(data["fileId"]) if data.get("fileId") is not None else None,
type_id=release_document_type.type_id,
type_code=release_document_type.type_code,
tenant_code=tenant.tenant_code,
tenant_name=tenant.tenant_name,
file_name=file_name,
)
try:
yield _make_document
finally:
for document_id in reversed(created_document_ids):
admin_client.delete(f"/api/documents/{document_id}", expected_status=None)
@pytest.fixture
def make_release_dataset(
admin_client: ReleaseApiClient,
) -> Callable[..., ReleaseDatasetSeed]:
created_dataset_ids: list[int] = []
def _make_dataset(
*,
tenant: TenantSeed,
dataset_name: str,
is_public: bool = False,
is_default: bool = False,
) -> ReleaseDatasetSeed:
response = admin_client.post(
"/api/v3/rag/datasets/admin",
json={
"tenant_code": tenant.tenant_code,
"tenant_name": tenant.tenant_name,
"area": tenant.tenant_name,
"name": dataset_name,
"description": f"{dataset_name} - pytest release",
"is_public": is_public,
"is_default": is_default,
"status": 1,
"sort_order": 0,
},
expected_status=200,
)
data = ReleaseApiClient.json_data(response)
dataset_id = int(data["id"])
created_dataset_ids.append(dataset_id)
return ReleaseDatasetSeed(
dataset_id=dataset_id,
dataset_name=str(data.get("name") or dataset_name),
tenant_code=str(data.get("tenantCode") or tenant.tenant_code),
tenant_name=str(data.get("tenantName") or tenant.tenant_name),
app_id=int(data["appId"]) if data.get("appId") is not None else None,
app_name=str(data.get("appName") or "") or None,
)
try:
yield _make_dataset
finally:
for dataset_id in reversed(created_dataset_ids):
admin_client.delete(f"/api/v3/rag/datasets/admin/{dataset_id}", expected_status=None)
+134
View File
@@ -0,0 +1,134 @@
from __future__ import annotations
from dataclasses import dataclass
from typing import Any
import httpx
@dataclass(frozen=True)
class ReleaseTestConfig:
base_url: str
admin_username: str
admin_password: str
timeout_seconds: float
tenant_a_code: str
tenant_a_name: str
tenant_b_code: str
tenant_b_name: str
module_name: str
module_path: str
class ReleaseApiClient:
def __init__(self, config: ReleaseTestConfig, token: str | None = None) -> None:
self.config = config
self.token = token
self._client = httpx.Client(base_url=config.base_url, timeout=config.timeout_seconds)
def close(self) -> None:
self._client.close()
def with_token(self, token: str) -> "ReleaseApiClient":
return ReleaseApiClient(self.config, token=token)
def request(self, method: str, path: str, *, expected_status: int | None = 200, **kwargs: Any) -> httpx.Response:
headers = dict(kwargs.pop("headers", {}) or {})
if self.token:
headers["Authorization"] = f"Bearer {self.token}"
response = self._client.request(method, path, headers=headers, **kwargs)
if expected_status is not None:
assert response.status_code == expected_status, self._format_error(response)
return response
def get(self, path: str, **kwargs: Any) -> httpx.Response:
return self.request("GET", path, **kwargs)
def post(self, path: str, **kwargs: Any) -> httpx.Response:
return self.request("POST", path, **kwargs)
def put(self, path: str, **kwargs: Any) -> httpx.Response:
return self.request("PUT", path, **kwargs)
def patch(self, path: str, **kwargs: Any) -> httpx.Response:
return self.request("PATCH", path, **kwargs)
def delete(self, path: str, **kwargs: Any) -> httpx.Response:
return self.request("DELETE", path, **kwargs)
def login_password(self, username: str, password: str) -> dict[str, Any]:
response = self.post(
"/api/auth/login",
json={"username": username, "password": password},
expected_status=200,
)
payload = response.json()
assert payload.get("success") is True, payload
data = payload.get("data") or {}
assert data.get("access_token"), payload
return data
def login_oauth(
self,
*,
sub: str,
username: str,
nickname: str,
ou_id: str,
ou_name: str,
area: str | None = None,
) -> dict[str, Any]:
response = self.post(
"/api/auth/login",
json={
"userInfo": {
"sub": sub,
"username": username,
"nickname": nickname,
"email": f"{username}@pytest.local",
"phone_number": "13800000000",
"ou_id": ou_id,
"ou_name": ou_name,
"is_leader": False,
},
"area": area,
"expiresIn": 3600,
},
expected_status=200,
)
payload = response.json()
assert payload.get("success") is True, payload
data = payload.get("data") or {}
assert data.get("access_token"), payload
return data
@staticmethod
def json_data(response: httpx.Response) -> Any:
payload = response.json()
if isinstance(payload, dict) and "data" in payload:
return payload["data"]
return payload
@staticmethod
def _format_error(response: httpx.Response) -> str:
try:
body = response.json()
except Exception:
body = response.text
return f"{response.request.method} {response.request.url} -> {response.status_code}: {body}"
def flatten_route_paths(routes: list[dict[str, Any]]) -> set[str]:
paths: set[str] = set()
def collect(items: list[dict[str, Any]]) -> None:
for item in items:
route_path = str(item.get("route_path") or "")
if route_path:
paths.add(route_path)
children = item.get("children") or []
if isinstance(children, list):
collect(children)
collect(routes)
return paths
+35
View File
@@ -0,0 +1,35 @@
from __future__ import annotations
import pytest
from .helpers import ReleaseApiClient, flatten_route_paths
@pytest.mark.release
def test_g1_admin_auth_and_rbac_context(admin_client: ReleaseApiClient) -> None:
me_response = admin_client.get("/api/auth/me")
me = ReleaseApiClient.json_data(me_response)
assert int(me["user_id"]) > 0
assert isinstance(me.get("roles"), list) and me["roles"]
assert isinstance(me.get("permissions"), list)
routes_response = admin_client.get("/api/rbac/user/routes")
routes_data = ReleaseApiClient.json_data(routes_response)
route_paths = flatten_route_paths(routes_data["routes"])
assert "/home" in route_paths
assert "/settings" in route_paths
assert "/role-permissions" in route_paths
users_response = admin_client.get("/api/v3/rbac/users?page=1&page_size=20")
users_data = ReleaseApiClient.json_data(users_response)
assert users_data["total"] >= 1
assert isinstance(users_data["items"], list)
org_response = admin_client.get("/api/admin/users/organizations/tree?include_users=false")
org_data = ReleaseApiClient.json_data(org_response)
assert "organizations" in org_data
assert org_data["total_organizations"] >= 1
tenant_option_response = admin_client.get("/api/v3/tenants/options?feature_key=home.entry_module")
tenant_option_data = ReleaseApiClient.json_data(tenant_option_response)
assert tenant_option_data["total"] >= 1
@@ -0,0 +1,90 @@
from __future__ import annotations
import pytest
from .conftest import SeededUser, TenantSeed
from .helpers import ReleaseApiClient
def _set_module_tenants(admin_client: ReleaseApiClient, module_id: int, module_name: str, tenants: list[TenantSeed], path: str) -> dict:
payload = {
"name": module_name,
"description": "pytest release acceptance only",
"path": path,
"route_path": path,
"tenants": [
{
"tenant_code": tenant.tenant_code,
"tenant_name": tenant.tenant_name,
"enabled": True,
"sort_order": index,
}
for index, tenant in enumerate(tenants, start=1)
],
}
response = admin_client.put(f"/api/v3/entry-modules/{module_id}", json=payload, expected_status=200)
return ReleaseApiClient.json_data(response)
def _home_module_names(client: ReleaseApiClient) -> list[str]:
response = client.get("/api/home/entry-modules")
modules = ReleaseApiClient.json_data(response)
return [str(item.get("name") or "") for item in modules]
@pytest.mark.release
def test_g2_tenant_detail_and_user_tenant_switch(
admin_client: ReleaseApiClient,
anonymous_client: ReleaseApiClient,
tenant_a: TenantSeed,
tenant_common_user_a: SeededUser,
) -> None:
detail_response = admin_client.get(f"/api/v3/tenants/{tenant_a.tenant_code}")
detail = ReleaseApiClient.json_data(detail_response)
assert detail["tenant_code"] == tenant_a.tenant_code
assert detail["tenant_name"] == tenant_a.tenant_name
assert "home.entry_module" in detail["feature_keys"]
switch_response = admin_client.put(
f"/api/v3/rbac/users/{tenant_common_user_a.user_id}/tenant",
json={"tenant_code": tenant_a.tenant_code},
expected_status=200,
)
switch_data = ReleaseApiClient.json_data(switch_response)
assert switch_data["tenant_code"] == tenant_a.tenant_code
relogin = anonymous_client.login_oauth(
sub=tenant_common_user_a.sub,
username=tenant_common_user_a.username,
nickname=tenant_common_user_a.nickname,
ou_id=f"pytest-{tenant_a.tenant_code.lower()}",
ou_name=f"{tenant_a.tenant_name}测试组织",
area=tenant_a.tenant_name,
)
assert relogin["user_info"]["tenant_code"] == tenant_a.tenant_code
@pytest.mark.release
def test_g3_entry_module_visibility_follows_tenant_assignment(
admin_client: ReleaseApiClient,
release_entry_module: dict,
release_config,
tenant_a: TenantSeed,
tenant_b: TenantSeed,
common_api_a: ReleaseApiClient,
common_api_b: ReleaseApiClient,
) -> None:
module_id = int(release_entry_module["id"])
module_name = str(release_entry_module["name"])
_set_module_tenants(admin_client, module_id, module_name, [tenant_b], release_config.module_path)
names_a = _home_module_names(common_api_a)
names_b = _home_module_names(common_api_b)
assert module_name not in names_a
assert module_name in names_b
_set_module_tenants(admin_client, module_id, module_name, [tenant_a, tenant_b], release_config.module_path)
names_a = _home_module_names(common_api_a)
names_b = _home_module_names(common_api_b)
assert module_name in names_a
assert module_name in names_b
+146
View File
@@ -0,0 +1,146 @@
from __future__ import annotations
import time
import fitz
import pytest
from .conftest import TenantSeed
from .helpers import ReleaseApiClient
def _document_ids(items: list[dict]) -> set[int]:
return {int(item["documentId"]) for item in items if item.get("documentId") is not None}
def _sample_pdf_bytes(text: str) -> bytes:
document = fitz.open()
page = document.new_page()
page.insert_text((72, 72), text)
return document.tobytes()
@pytest.mark.release
def test_g4_document_list_and_detail_respect_tenant_boundary(
tenant_admin_api: ReleaseApiClient,
tenant_admin_api_b: ReleaseApiClient,
tenant_a: TenantSeed,
tenant_b: TenantSeed,
make_release_document,
) -> None:
own_doc = make_release_document(
client=tenant_admin_api,
tenant=tenant_a,
file_name=f"pytest-g4-a-{int(time.time())}.txt",
)
other_doc = make_release_document(
client=tenant_admin_api_b,
tenant=tenant_b,
file_name=f"pytest-g4-b-{int(time.time())}.txt",
)
own_list = ReleaseApiClient.json_data(tenant_admin_api.get("/api/documents/list?page=1&pageSize=100"))
own_ids = _document_ids(own_list["documents"])
assert own_doc.document_id in own_ids
assert other_doc.document_id not in own_ids
own_detail = ReleaseApiClient.json_data(tenant_admin_api.get(f"/api/documents/{own_doc.document_id}"))
assert int(own_detail["documentId"]) == own_doc.document_id
assert str(own_detail.get("tenantCode") or "") == tenant_a.tenant_code
cross_detail = tenant_admin_api.get(f"/api/documents/{other_doc.document_id}", expected_status=404)
assert "无权访问" in cross_detail.text or "不存在" in cross_detail.text
@pytest.mark.release
def test_g4_document_update_append_and_delete_reject_cross_tenant_access(
tenant_admin_api: ReleaseApiClient,
tenant_admin_api_b: ReleaseApiClient,
tenant_a: TenantSeed,
make_release_document,
) -> None:
own_doc = make_release_document(
client=tenant_admin_api,
tenant=tenant_a,
file_name=f"pytest-g4-own-{int(time.time())}.pdf",
content=_sample_pdf_bytes("pytest g4 own document"),
content_type="application/pdf",
)
update_response = tenant_admin_api.put(
f"/api/documents/{own_doc.document_id}",
json={"remark": "pytest g4 own update"},
expected_status=200,
)
update_data = ReleaseApiClient.json_data(update_response)
assert int(update_data["documentId"]) == own_doc.document_id
append_response = tenant_admin_api.post(
f"/api/documents/{own_doc.document_id}/attachments",
data={"mergeMode": "new", "remark": "pytest attachment"},
files=[("files", ("attachment.pdf", _sample_pdf_bytes("pytest attachment"), "application/pdf"))],
expected_status=200,
)
append_data = ReleaseApiClient.json_data(append_response)
new_document_id = int(append_data["documentId"])
assert new_document_id != own_doc.document_id
assert str(append_data.get("tenantCode") or "") == tenant_a.tenant_code
assert int(append_data.get("previousVersionId") or 0) == own_doc.document_id
cross_update = tenant_admin_api_b.put(
f"/api/documents/{new_document_id}",
json={"remark": "should fail"},
expected_status=404,
)
assert "无权访问" in cross_update.text or "不存在" in cross_update.text
cross_append = tenant_admin_api_b.post(
f"/api/documents/{new_document_id}/attachments",
data={"mergeMode": "new", "remark": "should fail"},
files=[("files", ("cross.pdf", _sample_pdf_bytes("cross tenant"), "application/pdf"))],
expected_status=404,
)
assert "无权访问" in cross_append.text or "不存在" in cross_append.text
cross_delete = tenant_admin_api_b.delete(f"/api/documents/{new_document_id}", expected_status=404)
assert "无权访问" in cross_delete.text or "不存在" in cross_delete.text
@pytest.mark.release
def test_g4_common_user_only_sees_self_created_documents(
common_api_a: ReleaseApiClient,
tenant_admin_api: ReleaseApiClient,
tenant_a: TenantSeed,
make_release_document,
) -> None:
user_doc = make_release_document(
client=common_api_a,
tenant=tenant_a,
file_name=f"pytest-g4-common-self-{int(time.time())}.txt",
)
admin_doc = make_release_document(
client=tenant_admin_api,
tenant=tenant_a,
file_name=f"pytest-g4-common-other-{int(time.time())}.txt",
)
own_list = ReleaseApiClient.json_data(common_api_a.get("/api/documents/list?page=1&pageSize=100"))
own_ids = _document_ids(own_list["documents"])
assert user_doc.document_id in own_ids
assert admin_doc.document_id not in own_ids
own_detail = ReleaseApiClient.json_data(common_api_a.get(f"/api/documents/{user_doc.document_id}"))
assert int(own_detail["documentId"]) == user_doc.document_id
hidden_detail = common_api_a.get(f"/api/documents/{admin_doc.document_id}", expected_status=404)
assert "无权访问" in hidden_detail.text or "不存在" in hidden_detail.text
@pytest.mark.release
def test_g4_govdoc_list_endpoint_does_not_fail_when_backfilling_version_groups(
tenant_admin_api: ReleaseApiClient,
) -> None:
response = tenant_admin_api.get("/api/govdoc/documents?page=1&pageSize=10", expected_status=200)
payload = response.json()
assert payload.get("code") in {0, 200}, payload
assert "data" in payload, payload
+175
View File
@@ -0,0 +1,175 @@
from __future__ import annotations
import time
import pytest
from .conftest import ReleaseDatasetSeed, TenantSeed
from .helpers import ReleaseApiClient
def _dataset_ids(items: list[dict]) -> set[int]:
return {int(item["id"]) for item in items if item.get("id") is not None}
def _app_ids(items: list[dict]) -> set[str]:
return {str(item["appId"]) for item in items if item.get("appId") is not None}
@pytest.mark.release
def test_g5_rag_dataset_admin_scope_is_limited_to_own_tenant(
admin_client: ReleaseApiClient,
tenant_admin_api: ReleaseApiClient,
tenant_admin_api_b: ReleaseApiClient,
tenant_a: TenantSeed,
tenant_b: TenantSeed,
make_release_dataset,
) -> None:
dataset_a = make_release_dataset(
tenant=tenant_a,
dataset_name=f"Pytest G5 A {int(time.time())}",
)
dataset_b = make_release_dataset(
tenant=tenant_b,
dataset_name=f"Pytest G5 B {int(time.time())}",
)
global_admin_list = ReleaseApiClient.json_data(admin_client.get("/api/v3/rag/datasets/admin?page=1&pageSize=100"))
ids_a = _dataset_ids(global_admin_list["data"])
assert dataset_a.dataset_id in ids_a
assert dataset_b.dataset_id in ids_a
tenant_admin_list = ReleaseApiClient.json_data(
tenant_admin_api.get("/api/v3/rag/datasets/admin?page=1&pageSize=100")
)
tenant_admin_ids = _dataset_ids(tenant_admin_list["data"])
assert dataset_a.dataset_id in tenant_admin_ids
assert dataset_b.dataset_id not in tenant_admin_ids
forbidden_query = tenant_admin_api.get(
f"/api/v3/rag/datasets/admin?page=1&pageSize=100&tenant_code={tenant_b.tenant_code}",
expected_status=403,
)
assert "本地区知识库配置" in forbidden_query.text or "本租户知识库" in forbidden_query.text
@pytest.mark.release
def test_g5_rag_dataset_detail_and_update_respect_tenant_boundary(
admin_client: ReleaseApiClient,
tenant_admin_api: ReleaseApiClient,
tenant_a: TenantSeed,
tenant_b: TenantSeed,
make_release_dataset,
) -> None:
dataset_a = make_release_dataset(
tenant=tenant_a,
dataset_name=f"Pytest G5 Detail A {int(time.time())}",
)
dataset_b = make_release_dataset(
tenant=tenant_b,
dataset_name=f"Pytest G5 Detail B {int(time.time())}",
)
own_detail = ReleaseApiClient.json_data(tenant_admin_api.get(f"/api/v3/rag/datasets/{dataset_a.dataset_id}"))
assert int(own_detail["id"]) == dataset_a.dataset_id
assert str(own_detail.get("tenantCode") or "") == tenant_a.tenant_code
hidden_detail = ReleaseApiClient.json_data(tenant_admin_api.get(f"/api/v3/rag/datasets/{dataset_b.dataset_id}"))
assert hidden_detail is None
own_update = ReleaseApiClient.json_data(
tenant_admin_api.patch(
f"/api/v3/rag/datasets/{dataset_a.dataset_id}",
json={"name": f"{dataset_a.dataset_name}-tenant-updated"},
expected_status=200,
)
)
assert str(own_update["name"]).endswith("-tenant-updated")
hidden_update = ReleaseApiClient.json_data(
tenant_admin_api.patch(
f"/api/v3/rag/datasets/{dataset_b.dataset_id}",
json={"name": "should-not-work"},
expected_status=200,
)
)
assert hidden_update is None
admin_update = ReleaseApiClient.json_data(
admin_client.patch(
f"/api/v3/rag/datasets/{dataset_a.dataset_id}",
json={"name": f"{dataset_a.dataset_name}-updated"},
expected_status=200,
)
)
assert str(admin_update["name"]).endswith("-updated")
@pytest.mark.release
def test_g5_rag_apps_and_public_dataset_visibility(
tenant_admin_api: ReleaseApiClient,
tenant_admin_api_b: ReleaseApiClient,
common_api_a: ReleaseApiClient,
common_api_b: ReleaseApiClient,
tenant_a: TenantSeed,
tenant_b: TenantSeed,
make_release_dataset,
) -> None:
dataset_a = make_release_dataset(
tenant=tenant_a,
dataset_name=f"Pytest G5 App A {int(time.time())}",
is_default=True,
)
dataset_b = make_release_dataset(
tenant=tenant_b,
dataset_name=f"Pytest G5 App B {int(time.time())}",
is_default=True,
)
public_dataset = make_release_dataset(
tenant=tenant_a,
dataset_name=f"Pytest G5 Public {int(time.time())}",
is_public=True,
is_default=False,
)
apps_a = ReleaseApiClient.json_data(common_api_a.get("/api/v3/rag/apps"))
app_ids_a = _app_ids(apps_a["data"])
assert dataset_a.app_id is not None
assert str(dataset_a.app_id) in app_ids_a
assert dataset_b.app_id is not None
assert str(dataset_b.app_id) not in app_ids_a
assert public_dataset.app_id is not None
assert str(public_dataset.app_id) in app_ids_a
apps_b = ReleaseApiClient.json_data(common_api_b.get("/api/v3/rag/apps"))
app_ids_b = _app_ids(apps_b["data"])
assert str(dataset_b.app_id) in app_ids_b
assert str(dataset_a.app_id) not in app_ids_b
assert str(public_dataset.app_id) in app_ids_b
default_app_a = ReleaseApiClient.json_data(common_api_a.get("/api/v3/rag/apps/default"))
assert default_app_a is not None
assert str(default_app_a.get("tenantCode") or "") in {tenant_a.tenant_code, public_dataset.tenant_code}
@pytest.mark.release
def test_g5_common_user_cannot_access_rag_admin_endpoints(
common_api_a: ReleaseApiClient,
tenant_a: TenantSeed,
) -> None:
forbidden_admin_list = common_api_a.get("/api/v3/rag/datasets/admin?page=1&pageSize=20", expected_status=403)
assert "管理知识库权限" in forbidden_admin_list.text
forbidden_create = common_api_a.post(
"/api/v3/rag/datasets/admin",
json={
"tenant_code": tenant_a.tenant_code,
"tenant_name": tenant_a.tenant_name,
"area": tenant_a.tenant_name,
"name": f"should-forbid-{int(time.time())}",
"description": "forbidden",
"status": 1,
},
expected_status=403,
)
assert "创建知识库权限" in forbidden_create.text
@@ -0,0 +1,222 @@
from __future__ import annotations
import time
import pytest
from .conftest import SeededUser, TenantSeed
from .helpers import ReleaseApiClient
@pytest.mark.release
def test_g5_rule_sets_are_globally_readable_but_bindings_and_groups_follow_tenant_scope(
admin_client: ReleaseApiClient,
tenant_admin_api: ReleaseApiClient,
common_api_a: ReleaseApiClient,
) -> None:
admin_rule_sets = ReleaseApiClient.json_data(admin_client.get("/api/rule-sets"))
tenant_rule_sets = ReleaseApiClient.json_data(tenant_admin_api.get("/api/rule-sets"))
common_rule_sets = ReleaseApiClient.json_data(common_api_a.get("/api/rule-sets"))
assert len(admin_rule_sets) > 0
assert len(tenant_rule_sets) == len(admin_rule_sets)
assert len(common_rule_sets) == len(admin_rule_sets)
admin_bindings = ReleaseApiClient.json_data(admin_client.get("/api/rule-sets/bindings"))
tenant_bindings = ReleaseApiClient.json_data(tenant_admin_api.get("/api/rule-sets/bindings"))
common_bindings = ReleaseApiClient.json_data(common_api_a.get("/api/rule-sets/bindings"))
assert isinstance(admin_bindings, list)
assert len(admin_bindings) > 0
assert tenant_bindings == []
assert common_bindings == []
admin_groups = admin_client.get("/api/v3/evaluation-point-groups/all")
tenant_groups = tenant_admin_api.get("/api/v3/evaluation-point-groups/all")
common_groups = common_api_a.get("/api/v3/evaluation-point-groups/all")
assert ReleaseApiClient.json_data(admin_groups) != []
assert ReleaseApiClient.json_data(tenant_groups) == []
assert ReleaseApiClient.json_data(common_groups) == []
@pytest.mark.release
def test_g5_group_detail_and_children_follow_same_scope_boundary(
admin_client: ReleaseApiClient,
tenant_admin_api: ReleaseApiClient,
common_api_a: ReleaseApiClient,
) -> None:
admin_groups = ReleaseApiClient.json_data(admin_client.get("/api/v3/evaluation-point-groups/all"))
assert admin_groups != []
target_group = admin_groups[0]
target_group_id = int(target_group["id"])
admin_detail = ReleaseApiClient.json_data(
admin_client.get(f"/api/v3/evaluation-point-groups/{target_group_id}")
)
assert int(admin_detail["id"]) == target_group_id
admin_children = admin_client.get(
f"/api/v3/evaluation-point-groups/{target_group_id}/children?page=1&page_size=20"
).json()
assert isinstance(admin_children, dict)
assert "data" in admin_children
tenant_detail = tenant_admin_api.get(
f"/api/v3/evaluation-point-groups/{target_group_id}",
expected_status=404,
)
assert "规则分组不存在" in tenant_detail.text
tenant_children = tenant_admin_api.get(
f"/api/v3/evaluation-point-groups/{target_group_id}/children?page=1&page_size=20",
expected_status=404,
)
assert "规则分组不存在" in tenant_children.text
common_detail = common_api_a.get(
f"/api/v3/evaluation-point-groups/{target_group_id}",
expected_status=404,
)
assert "规则分组不存在" in common_detail.text
@pytest.mark.release
def test_g5_cross_review_rejects_cross_tenant_documents_and_members(
tenant_admin_api: ReleaseApiClient,
common_api_b: ReleaseApiClient,
tenant_a: TenantSeed,
tenant_b: TenantSeed,
tenant_common_user_a: SeededUser,
tenant_common_user_b: SeededUser,
make_release_document,
) -> None:
doc_a = make_release_document(
client=tenant_admin_api,
tenant=tenant_a,
file_name=f"pytest-cross-a-{int(time.time())}.txt",
)
doc_b = make_release_document(
client=common_api_b,
tenant=tenant_b,
file_name=f"pytest-cross-b-{int(time.time())}.txt",
)
same_tenant = tenant_admin_api.post(
"/api/v3/cross-review/tasks",
json={
"taskName": f"Pytest Cross Same {int(time.time())}",
"taskType": "CITY",
"memberUserIds": [tenant_common_user_a.user_id],
"principalUserIds": [],
"documentIds": [doc_a.document_id],
},
expected_status=200,
)
same_tenant_data = ReleaseApiClient.json_data(same_tenant)
assert int(same_tenant_data["documentCount"]) == 1
cross_doc = tenant_admin_api.post(
"/api/v3/cross-review/tasks",
json={
"taskName": f"Pytest Cross Doc {int(time.time())}",
"taskType": "CITY",
"memberUserIds": [tenant_common_user_a.user_id],
"principalUserIds": [],
"documentIds": [doc_b.document_id],
},
expected_status=403,
)
assert "其他租户文档" in cross_doc.text
cross_member = tenant_admin_api.post(
"/api/v3/cross-review/tasks",
json={
"taskName": f"Pytest Cross Member {int(time.time())}",
"taskType": "CITY",
"memberUserIds": [tenant_common_user_b.user_id],
"principalUserIds": [],
"documentIds": [doc_a.document_id],
},
expected_status=403,
)
assert "其他租户用户" in cross_member.text
@pytest.mark.release
def test_g5_cross_review_task_visibility_progress_and_documents_follow_member_scope(
tenant_admin_api: ReleaseApiClient,
tenant_admin_api_b: ReleaseApiClient,
tenant_a: TenantSeed,
tenant_common_user_a: SeededUser,
make_release_document,
) -> None:
doc_a = make_release_document(
client=tenant_admin_api,
tenant=tenant_a,
file_name=f"pytest-cross-query-{int(time.time())}.txt",
)
created = tenant_admin_api.post(
"/api/v3/cross-review/tasks",
json={
"taskName": f"Pytest Query Task {int(time.time())}",
"taskType": "CITY",
"memberUserIds": [tenant_common_user_a.user_id],
"principalUserIds": [],
"documentIds": [doc_a.document_id],
},
expected_status=200,
)
created_data = ReleaseApiClient.json_data(created)
task_id = int(created_data["taskId"])
queried = ReleaseApiClient.json_data(
tenant_admin_api.post(
"/api/v3/cross-review/tasks/query",
json={"page": 1, "pageSize": 50, "keyword": "Pytest Query Task"},
expected_status=200,
)
)
task_ids = {int(item["taskId"]) for item in queried["items"]}
assert task_id in task_ids
created_item = next(item for item in queried["items"] if int(item["taskId"]) == task_id)
evaluation_tenant_codes = {str(item.get("tenantCode") or "") for item in created_item.get("evaluationTenants") or []}
assert tenant_a.tenant_code in evaluation_tenant_codes
progress = ReleaseApiClient.json_data(
tenant_admin_api.get(f"/api/v3/cross-review/tasks/{task_id}/progress", expected_status=200)
)
assert int(progress["taskId"]) == task_id
assert int(progress["totalDocuments"]) == 1
documents = ReleaseApiClient.json_data(
tenant_admin_api.get(
f"/api/v3/cross-review/tasks/{task_id}/documents?page=1&pageSize=20",
expected_status=200,
)
)
assert int(documents["taskId"]) == task_id
returned_document_ids = {int(item["documentId"]) for item in documents["items"]}
assert doc_a.document_id in returned_document_ids
cross_tenant_query = ReleaseApiClient.json_data(
tenant_admin_api_b.post(
"/api/v3/cross-review/tasks/query",
json={"page": 1, "pageSize": 50, "keyword": "Pytest Query Task"},
expected_status=200,
)
)
cross_tenant_task_ids = {int(item["taskId"]) for item in cross_tenant_query["items"]}
assert task_id not in cross_tenant_task_ids
cross_tenant_progress = tenant_admin_api_b.get(
f"/api/v3/cross-review/tasks/{task_id}/progress",
expected_status=403,
)
assert "当前用户不是交叉评查任务成员" in cross_tenant_progress.text
cross_tenant_documents = tenant_admin_api_b.get(
f"/api/v3/cross-review/tasks/{task_id}/documents?page=1&pageSize=20",
expected_status=403,
)
assert "当前用户不是交叉评查任务成员" in cross_tenant_documents.text
@@ -0,0 +1,159 @@
from __future__ import annotations
import os
import re
import time
from typing import Any
import pytest
from .helpers import ReleaseApiClient
def _select_rule_set(client: ReleaseApiClient) -> dict[str, Any]:
preferred_rule_type = os.getenv("LEAUDIT_TEST_RULE_TYPE", "contract.entrust").strip()
rule_sets = ReleaseApiClient.json_data(client.get("/api/rule-sets"))
assert isinstance(rule_sets, list)
assert rule_sets, "当前环境没有规则集,无法测试版本管理"
for item in rule_sets:
if str(item.get("ruleType") or "") == preferred_rule_type and item.get("currentVersionId"):
return item
for item in rule_sets:
if item.get("currentVersionId"):
return item
pytest.skip("当前环境没有带 currentVersionId 的规则集,无法测试发布/回滚闭环")
def _set_metadata_version(yaml_text: str, version_no: str) -> str:
lines = yaml_text.splitlines()
metadata_index: int | None = None
version_pattern = re.compile(r"^(\s*)version\s*:")
for index, line in enumerate(lines):
if line.strip() == "metadata:":
metadata_index = index
continue
if metadata_index is None:
continue
if line and not line.startswith((" ", "\t")):
break
match = version_pattern.match(line)
if match:
lines[index] = f"{match.group(1)}version: '{version_no}'"
return "\n".join(lines) + ("\n" if yaml_text.endswith("\n") else "")
if metadata_index is None:
return f"metadata:\n version: '{version_no}'\n{yaml_text}"
lines.insert(metadata_index + 1, f" version: '{version_no}'")
return "\n".join(lines) + ("\n" if yaml_text.endswith("\n") else "")
def _rule_set_by_type(client: ReleaseApiClient, rule_type: str) -> dict[str, Any]:
rule_sets = ReleaseApiClient.json_data(client.get("/api/rule-sets"))
for item in rule_sets:
if str(item.get("ruleType") or "") == rule_type:
return item
raise AssertionError(f"规则集不存在: {rule_type}")
@pytest.mark.release
def test_g6_rule_detail_version_management_save_publish_and_rollback(admin_client: ReleaseApiClient) -> None:
"""覆盖规则配置详情页的版本管理闭环。
该用例会创建一个真实历史版本并短暂发布,最后回滚到执行前的 currentVersionId。
"""
target_rule_set = _select_rule_set(admin_client)
rule_type = str(target_rule_set["ruleType"])
original_rule_set_id = int(target_rule_set["id"])
original_current_version_id = int(target_rule_set["currentVersionId"])
original_tenant_code = str(target_rule_set.get("effectiveTenantCode") or "")
versions_before = ReleaseApiClient.json_data(admin_client.get(f"/api/rule-sets/{rule_type}/versions"))
assert isinstance(versions_before, list)
assert {int(item["ruleSetId"]) for item in versions_before} == {original_rule_set_id}
assert original_current_version_id in {int(item["id"]) for item in versions_before}
content = ReleaseApiClient.json_data(admin_client.get(f"/api/rule-sets/versions/{original_current_version_id}/content"))
assert int(content["ruleSetId"]) == original_rule_set_id
assert str(content["ruleType"]) == rule_type
assert str(content["yamlText"]).strip()
new_version_no = f"pytest-vm-{int(time.time())}"
yaml_text = _set_metadata_version(str(content["yamlText"]), new_version_no)
created_version_id: int | None = None
try:
created = ReleaseApiClient.json_data(
admin_client.post(
f"/api/rule-sets/{rule_type}/versions",
json={
"yamlText": yaml_text,
"changeNote": f"pytest rule detail version management smoke {new_version_no}",
},
expected_status=200,
)
)
created_version_id = int(created["id"])
assert int(created["ruleSetId"]) == original_rule_set_id
assert str(created["versionNo"]) == new_version_no
assert str(created["status"]) == "draft"
versions_after_create = ReleaseApiClient.json_data(admin_client.get(f"/api/rule-sets/{rule_type}/versions"))
assert created_version_id in {int(item["id"]) for item in versions_after_create}
assert {int(item["ruleSetId"]) for item in versions_after_create} == {original_rule_set_id}
published = ReleaseApiClient.json_data(
admin_client.post(
f"/api/rule-sets/{rule_type}/publish",
json={"versionId": created_version_id},
expected_status=200,
)
)
assert int(published["id"]) == created_version_id
assert int(published["ruleSetId"]) == original_rule_set_id
assert str(published["status"]) == "published"
current_after_publish = _rule_set_by_type(admin_client, rule_type)
assert int(current_after_publish["id"]) == original_rule_set_id
assert int(current_after_publish["currentVersionId"]) == created_version_id
assert str(current_after_publish.get("effectiveTenantCode") or "") == original_tenant_code
finally:
if created_version_id:
admin_client.post(
f"/api/rule-sets/{rule_type}/rollback",
json={"versionId": original_current_version_id},
expected_status=200,
)
restored = _rule_set_by_type(admin_client, rule_type)
assert int(restored["id"]) == original_rule_set_id
assert int(restored["currentVersionId"]) == original_current_version_id
assert str(restored.get("effectiveTenantCode") or "") == original_tenant_code
@pytest.mark.release
def test_g6_rule_detail_version_management_rejects_cross_rule_set_publish(admin_client: ReleaseApiClient) -> None:
"""发布接口必须拒绝把其他规则集的版本发布到当前规则类型。"""
rule_sets = ReleaseApiClient.json_data(admin_client.get("/api/rule-sets"))
candidates = [item for item in rule_sets if item.get("currentVersionId")]
if len(candidates) < 2:
pytest.skip("当前环境少于两个带 currentVersionId 的规则集,无法测试跨规则集发布拦截")
left = candidates[0]
right = next((item for item in candidates[1:] if str(item["ruleType"]) != str(left["ruleType"])), None)
if right is None:
pytest.skip("当前环境没有可用于跨规则类型发布拦截的第二个规则集")
response = admin_client.post(
f"/api/rule-sets/{left['ruleType']}/publish",
json={"versionId": int(right["currentVersionId"])},
expected_status=403,
)
assert "当前租户不能发布或回滚其他租户的规则版本" in response.text
+105
View File
@@ -0,0 +1,105 @@
from __future__ import annotations
import pytest
from .conftest import SeededUser, TenantSeed
from .helpers import ReleaseApiClient
@pytest.mark.release
def test_global_admin_can_query_cross_tenant_scope(
admin_client: ReleaseApiClient,
tenant_a: TenantSeed,
tenant_b: TenantSeed,
) -> None:
data_a = ReleaseApiClient.json_data(
admin_client.get(f"/api/v3/rbac/users?page=1&page_size=100&tenant_code={tenant_a.tenant_code}")
)
data_b = ReleaseApiClient.json_data(
admin_client.get(f"/api/v3/rbac/users?page=1&page_size=100&tenant_code={tenant_b.tenant_code}")
)
assert isinstance(data_a["items"], list)
assert isinstance(data_b["items"], list)
@pytest.mark.release
def test_tenant_admin_is_limited_to_own_tenant_scope(
tenant_admin_api: ReleaseApiClient,
tenant_a: TenantSeed,
tenant_b: TenantSeed,
tenant_common_user_a: SeededUser,
tenant_common_user_b: SeededUser,
) -> None:
own_scope = ReleaseApiClient.json_data(tenant_admin_api.get("/api/v3/rbac/users?page=1&page_size=100"))
tenant_codes = {str(item.get("tenant_code") or "") for item in own_scope["items"]}
assert tenant_a.tenant_code in tenant_codes
assert tenant_b.tenant_code not in tenant_codes
forbidden_query = tenant_admin_api.get(
f"/api/v3/rbac/users?page=1&page_size=20&tenant_code={tenant_b.tenant_code}",
expected_status=403,
)
assert "不能查询其他租户用户" in forbidden_query.text
same_tenant_update = tenant_admin_api.put(
f"/api/v3/rbac/users/{tenant_common_user_a.user_id}/tenant",
json={"tenant_code": tenant_a.tenant_code},
expected_status=200,
)
same_tenant_data = ReleaseApiClient.json_data(same_tenant_update)
assert same_tenant_data["tenant_code"] == tenant_a.tenant_code
cross_tenant_update = tenant_admin_api.put(
f"/api/v3/rbac/users/{tenant_common_user_b.user_id}/tenant",
json={"tenant_code": tenant_a.tenant_code},
expected_status=403,
)
assert "不能修改其他租户用户" in cross_tenant_update.text
@pytest.mark.release
def test_common_user_cannot_access_management_but_keeps_business_entry(
common_api_a: ReleaseApiClient,
release_entry_module: dict,
admin_client: ReleaseApiClient,
release_config,
tenant_a: TenantSeed,
tenant_b: TenantSeed,
) -> None:
module_id = int(release_entry_module["id"])
module_name = str(release_entry_module["name"])
admin_client.put(
f"/api/v3/entry-modules/{module_id}",
json={
"name": module_name,
"description": "pytest release acceptance only",
"path": release_config.module_path,
"route_path": release_config.module_path,
"tenants": [
{
"tenant_code": tenant_a.tenant_code,
"tenant_name": tenant_a.tenant_name,
"enabled": True,
"sort_order": 1,
},
{
"tenant_code": tenant_b.tenant_code,
"tenant_name": tenant_b.tenant_name,
"enabled": True,
"sort_order": 2,
},
],
},
expected_status=200,
)
users_response = common_api_a.get("/api/v3/rbac/users?page=1&page_size=20", expected_status=403)
assert "系统设置管理权限" in users_response.text
tenants_response = common_api_a.get("/api/v3/tenants", expected_status=403)
assert "租户" in tenants_response.text
home_response = common_api_a.get("/api/home/entry-modules")
home_modules = ReleaseApiClient.json_data(home_response)
home_names = [str(item.get("name") or "") for item in home_modules]
assert module_name in home_names