admin_permission.py 9.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252
  1. """管理类权限判定:分类/机构/用户编辑开关与管理范围锚点。
  2. 语义(管理权限矩阵化后):
  3. - 三管理位(manage_category_edit / manage_org_edit / manage_user_edit)挂在
  4. 角色上,判定读"生效角色"(用户显式角色,未挂角色回退默认角色),
  5. 且仅对 ADMIN 类别生效;分类编辑全局生效,机构/用户编辑受管理范围约束;
  6. - 角色上 managed_org_id 为管理范围锚点:NULL=不受范围约束(全局),否则
  7. 机构/用户编辑限定锚点部门及其下级子树(沿启用边BFS);
  8. - 内置 admin 账号恒为全权超管,不受开关与范围约束。
  9. """
  10. from __future__ import annotations
  11. from sqlalchemy import select
  12. from dms.common.enums import EnabledStatus, RoleCode
  13. from dms.common.errors import ForbiddenError
  14. from dms.models import Organization, Role, User
  15. from dms.security.auth_context import get_auth_context
  16. SUPER_ADMIN_USERNAME = "admin"
  17. # managed_org_id 哨兵值 0 = "SELF"(锚点跟随用户所在部门):
  18. # 0 不是合法机构ID,机构/用户编辑范围按各用户自身 organization_id 收敛。
  19. MANAGED_ORG_SELF_ANCHOR = 0
  20. MANAGED_ORG_SELF = "SELF"
  21. def effective_role_snapshot(session, role_id: int | None) -> Role | None:
  22. """按显式角色ID解析生效角色快照(活跃且启用优先,回退默认角色)。
  23. 与 effective_manage_role 同语义,用于尚未落库的目标用户
  24. (创建/编辑用户时的防提权校验)。
  25. """
  26. if role_id is not None:
  27. role = session.get(Role, role_id)
  28. if (
  29. role is not None
  30. and not role.is_deleted
  31. and role.status == EnabledStatus.ENABLED.value
  32. ):
  33. return role
  34. return session.scalar(
  35. select(Role).where(
  36. Role.is_default.is_(True),
  37. Role.is_deleted.is_(False),
  38. Role.status == EnabledStatus.ENABLED.value,
  39. )
  40. )
  41. def effective_manage_role(session, user: User) -> Role | None:
  42. """生效角色:显式角色(活跃且启用)优先,否则回退默认角色(活跃且启用)。"""
  43. return effective_role_snapshot(session, user.role_id)
  44. def is_super_admin(user: User) -> bool:
  45. """内置 admin 账号恒为超管,不受三管理位与管理范围约束。"""
  46. return user.username == SUPER_ADMIN_USERNAME
  47. def _has_manage_flag(session, user: User, flag: str) -> bool:
  48. if user.role_code != RoleCode.ADMIN.value:
  49. return False
  50. role = effective_manage_role(session, user)
  51. return role is not None and bool(getattr(role, flag))
  52. def can_edit_categories(session, user: User) -> bool:
  53. if is_super_admin(user):
  54. return True
  55. return _has_manage_flag(session, user, "manage_category_edit")
  56. def can_edit_organizations(session, user: User) -> bool:
  57. if is_super_admin(user):
  58. return True
  59. return _has_manage_flag(session, user, "manage_org_edit")
  60. def can_edit_users(session, user: User) -> bool:
  61. if is_super_admin(user):
  62. return True
  63. return _has_manage_flag(session, user, "manage_user_edit")
  64. def can_manage_system(session, user: User) -> bool:
  65. """系统管理页签(契约1.23):manage_system 管理位。"""
  66. if is_super_admin(user):
  67. return True
  68. return _has_manage_flag(session, user, "manage_system")
  69. def can_view_audit_log(session, user: User) -> bool:
  70. """日志审计页签(契约1.23):manage_audit_log 管理位。"""
  71. if is_super_admin(user):
  72. return True
  73. return _has_manage_flag(session, user, "manage_audit_log")
  74. def can_edit_roles(session, user: User) -> bool:
  75. """角色管理写操作(契约1.25):manage_role_edit 管理位。
  76. 角色为全局对象,不受管理范围锚点约束(同分类编辑)。
  77. """
  78. if is_super_admin(user):
  79. return True
  80. return _has_manage_flag(session, user, "manage_role_edit")
  81. def require_category_editor(session) -> User:
  82. """取当前认证用户并校验分类编辑权限。"""
  83. actor = session.get(User, get_auth_context().user_id)
  84. if actor is None or not can_edit_categories(session, actor):
  85. raise ForbiddenError("无分类编辑权限")
  86. return actor
  87. def require_organization_editor(session) -> User:
  88. """取当前认证用户并校验机构编辑权限(范围按目标另行判定)。"""
  89. actor = session.get(User, get_auth_context().user_id)
  90. if actor is None or not can_edit_organizations(session, actor):
  91. raise ForbiddenError("无机构编辑权限")
  92. return actor
  93. def require_user_editor(session) -> User:
  94. """取当前认证用户并校验用户编辑权限(范围按目标另行判定)。"""
  95. actor = session.get(User, get_auth_context().user_id)
  96. if actor is None or not can_edit_users(session, actor):
  97. raise ForbiddenError("无用户编辑权限")
  98. return actor
  99. def require_system_manager(session) -> User:
  100. """取当前认证用户并校验系统管理权限(契约1.23)。"""
  101. actor = session.get(User, get_auth_context().user_id)
  102. if actor is None or not can_manage_system(session, actor):
  103. raise ForbiddenError("无系统管理权限")
  104. return actor
  105. def require_audit_log_viewer(session) -> User:
  106. """取当前认证用户并校验日志审计查看权限(契约1.23)。"""
  107. actor = session.get(User, get_auth_context().user_id)
  108. if actor is None or not can_view_audit_log(session, actor):
  109. raise ForbiddenError("无日志审计权限")
  110. return actor
  111. def require_role_editor(session) -> User:
  112. """取当前认证用户并校验角色编辑权限(契约1.25)。"""
  113. actor = session.get(User, get_auth_context().user_id)
  114. if actor is None or not can_edit_roles(session, actor):
  115. raise ForbiddenError("无角色编辑权限")
  116. return actor
  117. def managed_scope_ids(session, user: User) -> set[int] | None:
  118. """操作者可管理的机构ID集合;None 表示不受范围约束(全局)。
  119. 实际范围以生效角色锚点为准(契约1.30):super_admin 与锚点 NULL 的
  120. 管理员不受约束;角色锚点为 SELF 哨兵(0)时回落用户级管理范围
  121. (sys_user.managed_org_id)——NULL=全局、SELF(0)=用户所在机构
  122. (无部门则无可管理范围)、机构ID=固定锚点;其余按锚点部门子树
  123. (仅沿启用边)收敛。
  124. """
  125. if is_super_admin(user):
  126. return None
  127. role = effective_manage_role(session, user)
  128. anchor = role.managed_org_id if role is not None else None
  129. if anchor is None:
  130. return None
  131. if anchor == MANAGED_ORG_SELF_ANCHOR:
  132. # 角色「跟随用户设置」:取用户自身管理范围(契约1.30)
  133. anchor = user.managed_org_id
  134. if anchor is None:
  135. return None
  136. if anchor == MANAGED_ORG_SELF_ANCHOR:
  137. # 用户范围「跟随所在机构」:按用户所在部门收敛
  138. if user.organization_id is None:
  139. return set()
  140. anchor = user.organization_id
  141. organizations = session.scalars(
  142. select(Organization).where(Organization.is_deleted.is_(False))
  143. ).all()
  144. children: dict[int | None, list[int]] = {}
  145. for item in organizations:
  146. if item.status == EnabledStatus.ENABLED.value:
  147. children.setdefault(item.parent_id, []).append(item.id)
  148. scope = {anchor}
  149. pending = [anchor]
  150. while pending:
  151. current = pending.pop()
  152. for child_id in children.get(current, []):
  153. if child_id not in scope:
  154. scope.add(child_id)
  155. pending.append(child_id)
  156. return scope
  157. def ensure_org_in_managed_scope(session, actor: User, org_id: int | None) -> None:
  158. """校验目标机构在操作者管理范围内。
  159. org_id 为 None 表示顶级机构(或无部门用户):仅全局管理员可操作。
  160. """
  161. scope = managed_scope_ids(session, actor)
  162. if scope is None:
  163. return
  164. if org_id is None or org_id not in scope:
  165. raise ForbiddenError("目标超出管理范围")
  166. def ensure_user_scope_in_managed_scope(
  167. session,
  168. actor: User,
  169. *,
  170. role_code: str,
  171. effective_role: Role | None,
  172. managed_org_id: int | None,
  173. organization_id: int | None,
  174. ) -> None:
  175. """防提权(契约1.30修订):目标用户最终生效管理范围不得超出操作者。
  176. 仅当目标为 ADMIN 且生效角色具备受锚点约束的管理位
  177. (机构/用户编辑位)时校验——无管理位的角色(含默认角色)不构成提权。
  178. 目标生效锚点按「角色锚点优先,角色 SELF 回落用户级三态」解析:
  179. 解析为全局(角色锚点或用户级为 NULL)或范围外机构即拒绝;
  180. 用户级 SELF 收敛到目标所在机构子树(其机构已由既有校验限制在
  181. 操作者范围内,树传递性恒安全)。
  182. """
  183. scope = managed_scope_ids(session, actor)
  184. if scope is None or role_code != RoleCode.ADMIN.value:
  185. return
  186. if effective_role is None or not (
  187. effective_role.manage_org_edit or effective_role.manage_user_edit
  188. ):
  189. return
  190. anchor = effective_role.managed_org_id
  191. if anchor is None:
  192. raise ForbiddenError("目标超出管理范围")
  193. if anchor == MANAGED_ORG_SELF_ANCHOR:
  194. if managed_org_id is None:
  195. raise ForbiddenError("目标超出管理范围")
  196. if managed_org_id == MANAGED_ORG_SELF_ANCHOR:
  197. if organization_id is None:
  198. return
  199. anchor = organization_id
  200. else:
  201. anchor = managed_org_id
  202. if anchor not in scope:
  203. raise ForbiddenError("目标超出管理范围")