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.
69 lines
2.4 KiB
Python
69 lines
2.4 KiB
Python
"""租户状态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}
|