From d7c52e12d7a7d002908feee3ea6dd9fd242cbe66 Mon Sep 17 00:00:00 2001 From: hongawen <83944980@qq.com> Date: Mon, 8 Jun 2026 13:16:32 +0800 Subject: [PATCH] =?UTF-8?q?feat(security):=20=E6=B7=BB=E5=8A=A0=E6=9D=83?= =?UTF-8?q?=E9=99=90=E6=A3=80=E6=9F=A5=E7=9A=84=E5=8F=AF=E8=A7=81=E6=80=A7?= =?UTF-8?q?=E5=85=9C=E5=BA=95=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 在 CheckObjectPermission 注解中新增 visibilityFallback 属性,用于读路径的成员/可见性兜底 - 实现 checkPermissionOrVisibility 方法,支持成员权限码验证或用户可见性配置兜底两种方式 - 修改 ObjectPermissionAspect 切面逻辑,增加 visibilityFallback 条件分支处理 - 更新项目执行和任务相关的服务类,在查询注解中启用 visibilityFallback 功能 - 添加完整的单元测试覆盖 visibilityFallback 的各种场景,包括成员权限、可见性配置等 - 集成用户可见性配置 API,支持按方向代码进行可见性判断 --- .../annotation/CheckObjectPermission.java | 7 + .../security/aop/ObjectPermissionAspect.java | 4 +- .../service/ObjectPermissionService.java | 11 ++ .../ProductObjectPermissionService.java | 48 ++++++ .../ProjectObjectPermissionService.java | 52 +++++++ .../ProjectStatusBoardServiceImpl.java | 6 +- .../ProjectExecutionAssigneeServiceImpl.java | 4 +- .../ProjectExecutionServiceImpl.java | 6 +- .../task/ProjectTaskAggregateServiceImpl.java | 8 +- .../project/task/ProjectTaskServiceImpl.java | 6 +- .../assignee/TaskAssigneeServiceImpl.java | 4 +- .../task/worklog/TaskWorklogServiceImpl.java | 2 +- .../AssigneeWorklogReadPermissionTest.java | 17 +- .../ProjectObjectPermissionServiceTest.java | 147 ++++++++++++++++++ 14 files changed, 295 insertions(+), 27 deletions(-) diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/framework/security/annotation/CheckObjectPermission.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/framework/security/annotation/CheckObjectPermission.java index 9b95de6..441a3a1 100644 --- a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/framework/security/annotation/CheckObjectPermission.java +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/framework/security/annotation/CheckObjectPermission.java @@ -38,4 +38,11 @@ public @interface CheckObjectPermission { */ boolean accessible() default false; + /** + * 读路径专用:成员凭 {@link #permission()} 查询权限码,否则凭「用户可见性配置」(system_user_visibility_config) 兜底。 + * 为 true 时切面调用 checkPermissionOrVisibility,需与 permission 同时给值。 + * 优先级:accessible > visibilityFallback > memberOnly > permission。仅用于读接口,写路径禁用。 + */ + boolean visibilityFallback() default false; + } diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/framework/security/aop/ObjectPermissionAspect.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/framework/security/aop/ObjectPermissionAspect.java index 07923cc..a2aba45 100644 --- a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/framework/security/aop/ObjectPermissionAspect.java +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/framework/security/aop/ObjectPermissionAspect.java @@ -41,9 +41,11 @@ public class ObjectPermissionAspect { throw invalidParamException("暂不支持对象类型:{}", checkObjectPermission.objectType()); } Long objectId = resolveObjectId(joinPoint, checkObjectPermission.objectId()); - // 分发优先级:accessible(可访问性门禁)> memberOnly / permission(权限码) + // 分发优先级:accessible(可访问性门禁)> visibilityFallback(读路径成员/可见性兜底)> memberOnly / permission(权限码) if (checkObjectPermission.accessible()) { permissionService.checkAccessible(objectId); + } else if (checkObjectPermission.visibilityFallback()) { + permissionService.checkPermissionOrVisibility(objectId, checkObjectPermission.permission()); } else { permissionService.checkPermission(objectId, checkObjectPermission.permission(), checkObjectPermission.memberOnly()); diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/framework/security/service/ObjectPermissionService.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/framework/security/service/ObjectPermissionService.java index 190988d..380a330 100644 --- a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/framework/security/service/ObjectPermissionService.java +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/framework/security/service/ObjectPermissionService.java @@ -39,4 +39,15 @@ public interface ObjectPermissionService { */ void checkAccessible(Long objectId); + /** + * 读路径鉴权(成员 OR 可见性配置兜底): + * 成员凭 permission 权限码放行;非成员 / 成员无该权限码时,凭用户可见性配置(通道 3, + * type=all 或方向命中)放行;都不满足抛 ..._OBJECT_PERMISSION_DENIED。 + * 仅用于对象内读接口,不调超管短路 / 组织负责人通道 / 完整数据范围。 + * + * @param objectId 对象编号(如 projectId) + * @param permission 权限码,如 {@code project:task:query} + */ + void checkPermissionOrVisibility(Long objectId, String permission); + } diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/framework/security/service/ProductObjectPermissionService.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/framework/security/service/ProductObjectPermissionService.java index 90e4859..8a11f16 100644 --- a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/framework/security/service/ProductObjectPermissionService.java +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/framework/security/service/ProductObjectPermissionService.java @@ -1,5 +1,6 @@ package com.njcn.rdms.module.project.framework.security.service; +import cn.hutool.core.collection.CollUtil; import com.njcn.rdms.framework.security.core.util.SecurityFrameworkUtils; import com.njcn.rdms.module.project.constant.ObjectRoleConstants; import com.njcn.rdms.module.project.constant.ProductObjectConstants; @@ -11,6 +12,8 @@ import com.njcn.rdms.module.project.enums.ErrorCodeConstants; import com.njcn.rdms.module.project.service.datascope.ObjectDataScope; import com.njcn.rdms.module.project.service.datascope.ObjectDataScopeService; import com.njcn.rdms.module.system.api.permission.ObjectPermissionApi; +import com.njcn.rdms.module.system.api.permission.UserVisibilityConfigApi; +import com.njcn.rdms.module.system.api.permission.dto.UserVisibilityConfigRespDTO; import jakarta.annotation.Resource; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; @@ -40,6 +43,8 @@ public class ProductObjectPermissionService implements ObjectPermissionService { private ProductMapper productMapper; @Resource private ObjectDataScopeService objectDataScopeService; + @Resource + private UserVisibilityConfigApi userVisibilityConfigApi; @Override public String getObjectType() { @@ -55,6 +60,49 @@ public class ProductObjectPermissionService implements ObjectPermissionService { return false; } + @Override + public void checkPermissionOrVisibility(Long objectId, String permission) { + if (objectId == null) { + throw invalidParamException("对象编号不能为空"); + } + Long loginUserId = SecurityFrameworkUtils.getLoginUserId(); + // 1) 成员 + 权限码 + List userRoles = userObjectRoleMapper + .selectActiveListByObjectAndUserId(ProductObjectConstants.OBJECT_TYPE, objectId, loginUserId); + if (!userRoles.isEmpty()) { + String normalizedPermission = normalizePermission(permission); + boolean allowed = userRoles.stream() + .map(UserObjectRoleDO::getRoleId) + .distinct() + .anyMatch(roleId -> getRolePermissions(roleId).contains(normalizedPermission)); + if (allowed) { + return; + } + } + // 2) 用户可见性配置兜底(按产品方向命中) + if (visibleByUserVisibilityConfig(loginUserId, objectId)) { + return; + } + // 3) 脱敏抛异常 + log.warn("[checkPermissionOrVisibility] 无读权限,objectId={}, permission={}", objectId, permission); + throw exception(ErrorCodeConstants.PRODUCT_OBJECT_PERMISSION_DENIED); + } + + private boolean visibleByUserVisibilityConfig(Long loginUserId, Long productId) { + UserVisibilityConfigRespDTO cfg = userVisibilityConfigApi.getConfig(loginUserId).getCheckedData(); + if (cfg == null) { + return false; + } + if ("all".equals(cfg.getType())) { + return true; + } + if ("directions".equals(cfg.getType()) && CollUtil.isNotEmpty(cfg.getDirectionCodes())) { + ProductDO product = productMapper.selectById(productId); + return product != null && cfg.getDirectionCodes().contains(product.getDirectionCode()); + } + return false; + } + @Override public void checkPermission(Long objectId, String permission, boolean memberOnly) { if (objectId == null) { diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/framework/security/service/ProjectObjectPermissionService.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/framework/security/service/ProjectObjectPermissionService.java index 193e5f6..f764343 100644 --- a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/framework/security/service/ProjectObjectPermissionService.java +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/framework/security/service/ProjectObjectPermissionService.java @@ -1,5 +1,6 @@ package com.njcn.rdms.module.project.framework.security.service; +import cn.hutool.core.collection.CollUtil; import cn.hutool.core.util.StrUtil; import com.njcn.rdms.framework.security.core.util.SecurityFrameworkUtils; import com.njcn.rdms.module.project.constant.ObjectRoleConstants; @@ -12,6 +13,8 @@ import com.njcn.rdms.module.project.enums.ErrorCodeConstants; import com.njcn.rdms.module.project.service.datascope.ObjectDataScope; import com.njcn.rdms.module.project.service.datascope.ObjectDataScopeService; import com.njcn.rdms.module.system.api.permission.ObjectPermissionApi; +import com.njcn.rdms.module.system.api.permission.UserVisibilityConfigApi; +import com.njcn.rdms.module.system.api.permission.dto.UserVisibilityConfigRespDTO; import jakarta.annotation.Resource; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; @@ -41,6 +44,8 @@ public class ProjectObjectPermissionService implements ObjectPermissionService { private ProjectMapper projectMapper; @Resource private ObjectDataScopeService objectDataScopeService; + @Resource + private UserVisibilityConfigApi userVisibilityConfigApi; @Override public String getObjectType() { @@ -129,6 +134,53 @@ public class ProjectObjectPermissionService implements ObjectPermissionService { } } + @Override + public void checkPermissionOrVisibility(Long objectId, String permission) { + if (objectId == null) { + throw invalidParamException("对象编号不能为空"); + } + Long loginUserId = SecurityFrameworkUtils.getLoginUserId(); + // 1) 成员 + 权限码:沿用 checkPermission 口径(成员里仍按 query 权限码细分) + List userRoles = userObjectRoleMapper + .selectActiveListByObjectAndUserId(ProjectObjectConstants.OBJECT_TYPE, objectId, loginUserId); + if (!userRoles.isEmpty()) { + String normalizedPermission = normalizePermission(permission); + boolean allowed = userRoles.stream() + .map(UserObjectRoleDO::getRoleId) + .distinct() + .anyMatch(roleId -> getRolePermissions(roleId).contains(normalizedPermission)); + if (allowed) { + return; + } + } + // 2) 非成员 / 成员无该权限码:落用户可见性配置兜底(只查通道 3,不开超管短路 / 组织负责人 / 完整 scope) + if (visibleByUserVisibilityConfig(loginUserId, objectId)) { + return; + } + // 3) 都不满足:脱敏抛异常(权限码 / 配置只进日志,见 用户可见错误文案规范) + log.warn("[checkPermissionOrVisibility] 无读权限,objectId={}, permission={}", objectId, permission); + throw exception(ErrorCodeConstants.PROJECT_OBJECT_PERMISSION_DENIED); + } + + /** + * 用户可见性配置(通道 3)兜底:type=all 直接放行;type=directions 时项目方向命中放行。 + * 不消费 type=projects(与现有数据范围口径一致)。 + */ + private boolean visibleByUserVisibilityConfig(Long loginUserId, Long projectId) { + UserVisibilityConfigRespDTO cfg = userVisibilityConfigApi.getConfig(loginUserId).getCheckedData(); + if (cfg == null) { + return false; + } + if ("all".equals(cfg.getType())) { + return true; + } + if ("directions".equals(cfg.getType()) && CollUtil.isNotEmpty(cfg.getDirectionCodes())) { + ProjectDO project = projectMapper.selectById(projectId); + return project != null && cfg.getDirectionCodes().contains(project.getDirectionCode()); + } + return false; + } + private Set getRolePermissions(Long roleId) { Set permissions = objectPermissionApi .getObjectRolePermissions(roleId, ObjectRoleConstants.ROLE_SCOPE_OBJECT, ProjectObjectConstants.OBJECT_TYPE) diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/project/ProjectStatusBoardServiceImpl.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/project/ProjectStatusBoardServiceImpl.java index 0532818..580a370 100644 --- a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/project/ProjectStatusBoardServiceImpl.java +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/project/ProjectStatusBoardServiceImpl.java @@ -46,7 +46,7 @@ public class ProjectStatusBoardServiceImpl implements ProjectStatusBoardService @Override @CheckObjectPermission(objectType = ProjectObjectConstants.OBJECT_TYPE, objectId = "#projectId", - permission = ProjectExecutionConstants.PERMISSION_QUERY) + permission = ProjectExecutionConstants.PERMISSION_QUERY, visibilityFallback = true) public ProjectExecutionStatusBoardRespVO getExecutionStatusBoard(Long projectId, ProjectExecutionStatusBoardReqVO reqVO) { List statusModels = objectStatusModelMapper.selectListByObjectTypeEnabled(ProjectExecutionConstants.OBJECT_TYPE); return buildExecutionStatusBoard(projectId, reqVO, statusModels); @@ -54,7 +54,7 @@ public class ProjectStatusBoardServiceImpl implements ProjectStatusBoardService @Override @CheckObjectPermission(objectType = ProjectObjectConstants.OBJECT_TYPE, objectId = "#projectId", - permission = ProjectTaskConstants.PERMISSION_QUERY) + permission = ProjectTaskConstants.PERMISSION_QUERY, visibilityFallback = true) public ProjectTaskStatusBoardRespVO getTaskStatusBoard(Long projectId, Long executionId, ProjectTaskStatusBoardReqVO reqVO) { List statusModels = objectStatusModelMapper.selectListByObjectTypeEnabled(ProjectTaskConstants.OBJECT_TYPE); return buildTaskStatusBoard(projectId, executionId, reqVO, statusModels); @@ -62,7 +62,7 @@ public class ProjectStatusBoardServiceImpl implements ProjectStatusBoardService @Override @CheckObjectPermission(objectType = ProjectObjectConstants.OBJECT_TYPE, objectId = "#projectId", - permission = ProjectTaskConstants.PERMISSION_QUERY) + permission = ProjectTaskConstants.PERMISSION_QUERY, visibilityFallback = true) public ProjectTaskBoardPageRespVO getTaskBoardPage(Long projectId, Long executionId, ProjectTaskBoardPageReqVO reqVO) { List statusModels = objectStatusModelMapper .selectListByObjectTypeEnabled(ProjectTaskConstants.OBJECT_TYPE); diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/project/execution/ProjectExecutionAssigneeServiceImpl.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/project/execution/ProjectExecutionAssigneeServiceImpl.java index b40a85f..8e6db3a 100644 --- a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/project/execution/ProjectExecutionAssigneeServiceImpl.java +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/project/execution/ProjectExecutionAssigneeServiceImpl.java @@ -79,7 +79,7 @@ public class ProjectExecutionAssigneeServiceImpl implements ProjectExecutionAssi @Override @CheckObjectPermission(objectType = ProjectObjectConstants.OBJECT_TYPE, objectId = "#projectId", - permission = ProjectExecutionConstants.PERMISSION_QUERY) + permission = ProjectExecutionConstants.PERMISSION_QUERY, visibilityFallback = true) public List getExecutionAssigneeList(Long projectId, Long executionId) { validateProjectExists(projectId); validateExecutionExists(projectId, executionId); @@ -153,7 +153,7 @@ public class ProjectExecutionAssigneeServiceImpl implements ProjectExecutionAssi @Override @CheckObjectPermission(objectType = ProjectObjectConstants.OBJECT_TYPE, objectId = "#projectId", - permission = ProjectExecutionConstants.PERMISSION_QUERY) + permission = ProjectExecutionConstants.PERMISSION_QUERY, visibilityFallback = true) public PageResult getExecutionAssigneeLogPage(Long projectId, Long executionId, ExecutionAssigneeLogPageReqVO reqVO) { validateProjectExists(projectId); diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/project/execution/ProjectExecutionServiceImpl.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/project/execution/ProjectExecutionServiceImpl.java index ddb6434..fb693ce 100644 --- a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/project/execution/ProjectExecutionServiceImpl.java +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/project/execution/ProjectExecutionServiceImpl.java @@ -212,7 +212,7 @@ public class ProjectExecutionServiceImpl implements ProjectExecutionService { @Override @CheckObjectPermission(objectType = ProjectObjectConstants.OBJECT_TYPE, objectId = "#projectId", - permission = ProjectExecutionConstants.PERMISSION_QUERY) + permission = ProjectExecutionConstants.PERMISSION_QUERY, visibilityFallback = true) public PageResult getExecutionPage(Long projectId, ProjectExecutionPageReqVO reqVO) { validateProjectExists(projectId); // "我参与 / 所有"视角完全由前端发不发 reqVO.involveUserId 决定。 @@ -227,7 +227,7 @@ public class ProjectExecutionServiceImpl implements ProjectExecutionService { @Override @CheckObjectPermission(objectType = ProjectObjectConstants.OBJECT_TYPE, objectId = "#projectId", - permission = ProjectExecutionConstants.PERMISSION_QUERY) + permission = ProjectExecutionConstants.PERMISSION_QUERY, visibilityFallback = true) public ProjectExecutionRespVO getExecutionRespVO(Long projectId, Long executionId) { ProjectExecutionDO execution = getExecution(projectId, executionId); ProjectExecutionRespVO respVO = BeanUtils.toBean(execution, ProjectExecutionRespVO.class); @@ -241,7 +241,7 @@ public class ProjectExecutionServiceImpl implements ProjectExecutionService { @Override @CheckObjectPermission(objectType = ProjectObjectConstants.OBJECT_TYPE, objectId = "#projectId", - permission = ProjectExecutionConstants.PERMISSION_QUERY) + permission = ProjectExecutionConstants.PERMISSION_QUERY, visibilityFallback = true) public PageResult getExecutionRespVOPage(Long projectId, ProjectExecutionPageReqVO reqVO) { PageResult pageResult = getExecutionPage(projectId, reqVO); PageResult voPageResult = BeanUtils.toBean(pageResult, ProjectExecutionRespVO.class); diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/project/task/ProjectTaskAggregateServiceImpl.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/project/task/ProjectTaskAggregateServiceImpl.java index 733a8c2..c7a6372 100644 --- a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/project/task/ProjectTaskAggregateServiceImpl.java +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/project/task/ProjectTaskAggregateServiceImpl.java @@ -68,7 +68,7 @@ public class ProjectTaskAggregateServiceImpl implements ProjectTaskAggregateServ @Override @CheckObjectPermission(objectType = ProjectObjectConstants.OBJECT_TYPE, objectId = "#projectId", - permission = ProjectTaskConstants.PERMISSION_QUERY) + permission = ProjectTaskConstants.PERMISSION_QUERY, visibilityFallback = true) public PageResult getAggregateTaskPage(Long projectId, ProjectTaskAggregatePageReqVO reqVO) { // 空数组语义短路:前端明确"按 0 个执行状态/执行 id 过滤" → 返空集合,不让 MyBatis 退化成"不过滤" if ((reqVO.getExecutionStatusCodes() != null && reqVO.getExecutionStatusCodes().isEmpty()) @@ -91,7 +91,7 @@ public class ProjectTaskAggregateServiceImpl implements ProjectTaskAggregateServ @Override @CheckObjectPermission(objectType = ProjectObjectConstants.OBJECT_TYPE, objectId = "#projectId", - permission = ProjectTaskConstants.PERMISSION_QUERY) + permission = ProjectTaskConstants.PERMISSION_QUERY, visibilityFallback = true) public ProjectTaskStatusBoardRespVO getAggregateTaskStatusBoard(Long projectId, ProjectTaskAggregateStatusBoardReqVO reqVO) { // 空数组语义短路:跳过 SQL,但保留状态列骨架(前端看板依赖全列表渲染),count 全部 0 if ((reqVO.getExecutionStatusCodes() != null && reqVO.getExecutionStatusCodes().isEmpty()) @@ -148,7 +148,7 @@ public class ProjectTaskAggregateServiceImpl implements ProjectTaskAggregateServ @Override @CheckObjectPermission(objectType = ProjectObjectConstants.OBJECT_TYPE, objectId = "#projectId", - permission = ProjectTaskConstants.PERMISSION_QUERY) + permission = ProjectTaskConstants.PERMISSION_QUERY, visibilityFallback = true) public ProjectTaskBoardPageRespVO getAggregateTaskBoardPage(Long projectId, ProjectTaskAggregateBoardPageReqVO reqVO) { // 空数组语义短路:executionStatusCodes 或 executionIds 为空数组 → 该范围明确为空,跳过 SQL,保留列骨架(每列 list=[] / total=0) boolean emptyExecScope = (reqVO.getExecutionStatusCodes() != null && reqVO.getExecutionStatusCodes().isEmpty()) @@ -238,7 +238,7 @@ public class ProjectTaskAggregateServiceImpl implements ProjectTaskAggregateServ @Override @CheckObjectPermission(objectType = ProjectObjectConstants.OBJECT_TYPE, objectId = "#projectId", - permission = ProjectTaskConstants.PERMISSION_QUERY) + permission = ProjectTaskConstants.PERMISSION_QUERY, visibilityFallback = true) public ProjectTaskSummaryRespVO getAggregateTaskSummary(Long projectId, ProjectTaskSummaryReqVO reqVO) { LocalDate today = today(); LocalDate weekStart = weekStart(today); diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/project/task/ProjectTaskServiceImpl.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/project/task/ProjectTaskServiceImpl.java index 56855ee..b06378a 100644 --- a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/project/task/ProjectTaskServiceImpl.java +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/project/task/ProjectTaskServiceImpl.java @@ -432,7 +432,7 @@ public class ProjectTaskServiceImpl implements ProjectTaskService { @Override @CheckObjectPermission(objectType = ProjectObjectConstants.OBJECT_TYPE, objectId = "#projectId", - permission = ProjectTaskConstants.PERMISSION_QUERY) + permission = ProjectTaskConstants.PERMISSION_QUERY, visibilityFallback = true) public PageResult getTaskPage(Long projectId, Long executionId, ProjectTaskPageReqVO reqVO) { validateExecutionExists(projectId, executionId); // 进得来 = 看执行下全部任务,"我参与 / 所有"由前端发不发 reqVO.ownerId / involveUserId 决定。 @@ -443,7 +443,7 @@ public class ProjectTaskServiceImpl implements ProjectTaskService { @Override @CheckObjectPermission(objectType = ProjectObjectConstants.OBJECT_TYPE, objectId = "#projectId", - permission = ProjectTaskConstants.PERMISSION_QUERY) + permission = ProjectTaskConstants.PERMISSION_QUERY, visibilityFallback = true) public ProjectTaskRespVO getTaskRespVO(Long projectId, Long executionId, Long taskId) { // 内联 validate,便于接住 execution 供前端 executionOwnerId 字段使用 ProjectExecutionDO execution = validateExecutionExists(projectId, executionId); @@ -467,7 +467,7 @@ public class ProjectTaskServiceImpl implements ProjectTaskService { @Override @CheckObjectPermission(objectType = ProjectObjectConstants.OBJECT_TYPE, objectId = "#projectId", - permission = ProjectTaskConstants.PERMISSION_QUERY) + permission = ProjectTaskConstants.PERMISSION_QUERY, visibilityFallback = true) public PageResult getTaskRespVOPage(Long projectId, Long executionId, ProjectTaskPageReqVO reqVO) { PageResult pageResult = getTaskPage(projectId, executionId, reqVO); return assembleTaskRespVOPage(projectId, executionId, pageResult); diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/project/task/assignee/TaskAssigneeServiceImpl.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/project/task/assignee/TaskAssigneeServiceImpl.java index 6782da5..909da28 100644 --- a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/project/task/assignee/TaskAssigneeServiceImpl.java +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/project/task/assignee/TaskAssigneeServiceImpl.java @@ -70,7 +70,7 @@ public class TaskAssigneeServiceImpl implements TaskAssigneeService { @Override @CheckObjectPermission(objectType = ProjectObjectConstants.OBJECT_TYPE, objectId = "#projectId", - permission = ProjectTaskConstants.PERMISSION_QUERY) + permission = ProjectTaskConstants.PERMISSION_QUERY, visibilityFallback = true) public List getAssigneeList(Long projectId, Long executionId, Long taskId) { validateExecutionAndTaskExists(projectId, executionId, taskId); List activeList = taskAssigneeMapper.selectActiveListByTaskId(taskId); @@ -124,7 +124,7 @@ public class TaskAssigneeServiceImpl implements TaskAssigneeService { @Override @CheckObjectPermission(objectType = ProjectObjectConstants.OBJECT_TYPE, objectId = "#projectId", - permission = ProjectTaskConstants.PERMISSION_QUERY) + permission = ProjectTaskConstants.PERMISSION_QUERY, visibilityFallback = true) public PageResult getAssigneeLogPage(Long projectId, Long executionId, Long taskId, TaskAssigneeLogPageReqVO reqVO) { validateExecutionAndTaskExists(projectId, executionId, taskId); diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/project/task/worklog/TaskWorklogServiceImpl.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/project/task/worklog/TaskWorklogServiceImpl.java index e74b1e1..e7333a7 100644 --- a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/project/task/worklog/TaskWorklogServiceImpl.java +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/project/task/worklog/TaskWorklogServiceImpl.java @@ -80,7 +80,7 @@ public class TaskWorklogServiceImpl implements TaskWorklogService { @Override @CheckObjectPermission(objectType = ProjectObjectConstants.OBJECT_TYPE, objectId = "#projectId", - permission = ProjectTaskConstants.PERMISSION_QUERY) + permission = ProjectTaskConstants.PERMISSION_QUERY, visibilityFallback = true) public PageResult getWorklogPage(Long projectId, Long executionId, Long taskId, TaskWorklogPageReqVO reqVO) { validateExecutionAndTaskExists(projectId, executionId, taskId); diff --git a/rdms-project/rdms-project-boot/src/test/java/com/njcn/rdms/module/project/framework/security/aop/AssigneeWorklogReadPermissionTest.java b/rdms-project/rdms-project-boot/src/test/java/com/njcn/rdms/module/project/framework/security/aop/AssigneeWorklogReadPermissionTest.java index 674f0c7..14d9314 100644 --- a/rdms-project/rdms-project-boot/src/test/java/com/njcn/rdms/module/project/framework/security/aop/AssigneeWorklogReadPermissionTest.java +++ b/rdms-project/rdms-project-boot/src/test/java/com/njcn/rdms/module/project/framework/security/aop/AssigneeWorklogReadPermissionTest.java @@ -36,8 +36,9 @@ import static org.mockito.Mockito.when; * {@link com.njcn.rdms.module.project.framework.security.annotation.CheckObjectPermission},且: *
    *
  • {@code objectId="#projectId"} 解析为 projectId(而非 executionId/taskId);
  • - *
  • permission 为同层 QUERY 码(执行域 project:execution:query、任务域 project:task:query);
  • - *
  • 无权(checkPermission 抛异常)时方法体被拦、不执行。
  • + *
  • permission 为同层 QUERY 码(执行域 project:execution:query、任务域 project:task:query), + * 且因挂了 {@code visibilityFallback=true} 走可见性兜底通道 {@code checkPermissionOrVisibility}(而非 checkPermission);
  • + *
  • 无权(checkPermissionOrVisibility 抛异常)时方法体被拦、不执行。
  • *
