data_scope.py 8.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284
  1. """数据权限过滤(V0.3:基于角色 data_scope 的查询级行级过滤)
  2. data_scope 值:
  3. 1 = 全部数据
  4. 2 = 本部门数据
  5. 3 = 本部门及以下数据
  6. 4 = 仅本人数据
  7. 5 = 自定义(sys_role_data_scope_detail)
  8. """
  9. from __future__ import annotations
  10. from typing import List, Optional, Set, Tuple
  11. from fastapi import HTTPException
  12. from sqlalchemy import or_
  13. from sqlalchemy.orm import Session
  14. from app.core.config import settings
  15. from app.models.models import (
  16. SysDept,
  17. SysRole,
  18. SysRoleDataScopeDetail,
  19. SysUser,
  20. SysUserRole,
  21. )
  22. def get_user_dept_id(db: Session, user_id: str) -> str | None:
  23. """从 SysUser 读取用户的 dept_id。"""
  24. row = (
  25. db.query(SysUser.dept_id)
  26. .filter(SysUser.id == user_id, SysUser.del_flag == 0)
  27. .first()
  28. )
  29. return row[0] if row and row[0] else None
  30. def _get_child_dept_ids(db: Session, dept_id: str, tenant_no: str) -> List[str]:
  31. """递归获取 dept_id 及其所有子部门 ID(含自身)。"""
  32. result: List[str] = [dept_id]
  33. queue: List[str] = [dept_id]
  34. while queue:
  35. parents = queue
  36. queue = []
  37. children = (
  38. db.query(SysDept.id)
  39. .filter(
  40. SysDept.parent_id.in_(parents),
  41. SysDept.del_flag == 0,
  42. SysDept.tenant_no == tenant_no,
  43. )
  44. .all()
  45. )
  46. for child in children:
  47. cid = child[0]
  48. if cid not in result:
  49. result.append(cid)
  50. queue.append(cid)
  51. return result
  52. def _resolve_user_role_scopes(
  53. db: Session, user_id: str, tenant_no: str
  54. ) -> List[dict]:
  55. """获取用户所有角色的数据权限配置。
  56. 返回: [{"data_scope": str, "details": [SysRoleDataScopeDetail, ...]}]
  57. """
  58. roles = (
  59. db.query(SysRole)
  60. .join(SysUserRole, SysUserRole.role_id == SysRole.id)
  61. .filter(
  62. SysUserRole.user_id == user_id,
  63. SysUserRole.del_flag == 0,
  64. SysUserRole.tenant_no == tenant_no,
  65. SysRole.del_flag == 0,
  66. SysRole.status == "1",
  67. SysRole.tenant_no == tenant_no,
  68. )
  69. .all()
  70. )
  71. if not roles:
  72. return []
  73. result: List[dict] = []
  74. role_ids = [r.id for r in roles]
  75. all_details = (
  76. db.query(SysRoleDataScopeDetail)
  77. .filter(
  78. SysRoleDataScopeDetail.role_id.in_(role_ids),
  79. SysRoleDataScopeDetail.del_flag == 0,
  80. SysRoleDataScopeDetail.tenant_no == tenant_no,
  81. )
  82. .all()
  83. )
  84. details_by_role: dict[str, list] = {}
  85. for d in all_details:
  86. details_by_role.setdefault(d.role_id, []).append(d)
  87. for role in roles:
  88. result.append(
  89. {
  90. "data_scope": role.data_scope or "1",
  91. "details": details_by_role.get(role.id, []),
  92. }
  93. )
  94. return result
  95. def _collect_scope_ids(
  96. db: Session,
  97. user_info: dict,
  98. user_id: str,
  99. effective_tenant: str,
  100. role_scopes: List[dict],
  101. ) -> Tuple[Set[str], Set[str]]:
  102. """合并多角色数据范围为 dept_ids / user_ids 并集。"""
  103. dept_ids: Set[str] = set()
  104. user_ids: Set[str] = set()
  105. user_dept_id = user_info.get("dept_id") or user_info.get("region_id") or get_user_dept_id(
  106. db, user_id
  107. )
  108. for rs in role_scopes:
  109. scope = rs["data_scope"]
  110. if scope == "2":
  111. if user_dept_id:
  112. dept_ids.add(user_dept_id)
  113. elif scope == "3":
  114. if user_dept_id:
  115. dept_ids.update(_get_child_dept_ids(db, user_dept_id, effective_tenant))
  116. elif scope == "4":
  117. user_ids.add(user_id)
  118. elif scope == "5":
  119. for detail in rs["details"]:
  120. val = (detail.scope_value or "").strip()
  121. if not val:
  122. continue
  123. if detail.scope_type == "dept":
  124. dept_ids.add(val)
  125. elif detail.scope_type == "user":
  126. user_ids.add(val)
  127. return dept_ids, user_ids
  128. def _append_user_scope_conditions(
  129. conditions: list,
  130. model,
  131. user_ids: Set[str],
  132. *,
  133. has_user: bool,
  134. owner_field: Optional[str],
  135. ) -> None:
  136. if not user_ids:
  137. return
  138. user_conds = []
  139. if has_user:
  140. user_conds.append(model.create_by.in_(user_ids))
  141. if owner_field and hasattr(model, owner_field):
  142. user_conds.append(getattr(model, owner_field).in_(user_ids))
  143. if user_conds:
  144. conditions.append(or_(*user_conds))
  145. def apply_data_scope(
  146. query,
  147. model,
  148. user_info: dict,
  149. db: Session,
  150. tenant_no: str = None,
  151. owner_field: Optional[str] = None,
  152. ):
  153. """根据用户角色的数据权限过滤查询。
  154. 适用于业务单据列表/详情(报销单、客户、运单等)。
  155. 以下场景 SHALL NOT 调用本函数(仅租户隔离即可):
  156. - 下拉/选项接口(/options)
  157. - 租户级主数据树与码表(部门、字典、费用项目、产品分类、民族等)
  158. - 已有专用归属过滤的接口(如发票夹按 owner_user_id、待办按审批人)
  159. Args:
  160. query: SQLAlchemy query 对象
  161. model: 查询的主模型类(需要有 create_dept_id / create_by 字段)
  162. user_info: 当前用户信息字典,需含 role / user_id / dept_id 等
  163. db: 数据库 Session
  164. tenant_no: 租户编号,默认使用 settings.DEFAULT_TENANT_NO
  165. owner_field: 可选负责人字段名(如 CRM 的 owner_user_id),与 create_by 取 OR
  166. Returns:
  167. 应用数据权限过滤后的 query
  168. """
  169. if not user_info:
  170. return query
  171. if user_info.get("role") == "super_admin":
  172. return query
  173. user_id = user_info.get("user_id")
  174. if not user_id:
  175. return query
  176. effective_tenant = tenant_no or settings.DEFAULT_TENANT_NO
  177. has_dept = hasattr(model, "create_dept_id")
  178. has_user = hasattr(model, "create_by")
  179. if not has_dept and not has_user:
  180. return query
  181. role_scopes = _resolve_user_role_scopes(db, user_id, effective_tenant)
  182. if not role_scopes:
  183. return query.filter(False)
  184. if any(rs["data_scope"] == "1" for rs in role_scopes):
  185. return query
  186. dept_ids, user_ids = _collect_scope_ids(db, user_info, user_id, effective_tenant, role_scopes)
  187. conditions = []
  188. if dept_ids and has_dept:
  189. conditions.append(model.create_dept_id.in_(dept_ids))
  190. _append_user_scope_conditions(
  191. conditions, model, user_ids, has_user=has_user, owner_field=owner_field
  192. )
  193. if not conditions:
  194. return query.filter(False)
  195. return query.filter(or_(*conditions))
  196. def row_in_data_scope(
  197. db: Session,
  198. row,
  199. user_info: dict,
  200. tenant_no: Optional[str] = None,
  201. owner_field: Optional[str] = None,
  202. ) -> bool:
  203. """判断单行数据是否在当前用户数据权限范围内。"""
  204. if not user_info or row is None:
  205. return False
  206. if user_info.get("role") == "super_admin":
  207. return True
  208. user_id = user_info.get("user_id")
  209. if not user_id:
  210. return False
  211. effective_tenant = tenant_no or settings.DEFAULT_TENANT_NO
  212. role_scopes = _resolve_user_role_scopes(db, user_id, effective_tenant)
  213. if not role_scopes:
  214. return False
  215. if any(rs["data_scope"] == "1" for rs in role_scopes):
  216. return True
  217. dept_ids, user_ids = _collect_scope_ids(db, user_info, user_id, effective_tenant, role_scopes)
  218. row_dept = (getattr(row, "create_dept_id", None) or "").strip()
  219. row_creator = (getattr(row, "create_by", None) or "").strip()
  220. row_owner = (
  221. (getattr(row, owner_field, None) or "").strip() if owner_field else ""
  222. )
  223. if dept_ids and row_dept and row_dept in dept_ids:
  224. return True
  225. if user_ids and row_creator and row_creator in user_ids:
  226. return True
  227. if user_ids and row_owner and row_owner in user_ids:
  228. return True
  229. return False
  230. def assert_row_in_data_scope(
  231. db: Session,
  232. row,
  233. _model,
  234. user_info: dict,
  235. tenant_no: Optional[str] = None,
  236. owner_field: Optional[str] = None,
  237. ) -> None:
  238. """不在数据权限范围内时抛出 403。"""
  239. if not row_in_data_scope(db, row, user_info, tenant_no, owner_field=owner_field):
  240. raise HTTPException(status_code=403, detail="无权访问该数据")