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:
fullstack-bot
2026-09-14 16:41:15 +08:00
parent aac1bd1df1
commit b815b7f623
18 changed files with 604 additions and 51 deletions
+7 -1
View File
@@ -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")