menu_guard.py 5.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150
  1. """菜单 code 权限守卫(V0.3:通过 sys_user_role → sys_role_menu → sys_menu.perms)"""
  2. from typing import Any, Callable, Dict, Iterable, Set
  3. from fastapi import Depends, HTTPException
  4. from sqlalchemy.orm import Session
  5. from app.core.auth import get_current_user
  6. from app.core.database import get_db
  7. from app.models.models import SysUser
  8. from app.services.web_auth_roles import iter_user_menu_perm_codes
  9. ADMIN_ROLES = {"admin", "super_admin"}
  10. SUPER_ADMIN_ROLE = "super_admin"
  11. def require_admin_or_super_admin(current_user: dict = Depends(get_current_user)):
  12. """运营端角色守卫:admin / super_admin 可访问。"""
  13. role = current_user.get("role")
  14. if role not in ADMIN_ROLES:
  15. raise HTTPException(status_code=403, detail="需要运营人员权限")
  16. return current_user
  17. def get_user_menu_codes(db: Session, user_id: str) -> Set[str]:
  18. """读取用户授权的菜单权限标识集合(与前端 meta.menuCode / perms 对齐)。"""
  19. return iter_user_menu_perm_codes(db, user_id or "")
  20. def user_has_menu_perm(db: Session, user: dict, perm_code: str) -> bool:
  21. """判断用户是否拥有指定权限码(不拦截请求,用于字段/按钮级能力控制)。"""
  22. code = (perm_code or "").strip()
  23. if not code:
  24. return False
  25. if user.get("role") == SUPER_ADMIN_ROLE:
  26. return True
  27. user_id = (user.get("user_id") or "").strip()
  28. if not user_id:
  29. return False
  30. return code in get_user_menu_codes(db, user_id)
  31. def strip_row_fields(rows: Iterable[Dict[str, Any]], *field_names: str) -> None:
  32. """从响应行中移除敏感字段(原地修改)。"""
  33. keys = [k for k in field_names if k]
  34. if not keys:
  35. return
  36. for row in rows:
  37. for key in keys:
  38. row.pop(key, None)
  39. def require_menu_code(menu_code: str) -> Callable:
  40. """接口菜单权限守卫(已统一放行 menuCode 校验,仅保留运营端登录)。"""
  41. _ = menu_code # 保留参数以兼容各路由 Depends(require_menu_code("..."))
  42. return require_admin_or_super_admin
  43. def require_any_menu_code(*menu_codes: str) -> Callable:
  44. """接口菜单权限守卫(已统一放行 menuCode 校验,仅保留运营端登录)。"""
  45. _ = menu_codes
  46. return require_admin_or_super_admin
  47. def _strict_user_identity(db: Session, user: dict) -> tuple[str, str]:
  48. """从用户表读取当前租户,拒绝信任 JWT/请求中的租户覆盖。"""
  49. user_id = str((user or {}).get("user_id") or "").strip()
  50. if not user_id:
  51. return "", ""
  52. row = (
  53. db.query(SysUser.id, SysUser.tenant_no)
  54. .filter(
  55. SysUser.id == user_id,
  56. SysUser.del_flag == 0,
  57. SysUser.status == "1",
  58. )
  59. .first()
  60. )
  61. if not row:
  62. return "", ""
  63. return str(row[0] or "").strip(), str(row[1] or "").strip()
  64. def strict_user_has_any_menu_perm(
  65. db: Session,
  66. user: dict,
  67. menu_codes: Iterable[str],
  68. ) -> bool:
  69. """按用户真实租户严格判断任一菜单权限,含直接与职位继承角色。"""
  70. required = {
  71. str(code or "").strip() for code in menu_codes if str(code or "").strip()
  72. }
  73. if not required:
  74. return False
  75. user_id, tenant_no = _strict_user_identity(db, user)
  76. if not user_id or not tenant_no:
  77. return False
  78. granted = iter_user_menu_perm_codes(db, user_id, tenant_no=tenant_no)
  79. return bool(required.intersection(granted))
  80. def strict_user_has_menu_perm(db: Session, user: dict, menu_code: str) -> bool:
  81. """按用户真实租户严格判断单个菜单权限。"""
  82. return strict_user_has_any_menu_perm(db, user, (menu_code,))
  83. def assert_strict_menu_code(
  84. db: Session,
  85. user: dict,
  86. menu_code: str,
  87. ) -> None:
  88. """路由内部字段级权限断言。"""
  89. if not strict_user_has_menu_perm(db, user, menu_code):
  90. raise HTTPException(status_code=403, detail="无对应菜单权限")
  91. def require_strict_menu_code(menu_code: str) -> Callable:
  92. """租户化的精确菜单权限依赖;与历史宽松守卫并存。"""
  93. required = str(menu_code or "").strip()
  94. async def dependency(
  95. user: dict = Depends(get_current_user),
  96. db: Session = Depends(get_db),
  97. ) -> dict:
  98. assert_strict_menu_code(db, user, required)
  99. return user
  100. dependency.strict_menu_codes = (required,)
  101. return dependency
  102. def require_strict_any_menu_code(*menu_codes: str) -> Callable:
  103. """租户化的任一菜单权限依赖。"""
  104. required = tuple(
  105. dict.fromkeys(
  106. str(code or "").strip()
  107. for code in menu_codes
  108. if str(code or "").strip()
  109. )
  110. )
  111. async def dependency(
  112. user: dict = Depends(get_current_user),
  113. db: Session = Depends(get_db),
  114. ) -> dict:
  115. if not strict_user_has_any_menu_perm(db, user, required):
  116. raise HTTPException(status_code=403, detail="无对应菜单权限")
  117. return user
  118. dependency.strict_menu_codes = required
  119. return dependency