Files
cma-management/backend/app/auth_middleware.py
T
Hermes CI Fix 3dddd36866 init: 管理会计OS初始代码
包含前后端完整代码:
- 前端:Vue3+Vite+ElementPlus
- 后端:FastAPI+SQLAlchemy
- 模块:驾驶舱/KPI/战略地图/预警/预算/成本/预测/改善行动
- 当前版本:v1.0.0
2026-05-28 17:32:22 +08:00

190 lines
6.5 KiB
Python

"""
角色权限中间件 — 管理会计OS
4角色: ceo(CEO/总览), finance(财务), business(业务), it(IT/运维)
权限配置支持从数据库动态加载
"""
from fastapi import Request, HTTPException, Depends
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
from sqlalchemy.orm import Session
from app.database import get_db
from app.models import User, RolePermission
import secrets
import json
# 角色定义(固定)
ROLES = {
"ceo": {"name": "CEO", "priority": 1},
"finance": {"name": "财务", "priority": 2},
"business": {"name": "业务", "priority": 3},
"it": {"name": "IT运维", "priority": 4},
}
# 默认权限(数据库没有时的 fallback)
DEFAULT_ROUTE_PERMISSIONS = {
"ceo": ["dashboard", "kpis", "kpi_detail", "maps", "alerts", "ai_analysis", "data_source", "user_manage", "system_config"],
"finance": ["dashboard", "kpis", "kpi_detail", "maps", "alerts", "ai_analysis", "data_source"],
"business": ["dashboard", "kpis", "kpi_detail", "alerts"],
"it": ["dashboard", "kpis", "kpi_detail", "alerts", "data_source", "user_manage", "system_config"],
}
DEFAULT_KPI_VISIBILITY = {
"ceo": ["*"], # CEO看全部维度
"finance": ["finance_*"], # 财务只看财务
"business": ["customer_*", "process_*", "learning_*"], # 业务看客户/流程/学习
"it": ["*"], # IT看全部(运维)
}
DEFAULT_ACTION_PERMISSIONS = {
"ceo": ["read", "approve"],
"finance": ["read", "write", "import", "export"],
"business": ["read", "write"],
"it": ["read", "write", "delete", "admin"],
}
import logging
logger = logging.getLogger("cma.auth")
# Redis token 存储(跨 worker 共享)
try:
import redis as redis_lib
_redis = redis_lib.Redis(
host="127.0.0.1", port=6379, db=1,
decode_responses=True, socket_connect_timeout=2, socket_timeout=3
)
_redis.ping()
_redis_available = True
except Exception:
_redis = None
_redis_available = False
logger.warning("Redis不可用,token存储降级到内存(不支持多worker)")
# 内存 fallback
_token_store: dict[str, int] = {}
TOKEN_PREFIX = "cma:token:"
TOKEN_TTL = 86400 # 24小时
# 缓存权限配置(每5分钟刷新)
_permissions_cache = {"route": None, "action": None, "ts": 0}
_PERM_CACHE_TTL = 300
def _load_permissions(db: Session = None):
"""从数据库加载权限配置"""
import time
now = time.time()
if db is None:
if now - _permissions_cache["ts"] < _PERM_CACHE_TTL:
return _permissions_cache["route"] or DEFAULT_ROUTE_PERMISSIONS, _permissions_cache["action"] or DEFAULT_ACTION_PERMISSIONS
return DEFAULT_ROUTE_PERMISSIONS, DEFAULT_ACTION_PERMISSIONS
try:
route_perm = db.query(RolePermission).filter(RolePermission.key == "route_permissions").first()
action_perm = db.query(RolePermission).filter(RolePermission.key == "action_permissions").first()
routes = route_perm.value if route_perm else DEFAULT_ROUTE_PERMISSIONS
actions = action_perm.value if action_perm else DEFAULT_ACTION_PERMISSIONS
_permissions_cache["route"] = routes
_permissions_cache["action"] = actions
_permissions_cache["ts"] = now
return routes, actions
except Exception:
return DEFAULT_ROUTE_PERMISSIONS, DEFAULT_ACTION_PERMISSIONS
def create_token(user_id: int) -> str:
token = secrets.token_hex(32)
if _redis_available:
_redis.setex(f"{TOKEN_PREFIX}{token}", TOKEN_TTL, user_id)
else:
_token_store[token] = user_id
return token
def _resolve_user_id(token: str) -> int | None:
if _redis_available:
val = _redis.get(f"{TOKEN_PREFIX}{token}")
if val is not None:
return int(val)
return None
return _token_store.get(token)
def require_auth(
credentials: HTTPAuthorizationCredentials = Depends(HTTPBearer(auto_error=True)),
db: Session = Depends(get_db),
) -> User:
token = credentials.credentials
user_id = _resolve_user_id(token)
if user_id is None:
raise HTTPException(401, "无效的token,请重新登录")
user = db.query(User).filter(User.id == user_id).first()
if not user:
raise HTTPException(401, "用户不存在")
return user
def require_role(*roles: str):
async def role_checker(
current_user: User = Depends(require_auth),
) -> User:
if current_user.role not in roles:
raise HTTPException(403, f"权限不足: 需要 {', '.join(roles)} 角色")
return current_user
return role_checker
def has_permission(user: User, module: str, db: Session = None) -> bool:
routes, _ = _load_permissions(db)
return module in routes.get(user.role, [])
def has_action(user: User, action: str, db: Session = None) -> bool:
_, actions = _load_permissions(db)
return action in actions.get(user.role, [])
# ─── KPI可见性(按维度/分类过滤) ───
def _load_kpi_visibility(db: Session = None):
"""从RolePermission表加载kpi_visibility配置(独立缓存)"""
import time
if not hasattr(_load_kpi_visibility, "_cache"):
_load_kpi_visibility._cache = {"data": None, "ts": 0}
cache = _load_kpi_visibility._cache
now = time.time()
if db is None or (cache["data"] and now - cache["ts"] < _PERM_CACHE_TTL):
return cache["data"] or DEFAULT_KPI_VISIBILITY
try:
perm = db.query(RolePermission).filter(RolePermission.key == "kpi_visibility").first()
cache["data"] = perm.value if perm else DEFAULT_KPI_VISIBILITY
cache["ts"] = now
return cache["data"]
except Exception:
return DEFAULT_KPI_VISIBILITY
def kpi_visible_dims(role: str, db: Session = None) -> list[str]:
"""返回角色可见的维度列表(空列表=全部可见)"""
vis = _load_kpi_visibility(db)
rules = vis.get(role, ["*"])
if "*" in rules:
return [] # 空=全部可见
# 提取维度前缀:finance_* -> finance
dims = set()
for r in rules:
if r.endswith("_*"):
dims.add(r[:-2])
return list(dims)
def filter_kpis_by_role(kpis: list, role: str, db: Session = None) -> list:
"""按角色可见性过滤KPI列表"""
dims = kpi_visible_dims(role, db)
if not dims:
return kpis # 全部可见
return [k for k in kpis if k.dimension in dims or (hasattr(k, 'dimension') and k.dimension in dims)]