test_role_authorization.py 9.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273
  1. """需求16 矩阵判定融合测试(authorization_service)。
  2. 覆盖:
  3. - OWN/ASSIGNED/OTHER 部门三分类矩阵判定
  4. - 矩阵与单文档 ACL 的 OR 叠加
  5. - 密级前置与状态前置不可被矩阵绕过
  6. - 无角色→默认角色→无默认则贡献为空
  7. - 显式角色停用后矩阵贡献为空且不回退默认
  8. - plan_allowed_actions 的矩阵操作位叠加(子方案过滤授权/挂载位)
  9. """
  10. from __future__ import annotations
  11. import pytest
  12. from dms.common.enums import RoleCode, SecurityLevel
  13. from dms.extensions import db
  14. from dms.models import Document, Organization, Role, RolePermission, User
  15. from dms.security.auth_context import AuthContext
  16. from dms.services.authorization_service import (
  17. evaluate_plan_access,
  18. plan_allowed_actions,
  19. )
  20. def _make_document(name: str, *, org: Organization, security: str = "SECRET",
  21. visibility: str = "ORGANIZATION", status: str = "PUBLISHED",
  22. document_type: str = "MAIN") -> Document:
  23. item = Document(
  24. document_name=name,
  25. summary=f"{name}摘要",
  26. document_type=document_type,
  27. document_status=status,
  28. security_level=security,
  29. visibility_type=visibility,
  30. organization_id=org.id,
  31. organization_name=org.org_name,
  32. tags=["矩阵测试"],
  33. original_file_name=f"{name}.docx",
  34. file_relative_path=f"original/MATRIX_{name}.docx",
  35. file_extension="docx",
  36. mime_type="application/vnd.openxmlformats-officedocument.wordprocessingml.document",
  37. file_size=128,
  38. file_hash="b" * 64,
  39. search_text=f"{name} 矩阵测试",
  40. created_by_name="系统管理员",
  41. updated_by_name="系统管理员",
  42. )
  43. db.session.add(item)
  44. db.session.flush()
  45. return item
  46. def _make_role(name: str, *, is_default: bool = False,
  47. status: str = "ENABLED", actor_id: int | None = None) -> Role:
  48. role = Role(
  49. role_name=name,
  50. sort_no=10,
  51. status=status,
  52. is_default=is_default,
  53. created_by=actor_id,
  54. updated_by=actor_id,
  55. )
  56. db.session.add(role)
  57. db.session.flush()
  58. return role
  59. def _matrix_row(role_id: int, level: str, scope: str, **flags) -> None:
  60. db.session.add(
  61. RolePermission(
  62. role_id=role_id,
  63. security_level=level,
  64. dept_scope=scope,
  65. **flags,
  66. )
  67. )
  68. def _user_context(user: User) -> AuthContext:
  69. return AuthContext(
  70. user_id=user.id,
  71. username=user.username,
  72. real_name=user.real_name,
  73. organization_id=user.organization_id,
  74. organization_name=user.organization_name,
  75. role_code=RoleCode(user.role_code),
  76. security_level=SecurityLevel(user.security_level),
  77. auth_version=user.auth_version,
  78. )
  79. @pytest.fixture()
  80. def matrix_env(b2_app, role_cleanup):
  81. """在 b2_app 基础上构造矩阵判定数据:机构、用户、文档。"""
  82. with b2_app.app_context():
  83. ops = db.session.scalar(
  84. db.select(Organization).where(Organization.org_code == "ORG_OPS")
  85. )
  86. comms = db.session.scalar(
  87. db.select(Organization).where(Organization.org_code == "ORG_COMMS")
  88. )
  89. user = db.session.scalar(
  90. db.select(User).where(User.username == "user")
  91. )
  92. assert ops and comms and user
  93. db.session.commit()
  94. ids = {"ops": ops.id, "comms": comms.id, "user": user.id}
  95. return b2_app, ids
  96. @pytest.mark.integration
  97. def test_own_scope_matrix_grants_view_when_acl_denies(matrix_env):
  98. app, ids = matrix_env
  99. with app.app_context():
  100. ops = db.session.get(Organization, ids["ops"])
  101. user = db.session.get(User, ids["user"])
  102. role = _make_role("作战部角色", actor_id=user.id)
  103. _matrix_row(
  104. role.id, "SECRET", "OWN", can_view=True, can_download=True
  105. )
  106. # ORGANIZATION 可见性且无匹配 ACL 行:纯矩阵放行
  107. doc = _make_document("矩阵本部门主案", org=ops, visibility="ORGANIZATION")
  108. db.session.commit()
  109. context = _user_context(user)
  110. access = evaluate_plan_access(doc, context)
  111. assert access.allowed is True
  112. assert access.can_download is True
  113. assert "VIEW" in access.matrix_actions
  114. assert "DOWNLOAD" in access.matrix_actions
  115. @pytest.mark.integration
  116. def test_other_scope_without_matrix_denies(matrix_env):
  117. app, ids = matrix_env
  118. with app.app_context():
  119. comms = db.session.get(Organization, ids["comms"])
  120. user = db.session.get(User, ids["user"])
  121. # 文档挂通信部:对作战部用户是 OTHER,且未配置部门
  122. doc = _make_document("矩阵其他部门主案", org=comms, visibility="ORGANIZATION")
  123. db.session.commit()
  124. context = _user_context(user)
  125. access = evaluate_plan_access(doc, context)
  126. assert access.allowed is False
  127. assert access.reason == "ACL"
  128. @pytest.mark.integration
  129. def test_assigned_scope_via_role_departments(matrix_env):
  130. from dms.models import RoleDepartment
  131. app, ids = matrix_env
  132. with app.app_context():
  133. comms = db.session.get(Organization, ids["comms"])
  134. user = db.session.get(User, ids["user"])
  135. role = _make_role("跨部门角色", actor_id=user.id)
  136. _matrix_row(role.id, "SECRET", "ASSIGNED", can_view=True)
  137. db.session.add(
  138. RoleDepartment(
  139. role_id=role.id,
  140. organization_id=comms.id,
  141. )
  142. )
  143. doc = _make_document("矩阵已配置部门主案", org=comms, visibility="ORGANIZATION")
  144. db.session.commit()
  145. context = _user_context(user)
  146. access = evaluate_plan_access(doc, context)
  147. assert access.allowed is True
  148. assert "VIEW" in access.matrix_actions
  149. @pytest.mark.integration
  150. def test_security_and_status_precedence_over_matrix(matrix_env):
  151. app, ids = matrix_env
  152. with app.app_context():
  153. ops = db.session.get(Organization, ids["ops"])
  154. user = db.session.get(User, ids["user"]) # 密级 SECRET
  155. role = _make_role("越权角色", actor_id=user.id)
  156. _matrix_row(role.id, "TOP_SECRET", "OWN", can_view=True)
  157. top_secret_doc = _make_document(
  158. "矩阵绝密主案", org=ops, security="TOP_SECRET"
  159. )
  160. draft_doc = _make_document(
  161. "矩阵草稿主案", org=ops, security="SECRET", status="DRAFT"
  162. )
  163. db.session.commit()
  164. context = _user_context(user)
  165. assert evaluate_plan_access(top_secret_doc, context).reason == "SECURITY"
  166. assert evaluate_plan_access(draft_doc, context).reason == "STATUS"
  167. @pytest.mark.integration
  168. def test_default_role_applies_and_disabled_role_yields_nothing(matrix_env):
  169. app, ids = matrix_env
  170. with app.app_context():
  171. ops = db.session.get(Organization, ids["ops"])
  172. user = db.session.get(User, ids["user"]) # 未挂角色
  173. default_role = _make_role("默认角色", is_default=True, actor_id=user.id)
  174. _matrix_row(default_role.id, "SECRET", "OWN", can_view=True)
  175. doc = _make_document("默认角色主案", org=ops, visibility="ORGANIZATION")
  176. db.session.commit()
  177. context = _user_context(user)
  178. assert evaluate_plan_access(doc, context).allowed is True
  179. # 默认角色停用:无显式角色且无默认 → 矩阵贡献为空
  180. default_role.status = "DISABLED"
  181. db.session.commit()
  182. denied = evaluate_plan_access(doc, context)
  183. assert denied.allowed is False
  184. # 显式挂停用角色:不回退默认角色
  185. disabled_role = _make_role(
  186. "停用角色", status="DISABLED", actor_id=user.id
  187. )
  188. _matrix_row(disabled_role.id, "SECRET", "OWN", can_view=True)
  189. user.role_id = disabled_role.id
  190. db.session.commit()
  191. assert evaluate_plan_access(doc, context).allowed is False
  192. @pytest.mark.integration
  193. def test_matrix_actions_expand_allowed_actions(matrix_env):
  194. app, ids = matrix_env
  195. with app.app_context():
  196. ops = db.session.get(Organization, ids["ops"])
  197. user = db.session.get(User, ids["user"])
  198. role = _make_role("完整权限角色", actor_id=user.id)
  199. _matrix_row(
  200. role.id,
  201. "SECRET",
  202. "OWN",
  203. can_view=True,
  204. can_download=True,
  205. can_edit=True,
  206. can_delete=True,
  207. can_config_permission=True,
  208. can_bind_attachment=True,
  209. can_unbind_attachment=True,
  210. )
  211. main_doc = _make_document("矩阵操作位主案", org=ops)
  212. sub_doc = _make_document(
  213. "矩阵操作位子案",
  214. org=ops,
  215. document_type="SUB_PLAN",
  216. )
  217. sub_doc.root_document_id = main_doc.id
  218. sub_doc.parent_document_id = main_doc.id
  219. db.session.commit()
  220. context = _user_context(user)
  221. main_actions = plan_allowed_actions(
  222. main_doc, context, evaluate_plan_access(main_doc, context)
  223. )
  224. assert set(main_actions) >= {
  225. "VIEW",
  226. "DOWNLOAD",
  227. "EDIT",
  228. "DELETE",
  229. "CONFIG_PERMISSION",
  230. }
  231. sub_actions = plan_allowed_actions(
  232. sub_doc, context, evaluate_plan_access(sub_doc, context)
  233. )
  234. # 子方案过滤授权/挂载/解除挂载
  235. assert "CONFIG_PERMISSION" not in sub_actions
  236. assert "BIND_ATTACHMENT" not in sub_actions
  237. assert "UNBIND_ATTACHMENT" not in sub_actions
  238. assert {"VIEW", "DOWNLOAD", "EDIT", "DELETE"} <= set(sub_actions)