Files
cma-management/backend/app/api/tenant.py
fullstack-bot b815b7f623 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.
2026-09-14 16:41:15 +08:00

69 lines
2.4 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""租户状态API — CMA系统切换公司时记录当前tenant,供项目Bot分发A2A任务"""
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
# 用户端点:统一要求会话 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"
class TenantSwitchRequest(BaseModel):
entity_id: int
source: str = "cma-system"
def _tenant_name(entity_id: int) -> str:
"""entity_id → tenant_id"""
return "company_b" if entity_id == 2 else "company_a"
@router.get("/current")
def get_current_tenant(db: Session = Depends(get_db)):
"""查询当前租户"""
cfg = db.query(SystemConfig).filter(SystemConfig.config_key == TENANT_KEY).first()
if cfg and cfg.config_value:
import json
try:
return json.loads(cfg.config_value)
except:
pass
return {"tenant_id": "company_a", "entity_id": 1, "name": "陕西酣客文化传媒"}
@router.post("/switch")
def switch_tenant(data: TenantSwitchRequest, db: Session = Depends(get_db)):
"""切换当前租户(CMA前端企业切换器调用)"""
ent = db.query(Entity).filter(Entity.id == data.entity_id).first()
if not ent:
raise HTTPException(404, "企业不存在")
import json
state = {
"tenant_id": _tenant_name(data.entity_id),
"entity_id": data.entity_id,
"name": ent.name,
"short_name": ent.short_name,
"source": data.source,
"switched_at": __import__("datetime").datetime.now().isoformat(),
}
cfg = db.query(SystemConfig).filter(SystemConfig.config_key == TENANT_KEY).first()
if cfg:
cfg.config_value = json.dumps(state, ensure_ascii=False)
else:
db.add(SystemConfig(
config_key=TENANT_KEY,
config_value=json.dumps(state, ensure_ascii=False),
description="当前租户状态(CMA企业切换联动)",
))
db.commit()
return {"success": True, "current": state}