web_auth_roles.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408
  1. """Web 管理端登录:基于 V0.3 sys_user / sys_user_role / sys_role 的角色与菜单权限解析
  2. 有效角色 = 用户直接角色 ∪ 经启用职位继承的角色(sys_post_role)。
  3. """
  4. from __future__ import annotations
  5. from typing import List, Optional, Set, Collection
  6. from sqlalchemy import and_
  7. from sqlalchemy.orm import Session
  8. from app.core.config import settings
  9. from app.models.models import (
  10. SysMenu,
  11. SysPost,
  12. SysPostRole,
  13. SysRole,
  14. SysRoleMenu,
  15. SysUserPost,
  16. SysUserRole,
  17. )
  18. # 无有效角色绑定时的 Web 端角色(不可为 super_admin,避免越权)
  19. NO_ROLE_WEB_ROLE = "guest"
  20. # 一级菜单权限码(用于顶部菜单权限控制)
  21. TOP_MENU_PERMS = frozenset({"menu:market", "menu:production", "menu:quality", "menu:operations", "menu:system-manage"})
  22. def _map_role_name_to_web(role_name: str) -> str:
  23. """将 sys_role.role_name 映射为 web-admin 使用的 role 字符串。"""
  24. n = (role_name or "").strip()
  25. if not n:
  26. return "admin"
  27. if "超级" in n or n in ("系统管理员", "超级管理员"):
  28. return "super_admin"
  29. if "管理" in n or "运营" in n:
  30. return "admin"
  31. return "admin"
  32. def _dedupe_role_id_rows(rows) -> List[str]:
  33. out: List[str] = []
  34. seen: Set[str] = set()
  35. for row in rows:
  36. rid = str(row[0]).strip() if row and row[0] else ""
  37. if rid and rid not in seen:
  38. seen.add(rid)
  39. out.append(rid)
  40. return out
  41. def get_direct_enabled_role_ids(
  42. db: Session, user_id: str, tenant_no: Optional[str] = None
  43. ) -> List[str]:
  44. """用户直接绑定且启用的角色 ID(sys_user_role → sys_role)。"""
  45. if not user_id:
  46. return []
  47. tenant = (tenant_no or settings.DEFAULT_TENANT_NO).strip()
  48. rows = (
  49. db.query(SysRole.id)
  50. .join(SysUserRole, SysUserRole.role_id == SysRole.id)
  51. .filter(
  52. SysUserRole.user_id == user_id,
  53. SysUserRole.del_flag == 0,
  54. SysUserRole.tenant_no == tenant,
  55. SysRole.del_flag == 0,
  56. SysRole.status == "1",
  57. SysRole.tenant_no == tenant,
  58. )
  59. .order_by(SysRole.create_time.desc())
  60. .all()
  61. )
  62. return _dedupe_role_id_rows(rows)
  63. def get_post_inherited_role_ids(
  64. db: Session, user_id: str, tenant_no: Optional[str] = None
  65. ) -> List[str]:
  66. """经启用职位继承的启用角色 ID(sys_user_post → sys_post → sys_post_role → sys_role)。"""
  67. if not user_id:
  68. return []
  69. tenant = (tenant_no or settings.DEFAULT_TENANT_NO).strip()
  70. rows = (
  71. db.query(SysRole.id)
  72. .join(SysPostRole, SysPostRole.role_id == SysRole.id)
  73. .join(
  74. SysPost,
  75. and_(
  76. SysPost.id == SysPostRole.post_id,
  77. SysPost.del_flag == 0,
  78. SysPost.status == "1",
  79. SysPost.tenant_no == tenant,
  80. ),
  81. )
  82. .join(
  83. SysUserPost,
  84. and_(
  85. SysUserPost.post_id == SysPost.id,
  86. SysUserPost.user_id == user_id,
  87. SysUserPost.del_flag == 0,
  88. SysUserPost.tenant_no == tenant,
  89. ),
  90. )
  91. .filter(
  92. SysPostRole.del_flag == 0,
  93. SysPostRole.tenant_no == tenant,
  94. SysRole.del_flag == 0,
  95. SysRole.status == "1",
  96. SysRole.tenant_no == tenant,
  97. )
  98. .order_by(SysRole.create_time.desc())
  99. .all()
  100. )
  101. return _dedupe_role_id_rows(rows)
  102. def get_effective_role_ids(
  103. db: Session, user_id: str, tenant_no: Optional[str] = None
  104. ) -> List[str]:
  105. """用户有效角色 ID:直接绑定 ∪ 启用职位关联的启用角色(去重保序)。"""
  106. if not user_id:
  107. return []
  108. out: List[str] = []
  109. seen: Set[str] = set()
  110. for rid in get_direct_enabled_role_ids(
  111. db, user_id, tenant_no=tenant_no
  112. ) + get_post_inherited_role_ids(
  113. db, user_id, tenant_no=tenant_no
  114. ):
  115. if rid not in seen:
  116. seen.add(rid)
  117. out.append(rid)
  118. return out
  119. def _normalize_candidate_user_ids(
  120. candidate_user_ids: Optional[Collection[str]],
  121. ) -> Optional[List[str]]:
  122. if candidate_user_ids is None:
  123. return None
  124. return list(dict.fromkeys([str(u).strip() for u in candidate_user_ids if u and str(u).strip()]))
  125. def list_user_ids_with_effective_role_name(
  126. db: Session,
  127. *,
  128. tenant_no: str,
  129. role_name: str,
  130. candidate_user_ids: Optional[Collection[str]] = None,
  131. ) -> Set[str]:
  132. """拥有指定角色名的用户 ID(直接绑定 ∪ 职位继承)。"""
  133. tenant = (tenant_no or "").strip()
  134. rn = (role_name or "").strip()
  135. if not tenant or not rn:
  136. return set()
  137. candidates = _normalize_candidate_user_ids(candidate_user_ids)
  138. if candidates is not None and not candidates:
  139. return set()
  140. direct_q = (
  141. db.query(SysUserRole.user_id)
  142. .join(SysRole, SysRole.id == SysUserRole.role_id)
  143. .filter(
  144. SysUserRole.del_flag == 0,
  145. SysUserRole.tenant_no == tenant,
  146. SysRole.del_flag == 0,
  147. SysRole.status == "1",
  148. SysRole.tenant_no == tenant,
  149. SysRole.role_name == rn,
  150. )
  151. )
  152. post_q = (
  153. db.query(SysUserPost.user_id)
  154. .join(
  155. SysPost,
  156. and_(
  157. SysPost.id == SysUserPost.post_id,
  158. SysPost.del_flag == 0,
  159. SysPost.status == "1",
  160. SysPost.tenant_no == tenant,
  161. ),
  162. )
  163. .join(
  164. SysPostRole,
  165. and_(
  166. SysPostRole.post_id == SysPost.id,
  167. SysPostRole.del_flag == 0,
  168. SysPostRole.tenant_no == tenant,
  169. ),
  170. )
  171. .join(
  172. SysRole,
  173. and_(
  174. SysRole.id == SysPostRole.role_id,
  175. SysRole.del_flag == 0,
  176. SysRole.status == "1",
  177. SysRole.tenant_no == tenant,
  178. SysRole.role_name == rn,
  179. ),
  180. )
  181. .filter(SysUserPost.del_flag == 0, SysUserPost.tenant_no == tenant)
  182. )
  183. if candidates is not None:
  184. direct_q = direct_q.filter(SysUserRole.user_id.in_(candidates))
  185. post_q = post_q.filter(SysUserPost.user_id.in_(candidates))
  186. out: Set[str] = set()
  187. for row in direct_q.all():
  188. uid = str(row[0]).strip() if row and row[0] else ""
  189. if uid:
  190. out.add(uid)
  191. for row in post_q.all():
  192. uid = str(row[0]).strip() if row and row[0] else ""
  193. if uid:
  194. out.add(uid)
  195. return out
  196. def list_user_ids_with_any_effective_role_ids(
  197. db: Session,
  198. *,
  199. tenant_no: str,
  200. role_ids: List[str],
  201. candidate_user_ids: Optional[Collection[str]] = None,
  202. ) -> List[str]:
  203. """拥有任一指定角色 ID 的用户 ID 列表(直接绑定 ∪ 职位继承,去重)。"""
  204. tenant = (tenant_no or "").strip()
  205. rids = list(dict.fromkeys([str(r).strip() for r in (role_ids or []) if r and str(r).strip()]))
  206. if not tenant or not rids:
  207. return []
  208. candidates = _normalize_candidate_user_ids(candidate_user_ids)
  209. if candidates is not None and not candidates:
  210. return []
  211. direct_q = (
  212. db.query(SysUserRole.user_id)
  213. .join(SysRole, SysRole.id == SysUserRole.role_id)
  214. .filter(
  215. SysUserRole.del_flag == 0,
  216. SysUserRole.tenant_no == tenant,
  217. SysUserRole.role_id.in_(rids),
  218. SysRole.del_flag == 0,
  219. SysRole.status == "1",
  220. SysRole.tenant_no == tenant,
  221. )
  222. )
  223. post_q = (
  224. db.query(SysUserPost.user_id)
  225. .join(
  226. SysPost,
  227. and_(
  228. SysPost.id == SysUserPost.post_id,
  229. SysPost.del_flag == 0,
  230. SysPost.status == "1",
  231. SysPost.tenant_no == tenant,
  232. ),
  233. )
  234. .join(
  235. SysPostRole,
  236. and_(
  237. SysPostRole.post_id == SysPost.id,
  238. SysPostRole.del_flag == 0,
  239. SysPostRole.tenant_no == tenant,
  240. SysPostRole.role_id.in_(rids),
  241. ),
  242. )
  243. .join(
  244. SysRole,
  245. and_(
  246. SysRole.id == SysPostRole.role_id,
  247. SysRole.del_flag == 0,
  248. SysRole.status == "1",
  249. SysRole.tenant_no == tenant,
  250. ),
  251. )
  252. .filter(SysUserPost.del_flag == 0, SysUserPost.tenant_no == tenant)
  253. )
  254. if candidates is not None:
  255. direct_q = direct_q.filter(SysUserRole.user_id.in_(candidates))
  256. post_q = post_q.filter(SysUserPost.user_id.in_(candidates))
  257. out: List[str] = []
  258. seen: Set[str] = set()
  259. for row in list(direct_q.all()) + list(post_q.all()):
  260. uid = str(row[0]).strip() if row and row[0] else ""
  261. if uid and uid not in seen:
  262. seen.add(uid)
  263. out.append(uid)
  264. return out
  265. def get_role_names_by_ids(
  266. db: Session, role_ids: List[str], tenant_no: Optional[str] = None
  267. ) -> List[str]:
  268. """按给定角色 ID 返回启用角色名称(保序、去重)。"""
  269. ordered = list(dict.fromkeys([str(r).strip() for r in (role_ids or []) if r]))
  270. if not ordered:
  271. return []
  272. tenant = (tenant_no or settings.DEFAULT_TENANT_NO).strip()
  273. rows = (
  274. db.query(SysRole.id, SysRole.role_name)
  275. .filter(
  276. SysRole.id.in_(ordered),
  277. SysRole.del_flag == 0,
  278. SysRole.status == "1",
  279. SysRole.tenant_no == tenant,
  280. )
  281. .all()
  282. )
  283. name_by_id = {
  284. str(rid).strip(): str(name).strip()
  285. for rid, name in rows
  286. if rid and name
  287. }
  288. out: List[str] = []
  289. seen: Set[str] = set()
  290. for rid in ordered:
  291. name = name_by_id.get(rid)
  292. if name and name not in seen:
  293. seen.add(name)
  294. out.append(name)
  295. return out
  296. def _active_role_name_rows(
  297. db: Session, user_id: str, tenant_no: Optional[str] = None
  298. ) -> list:
  299. """查询用户当前有效的 sys_role.role_name 列表(含职位继承)。"""
  300. return get_role_names_by_ids(
  301. db,
  302. get_effective_role_ids(db, user_id, tenant_no=tenant_no),
  303. tenant_no=tenant_no,
  304. )
  305. def user_has_active_roles(
  306. db: Session, user_id: str, tenant_no: Optional[str] = None
  307. ) -> bool:
  308. """用户是否至少拥有一个有效角色(直接或经职位继承)。"""
  309. return bool(get_effective_role_ids(db, user_id, tenant_no=tenant_no))
  310. def get_user_role_names(
  311. db: Session, user_id: str, tenant_no: Optional[str] = None
  312. ) -> list:
  313. """用户当前有效的 sys_role.role_name 列表(去重、保序)。"""
  314. names = _active_role_name_rows(db, user_id, tenant_no=tenant_no)
  315. return list(dict.fromkeys(names))
  316. def resolve_web_role_for_user(
  317. db: Session, user_id: str, tenant_no: Optional[str] = None
  318. ) -> str:
  319. """根据用户有效角色解析 Web 端 role(admin / super_admin / guest)。"""
  320. names = _active_role_name_rows(db, user_id, tenant_no=tenant_no)
  321. if not names:
  322. return NO_ROLE_WEB_ROLE
  323. mapped = [_map_role_name_to_web(n) for n in names]
  324. if "super_admin" in mapped:
  325. return "super_admin"
  326. return "admin"
  327. def iter_user_menu_perm_codes(
  328. db: Session, user_id: str, tenant_no: Optional[str] = None
  329. ) -> Set[str]:
  330. """用户通过有效角色关联得到的菜单权限标识(sys_menu.perms,空则退回菜单 id)。
  331. 注:不过滤 visible——F 类型按钮等 visible=0 的菜单仍可通过角色授权生效;
  332. visible 仅控制侧栏/菜单树展示,不影响权限码解析。
  333. 一级菜单(menu:market 等)同样经本函数返回,用于前端顶部菜单权限控制。
  334. """
  335. if not user_id:
  336. return set()
  337. tenant = (tenant_no or settings.DEFAULT_TENANT_NO).strip()
  338. role_ids = get_effective_role_ids(db, user_id, tenant_no=tenant)
  339. if not role_ids:
  340. return set()
  341. try:
  342. q = (
  343. db.query(SysMenu.perms, SysMenu.id)
  344. .join(
  345. SysRoleMenu,
  346. and_(
  347. SysRoleMenu.menu_id == SysMenu.id,
  348. SysRoleMenu.del_flag == 0,
  349. SysRoleMenu.tenant_no == tenant,
  350. SysRoleMenu.role_id.in_(role_ids),
  351. ),
  352. )
  353. .filter(
  354. SysMenu.del_flag == 0,
  355. SysMenu.tenant_no == tenant,
  356. SysMenu.status == "1",
  357. )
  358. .distinct()
  359. )
  360. out: Set[str] = set()
  361. for perms, mid in q.all():
  362. p = (perms or "").strip()
  363. if p:
  364. out.add(p)
  365. elif mid:
  366. out.add(str(mid).strip())
  367. return out
  368. except Exception:
  369. return set()