Files
cma-management/backend/app/api/permissions.py
T
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

126 lines
4.6 KiB
Python
Raw 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 — 管理会计OS
支持在页面上配置角色可访问的模块和操作权限
"""
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, require_auth_401
# 用户端点:统一要求会话 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 = [
{"key": "dashboard", "name": "驾驶舱"},
{"key": "kpis", "name": "KPI字典"},
{"key": "kpi_detail", "name": "KPI详情"},
{"key": "maps", "name": "战略地图"},
{"key": "alerts", "name": "预警中心"},
{"key": "ai_analysis", "name": "AI分析"},
{"key": "data_source", "name": "数据管理"},
{"key": "budget", "name": "预算管理"},
{"key": "deviation", "name": "差异分析"},
{"key": "cost", "name": "成本分析"},
{"key": "predict", "name": "预测模拟"},
{"key": "org", "name": "组织管理"},
{"key": "user_manage", "name": "用户管理"},
{"key": "system_config", "name": "通知配置"},
{"key": "role_permissions", "name": "角色权限"},
{"key": "action_plans", "name": "改善行动"},
{"key": "alignment", "name": "KPI目标对齐"},
]
ACTIONS = [
{"key": "read", "name": "读取"},
{"key": "write", "name": "写入"},
{"key": "import", "name": "导入"},
{"key": "export", "name": "导出"},
{"key": "delete", "name": "删除"},
{"key": "approve", "name": "审批"},
{"key": "admin", "name": "管理"},
]
ROLES = [
{"code": "ceo", "name": "CEO"},
{"code": "finance", "name": "财务"},
{"code": "business", "name": "业务"},
{"code": "it", "name": "IT运维"},
]
# 默认权限
DEFAULT_ROUTE_PERMISSIONS = {
"ceo": ["dashboard", "kpis", "kpi_detail", "maps", "alerts", "ai_analysis", "data_source", "budget", "deviation", "cost", "predict", "org", "user_manage", "system_config", "role_permissions", "action_plans", "alignment"],
"finance": ["dashboard", "kpis", "kpi_detail", "maps", "alerts", "ai_analysis", "data_source", "budget", "deviation", "cost", "predict"],
"business": ["dashboard", "kpis", "kpi_detail", "alerts", "budget", "deviation"],
"it": ["dashboard", "kpis", "kpi_detail", "alerts", "data_source", "budget", "deviation", "cost", "predict", "org", "user_manage", "system_config"],
}
DEFAULT_ACTION_PERMISSIONS = {
"ceo": ["read", "approve"],
"finance": ["read", "write", "import", "export"],
"business": ["read", "write"],
"it": ["read", "write", "delete", "admin"],
}
def _get_or_create_defaults(db: Session):
"""获取配置,不存在则创建默认值"""
route_perm = db.query(RolePermission).filter(RolePermission.key == "route_permissions").first()
if not route_perm:
route_perm = RolePermission(key="route_permissions", value=DEFAULT_ROUTE_PERMISSIONS)
db.add(route_perm)
action_perm = db.query(RolePermission).filter(RolePermission.key == "action_permissions").first()
if not action_perm:
action_perm = RolePermission(key="action_permissions", value=DEFAULT_ACTION_PERMISSIONS)
db.add(action_perm)
db.commit()
db.refresh(route_perm)
db.refresh(action_perm)
return route_perm, action_perm
@router.get("/modules")
def list_modules():
"""返回模块和动作定义"""
return {
"modules": MODULES,
"actions": ACTIONS,
"roles": ROLES,
}
@router.get("/config")
def get_permissions(db: Session = Depends(get_db)):
"""获取当前权限配置"""
route_perm, action_perm = _get_or_create_defaults(db)
return {
"route_permissions": route_perm.value,
"action_permissions": action_perm.value,
}
@router.put("/config")
def update_permissions(
data: dict,
db: Session = Depends(get_db),
_=Depends(require_role("ceo", "it")),
):
"""更新权限配置"""
route_perm, action_perm = _get_or_create_defaults(db)
if "route_permissions" in data:
route_perm.value = data["route_permissions"]
if "action_permissions" in data:
action_perm.value = data["action_permissions"]
db.commit()
return {"message": "权限配置已更新"}