fix(security): close unauthenticated CMA user API surface + audit tool (cma-api-auth-surface-audit-20260914)
Root cause: main.py include_router had no global auth dependency; user routers (products/cost/predict/...) relied on assert_entity_scope, which treats a missing session token as the Bot service channel and lets the request through. Result: 46 user endpoints returned data without any token on a public site. Changes: - auth_middleware.py: add require_auth_401 (missing Bearer -> 401, delegates to require_auth otherwise); 401/403 semantics are explicit. - router-level Depends(require_auth_401) on products, cost, predict, notifications, permissions, tenant, alignment; endpoint-level on /auth/roles and /admin/cache/clear. - deps.py: document that get_entity_id/assert_entity_scope are NOT auth. - frontend: add the missing Bearer header to 5 tokenless fetch/axios calls that hit the newly protected endpoints (KPIAlignment x2, RolePermissions x2, Dashboard x1). - tests: new test_api_auth_surface_20260914.py (58 cases: no-token 401, token 200, cross-entity 403, bot channel unchanged); adapt test_predict / test_kpi_forecast to send session tokens like production. - scripts/audit_api_auth_surface.py: reusable FastAPI dependency-graph audit (423 endpoints; A bot / B gap / C public), exits 1 when a B-class gap exists.
This commit is contained in:
@@ -9,11 +9,14 @@ from datetime import datetime
|
||||
import json
|
||||
|
||||
from app.database import get_db
|
||||
from app.auth_middleware import require_auth, require_role
|
||||
from app.auth_middleware import require_auth, require_role, require_auth_401
|
||||
from app.models import KPIDefinition, OperationLog, RolePermission
|
||||
|
||||
router = APIRouter(prefix="/api/cma/alignment", tags=["KPI目标对齐"],
|
||||
# 不设全局权限,每个接口单独控制
|
||||
# 用户端点:router 级要求会话 Bearer 鉴权(cma-api-auth-surface-audit-20260914);
|
||||
# 原注释「不设全局权限,每个接口单独控制」保留语义——写接口仍各自叠加 require_role,
|
||||
# router 级只是「至少已认证」底线(补齐 /modes、GET /config 原无鉴权的缺口)。
|
||||
dependencies=[Depends(require_auth_401)],
|
||||
)
|
||||
|
||||
# 三种对齐模式定义
|
||||
|
||||
@@ -4,7 +4,7 @@ from fastapi import APIRouter, Depends, HTTPException, Request
|
||||
from sqlalchemy.orm import Session
|
||||
from app.database import get_db
|
||||
from app.models import User, UserEntity, Entity, OperationLog
|
||||
from app.auth_middleware import create_token, require_auth, ROLES
|
||||
from app.auth_middleware import create_token, require_auth, require_auth_401, ROLES
|
||||
|
||||
router = APIRouter(prefix="/api/cma/auth", tags=["认证"])
|
||||
|
||||
@@ -111,8 +111,13 @@ def get_me(request: Request, current_user: User = Depends(require_auth), db: Ses
|
||||
|
||||
|
||||
@router.get("/roles")
|
||||
def list_roles():
|
||||
"""返回角色列表(给前端用)"""
|
||||
def list_roles(_=Depends(require_auth_401)):
|
||||
"""返回角色列表(给前端用)
|
||||
|
||||
2026-09-14 安全修复(cma-api-auth-surface-audit-20260914):原无鉴权公开返回角色
|
||||
枚举(对外暴露系统角色模型 + 便于针对性撞库)。仓内检索确认无任何前端/脚本调用方
|
||||
(前端用 /permissions/modules 的 roles 字段),故直接补 require_auth_401。
|
||||
"""
|
||||
return {
|
||||
"data": [
|
||||
{"code": k, "name": v["name"], "priority": v["priority"]}
|
||||
|
||||
@@ -6,6 +6,7 @@ from fastapi import APIRouter, Depends, Query, HTTPException
|
||||
from sqlalchemy.orm import Session
|
||||
from app.database import get_db
|
||||
from app.deps import get_entity_id
|
||||
from app.auth_middleware import require_auth_401
|
||||
from app.models import StandardCost, ActualCost, AbcActivity, AbcAllocation
|
||||
from app.models.cost_model import RESEARCH_PROJECT_TYPES
|
||||
from app.utils.cost_engine import (
|
||||
@@ -14,7 +15,12 @@ from app.utils.cost_engine import (
|
||||
)
|
||||
|
||||
logger = logging.getLogger("cma.cost")
|
||||
router = APIRouter(prefix="/api/cma/cost", tags=["成本分析"])
|
||||
# 用户端点:统一要求会话 Bearer 鉴权(cma-api-auth-surface-audit-20260914)
|
||||
# 本 router 原无任何鉴权依赖(端点仅 assert_entity_scope,无 token 走"Bot 服务通道"分支放行)
|
||||
# → 无 token 可读成本数据、可写标准成本。收口为 require_auth_401(无 token → 401)。
|
||||
# Bot 通道走 /api/cma/bot/cost/*(X-BOT-KEY)不受影响。
|
||||
router = APIRouter(prefix="/api/cma/cost", tags=["成本分析"],
|
||||
dependencies=[Depends(require_auth_401)])
|
||||
|
||||
|
||||
def _resolve_research_dims(data: dict, current=None) -> tuple:
|
||||
|
||||
@@ -3,10 +3,14 @@ from fastapi import APIRouter, Depends, HTTPException
|
||||
from sqlalchemy.orm import Session
|
||||
from app.database import get_db
|
||||
from app.models import NotificationChannel, NotificationLog
|
||||
from app.auth_middleware import require_role
|
||||
from app.auth_middleware import require_role, require_auth_401
|
||||
from datetime import datetime
|
||||
|
||||
router = APIRouter(prefix="/api/cma/notifications", tags=["通知配置"])
|
||||
# 用户端点:统一要求会话 Bearer 鉴权(cma-api-auth-surface-audit-20260914)
|
||||
# 缺口:本 router 6 个端点全部无鉴权——无 token 可读写通知渠道(config 内含 webhook
|
||||
# 地址/令牌等凭据)、可触发渠道测试推送、可读推送日志。收口为 require_auth_401。
|
||||
router = APIRouter(prefix="/api/cma/notifications", tags=["通知配置"],
|
||||
dependencies=[Depends(require_auth_401)])
|
||||
|
||||
|
||||
def ch_to_dict(c):
|
||||
|
||||
@@ -7,9 +7,13 @@ from fastapi import APIRouter, Depends, HTTPException
|
||||
from sqlalchemy.orm import Session
|
||||
from app.database import get_db
|
||||
from app.models import RolePermission
|
||||
from app.auth_middleware import require_auth, require_role
|
||||
from app.auth_middleware import require_auth, require_role, require_auth_401
|
||||
|
||||
router = APIRouter(prefix="/api/cma/permissions", tags=["权限管理"])
|
||||
# 用户端点:统一要求会话 Bearer 鉴权(cma-api-auth-surface-audit-20260914)
|
||||
# 缺口:/modules 与 GET /config 无鉴权——无 token 可读全量角色权限矩阵(横向越权情报)。
|
||||
# 写端点 PUT /config 原有 require_role(ceo,it),保留(router 级为「至少已认证」底线)。
|
||||
router = APIRouter(prefix="/api/cma/permissions", tags=["权限管理"],
|
||||
dependencies=[Depends(require_auth_401)])
|
||||
|
||||
# 模块定义(所有可配置的模块)
|
||||
MODULES = [
|
||||
|
||||
@@ -11,10 +11,16 @@ from app.utils.cash_forecast_engine import (
|
||||
)
|
||||
from app.database import get_db
|
||||
from app.deps import get_entity_id, resolve_entity_for_request
|
||||
from app.auth_middleware import require_auth_401
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
logger = logging.getLogger("cma.predict")
|
||||
router = APIRouter(prefix="/api/cma/predict", tags=["预测模拟"])
|
||||
# 用户端点:统一要求会话 Bearer 鉴权(cma-api-auth-surface-audit-20260914)
|
||||
# 本 router 原无任何鉴权依赖:无 token 可调用全部预测/模拟端点(含多个 CPU 密集的
|
||||
# POST 计算端点与 cash-forecast 落库),既泄露经营数据也构成计算资源滥用面。
|
||||
# 收口为 require_auth_401(无 token → 401)。Bot 通道(X-BOT-KEY)不走本 router。
|
||||
router = APIRouter(prefix="/api/cma/predict", tags=["预测模拟"],
|
||||
dependencies=[Depends(require_auth_401)])
|
||||
|
||||
|
||||
@router.post("/cvp")
|
||||
|
||||
@@ -4,10 +4,14 @@ from typing import Optional
|
||||
from sqlalchemy.orm import Session
|
||||
from app.database import get_db
|
||||
from app.deps import assert_entity_scope
|
||||
from app.auth_middleware import require_auth_401
|
||||
from app.models import ProductSales
|
||||
from collections import defaultdict
|
||||
|
||||
router = APIRouter(prefix="/api/cma/products", tags=["产品矩阵"])
|
||||
# 用户端点:统一要求会话 Bearer 鉴权(cma-api-auth-surface-audit-20260914)
|
||||
# 无 token → 401(原「无 token 亦可取数」缺口收口);Bot 通道走 /api/cma/bot/*(X-BOT-KEY)不受影响。
|
||||
router = APIRouter(prefix="/api/cma/products", tags=["产品矩阵"],
|
||||
dependencies=[Depends(require_auth_401)])
|
||||
|
||||
|
||||
def _calc_quadrant(trend: float, margin: float) -> str:
|
||||
|
||||
@@ -3,9 +3,14 @@ from fastapi import APIRouter, Depends, HTTPException
|
||||
from pydantic import BaseModel
|
||||
from sqlalchemy.orm import Session
|
||||
from app.database import get_db
|
||||
from app.auth_middleware import require_auth_401
|
||||
from app.models import SystemConfig, Entity
|
||||
|
||||
router = APIRouter(prefix="/api/cma/tenant", tags=["多租户"])
|
||||
# 用户端点:统一要求会话 Bearer 鉴权(cma-api-auth-surface-audit-20260914)
|
||||
# 缺口:本 router 原无鉴权——无 token 可读当前租户、并可 POST /switch 改写
|
||||
# system_config.current_tenant(状态改写面)。收口为 require_auth_401。
|
||||
router = APIRouter(prefix="/api/cma/tenant", tags=["多租户"],
|
||||
dependencies=[Depends(require_auth_401)])
|
||||
|
||||
TENANT_KEY = "current_tenant"
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
|
||||
from sqlalchemy.orm import Session
|
||||
from app.database import get_db
|
||||
from app.models import User, RolePermission
|
||||
from typing import Optional
|
||||
import secrets
|
||||
import json
|
||||
|
||||
@@ -199,6 +200,23 @@ def require_role(*roles: str):
|
||||
return role_checker
|
||||
|
||||
|
||||
# ── 401 语义的认证依赖(cma-api-auth-surface-audit-20260914)──────────────
|
||||
# FastAPI 的 HTTPBearer(auto_error=True) 对"缺 Authorization 头"返回 403;
|
||||
# 安全语义上「未认证」应为 401(401=未认证 / 403=无权)。
|
||||
# 本依赖用 auto_error=False 自行判空 → 缺 token 显式 401,有 token 仍完全委托 require_auth。
|
||||
_optional_bearer = HTTPBearer(auto_error=False)
|
||||
|
||||
|
||||
def require_auth_401(
|
||||
credentials: Optional[HTTPAuthorizationCredentials] = Depends(_optional_bearer),
|
||||
db: Session = Depends(get_db),
|
||||
) -> User:
|
||||
"""用户端点统一鉴权入口:缺 Bearer → 401;有 token → require_auth 校验(无效 → 401)"""
|
||||
if credentials is None or not credentials.credentials:
|
||||
raise HTTPException(status_code=401, detail="未认证:缺少 Bearer token")
|
||||
return require_auth(credentials=credentials, db=db)
|
||||
|
||||
|
||||
def has_permission(user: User, module: str, db: Session = None) -> bool:
|
||||
routes, _ = _load_permissions(db)
|
||||
return module in routes.get(user.role, [])
|
||||
|
||||
@@ -116,6 +116,15 @@ def assert_entity_scope(
|
||||
- 会话 token 绑定实体存在(登录用户):explicit 与之不符 → 403(含 0=全部)
|
||||
- 无 token(Bot 服务通道):explicit 需存在且 active,否则 403(防越权注入)
|
||||
返回会话账套 entity_id(无 token → None,由调用方按原口径兜底)。
|
||||
|
||||
⚠️ 契约(cma-api-auth-surface-audit-20260914):本函数**不是鉴权**。
|
||||
「无 token → 按 Bot 服务通道放行」这一分支**仅对真正以 X-BOT-KEY 鉴权的 Bot 端点有效**
|
||||
(bot_bridge / bot_kpis / bot_iron_law / bot_suggestions / cash_vouchers_json 等,
|
||||
见 app/api/bot_bridge.py 的 verify_bot_key)。
|
||||
用户端点(前端 Bearer 调用)**必须**在 router 级或端点级挂 `require_auth` /
|
||||
`require_auth_401`;否则未挂鉴权的用户端点 = 公开可访问(无 token 直接取数/写数)。
|
||||
自查:`grep -n 'require_auth' app/api/<文件>.py` 无命中 且 该文件路由非 /api/cma/bot* →
|
||||
即存在鉴权缺口。
|
||||
"""
|
||||
session_entity = get_session_entity(request)
|
||||
if session_entity is not None:
|
||||
|
||||
+10
-15
@@ -10,7 +10,7 @@ from app.database import init_db, get_db
|
||||
from app.api import auth, kpis, kpi_governance, templates, maps, dashboard, data, alerts, ai_analysis, ai_suggestions, alert_rules, users, thresholds, notifications, permissions, action_plans, alignment, org, objectives, versions, budget, cost, predict, reports, security, knowledge, bot_bridge, bot_bridge_v2, bot_suggestions, lead, tenant, customer_dashboard, deviation_push, budget_generate, knowledge_articles, kpi_causality, data_quality, bi_reports, entities, bsc_layers, okr, okr_templates, subjects, driver_budget, bot_kpis, ontology, bot_iron_law, analysis_results, expenses, cash, tax_compliance, verify, growth_quality, products, data_classification, value_sources, zero_based, derivation_rules, cash_classify, cashflow_structure, cash_vouchers_json
|
||||
from app.utils.cache import clear_all as clear_cache, delete as delete_cache
|
||||
from scripts.erp_sync import run_sync as run_erp_sync
|
||||
from app.auth_middleware import require_auth
|
||||
from app.auth_middleware import require_auth, require_auth_401
|
||||
from app.api.audit_log import bot_audit_middleware
|
||||
|
||||
load_dotenv()
|
||||
@@ -118,18 +118,8 @@ _ERP_CHANNEL_DISABLED_REASON = ("ERP直连同步通道已于2026-08-31停用(e
|
||||
|
||||
# ── admin 端点鉴权(2026-09-13)──────────────────────────────
|
||||
# 未带 token → 显式 401(缺少认证),校验逻辑仍完全委托 require_auth(无效 token → 401)。
|
||||
# 说明:FastAPI 的 HTTPBearer(auto_error=True) 对"缺 header"返回 403;本端点验收要求 401,
|
||||
# 故用 auto_error=False 的实例自行判空,不改动全局 require_auth(避免其他端点行为漂移)。
|
||||
_optional_bearer = HTTPBearer(auto_error=False)
|
||||
|
||||
|
||||
def require_auth_401(
|
||||
credentials: Optional[HTTPAuthorizationCredentials] = Depends(_optional_bearer),
|
||||
db=Depends(get_db),
|
||||
):
|
||||
if credentials is None or not credentials.credentials:
|
||||
raise HTTPException(status_code=401, detail="未认证:缺少 Bearer token")
|
||||
return require_auth(credentials=credentials, db=db)
|
||||
# 依赖实现已上移到 app/auth_middleware.py 的 require_auth_401(用户端点统一入口,
|
||||
# cma-api-auth-surface-audit-20260914):原局部定义等价迁移,其他端点行为不变。
|
||||
|
||||
|
||||
@app.post("/api/cma/admin/erp-sync")
|
||||
@@ -184,8 +174,13 @@ def admin_check_alerts(entity_id: Optional[int] = None, user=Depends(require_aut
|
||||
|
||||
|
||||
@app.post("/api/cma/admin/cache/clear")
|
||||
def admin_clear_cache(module: str = None):
|
||||
"""清空缓存,指定module则只清该模块"""
|
||||
def admin_clear_cache(module: str = None, user=Depends(require_auth_401)):
|
||||
"""清空缓存,指定module则只清该模块
|
||||
|
||||
2026-09-14 安全修复(cma-api-auth-surface-audit-20260914):本端点原为无鉴权公开
|
||||
可写端点(无 token 亦可清空全站缓存 → 可被用于性能打击);补 require_auth_401,
|
||||
未带 token → 401。
|
||||
"""
|
||||
if module:
|
||||
delete_cache(module)
|
||||
return {"message": f"缓存已清空: {module}"}
|
||||
|
||||
Reference in New Issue
Block a user