| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216 |
- package com.dms.service;
- import static org.assertj.core.api.Assertions.assertThat;
- import static org.assertj.core.api.Assertions.assertThatCode;
- import static org.assertj.core.api.Assertions.assertThatThrownBy;
- import static org.mockito.Mockito.mock;
- import static org.mockito.Mockito.when;
- import com.dms.common.DmsException;
- import com.dms.mapper.AttachmentBindingMapper;
- import com.dms.mapper.CategoryMapper;
- import com.dms.mapper.DocumentMapper;
- import com.dms.mapper.OrganizationMapper;
- import com.dms.mapper.PermissionMapper;
- import com.dms.mapper.RoleMapper;
- import com.dms.mapper.RolePermissionMapper;
- import com.dms.mapper.UserMapper;
- import com.dms.model.DocumentEntity;
- import com.dms.model.OrganizationEntity;
- import com.dms.model.PermissionEntity;
- import com.dms.model.RoleEntity;
- import com.dms.model.RolePermissionEntity;
- import com.dms.security.AuthContext;
- import java.time.LocalDateTime;
- import java.time.ZoneOffset;
- import java.util.List;
- import org.junit.jupiter.api.Test;
- /**
- * 契约1.36「操作位完全拆分」专项回归(对齐 Python 端
- * test_role_authorization 的 1.36 用例,Python 版废弃后由本类守护):
- * 查看、下载、编辑、删除等操作位各自独立判定,查看不再是其他位的前置;
- * 密级闸仍优先于一切操作位不可绕过。
- */
- class AuthorizationServicePlanSplitTest {
- private final DocumentMapper documentMapper = mock(DocumentMapper.class);
- private final PermissionMapper permissionMapper = mock(PermissionMapper.class);
- private final AttachmentBindingMapper attachmentBindingMapper = mock(AttachmentBindingMapper.class);
- private final OrganizationMapper organizationMapper = mock(OrganizationMapper.class);
- private final UserMapper userMapper = mock(UserMapper.class);
- private final RoleMapper roleMapper = mock(RoleMapper.class);
- private final RolePermissionMapper rolePermissionMapper = mock(RolePermissionMapper.class);
- private final CategoryMapper categoryMapper = mock(CategoryMapper.class);
- private final AuthorizationService service = new AuthorizationService(
- documentMapper, categoryMapper, permissionMapper, attachmentBindingMapper,
- organizationMapper, userMapper, roleMapper, rolePermissionMapper);
- // ------------------------------------------------------------ 固定场景
- /** 已发布 MAIN 主案,密级 SECRET,归属机构 10。 */
- private DocumentEntity mainDoc() {
- DocumentEntity doc = new DocumentEntity();
- doc.setId(1L);
- doc.setDocumentType("MAIN");
- doc.setDocumentStatus("PUBLISHED");
- doc.setSecurityLevel("SECRET");
- doc.setOrganizationId(10L);
- doc.setCategoryId(21L);
- return doc;
- }
- /** USER 角色用户(id=9),机构 10,密级 SECRET(满足文档密级),会话角色100。 */
- private AuthContext userContext() {
- return new AuthContext(9L, "editor", "编辑用户", 10L, "本部门",
- "USER", "SECRET", 1, null, 100L);
- }
- /** 矩阵行(角色100 × SECRET × OWN),按入参开位。 */
- private RolePermissionEntity matrix(Boolean canView, Boolean canDownload,
- Boolean canEdit, Boolean canDelete) {
- RolePermissionEntity row = new RolePermissionEntity();
- row.setRoleId(100L);
- row.setSecurityLevel("SECRET");
- row.setDeptScope("OWN");
- row.setCanView(canView);
- row.setCanDownload(canDownload);
- row.setCanEdit(canEdit);
- row.setCanDelete(canDelete);
- return row;
- }
- /** 生效角色100(会话角色直达,不查库)、用户与文档同机构(OWN)、
- * 文档一级分类 21(契约1.48),矩阵行按入参生效。 */
- private void stubUserRoleOrgAndMatrix(RolePermissionEntity row) {
- RoleEntity role = new RoleEntity();
- role.setId(100L);
- when(roleMapper.findActiveEnabledById(100L)).thenReturn(role);
- OrganizationEntity org = new OrganizationEntity();
- org.setId(10L);
- org.setOrgPath("/root/dept-a/");
- when(organizationMapper.findByIdNotDeleted(10L)).thenReturn(org);
- com.dms.model.CategoryEntity top = new com.dms.model.CategoryEntity();
- top.setId(21L);
- top.setParentId(null);
- when(categoryMapper.findByIdNotDeleted(21L)).thenReturn(top);
- when(rolePermissionMapper.findMatrixRow(100L, 21L, "MAIN", "SECRET", "OWN"))
- .thenReturn(row);
- }
- /** 借阅 ACL 行:本用户、限时生效、按入参开位。 */
- private PermissionEntity acl(Boolean canView, Boolean canDownload) {
- PermissionEntity permission = new PermissionEntity();
- permission.setDocumentId(1L);
- permission.setSubjectId(9L);
- permission.setCanView(canView);
- permission.setCanDownload(canDownload);
- permission.setExpiresAt(LocalDateTime.now(ZoneOffset.UTC).plusDays(1));
- return permission;
- }
- // ------------------------------------------------------------ 用例
- @Test
- void 矩阵仅勾编辑位_查看与下载均不可_编辑写路径放行其余拒绝() {
- stubUserRoleOrgAndMatrix(matrix(false, false, true, false));
- when(permissionMapper.listActiveByDocument(1L)).thenReturn(List.of());
- DocumentEntity doc = mainDoc();
- AuthContext context = userContext();
- AuthorizationService.PlanAccess access =
- service.evaluatePlanAccess(doc, context);
- assertThat(access.allowed()).isFalse();
- assertThat(access.reason()).isEqualTo("ACL");
- assertThat(access.canDownload()).isFalse();
- assertThat(access.matrixActions()).containsExactly("EDIT");
- assertThat(service.planAllowedActions(doc, context, access))
- .containsExactly("EDIT");
- // 编辑位独立生效(不要求查看前置)
- assertThatCode(() -> service.requirePlanAction(doc, context, "EDIT"))
- .doesNotThrowAnyException();
- // 未授予的查看/下载/删除/授权逐位拒绝
- assertThatThrownBy(() -> service.requirePlanAction(doc, context, "VIEW"))
- .isInstanceOf(DmsException.class)
- .hasMessageContaining("无权查看该文档");
- assertThatThrownBy(() -> service.requirePlanAction(doc, context, "DOWNLOAD"))
- .isInstanceOf(DmsException.class)
- .hasMessageContaining("无权下载该文档");
- assertThatThrownBy(() -> service.requirePlanAction(doc, context, "DELETE"))
- .isInstanceOf(DmsException.class)
- .hasMessageContaining("无权删除该文档");
- assertThatThrownBy(() -> service.requirePlanAction(doc, context, "CONFIG_PERMISSION"))
- .isInstanceOf(DmsException.class)
- .hasMessageContaining("无权授权该文档");
- }
- @Test
- void 矩阵仅勾下载位_不可查看但可下载() {
- stubUserRoleOrgAndMatrix(matrix(false, true, false, false));
- when(permissionMapper.listActiveByDocument(1L)).thenReturn(List.of());
- DocumentEntity doc = mainDoc();
- AuthContext context = userContext();
- AuthorizationService.PlanAccess access =
- service.evaluatePlanAccess(doc, context);
- assertThat(access.allowed()).isFalse();
- assertThat(access.reason()).isEqualTo("ACL");
- assertThat(access.canDownload()).isTrue();
- assertThat(service.planAllowedActions(doc, context, access))
- .containsExactly("DOWNLOAD");
- assertThatCode(() -> service.requirePlanAction(doc, context, "DOWNLOAD"))
- .doesNotThrowAnyException();
- assertThatThrownBy(() -> service.requirePlanAction(doc, context, "VIEW"))
- .isInstanceOf(DmsException.class)
- .hasMessageContaining("无权查看该文档");
- }
- @Test
- void 借阅ACL仅勾下载_无矩阵命中_下载独立生效() {
- stubUserRoleOrgAndMatrix(null);
- when(permissionMapper.listActiveByDocument(1L))
- .thenReturn(List.of(acl(false, true)));
- DocumentEntity doc = mainDoc();
- AuthContext context = userContext();
- AuthorizationService.PlanAccess access =
- service.evaluatePlanAccess(doc, context);
- assertThat(access.allowed()).isFalse();
- assertThat(access.canDownload()).isTrue();
- assertThat(access.permissions()).hasSize(1);
- assertThat(service.planAllowedActions(doc, context, access))
- .containsExactly("DOWNLOAD");
- assertThatCode(() -> service.requirePlanAction(doc, context, "DOWNLOAD"))
- .doesNotThrowAnyException();
- assertThatThrownBy(() -> service.requirePlanAction(doc, context, "VIEW"))
- .isInstanceOf(DmsException.class)
- .hasMessageContaining("无权查看该文档");
- }
- @Test
- void 密级不足时操作位拆分不放松密级闸_任何操作一律拒绝() {
- // 用户密级 INTERNAL(20) 低于文档 SECRET(30),矩阵即便勾编辑也不放行
- stubUserRoleOrgAndMatrix(matrix(false, false, true, false));
- DocumentEntity doc = mainDoc();
- AuthContext context = new AuthContext(9L, "editor", "低密级用户", 10L,
- "本部门", "USER", "INTERNAL", 1, null, 100L);
- assertThatThrownBy(() -> service.requirePlanAction(doc, context, "EDIT"))
- .isInstanceOf(DmsException.class)
- .hasMessageContaining("用户密级不足");
- assertThatThrownBy(() -> service.requirePlanAction(doc, context, "DOWNLOAD"))
- .isInstanceOf(DmsException.class)
- .hasMessageContaining("用户密级不足");
- AuthorizationService.PlanAccess access =
- service.evaluatePlanAccess(doc, context);
- assertThat(access.allowed()).isFalse();
- assertThat(access.reason()).isEqualTo("SECURITY");
- assertThat(access.canDownload()).isFalse();
- }
- }
|