fix(security): retire check-governance + scope /reports/dupont to session entity
- data_quality.py: remove GET /check-governance (no caller repo-wide; its entity_id param had no session auth -> cross-tenant read; duplicate of governance-check) -> the path now returns 404 - reports.py: /reports/dupont resolves the analysis target from the session account (assert_entity_slug_scope / resolve_session_slug); explicit cross-entity entity param -> 403, illegal value -> 400; the 2026-08-31 cross-entity exemption is revoked (docstring updated) - frontend: drop the checkGovernance api definition; DupontAnalysis.vue entity radio group -> read-only session entity tag, request without param - tests: TestCheckGovernanceRetired (404 x2), TestDupontSessionScope (3), TestDupont reworked to account-bound tokens (8 tests) - docs/api-verification-split.md: drop the retired row, note 334/404 -> 333/403 pytest: 10 failed / 957 passed / 1 xfailed vs HEAD baseline 10 failed / 951 passed / 1 xfailed (identical failure set -> zero regression) (cma-residual-cross-entity-cleanup-20260914)
This commit is contained in:
@@ -5,7 +5,7 @@
|
||||
- alert_rules create 写入 entity_id(跨 entity 不可见)
|
||||
- reports 跨 entity 过滤(profit-summary / kpi-trends)
|
||||
- _eval_threshold invert 参数(低于阈值触发红灯)
|
||||
- /check-governance 在 SQLite 测试库不 500(data_quality 收敛后复用 Python 解析)
|
||||
- /check-governance 下线(cma-residual-cross-entity-cleanup-20260914)→ 404;governance-check 保留
|
||||
"""
|
||||
import pytest
|
||||
from fastapi.testclient import TestClient
|
||||
@@ -204,24 +204,24 @@ class TestInvertThreshold:
|
||||
|
||||
|
||||
# ============================================================
|
||||
# /check-governance SQLite 兼容(P1-2)
|
||||
# /check-governance 下线(cma-residual-cross-entity-cleanup-20260914)
|
||||
# ============================================================
|
||||
class TestCheckGovernanceSQLite:
|
||||
def test_check_governance_no_500_on_sqlite(self, client: TestClient, db: Session):
|
||||
"""data_quality 收敛后 /check-governance 在 SQLite 测试库不 500"""
|
||||
class TestCheckGovernanceRetired:
|
||||
def test_check_governance_endpoint_removed(self, client: TestClient, db: Session):
|
||||
"""/check-governance 已下线 → 404(该端点 entity_id 参数无会话鉴权,跨租户)"""
|
||||
_seed_entity_kpi(db, entity_id=1, code="GOV_KPI")
|
||||
db.commit()
|
||||
create_test_user(db)
|
||||
token = get_token_for_user(client, "testadmin", "admin123")
|
||||
|
||||
resp = client.get("/api/cma/data-quality/check-governance", headers=auth_header(token))
|
||||
assert resp.status_code == 200, f"check-governance 500: {resp.text[:300]}"
|
||||
data = resp.json()
|
||||
assert "score" in data
|
||||
assert data["total_rules"] == 7
|
||||
assert resp.status_code == 404, f"应 404,实际 {resp.status_code}: {resp.text[:200]}"
|
||||
# 跨租户取数路径随之消失(同参数不再有响应体)
|
||||
resp2 = client.get("/api/cma/data-quality/check-governance?entity_id=2", headers=auth_header(token))
|
||||
assert resp2.status_code == 404, resp2.text[:200]
|
||||
|
||||
def test_governance_check_still_works(self, client: TestClient, db: Session):
|
||||
"""governance-check 端点(新口径)在收敛后仍正常"""
|
||||
"""governance-check 端点(唯一入口)在收敛后仍正常"""
|
||||
_seed_entity_kpi(db, entity_id=1, code="GOV2_KPI")
|
||||
db.commit()
|
||||
create_test_user(db)
|
||||
@@ -340,3 +340,38 @@ class TestCrossEntityEndpointAuth:
|
||||
assert r1.status_code == 403, r1.text[:200]
|
||||
r2 = client.get("/api/cma/predict/accuracy?entity_id=2", headers=auth_header(token))
|
||||
assert r2.status_code == 403, r2.text[:200]
|
||||
|
||||
|
||||
# ============================================================
|
||||
# reports/dupont 口径收敛(cma-residual-cross-entity-cleanup-20260914)
|
||||
# 旧「跨实体对比」豁免(2026-08-31)撤销 → 分析对象=会话账套
|
||||
# ============================================================
|
||||
class TestDupontSessionScope:
|
||||
BASE = "/api/cma/reports/dupont"
|
||||
|
||||
def _login(self, client, db):
|
||||
_ensure_entity(db, 2)
|
||||
create_test_user(db)
|
||||
return get_token_for_user(client, "testadmin", "admin123") # 登录账套 entity_id=1
|
||||
|
||||
def test_dupont_cross_entity_bohai_403(self, client, db):
|
||||
"""酣客会话显式 entity=bohai(博海真实KPI)→ 403(旧豁免已撤销)"""
|
||||
token = self._login(client, db)
|
||||
r = client.get(f"{self.BASE}?entity=bohai", headers=auth_header(token))
|
||||
assert r.status_code == 403, f"跨实体应 403,实际 {r.status_code}: {r.text[:200]}"
|
||||
assert "无权访问企业 entity_id=2(当前账套: 1)" in r.text
|
||||
|
||||
def test_dupont_no_param_is_session_entity(self, client, db):
|
||||
"""无 entity 参数 → 按会话账套(酣客),不再默认博海"""
|
||||
token = self._login(client, db)
|
||||
r = client.get(self.BASE, headers=auth_header(token))
|
||||
assert r.status_code == 200, r.text[:200]
|
||||
assert r.json()["entity"] == "hanke"
|
||||
assert "酣客" in r.json()["entity_name"]
|
||||
|
||||
def test_dupont_same_entity_200(self, client, db):
|
||||
"""同实体显式参数 → 200"""
|
||||
token = self._login(client, db)
|
||||
r = client.get(f"{self.BASE}?entity=hanke", headers=auth_header(token))
|
||||
assert r.status_code == 200, r.text[:200]
|
||||
assert r.json()["entity"] == "hanke"
|
||||
|
||||
Reference in New Issue
Block a user