fix(security): 4端点补跨租户鉴权 — 实体1会话读实体2数据(200)→403 (cma-cross-entity-endpoints-auth-20260914)

背景(项目Bot 补充复验 §四#2):以下 4 个只读端点未做会话账套鉴权,
用 entity=1(酣客) 会话 token + 显式 entity 参数可读到 entity=2(博海) 数据(HTTP 200):
  - GET /data-quality/governance-check?entity_id=2   → 200 score=88(对照 entity1=72)
  - GET /products/matrix?entity_id=2                 → 200
  - GET /alerts/risk-matrix?entity=bohai             → 200 博海风险矩阵
  - GET /action-plans/coso-checklist?entity=bohai    → 200 博海 COSO total_score=55
另:risk-matrix / coso-checklist 在非法实体值下静默回落酣客数据,UI 标签与数据不一致。

改动:
1. deps.py 新增统一防护入口 assert_entity_scope / assert_entity_slug_scope /
   get_session_entity / resolve_session_slug(文案对齐 get_entity_id:无权访问企业 entity_id=X(当前账套: Y))
2. governance-check / products/matrix:entity_id 须等于会话账套,否则 403;
   缺省按会话账套(governance-check 原缺省 0=全部 → 登录态下按账套收敛)
3. risk-matrix / coso-checklist:entity 须等于会话账套,否则 403;
   非法值 → 400 显式报错(不再静默回落酣客)
4. P3:POST /auth/switch-entity 加 DEPRECATED 注释(前端已无调用,保留不删)
5. 测试:tests/test_security_multitenant.py 新增 TestCrossEntityEndpointAuth 15 例
   (跨实体 403 / 同实体 200 / 缺省按账套 / 非法显式报错 / 参照端点行为不变)

验证:pytest tests/ -q → 951 passed(10 项失败为改动前既有基线,已 stash 对照确认);
      真实后端冒烟 19/19 PASS(entity=1 会话 4 端点跨实体全 403)。
This commit is contained in:
Hermes CI Fix
2026-09-14 11:19:05 +08:00
parent 1b73dbb272
commit 115a0ec755
7 changed files with 258 additions and 22 deletions
+16 -7
View File
@@ -1,5 +1,5 @@
"""改善行动计划 API — 管理会计OS"""
from fastapi import APIRouter, Depends, HTTPException
from fastapi import APIRouter, Depends, HTTPException, Request
from sqlalchemy.orm import Session
from datetime import datetime, timezone
from typing import Optional
@@ -7,7 +7,7 @@ import re
import logging
from calendar import monthrange
from app.database import get_db
from app.deps import get_entity_id
from app.deps import get_entity_id, assert_entity_slug_scope, resolve_session_slug
from app.auth_middleware import require_role, require_auth
from app.models import ActionPlan, KPIAlert, KPIDefinition, KPIValue, User, Objective, KR
from app.api.verify import build_auto_verify_rule
@@ -430,10 +430,19 @@ COSO_CHECKLIST_DATA = {
@router.get("/coso-checklist")
def get_coso_checklist(entity: str = "hanke"):
"""COSO内控自检表 - CMA P1 COSO五要素"""
data = COSO_CHECKLIST_DATA.get(entity)
def get_coso_checklist(
request: Request,
entity: Optional[str] = None,
):
"""COSO内控自检表 - CMA P1 COSO五要素
多租户(cma-cross-entity-endpoints-auth-20260914):
非法实体值 → 400 显式报错(不再静默回落酣客数据);跨实体 → 403。
"""
slug = assert_entity_slug_scope(request, entity)
if slug is None:
slug = resolve_session_slug(request, "hanke")
data = COSO_CHECKLIST_DATA.get(slug)
if not data:
data = COSO_CHECKLIST_DATA["hanke"]
data["entity_name"] = f"未知实体({entity}),默认返回酣客数据"
raise HTTPException(404, f"实体 entity={slug} 无 COSO 自检数据")
return data