role.py 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207
  1. """自定义业务角色模型:角色、权限矩阵。
  2. 内置 ADMIN/USER 之外的第三层授权体系:
  3. - Role 本体(含全局唯一默认角色标记);
  4. - RolePermission 权限矩阵(密级 × 部门范围 × 7 操作,每角色固定 10 行)。
  5. """
  6. from __future__ import annotations
  7. from sqlalchemy import (
  8. BigInteger,
  9. Boolean,
  10. CheckConstraint,
  11. Computed,
  12. ForeignKey,
  13. Index,
  14. Integer,
  15. String,
  16. UniqueConstraint,
  17. text,
  18. )
  19. from sqlalchemy.orm import Mapped, mapped_column
  20. from dms.database.base import BusinessTableMixin
  21. from dms.extensions import db
  22. class Role(BusinessTableMixin, db.Model):
  23. __tablename__ = "sys_role"
  24. __table_args__ = (
  25. CheckConstraint(
  26. "status IN ('ENABLED', 'DISABLED')",
  27. name="role_status",
  28. ),
  29. # 活跃行角色名唯一:软删后可重建同名角色
  30. UniqueConstraint(
  31. "role_name",
  32. "active_marker",
  33. name="uq_sys_role_role_name_active",
  34. ),
  35. # 全局至多一个默认角色:default_marker 虚拟生成列(非默认/已软删 → NULL)
  36. UniqueConstraint(
  37. "default_marker",
  38. name="uq_sys_role_default_active",
  39. ),
  40. Index(
  41. "ix_sys_role_status_active",
  42. "status",
  43. "is_deleted",
  44. ),
  45. )
  46. role_name: Mapped[str] = mapped_column(String(64), nullable=False)
  47. sort_no: Mapped[int] = mapped_column(
  48. Integer,
  49. nullable=False,
  50. server_default=text("0"),
  51. )
  52. status: Mapped[str] = mapped_column(
  53. String(20),
  54. nullable=False,
  55. server_default=text("'ENABLED'"),
  56. )
  57. remark: Mapped[str | None] = mapped_column(String(255), nullable=True)
  58. is_default: Mapped[bool] = mapped_column(
  59. Boolean,
  60. nullable=False,
  61. server_default=text("0"),
  62. )
  63. # 管理权限位(角色级):分类/机构/用户编辑,判定按用户生效角色读取
  64. manage_category_edit: Mapped[bool] = mapped_column(
  65. Boolean,
  66. nullable=False,
  67. server_default=text("0"),
  68. )
  69. manage_org_edit: Mapped[bool] = mapped_column(
  70. Boolean,
  71. nullable=False,
  72. server_default=text("0"),
  73. )
  74. manage_user_edit: Mapped[bool] = mapped_column(
  75. Boolean,
  76. nullable=False,
  77. server_default=text("0"),
  78. )
  79. # 系统管理(0029迁移契约1.23):系统管理页签与备份/监控/维护接口
  80. manage_system: Mapped[bool] = mapped_column(
  81. Boolean,
  82. nullable=False,
  83. server_default=text("0"),
  84. )
  85. # 日志审计(0029迁移契约1.23):日志审计页签与审计查询接口
  86. manage_audit_log: Mapped[bool] = mapped_column(
  87. Boolean,
  88. nullable=False,
  89. server_default=text("0"),
  90. )
  91. # 角色编辑(0030迁移契约1.25):角色增删改/矩阵/成员关联的写权限,
  92. # 角色为全局对象,不受管理范围锚点约束(同分类编辑)
  93. manage_role_edit: Mapped[bool] = mapped_column(
  94. Boolean,
  95. nullable=False,
  96. server_default=text("0"),
  97. )
  98. # 管理范围锚点部门(NULL=不受范围约束即全局)
  99. managed_org_id: Mapped[int | None] = mapped_column(BigInteger, nullable=True)
  100. active_marker: Mapped[int | None] = mapped_column(
  101. Integer,
  102. Computed("IF(is_deleted = 0, 1, NULL)", persisted=False),
  103. nullable=True,
  104. )
  105. default_marker: Mapped[int | None] = mapped_column(
  106. Integer,
  107. Computed(
  108. "IF(is_default = 1 AND is_deleted = 0, 1, NULL)",
  109. persisted=False,
  110. ),
  111. nullable=True,
  112. )
  113. class RolePermission(BusinessTableMixin, db.Model):
  114. """角色权限矩阵行:密级 × 部门范围 → 8 个操作布尔位。
  115. 每角色固定 10 行(5 密级 × 2 部门范围),整存整取;
  116. 与文档 ACL 为 OR 叠加,矩阵是主权限源。
  117. """
  118. __tablename__ = "sys_role_permission"
  119. __table_args__ = (
  120. CheckConstraint(
  121. "security_level IN "
  122. "('PUBLIC', 'INTERNAL', 'SECRET', 'CONFIDENTIAL', 'TOP_SECRET')",
  123. name="role_permission_security_level",
  124. ),
  125. CheckConstraint(
  126. "dept_scope IN ('OWN', 'OTHER')",
  127. name="dept_scope_v2",
  128. ),
  129. UniqueConstraint(
  130. "role_id",
  131. "security_level",
  132. "dept_scope",
  133. "active_marker",
  134. name="uq_sys_role_permission_active",
  135. ),
  136. Index(
  137. "ix_sys_role_permission_role_active",
  138. "role_id",
  139. "is_deleted",
  140. ),
  141. )
  142. role_id: Mapped[int] = mapped_column(
  143. BigInteger,
  144. ForeignKey("sys_role.id", ondelete="RESTRICT"),
  145. nullable=False,
  146. )
  147. security_level: Mapped[str] = mapped_column(String(32), nullable=False)
  148. dept_scope: Mapped[str] = mapped_column(String(32), nullable=False)
  149. can_view: Mapped[bool] = mapped_column(
  150. Boolean,
  151. nullable=False,
  152. server_default=text("0"),
  153. )
  154. # 上传权限(契约1.31):仅 OWN 行有意义,OTHER 行恒为0(界面不可选);
  155. # 控制管理员"上传主体方案/批量导入方案"入口与 POST /documents 链路
  156. can_upload: Mapped[bool] = mapped_column(
  157. Boolean,
  158. nullable=False,
  159. server_default=text("0"),
  160. )
  161. can_download: Mapped[bool] = mapped_column(
  162. Boolean,
  163. nullable=False,
  164. server_default=text("0"),
  165. )
  166. can_edit: Mapped[bool] = mapped_column(
  167. Boolean,
  168. nullable=False,
  169. server_default=text("0"),
  170. )
  171. can_delete: Mapped[bool] = mapped_column(
  172. Boolean,
  173. nullable=False,
  174. server_default=text("0"),
  175. )
  176. can_config_permission: Mapped[bool] = mapped_column(
  177. Boolean,
  178. nullable=False,
  179. server_default=text("0"),
  180. )
  181. can_bind_attachment: Mapped[bool] = mapped_column(
  182. Boolean,
  183. nullable=False,
  184. server_default=text("0"),
  185. )
  186. can_unbind_attachment: Mapped[bool] = mapped_column(
  187. Boolean,
  188. nullable=False,
  189. server_default=text("0"),
  190. )
  191. active_marker: Mapped[int | None] = mapped_column(
  192. Integer,
  193. Computed("IF(is_deleted = 0, 1, NULL)", persisted=False),
  194. nullable=True,
  195. )