| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236 |
- package com.dms.service;
- import static org.assertj.core.api.Assertions.assertThat;
- import static org.assertj.core.api.Assertions.assertThatThrownBy;
- import static org.mockito.ArgumentMatchers.any;
- import static org.mockito.ArgumentMatchers.eq;
- import static org.mockito.Mockito.mock;
- import static org.mockito.Mockito.never;
- import static org.mockito.Mockito.verify;
- import static org.mockito.Mockito.when;
- import com.dms.common.DeadlockRetry;
- import com.dms.common.DmsException;
- import com.dms.common.Errors;
- import com.dms.config.DmsProperties;
- import com.dms.mapper.AttachmentBindingMapper;
- import com.dms.mapper.CategoryMapper;
- import com.dms.mapper.DocumentMapper;
- import com.dms.mapper.DocumentRelationMapper;
- import com.dms.mapper.SubPlanBindingMapper;
- import com.dms.model.DocumentEntity;
- import com.dms.security.AuthContext;
- import com.dms.service.AuthorizationService.AttachmentAccess;
- import com.dms.service.AuthorizationService.PlanAccess;
- import com.fasterxml.jackson.databind.ObjectMapper;
- import java.nio.charset.StandardCharsets;
- import java.nio.file.Path;
- import java.util.List;
- import java.util.Set;
- import org.junit.jupiter.api.AfterEach;
- import org.junit.jupiter.api.BeforeEach;
- import org.junit.jupiter.api.Test;
- import org.junit.jupiter.api.io.TempDir;
- import org.springframework.mock.web.MockHttpServletRequest;
- import org.springframework.transaction.support.TransactionTemplate;
- /**
- * 操作位隔离补全(编辑视图):无查看权但具备任一操作位的用户,详情、
- * 附件详情与关联列表放行元数据视图——正文不下发、查看计数与查看审计
- * 不记录;无任何操作位与密级不足维持拒绝。写路径判定不受影响。
- */
- class EditAccessIsolationTest {
- private final DocumentMapper documentMapper = mock(DocumentMapper.class);
- private final AttachmentBindingMapper attachmentBindingMapper =
- mock(AttachmentBindingMapper.class);
- private final CategoryMapper categoryMapper = mock(CategoryMapper.class);
- private final SubPlanBindingMapper subPlanBindingMapper = mock(SubPlanBindingMapper.class);
- private final AuthorizationService authorizationService = mock(AuthorizationService.class);
- private final AuditService auditService = mock(AuditService.class);
- private final TransactionTemplate transactionTemplate = mock(TransactionTemplate.class);
- private final DocumentQueryService queryService = new DocumentQueryService(
- documentMapper, attachmentBindingMapper, categoryMapper, subPlanBindingMapper,
- authorizationService, auditService, new ObjectMapper(), transactionTemplate);
- @BeforeEach
- void setUp() {
- AuthContext.set(new AuthContext(9L, "editor", "编辑用户", 10L, "本部门",
- "USER", "SECRET", 1, null, 100L));
- }
- @AfterEach
- void clearContext() {
- AuthContext.clear();
- }
- /** 已发布 MAIN 主案,无方案密码,查看计数 7、rowVersion 3、含正文。 */
- private DocumentEntity mainDoc() {
- DocumentEntity doc = new DocumentEntity();
- doc.setId(1L);
- doc.setDocumentType("MAIN");
- doc.setDocumentStatus("PUBLISHED");
- doc.setSecurityLevel("SECRET");
- doc.setDocumentName("年度演练方案");
- doc.setOrganizationId(10L);
- doc.setCategoryId(21L);
- doc.setRowVersion(3);
- doc.setViewCount(7L);
- doc.setContentText("机密正文内容");
- java.time.LocalDateTime now = java.time.LocalDateTime.now(java.time.ZoneOffset.UTC);
- doc.setCreatedAt(now);
- doc.setUpdatedAt(now);
- return doc;
- }
- /** 无查看权、矩阵命中 EDIT 的访问结论(reason=ACL 借贷语义)。 */
- private PlanAccess editOnlyAccess(DocumentEntity doc) {
- return new PlanAccess(false, "ACL", doc, List.of(), false, Set.of("EDIT"));
- }
- // ------------------------------------------------------------ 文档详情
- @Test
- void documentDetailServesMetadataViewForEditOnlyUser() {
- DocumentEntity doc = mainDoc();
- PlanAccess access = editOnlyAccess(doc);
- when(documentMapper.findActivePlanById(1L)).thenReturn(doc);
- when(authorizationService.evaluatePlanAccess(doc, AuthContext.get())).thenReturn(access);
- when(authorizationService.planAllowedActions(doc, AuthContext.get(), access))
- .thenReturn(List.of("EDIT"));
- when(authorizationService.detailAccessAllowed(doc, AuthContext.get(), access))
- .thenReturn(true);
- var result = queryService.getDocument(1L,
- new MockHttpServletRequest("GET", "/api/v1/documents/1"));
- assertThat(result.get("rowVersion")).isEqualTo(3);
- // 编辑视图:正文属查看类内容不下发;查看计数仅回显
- assertThat(result.get("contentText")).isNull();
- assertThat(result.get("viewCount")).isEqualTo(7L);
- verify(documentMapper, never()).incrementViewCount(1L);
- verify(auditService, never()).insert(any());
- }
- @Test
- void documentDetailStillForbiddenWithoutAnyAction() {
- DocumentEntity doc = mainDoc();
- PlanAccess access = new PlanAccess(false, "ACL", doc, List.of(), false, Set.of());
- when(documentMapper.findActivePlanById(1L)).thenReturn(doc);
- when(authorizationService.evaluatePlanAccess(doc, AuthContext.get())).thenReturn(access);
- when(authorizationService.planAllowedActions(doc, AuthContext.get(), access))
- .thenReturn(List.of());
- assertThatThrownBy(() -> queryService.getDocument(1L,
- new MockHttpServletRequest("GET", "/api/v1/documents/1")))
- .isInstanceOf(DmsException.class)
- .hasMessage("无权查看该文档");
- }
- @Test
- void documentDetailStillForbiddenForSecurityLevel() {
- DocumentEntity doc = mainDoc();
- PlanAccess access = new PlanAccess(false, "SECURITY", doc, List.of(), false, Set.of());
- when(documentMapper.findActivePlanById(1L)).thenReturn(doc);
- when(authorizationService.evaluatePlanAccess(doc, AuthContext.get())).thenReturn(access);
- assertThatThrownBy(() -> queryService.getDocument(1L,
- new MockHttpServletRequest("GET", "/api/v1/documents/1")))
- .isInstanceOf(DmsException.class)
- .hasMessage(Errors.securityLevelForbidden().getMessage());
- }
- // ------------------------------------------------------------ 关联列表
- @Test
- void relationsReadableInEditView(@TempDir Path tempDir) throws Exception {
- DmsProperties properties = mock(DmsProperties.class);
- when(properties.documentRelationPresetConfigPath())
- .thenReturn(tempDir.resolve("relation-presets.json"));
- java.nio.file.Files.writeString(tempDir.resolve("relation-presets.json"),
- "{\"presets\":[{\"code\":\"BASIS\",\"label\":\"依据\",\"sortNo\":10}]}",
- StandardCharsets.UTF_8);
- DocumentRelationMapper relationMapper = mock(DocumentRelationMapper.class);
- DocumentRelationService relationService = new DocumentRelationService(
- documentMapper, relationMapper, queryService, authorizationService,
- auditService, mock(DeadlockRetry.class), properties);
- DocumentEntity doc = mainDoc();
- PlanAccess access = editOnlyAccess(doc);
- when(documentMapper.findActivePlanById(1L)).thenReturn(doc);
- when(authorizationService.evaluatePlanAccess(doc, AuthContext.get())).thenReturn(access);
- when(authorizationService.planAllowedActions(doc, AuthContext.get(), access))
- .thenReturn(List.of("EDIT"));
- when(authorizationService.detailAccessAllowed(doc, AuthContext.get(), access))
- .thenReturn(true);
- when(relationMapper.listRelationItems(1L)).thenReturn(List.of());
- var result = relationService.listDocumentRelations(1L);
- assertThat(result.get("items")).isEqualTo(List.of());
- }
- // ------------------------------------------------------------ 附件详情
- private DocumentEntity attachmentDoc() {
- DocumentEntity attachment = new DocumentEntity();
- attachment.setId(5L);
- attachment.setDocumentType("ATTACHMENT");
- attachment.setDocumentStatus("PUBLISHED");
- attachment.setSecurityLevel("SECRET");
- attachment.setDocumentName("配套方案.pdf");
- attachment.setRowVersion(2);
- attachment.setViewCount(4L);
- java.time.LocalDateTime now = java.time.LocalDateTime.now(java.time.ZoneOffset.UTC);
- attachment.setCreatedAt(now);
- attachment.setUpdatedAt(now);
- return attachment;
- }
- @Test
- void attachmentDetailServesMetadataViewForEditOnlyUser() {
- DocumentQueryService documentQueryService = mock(DocumentQueryService.class);
- AttachmentQueryService attachmentService = new AttachmentQueryService(
- attachmentBindingMapper, documentMapper, authorizationService,
- documentQueryService);
- DocumentEntity attachment = attachmentDoc();
- when(documentMapper.findActiveAttachmentById(5L)).thenReturn(attachment);
- when(authorizationService.requireAttachmentAccess(attachment, AuthContext.get(), false))
- .thenThrow(Errors.documentViewForbidden());
- when(authorizationService.attachmentAllowedActions(
- eq(AuthContext.get()), eq(attachment), any()))
- .thenReturn(List.of("EDIT"));
- when(authorizationService.evaluateAttachmentAccess(attachment, AuthContext.get()))
- .thenReturn(new AttachmentAccess(false, false));
- when(authorizationService.attachmentEditAllowed(attachment, AuthContext.get()))
- .thenReturn(true);
- var result = attachmentService.getAttachment(5L,
- new MockHttpServletRequest("GET", "/api/v1/attachments/5"));
- // 编辑视图:VIEW 位不下发、查看计数仅回显不增计
- assertThat(result.get("allowedActions")).isEqualTo(List.of("EDIT"));
- assertThat(result.get("viewCount")).isEqualTo(4L);
- verify(documentQueryService, never()).recordView(any(), any());
- }
- @Test
- void attachmentDetailStillForbiddenWithoutAnyAction() {
- DocumentQueryService documentQueryService = mock(DocumentQueryService.class);
- AttachmentQueryService attachmentService = new AttachmentQueryService(
- attachmentBindingMapper, documentMapper, authorizationService,
- documentQueryService);
- DocumentEntity attachment = attachmentDoc();
- when(documentMapper.findActiveAttachmentById(5L)).thenReturn(attachment);
- when(authorizationService.requireAttachmentAccess(attachment, AuthContext.get(), false))
- .thenThrow(Errors.documentViewForbidden());
- when(authorizationService.attachmentAllowedActions(
- eq(AuthContext.get()), eq(attachment), any()))
- .thenReturn(List.of());
- assertThatThrownBy(() -> attachmentService.getAttachment(5L,
- new MockHttpServletRequest("GET", "/api/v1/attachments/5")))
- .isInstanceOf(DmsException.class)
- .hasMessage("无权查看该文档");
- }
- }
|