EditAccessIsolationTest.java 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236
  1. package com.dms.service;
  2. import static org.assertj.core.api.Assertions.assertThat;
  3. import static org.assertj.core.api.Assertions.assertThatThrownBy;
  4. import static org.mockito.ArgumentMatchers.any;
  5. import static org.mockito.ArgumentMatchers.eq;
  6. import static org.mockito.Mockito.mock;
  7. import static org.mockito.Mockito.never;
  8. import static org.mockito.Mockito.verify;
  9. import static org.mockito.Mockito.when;
  10. import com.dms.common.DeadlockRetry;
  11. import com.dms.common.DmsException;
  12. import com.dms.common.Errors;
  13. import com.dms.config.DmsProperties;
  14. import com.dms.mapper.AttachmentBindingMapper;
  15. import com.dms.mapper.CategoryMapper;
  16. import com.dms.mapper.DocumentMapper;
  17. import com.dms.mapper.DocumentRelationMapper;
  18. import com.dms.mapper.SubPlanBindingMapper;
  19. import com.dms.model.DocumentEntity;
  20. import com.dms.security.AuthContext;
  21. import com.dms.service.AuthorizationService.AttachmentAccess;
  22. import com.dms.service.AuthorizationService.PlanAccess;
  23. import com.fasterxml.jackson.databind.ObjectMapper;
  24. import java.nio.charset.StandardCharsets;
  25. import java.nio.file.Path;
  26. import java.util.List;
  27. import java.util.Set;
  28. import org.junit.jupiter.api.AfterEach;
  29. import org.junit.jupiter.api.BeforeEach;
  30. import org.junit.jupiter.api.Test;
  31. import org.junit.jupiter.api.io.TempDir;
  32. import org.springframework.mock.web.MockHttpServletRequest;
  33. import org.springframework.transaction.support.TransactionTemplate;
  34. /**
  35. * 操作位隔离补全(编辑视图):无查看权但具备任一操作位的用户,详情、
  36. * 附件详情与关联列表放行元数据视图——正文不下发、查看计数与查看审计
  37. * 不记录;无任何操作位与密级不足维持拒绝。写路径判定不受影响。
  38. */
  39. class EditAccessIsolationTest {
  40. private final DocumentMapper documentMapper = mock(DocumentMapper.class);
  41. private final AttachmentBindingMapper attachmentBindingMapper =
  42. mock(AttachmentBindingMapper.class);
  43. private final CategoryMapper categoryMapper = mock(CategoryMapper.class);
  44. private final SubPlanBindingMapper subPlanBindingMapper = mock(SubPlanBindingMapper.class);
  45. private final AuthorizationService authorizationService = mock(AuthorizationService.class);
  46. private final AuditService auditService = mock(AuditService.class);
  47. private final TransactionTemplate transactionTemplate = mock(TransactionTemplate.class);
  48. private final DocumentQueryService queryService = new DocumentQueryService(
  49. documentMapper, attachmentBindingMapper, categoryMapper, subPlanBindingMapper,
  50. authorizationService, auditService, new ObjectMapper(), transactionTemplate);
  51. @BeforeEach
  52. void setUp() {
  53. AuthContext.set(new AuthContext(9L, "editor", "编辑用户", 10L, "本部门",
  54. "USER", "SECRET", 1, null, 100L));
  55. }
  56. @AfterEach
  57. void clearContext() {
  58. AuthContext.clear();
  59. }
  60. /** 已发布 MAIN 主案,无方案密码,查看计数 7、rowVersion 3、含正文。 */
  61. private DocumentEntity mainDoc() {
  62. DocumentEntity doc = new DocumentEntity();
  63. doc.setId(1L);
  64. doc.setDocumentType("MAIN");
  65. doc.setDocumentStatus("PUBLISHED");
  66. doc.setSecurityLevel("SECRET");
  67. doc.setDocumentName("年度演练方案");
  68. doc.setOrganizationId(10L);
  69. doc.setCategoryId(21L);
  70. doc.setRowVersion(3);
  71. doc.setViewCount(7L);
  72. doc.setContentText("机密正文内容");
  73. java.time.LocalDateTime now = java.time.LocalDateTime.now(java.time.ZoneOffset.UTC);
  74. doc.setCreatedAt(now);
  75. doc.setUpdatedAt(now);
  76. return doc;
  77. }
  78. /** 无查看权、矩阵命中 EDIT 的访问结论(reason=ACL 借贷语义)。 */
  79. private PlanAccess editOnlyAccess(DocumentEntity doc) {
  80. return new PlanAccess(false, "ACL", doc, List.of(), false, Set.of("EDIT"));
  81. }
  82. // ------------------------------------------------------------ 文档详情
  83. @Test
  84. void documentDetailServesMetadataViewForEditOnlyUser() {
  85. DocumentEntity doc = mainDoc();
  86. PlanAccess access = editOnlyAccess(doc);
  87. when(documentMapper.findActivePlanById(1L)).thenReturn(doc);
  88. when(authorizationService.evaluatePlanAccess(doc, AuthContext.get())).thenReturn(access);
  89. when(authorizationService.planAllowedActions(doc, AuthContext.get(), access))
  90. .thenReturn(List.of("EDIT"));
  91. when(authorizationService.detailAccessAllowed(doc, AuthContext.get(), access))
  92. .thenReturn(true);
  93. var result = queryService.getDocument(1L,
  94. new MockHttpServletRequest("GET", "/api/v1/documents/1"));
  95. assertThat(result.get("rowVersion")).isEqualTo(3);
  96. // 编辑视图:正文属查看类内容不下发;查看计数仅回显
  97. assertThat(result.get("contentText")).isNull();
  98. assertThat(result.get("viewCount")).isEqualTo(7L);
  99. verify(documentMapper, never()).incrementViewCount(1L);
  100. verify(auditService, never()).insert(any());
  101. }
  102. @Test
  103. void documentDetailStillForbiddenWithoutAnyAction() {
  104. DocumentEntity doc = mainDoc();
  105. PlanAccess access = new PlanAccess(false, "ACL", doc, List.of(), false, Set.of());
  106. when(documentMapper.findActivePlanById(1L)).thenReturn(doc);
  107. when(authorizationService.evaluatePlanAccess(doc, AuthContext.get())).thenReturn(access);
  108. when(authorizationService.planAllowedActions(doc, AuthContext.get(), access))
  109. .thenReturn(List.of());
  110. assertThatThrownBy(() -> queryService.getDocument(1L,
  111. new MockHttpServletRequest("GET", "/api/v1/documents/1")))
  112. .isInstanceOf(DmsException.class)
  113. .hasMessage("无权查看该文档");
  114. }
  115. @Test
  116. void documentDetailStillForbiddenForSecurityLevel() {
  117. DocumentEntity doc = mainDoc();
  118. PlanAccess access = new PlanAccess(false, "SECURITY", doc, List.of(), false, Set.of());
  119. when(documentMapper.findActivePlanById(1L)).thenReturn(doc);
  120. when(authorizationService.evaluatePlanAccess(doc, AuthContext.get())).thenReturn(access);
  121. assertThatThrownBy(() -> queryService.getDocument(1L,
  122. new MockHttpServletRequest("GET", "/api/v1/documents/1")))
  123. .isInstanceOf(DmsException.class)
  124. .hasMessage(Errors.securityLevelForbidden().getMessage());
  125. }
  126. // ------------------------------------------------------------ 关联列表
  127. @Test
  128. void relationsReadableInEditView(@TempDir Path tempDir) throws Exception {
  129. DmsProperties properties = mock(DmsProperties.class);
  130. when(properties.documentRelationPresetConfigPath())
  131. .thenReturn(tempDir.resolve("relation-presets.json"));
  132. java.nio.file.Files.writeString(tempDir.resolve("relation-presets.json"),
  133. "{\"presets\":[{\"code\":\"BASIS\",\"label\":\"依据\",\"sortNo\":10}]}",
  134. StandardCharsets.UTF_8);
  135. DocumentRelationMapper relationMapper = mock(DocumentRelationMapper.class);
  136. DocumentRelationService relationService = new DocumentRelationService(
  137. documentMapper, relationMapper, queryService, authorizationService,
  138. auditService, mock(DeadlockRetry.class), properties);
  139. DocumentEntity doc = mainDoc();
  140. PlanAccess access = editOnlyAccess(doc);
  141. when(documentMapper.findActivePlanById(1L)).thenReturn(doc);
  142. when(authorizationService.evaluatePlanAccess(doc, AuthContext.get())).thenReturn(access);
  143. when(authorizationService.planAllowedActions(doc, AuthContext.get(), access))
  144. .thenReturn(List.of("EDIT"));
  145. when(authorizationService.detailAccessAllowed(doc, AuthContext.get(), access))
  146. .thenReturn(true);
  147. when(relationMapper.listRelationItems(1L)).thenReturn(List.of());
  148. var result = relationService.listDocumentRelations(1L);
  149. assertThat(result.get("items")).isEqualTo(List.of());
  150. }
  151. // ------------------------------------------------------------ 附件详情
  152. private DocumentEntity attachmentDoc() {
  153. DocumentEntity attachment = new DocumentEntity();
  154. attachment.setId(5L);
  155. attachment.setDocumentType("ATTACHMENT");
  156. attachment.setDocumentStatus("PUBLISHED");
  157. attachment.setSecurityLevel("SECRET");
  158. attachment.setDocumentName("配套方案.pdf");
  159. attachment.setRowVersion(2);
  160. attachment.setViewCount(4L);
  161. java.time.LocalDateTime now = java.time.LocalDateTime.now(java.time.ZoneOffset.UTC);
  162. attachment.setCreatedAt(now);
  163. attachment.setUpdatedAt(now);
  164. return attachment;
  165. }
  166. @Test
  167. void attachmentDetailServesMetadataViewForEditOnlyUser() {
  168. DocumentQueryService documentQueryService = mock(DocumentQueryService.class);
  169. AttachmentQueryService attachmentService = new AttachmentQueryService(
  170. attachmentBindingMapper, documentMapper, authorizationService,
  171. documentQueryService);
  172. DocumentEntity attachment = attachmentDoc();
  173. when(documentMapper.findActiveAttachmentById(5L)).thenReturn(attachment);
  174. when(authorizationService.requireAttachmentAccess(attachment, AuthContext.get(), false))
  175. .thenThrow(Errors.documentViewForbidden());
  176. when(authorizationService.attachmentAllowedActions(
  177. eq(AuthContext.get()), eq(attachment), any()))
  178. .thenReturn(List.of("EDIT"));
  179. when(authorizationService.evaluateAttachmentAccess(attachment, AuthContext.get()))
  180. .thenReturn(new AttachmentAccess(false, false));
  181. when(authorizationService.attachmentEditAllowed(attachment, AuthContext.get()))
  182. .thenReturn(true);
  183. var result = attachmentService.getAttachment(5L,
  184. new MockHttpServletRequest("GET", "/api/v1/attachments/5"));
  185. // 编辑视图:VIEW 位不下发、查看计数仅回显不增计
  186. assertThat(result.get("allowedActions")).isEqualTo(List.of("EDIT"));
  187. assertThat(result.get("viewCount")).isEqualTo(4L);
  188. verify(documentQueryService, never()).recordView(any(), any());
  189. }
  190. @Test
  191. void attachmentDetailStillForbiddenWithoutAnyAction() {
  192. DocumentQueryService documentQueryService = mock(DocumentQueryService.class);
  193. AttachmentQueryService attachmentService = new AttachmentQueryService(
  194. attachmentBindingMapper, documentMapper, authorizationService,
  195. documentQueryService);
  196. DocumentEntity attachment = attachmentDoc();
  197. when(documentMapper.findActiveAttachmentById(5L)).thenReturn(attachment);
  198. when(authorizationService.requireAttachmentAccess(attachment, AuthContext.get(), false))
  199. .thenThrow(Errors.documentViewForbidden());
  200. when(authorizationService.attachmentAllowedActions(
  201. eq(AuthContext.get()), eq(attachment), any()))
  202. .thenReturn(List.of());
  203. assertThatThrownBy(() -> attachmentService.getAttachment(5L,
  204. new MockHttpServletRequest("GET", "/api/v1/attachments/5")))
  205. .isInstanceOf(DmsException.class)
  206. .hasMessage("无权查看该文档");
  207. }
  208. }