AuthorizationServicePlanSplitTest.java 9.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216
  1. package com.dms.service;
  2. import static org.assertj.core.api.Assertions.assertThat;
  3. import static org.assertj.core.api.Assertions.assertThatCode;
  4. import static org.assertj.core.api.Assertions.assertThatThrownBy;
  5. import static org.mockito.Mockito.mock;
  6. import static org.mockito.Mockito.when;
  7. import com.dms.common.DmsException;
  8. import com.dms.mapper.AttachmentBindingMapper;
  9. import com.dms.mapper.CategoryMapper;
  10. import com.dms.mapper.DocumentMapper;
  11. import com.dms.mapper.OrganizationMapper;
  12. import com.dms.mapper.PermissionMapper;
  13. import com.dms.mapper.RoleMapper;
  14. import com.dms.mapper.RolePermissionMapper;
  15. import com.dms.mapper.UserMapper;
  16. import com.dms.model.DocumentEntity;
  17. import com.dms.model.OrganizationEntity;
  18. import com.dms.model.PermissionEntity;
  19. import com.dms.model.RoleEntity;
  20. import com.dms.model.RolePermissionEntity;
  21. import com.dms.security.AuthContext;
  22. import java.time.LocalDateTime;
  23. import java.time.ZoneOffset;
  24. import java.util.List;
  25. import org.junit.jupiter.api.Test;
  26. /**
  27. * 契约1.36「操作位完全拆分」专项回归(对齐 Python 端
  28. * test_role_authorization 的 1.36 用例,Python 版废弃后由本类守护):
  29. * 查看、下载、编辑、删除等操作位各自独立判定,查看不再是其他位的前置;
  30. * 密级闸仍优先于一切操作位不可绕过。
  31. */
  32. class AuthorizationServicePlanSplitTest {
  33. private final DocumentMapper documentMapper = mock(DocumentMapper.class);
  34. private final PermissionMapper permissionMapper = mock(PermissionMapper.class);
  35. private final AttachmentBindingMapper attachmentBindingMapper = mock(AttachmentBindingMapper.class);
  36. private final OrganizationMapper organizationMapper = mock(OrganizationMapper.class);
  37. private final UserMapper userMapper = mock(UserMapper.class);
  38. private final RoleMapper roleMapper = mock(RoleMapper.class);
  39. private final RolePermissionMapper rolePermissionMapper = mock(RolePermissionMapper.class);
  40. private final CategoryMapper categoryMapper = mock(CategoryMapper.class);
  41. private final AuthorizationService service = new AuthorizationService(
  42. documentMapper, categoryMapper, permissionMapper, attachmentBindingMapper,
  43. organizationMapper, userMapper, roleMapper, rolePermissionMapper);
  44. // ------------------------------------------------------------ 固定场景
  45. /** 已发布 MAIN 主案,密级 SECRET,归属机构 10。 */
  46. private DocumentEntity mainDoc() {
  47. DocumentEntity doc = new DocumentEntity();
  48. doc.setId(1L);
  49. doc.setDocumentType("MAIN");
  50. doc.setDocumentStatus("PUBLISHED");
  51. doc.setSecurityLevel("SECRET");
  52. doc.setOrganizationId(10L);
  53. doc.setCategoryId(21L);
  54. return doc;
  55. }
  56. /** USER 角色用户(id=9),机构 10,密级 SECRET(满足文档密级),会话角色100。 */
  57. private AuthContext userContext() {
  58. return new AuthContext(9L, "editor", "编辑用户", 10L, "本部门",
  59. "USER", "SECRET", 1, null, 100L);
  60. }
  61. /** 矩阵行(角色100 × SECRET × OWN),按入参开位。 */
  62. private RolePermissionEntity matrix(Boolean canView, Boolean canDownload,
  63. Boolean canEdit, Boolean canDelete) {
  64. RolePermissionEntity row = new RolePermissionEntity();
  65. row.setRoleId(100L);
  66. row.setSecurityLevel("SECRET");
  67. row.setDeptScope("OWN");
  68. row.setCanView(canView);
  69. row.setCanDownload(canDownload);
  70. row.setCanEdit(canEdit);
  71. row.setCanDelete(canDelete);
  72. return row;
  73. }
  74. /** 生效角色100(会话角色直达,不查库)、用户与文档同机构(OWN)、
  75. * 文档一级分类 21(契约1.48),矩阵行按入参生效。 */
  76. private void stubUserRoleOrgAndMatrix(RolePermissionEntity row) {
  77. RoleEntity role = new RoleEntity();
  78. role.setId(100L);
  79. when(roleMapper.findActiveEnabledById(100L)).thenReturn(role);
  80. OrganizationEntity org = new OrganizationEntity();
  81. org.setId(10L);
  82. org.setOrgPath("/root/dept-a/");
  83. when(organizationMapper.findByIdNotDeleted(10L)).thenReturn(org);
  84. com.dms.model.CategoryEntity top = new com.dms.model.CategoryEntity();
  85. top.setId(21L);
  86. top.setParentId(null);
  87. when(categoryMapper.findByIdNotDeleted(21L)).thenReturn(top);
  88. when(rolePermissionMapper.findMatrixRow(100L, 21L, "MAIN", "SECRET", "OWN"))
  89. .thenReturn(row);
  90. }
  91. /** 借阅 ACL 行:本用户、限时生效、按入参开位。 */
  92. private PermissionEntity acl(Boolean canView, Boolean canDownload) {
  93. PermissionEntity permission = new PermissionEntity();
  94. permission.setDocumentId(1L);
  95. permission.setSubjectId(9L);
  96. permission.setCanView(canView);
  97. permission.setCanDownload(canDownload);
  98. permission.setExpiresAt(LocalDateTime.now(ZoneOffset.UTC).plusDays(1));
  99. return permission;
  100. }
  101. // ------------------------------------------------------------ 用例
  102. @Test
  103. void 矩阵仅勾编辑位_查看与下载均不可_编辑写路径放行其余拒绝() {
  104. stubUserRoleOrgAndMatrix(matrix(false, false, true, false));
  105. when(permissionMapper.listActiveByDocument(1L)).thenReturn(List.of());
  106. DocumentEntity doc = mainDoc();
  107. AuthContext context = userContext();
  108. AuthorizationService.PlanAccess access =
  109. service.evaluatePlanAccess(doc, context);
  110. assertThat(access.allowed()).isFalse();
  111. assertThat(access.reason()).isEqualTo("ACL");
  112. assertThat(access.canDownload()).isFalse();
  113. assertThat(access.matrixActions()).containsExactly("EDIT");
  114. assertThat(service.planAllowedActions(doc, context, access))
  115. .containsExactly("EDIT");
  116. // 编辑位独立生效(不要求查看前置)
  117. assertThatCode(() -> service.requirePlanAction(doc, context, "EDIT"))
  118. .doesNotThrowAnyException();
  119. // 未授予的查看/下载/删除/授权逐位拒绝
  120. assertThatThrownBy(() -> service.requirePlanAction(doc, context, "VIEW"))
  121. .isInstanceOf(DmsException.class)
  122. .hasMessageContaining("无权查看该文档");
  123. assertThatThrownBy(() -> service.requirePlanAction(doc, context, "DOWNLOAD"))
  124. .isInstanceOf(DmsException.class)
  125. .hasMessageContaining("无权下载该文档");
  126. assertThatThrownBy(() -> service.requirePlanAction(doc, context, "DELETE"))
  127. .isInstanceOf(DmsException.class)
  128. .hasMessageContaining("无权删除该文档");
  129. assertThatThrownBy(() -> service.requirePlanAction(doc, context, "CONFIG_PERMISSION"))
  130. .isInstanceOf(DmsException.class)
  131. .hasMessageContaining("无权授权该文档");
  132. }
  133. @Test
  134. void 矩阵仅勾下载位_不可查看但可下载() {
  135. stubUserRoleOrgAndMatrix(matrix(false, true, false, false));
  136. when(permissionMapper.listActiveByDocument(1L)).thenReturn(List.of());
  137. DocumentEntity doc = mainDoc();
  138. AuthContext context = userContext();
  139. AuthorizationService.PlanAccess access =
  140. service.evaluatePlanAccess(doc, context);
  141. assertThat(access.allowed()).isFalse();
  142. assertThat(access.reason()).isEqualTo("ACL");
  143. assertThat(access.canDownload()).isTrue();
  144. assertThat(service.planAllowedActions(doc, context, access))
  145. .containsExactly("DOWNLOAD");
  146. assertThatCode(() -> service.requirePlanAction(doc, context, "DOWNLOAD"))
  147. .doesNotThrowAnyException();
  148. assertThatThrownBy(() -> service.requirePlanAction(doc, context, "VIEW"))
  149. .isInstanceOf(DmsException.class)
  150. .hasMessageContaining("无权查看该文档");
  151. }
  152. @Test
  153. void 借阅ACL仅勾下载_无矩阵命中_下载独立生效() {
  154. stubUserRoleOrgAndMatrix(null);
  155. when(permissionMapper.listActiveByDocument(1L))
  156. .thenReturn(List.of(acl(false, true)));
  157. DocumentEntity doc = mainDoc();
  158. AuthContext context = userContext();
  159. AuthorizationService.PlanAccess access =
  160. service.evaluatePlanAccess(doc, context);
  161. assertThat(access.allowed()).isFalse();
  162. assertThat(access.canDownload()).isTrue();
  163. assertThat(access.permissions()).hasSize(1);
  164. assertThat(service.planAllowedActions(doc, context, access))
  165. .containsExactly("DOWNLOAD");
  166. assertThatCode(() -> service.requirePlanAction(doc, context, "DOWNLOAD"))
  167. .doesNotThrowAnyException();
  168. assertThatThrownBy(() -> service.requirePlanAction(doc, context, "VIEW"))
  169. .isInstanceOf(DmsException.class)
  170. .hasMessageContaining("无权查看该文档");
  171. }
  172. @Test
  173. void 密级不足时操作位拆分不放松密级闸_任何操作一律拒绝() {
  174. // 用户密级 INTERNAL(20) 低于文档 SECRET(30),矩阵即便勾编辑也不放行
  175. stubUserRoleOrgAndMatrix(matrix(false, false, true, false));
  176. DocumentEntity doc = mainDoc();
  177. AuthContext context = new AuthContext(9L, "editor", "低密级用户", 10L,
  178. "本部门", "USER", "INTERNAL", 1, null, 100L);
  179. assertThatThrownBy(() -> service.requirePlanAction(doc, context, "EDIT"))
  180. .isInstanceOf(DmsException.class)
  181. .hasMessageContaining("用户密级不足");
  182. assertThatThrownBy(() -> service.requirePlanAction(doc, context, "DOWNLOAD"))
  183. .isInstanceOf(DmsException.class)
  184. .hasMessageContaining("用户密级不足");
  185. AuthorizationService.PlanAccess access =
  186. service.evaluatePlanAccess(doc, context);
  187. assertThat(access.allowed()).isFalse();
  188. assertThat(access.reason()).isEqualTo("SECURITY");
  189. assertThat(access.canDownload()).isFalse();
  190. }
  191. }