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:
Hermes CI Fix
2026-09-14 12:09:04 +08:00
parent 72c1bbce16
commit aac1bd1df1
7 changed files with 147 additions and 102 deletions
+16 -9
View File
@@ -8,7 +8,7 @@ CMA管理报表中心 — 管理会计OS
3. KPI趋势报告 — 选定KPI的历史趋势
4. 四维度绩效评分卡 — BSC健康度雷达图
"""
from fastapi import APIRouter, Depends, Query, HTTPException
from fastapi import APIRouter, Depends, Query, HTTPException, Request
import json
from pydantic import BaseModel
from sqlalchemy.orm import Session
@@ -19,7 +19,7 @@ from app.database import get_db
from app.auth_middleware import require_role, require_auth
from app.models import KPIDefinition, KPIValue, BudgetPlan, StrategicMap, KPIAlert, User, Subject, ActionPlan, OperationLog, ReportHistory
from app.utils.deviation_engine import calc_period_deviation, calc_period_diff, calc_deviation, get_budget_for_kpi
from app.deps import get_entity_id
from app.deps import get_entity_id, assert_entity_slug_scope, resolve_session_slug
import logging
logger = logging.getLogger("cma.reports")
@@ -1885,15 +1885,22 @@ def _get_dupont_kpi(db: Session, entity_id: int, kpi_code: str, period: str = "2
@router.get("/dupont")
def get_dupont_analysis(
entity: str = Query("bohai"),
request: Request,
entity: Optional[str] = Query(None, description="hanke/bohai(须与会话账套一致);缺省按会话账套"),
db: Session = Depends(get_db),
):
"""杜邦分析 — ROE三级拆解 (CMA P2)
豁免多租户隔离(2026-08-31):跨实体对比分析端点,entity 参数显式指定
分析对象(bohai→entity 2 / hanke→entity 1),非默认全库查询,故不叠加
Depends(get_entity_id)(叠加会导致 token 绑定的 entity 与显式 entity 参数
不一致时被 403 拦截,破坏跨企业对比功能)"""
if entity == "bohai":
多租户口径(cma-residual-cross-entity-cleanup-20260914,取代 2026-08-31 豁免):
分析对象 = 会话账套(登录即定企业,页内无跨企业切换)。
- 显式 entity 与会话账套不一致 → 403(文案同 get_entity_id);非法值 → 400
- 无会话 token(Bot/存量脚本通道)→ 缺省 bohai(保留原默认,仅非会话通道)
旧豁免("跨实体对比分析端点")已撤销:该端点为单对象分析,非并排对比。
"""
slug = assert_entity_slug_scope(request, entity)
if slug is None:
slug = resolve_session_slug(request, "bohai")
if slug == "bohai":
# 博海标准KPI(F_REVENUE/F_NET_PROFIT)无verified值 → 优先DB读,读不到回退文档确认常量
net_profit = _get_dupont_kpi(db, 2, "F_NET_PROFIT")
if net_profit is None:
@@ -1963,7 +1970,7 @@ def get_dupont_analysis(
},
}
if entity == "hanke":
if slug == "hanke":
# 酣客: F_REVENUE/F_NET_PROFIT 从DB读 verified 值(2026H1: 713.27 / -90.86)
revenue = _get_dupont_kpi(db, 1, "F_REVENUE")
net_profit = _get_dupont_kpi(db, 1, "F_NET_PROFIT")