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:
@@ -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
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
"""预警 API"""
|
||||
from fastapi import APIRouter, Depends, Query, HTTPException
|
||||
from fastapi import APIRouter, Depends, Query, HTTPException, Request
|
||||
from typing import Optional
|
||||
from sqlalchemy.orm import Session
|
||||
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_auth, require_role
|
||||
from app.models import KPIAlert, OperationLog, ActionPlan, KPIDefinition
|
||||
import logging
|
||||
@@ -328,10 +329,19 @@ RISK_MATRIX_DATA = {
|
||||
|
||||
|
||||
@router.get("/risk-matrix")
|
||||
def get_risk_matrix(entity: str = Query("hanke", description="hanke/bohai")):
|
||||
"""风险矩阵热力图数据 - CMA P2 ERM四象限"""
|
||||
data = RISK_MATRIX_DATA.get(entity)
|
||||
def get_risk_matrix(
|
||||
request: Request,
|
||||
entity: Optional[str] = Query(None, description="hanke/bohai(须与会话账套一致);缺省按会话账套"),
|
||||
):
|
||||
"""风险矩阵热力图数据 - CMA P2 ERM四象限
|
||||
|
||||
多租户(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 = RISK_MATRIX_DATA.get(slug)
|
||||
if not data:
|
||||
data = RISK_MATRIX_DATA["hanke"]
|
||||
data["entity_name"] = f"未知实体({entity}),默认返回酣客数据"
|
||||
raise HTTPException(404, f"实体 entity={slug} 无风险矩阵数据")
|
||||
return data
|
||||
|
||||
@@ -168,7 +168,13 @@ def switch_entity(
|
||||
current_user: User = Depends(require_auth),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""账套切换:校验授权 → 重新签发token → 写操作日志(方案B:无缝刷新token)"""
|
||||
"""账套切换:校验授权 → 重新签发token → 写操作日志(方案B:无缝刷新token)
|
||||
|
||||
DEPRECATED(cma-cross-entity-endpoints-auth-20260914,P3):
|
||||
自 2026-09-14 移除应用内企业切换器后,前端已无任何调用(换企业 = 退出重登)。
|
||||
端点保留仅为兼容存量脚本/回滚(不删);新代码请改用「重新登录携带 entity_id」。
|
||||
注:本端点曾用于切换会话账套;当前账套口径以登录 token 绑定为准。
|
||||
"""
|
||||
entity_id = data.get("entity_id")
|
||||
if not entity_id:
|
||||
raise HTTPException(400, "缺少entity_id")
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"""自动数据质量监控 — 任务3
|
||||
定期检查KPI值异常、连续持平、数据缺失等
|
||||
"""
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query, Request
|
||||
from sqlalchemy.orm import Session
|
||||
from sqlalchemy import func, and_, text
|
||||
from typing import Optional
|
||||
@@ -10,6 +10,7 @@ import json
|
||||
import logging
|
||||
|
||||
from app.database import get_db
|
||||
from app.deps import assert_entity_scope
|
||||
from app.auth_middleware import require_auth, require_role
|
||||
from app.models import KPIDefinition, KPIValue, KpiDataQualityLog, OperationLog
|
||||
from app.api.kpis import kpi_to_dict
|
||||
@@ -564,13 +565,24 @@ def _run_governance_checks(db: Session, entity_id: int = 0):
|
||||
|
||||
@router.get("/governance-check")
|
||||
def governance_check(
|
||||
entity_id: Optional[int] = Query(0, description="实体ID: 0=全部, 1=酣客, 2=博海"),
|
||||
request: Request,
|
||||
entity_id: Optional[int] = Query(None, ge=0, description="实体ID: 0=全部(仅Bot通道), 1=酣客, 2=博海;须与会话账套一致"),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""财务七规则检查(DAMA数据治理)— 只读幂等,不写库。
|
||||
|
||||
7条规则: unit_check/dup_alert/orphan_check/virtual_pollution/entity_check/kpi_completeness/reconciliation
|
||||
评分: 满分100,error 扣 min(15, count*3),warning 扣 min(10, count*1),score=max(0, 100-总扣分)
|
||||
实体筛选: entity_id 参数(0=全部)。
|
||||
实体筛选: entity_id 参数(0=全部,仅无会话token的Bot通道)。
|
||||
|
||||
多租户(cma-cross-entity-endpoints-auth-20260914):
|
||||
登录会话按 token 绑定账套取数,显式 entity_id 与会话账套不一致 → 403(同 growth-quality 文案);
|
||||
未传 entity_id 时按会话账套(不再默认全部实体)。
|
||||
"""
|
||||
return _run_governance_checks(db, entity_id or 0)
|
||||
session_entity = assert_entity_scope(request, entity_id, db)
|
||||
if session_entity is not None:
|
||||
scope = session_entity
|
||||
else:
|
||||
# Bot 通道(无 token):保持原口径(缺省 0=全部)
|
||||
scope = 0 if entity_id is None else entity_id
|
||||
return _run_governance_checks(db, scope)
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
"""波士顿产品矩阵 API — 四象限分析"""
|
||||
from fastapi import APIRouter, Depends, Query
|
||||
from fastapi import APIRouter, Depends, Query, Request
|
||||
from typing import Optional
|
||||
from sqlalchemy.orm import Session
|
||||
from app.database import get_db
|
||||
from app.deps import assert_entity_scope
|
||||
from app.models import ProductSales
|
||||
from collections import defaultdict
|
||||
|
||||
@@ -29,11 +31,19 @@ def _calc_quadrant(trend: float, margin: float) -> str:
|
||||
|
||||
@router.get("/matrix")
|
||||
def get_product_matrix(
|
||||
entity_id: int = Query(1, description="1=酣客 2=博海"),
|
||||
request: Request,
|
||||
entity_id: Optional[int] = Query(None, ge=1, description="实体ID(须与会话账套一致);缺省按会话账套"),
|
||||
months: int = Query(3, ge=1, le=6, description="趋势计算月数"),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""产品矩阵:横轴=销售趋势,纵轴=毛利率,气泡=销售额"""
|
||||
"""产品矩阵:横轴=销售趋势,纵轴=毛利率,气泡=销售额
|
||||
|
||||
多租户(cma-cross-entity-endpoints-auth-20260914):
|
||||
登录会话以 token 绑定账套为准,显式 entity_id 不一致 → 403(同 growth-quality 文案)。
|
||||
"""
|
||||
session_entity = assert_entity_scope(request, entity_id, db)
|
||||
target_entity = session_entity if session_entity is not None else (entity_id or 1)
|
||||
entity_id = target_entity
|
||||
# 取最近 months+1 个月(多取1个月用于计算趋势)
|
||||
periods = db.query(ProductSales.period_month).filter(
|
||||
ProductSales.entity_id == entity_id
|
||||
|
||||
Reference in New Issue
Block a user