* 沿用 {@link ObjectPermissionAspectTest} 的 {@link AspectJProxyFactory} 手动织入方式,纯单测、不连库、不起容器。 * 真实的「有无权限」判定逻辑由 ProjectObjectPermissionServiceTest 覆盖,本类不重复。 @@ -59,7 +60,7 @@ class AssigneeWorklogReadPermissionTest extends BaseMockitoUnitTest { assertThrows(ServiceException.class, () -> proxy.getExecutionAssigneeList(PROJECT_ID, EXECUTION_ID)); - verify(objectPermissionService).checkPermission(PROJECT_ID, ProjectExecutionConstants.PERMISSION_QUERY, false); + verify(objectPermissionService).checkPermissionOrVisibility(PROJECT_ID, ProjectExecutionConstants.PERMISSION_QUERY); } @Test @@ -71,7 +72,7 @@ class AssigneeWorklogReadPermissionTest extends BaseMockitoUnitTest { assertThrows(ServiceException.class, () -> proxy.getExecutionAssigneeLogPage(PROJECT_ID, EXECUTION_ID, new ExecutionAssigneeLogPageReqVO())); - verify(objectPermissionService).checkPermission(PROJECT_ID, ProjectExecutionConstants.PERMISSION_QUERY, false); + verify(objectPermissionService).checkPermissionOrVisibility(PROJECT_ID, ProjectExecutionConstants.PERMISSION_QUERY); } @Test @@ -81,7 +82,7 @@ class AssigneeWorklogReadPermissionTest extends BaseMockitoUnitTest { assertThrows(ServiceException.class, () -> proxy.getAssigneeList(PROJECT_ID, EXECUTION_ID, TASK_ID)); - verify(objectPermissionService).checkPermission(PROJECT_ID, ProjectTaskConstants.PERMISSION_QUERY, false); + verify(objectPermissionService).checkPermissionOrVisibility(PROJECT_ID, ProjectTaskConstants.PERMISSION_QUERY); } @Test @@ -92,7 +93,7 @@ class AssigneeWorklogReadPermissionTest extends BaseMockitoUnitTest { assertThrows(ServiceException.class, () -> proxy.getAssigneeLogPage(PROJECT_ID, EXECUTION_ID, TASK_ID, new TaskAssigneeLogPageReqVO())); - verify(objectPermissionService).checkPermission(PROJECT_ID, ProjectTaskConstants.PERMISSION_QUERY, false); + verify(objectPermissionService).checkPermissionOrVisibility(PROJECT_ID, ProjectTaskConstants.PERMISSION_QUERY); } @Test @@ -103,7 +104,7 @@ class AssigneeWorklogReadPermissionTest extends BaseMockitoUnitTest { assertThrows(ServiceException.class, () -> proxy.getWorklogPage(PROJECT_ID, EXECUTION_ID, TASK_ID, new TaskWorklogPageReqVO())); - verify(objectPermissionService).checkPermission(PROJECT_ID, ProjectTaskConstants.PERMISSION_QUERY, false); + verify(objectPermissionService).checkPermissionOrVisibility(PROJECT_ID, ProjectTaskConstants.PERMISSION_QUERY); } /** @@ -122,7 +123,7 @@ class AssigneeWorklogReadPermissionTest extends BaseMockitoUnitTest { */ private void denyPermission(String permission) { doThrow(exception(ErrorCodeConstants.PROJECT_OBJECT_PERMISSION_DENIED)) - .when(objectPermissionService).checkPermission(eq(PROJECT_ID), eq(permission), eq(false)); + .when(objectPermissionService).checkPermissionOrVisibility(eq(PROJECT_ID), eq(permission)); } } diff --git a/rdms-project/rdms-project-boot/src/test/java/com/njcn/rdms/module/project/framework/security/service/ProjectObjectPermissionServiceTest.java b/rdms-project/rdms-project-boot/src/test/java/com/njcn/rdms/module/project/framework/security/service/ProjectObjectPermissionServiceTest.java index 189105b..c62ccd7 100644 --- a/rdms-project/rdms-project-boot/src/test/java/com/njcn/rdms/module/project/framework/security/service/ProjectObjectPermissionServiceTest.java +++ b/rdms-project/rdms-project-boot/src/test/java/com/njcn/rdms/module/project/framework/security/service/ProjectObjectPermissionServiceTest.java @@ -11,6 +11,7 @@ import com.njcn.rdms.module.project.enums.ErrorCodeConstants; import com.njcn.rdms.module.project.service.datascope.ObjectDataScope; import com.njcn.rdms.module.project.service.datascope.ObjectDataScopeService; import com.njcn.rdms.module.system.api.permission.ObjectPermissionApi; +import com.njcn.rdms.module.system.api.permission.dto.UserVisibilityConfigRespDTO; import com.njcn.rdms.module.system.enums.permission.PermissionScopeTypeEnum; import org.junit.jupiter.api.Test; import org.mockito.InjectMocks; @@ -42,6 +43,8 @@ class ProjectObjectPermissionServiceTest extends BaseMockitoUnitTest { private ProjectMapper projectMapper; @Mock private ObjectDataScopeService objectDataScopeService; + @Mock + private com.njcn.rdms.module.system.api.permission.UserVisibilityConfigApi userVisibilityConfigApi; @Test void checkPermission_whenMemberOnlyAndActiveMember_shouldPass() { @@ -213,6 +216,150 @@ class ProjectObjectPermissionServiceTest extends BaseMockitoUnitTest { verifyNoInteractions(objectDataScopeService); } + // ===== checkPermissionOrVisibility ===== + + @Test + void checkPermissionOrVisibility_whenMemberHasQuery_shouldPassWithoutVisibility() { + Long projectId = 1201L, loginUserId = 2201L; + when(userObjectRoleMapper.selectActiveListByObjectAndUserId("project", projectId, loginUserId)) + .thenReturn(List.of(createMember(projectId, loginUserId, 3201L))); + when(objectPermissionApi.getObjectRolePermissions(3201L, + PermissionScopeTypeEnum.OBJECT.getScopeType(), "project")) + .thenReturn(success(Set.of("project:task:query"))); + + try (MockedStatic s = mockLoginUser(loginUserId)) { + assertDoesNotThrow(() -> permissionService.checkPermissionOrVisibility(projectId, "project:task:query")); + } + // 成员凭权限码即放行,不触达可见性配置兜底 + verifyNoInteractions(userVisibilityConfigApi); + } + + @Test + void checkPermissionOrVisibility_whenMemberNoQueryButVisibilityAll_shouldPass() { + Long projectId = 1202L, loginUserId = 2202L; + when(userObjectRoleMapper.selectActiveListByObjectAndUserId("project", projectId, loginUserId)) + .thenReturn(List.of(createMember(projectId, loginUserId, 3202L))); + when(objectPermissionApi.getObjectRolePermissions(3202L, + PermissionScopeTypeEnum.OBJECT.getScopeType(), "project")) + .thenReturn(success(Set.of("project:task:update"))); // 成员有角色但无 query + when(userVisibilityConfigApi.getConfig(loginUserId)).thenReturn(success(cfgAll())); + + try (MockedStatic s = mockLoginUser(loginUserId)) { + assertDoesNotThrow(() -> permissionService.checkPermissionOrVisibility(projectId, "project:task:query")); + } + } + + @Test + void checkPermissionOrVisibility_whenMemberNoQueryButVisibilityDirectionHit_shouldPass() { + Long projectId = 1203L, loginUserId = 2203L; + when(userObjectRoleMapper.selectActiveListByObjectAndUserId("project", projectId, loginUserId)) + .thenReturn(List.of(createMember(projectId, loginUserId, 3203L))); + when(objectPermissionApi.getObjectRolePermissions(3203L, + PermissionScopeTypeEnum.OBJECT.getScopeType(), "project")) + .thenReturn(success(Set.of("project:task:update"))); + when(userVisibilityConfigApi.getConfig(loginUserId)).thenReturn(success(cfgDirections("D-NET"))); + when(projectMapper.selectById(projectId)).thenReturn(projectOf(projectId, "D-NET")); + + try (MockedStatic s = mockLoginUser(loginUserId)) { + assertDoesNotThrow(() -> permissionService.checkPermissionOrVisibility(projectId, "project:task:query")); + } + } + + @Test + void checkPermissionOrVisibility_whenNonMemberButVisibilityAll_shouldPass() { + Long projectId = 1204L, loginUserId = 2204L; + when(userObjectRoleMapper.selectActiveListByObjectAndUserId("project", projectId, loginUserId)) + .thenReturn(Collections.emptyList()); + when(userVisibilityConfigApi.getConfig(loginUserId)).thenReturn(success(cfgAll())); + + try (MockedStatic s = mockLoginUser(loginUserId)) { + assertDoesNotThrow(() -> permissionService.checkPermissionOrVisibility(projectId, "project:task:query")); + } + // 非成员不查角色权限码 + verifyNoInteractions(objectPermissionApi); + } + + @Test + void checkPermissionOrVisibility_whenNonMemberDirectionHit_shouldPass() { + Long projectId = 1205L, loginUserId = 2205L; + when(userObjectRoleMapper.selectActiveListByObjectAndUserId("project", projectId, loginUserId)) + .thenReturn(Collections.emptyList()); + when(userVisibilityConfigApi.getConfig(loginUserId)).thenReturn(success(cfgDirections("D-NET"))); + when(projectMapper.selectById(projectId)).thenReturn(projectOf(projectId, "D-NET")); + + try (MockedStatic s = mockLoginUser(loginUserId)) { + assertDoesNotThrow(() -> permissionService.checkPermissionOrVisibility(projectId, "project:task:query")); + } + } + + @Test + void checkPermissionOrVisibility_whenNonMemberNoConfig_shouldThrowDenied() { + Long projectId = 1206L, loginUserId = 2206L; + when(userObjectRoleMapper.selectActiveListByObjectAndUserId("project", projectId, loginUserId)) + .thenReturn(Collections.emptyList()); + when(userVisibilityConfigApi.getConfig(loginUserId)).thenReturn(success(null)); // 无配置 + + try (MockedStatic s = mockLoginUser(loginUserId)) { + ServiceException ex = assertThrows(ServiceException.class, + () -> permissionService.checkPermissionOrVisibility(projectId, "project:task:query")); + assertEquals(ErrorCodeConstants.PROJECT_OBJECT_PERMISSION_DENIED.getCode(), ex.getCode()); + assertFalse(ex.getMessage().contains(":"), "message 不应外泄权限码"); + } + } + + @Test + void checkPermissionOrVisibility_whenNonMemberDirectionMiss_shouldThrowDenied() { + Long projectId = 1207L, loginUserId = 2207L; + when(userObjectRoleMapper.selectActiveListByObjectAndUserId("project", projectId, loginUserId)) + .thenReturn(Collections.emptyList()); + when(userVisibilityConfigApi.getConfig(loginUserId)).thenReturn(success(cfgDirections("D-OTHER"))); + when(projectMapper.selectById(projectId)).thenReturn(projectOf(projectId, "D-NET")); + + try (MockedStatic s = mockLoginUser(loginUserId)) { + ServiceException ex = assertThrows(ServiceException.class, + () -> permissionService.checkPermissionOrVisibility(projectId, "project:task:query")); + assertEquals(ErrorCodeConstants.PROJECT_OBJECT_PERMISSION_DENIED.getCode(), ex.getCode()); + } + } + + @Test + void checkPermissionOrVisibility_whenMemberNoQueryAndNoVisibility_shouldThrowDenied() { + Long projectId = 1208L, loginUserId = 2208L; + when(userObjectRoleMapper.selectActiveListByObjectAndUserId("project", projectId, loginUserId)) + .thenReturn(List.of(createMember(projectId, loginUserId, 3208L))); + when(objectPermissionApi.getObjectRolePermissions(3208L, + PermissionScopeTypeEnum.OBJECT.getScopeType(), "project")) + .thenReturn(success(Set.of("project:task:update"))); + when(userVisibilityConfigApi.getConfig(loginUserId)).thenReturn(success(null)); + + try (MockedStatic s = mockLoginUser(loginUserId)) { + ServiceException ex = assertThrows(ServiceException.class, + () -> permissionService.checkPermissionOrVisibility(projectId, "project:task:query")); + assertEquals(ErrorCodeConstants.PROJECT_OBJECT_PERMISSION_DENIED.getCode(), ex.getCode()); + assertFalse(ex.getMessage().contains(":"), "message 不应外泄权限码"); + } + } + + private UserVisibilityConfigRespDTO cfgAll() { + UserVisibilityConfigRespDTO cfg = new UserVisibilityConfigRespDTO(); + cfg.setType("all"); + return cfg; + } + + private UserVisibilityConfigRespDTO cfgDirections(String... codes) { + UserVisibilityConfigRespDTO cfg = new UserVisibilityConfigRespDTO(); + cfg.setType("directions"); + cfg.setDirectionCodes(Set.of(codes)); + return cfg; + } + + private ProjectDO projectOf(Long projectId, String directionCode) { + ProjectDO project = new ProjectDO(); + project.setId(projectId); + project.setDirectionCode(directionCode); + return project; + } + private UserObjectRoleDO createMember(Long projectId, Long loginUserId, Long roleId) { UserObjectRoleDO member = new UserObjectRoleDO(); member.setId(9001L);