feat(security): 添加权限检查的可见性兜底功能
- 在 CheckObjectPermission 注解中新增 visibilityFallback 属性,用于读路径的成员/可见性兜底 - 实现 checkPermissionOrVisibility 方法,支持成员权限码验证或用户可见性配置兜底两种方式 - 修改 ObjectPermissionAspect 切面逻辑,增加 visibilityFallback 条件分支处理 - 更新项目执行和任务相关的服务类,在查询注解中启用 visibilityFallback 功能 - 添加完整的单元测试覆盖 visibilityFallback 的各种场景,包括成员权限、可见性配置等 - 集成用户可见性配置 API,支持按方向代码进行可见性判断
This commit is contained in:
@@ -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;
|
||||
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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);
|
||||
|
||||
}
|
||||
|
||||
@@ -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<UserObjectRoleDO> 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) {
|
||||
|
||||
@@ -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<UserObjectRoleDO> 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<String> getRolePermissions(Long roleId) {
|
||||
Set<String> permissions = objectPermissionApi
|
||||
.getObjectRolePermissions(roleId, ObjectRoleConstants.ROLE_SCOPE_OBJECT, ProjectObjectConstants.OBJECT_TYPE)
|
||||
|
||||
@@ -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<ObjectStatusModelDO> 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<ObjectStatusModelDO> 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<ObjectStatusModelDO> statusModels = objectStatusModelMapper
|
||||
.selectListByObjectTypeEnabled(ProjectTaskConstants.OBJECT_TYPE);
|
||||
|
||||
@@ -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<ExecutionAssigneeRespVO> 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<ExecutionAssigneeLogRespVO> getExecutionAssigneeLogPage(Long projectId, Long executionId,
|
||||
ExecutionAssigneeLogPageReqVO reqVO) {
|
||||
validateProjectExists(projectId);
|
||||
|
||||
@@ -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<ProjectExecutionDO> 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<ProjectExecutionRespVO> getExecutionRespVOPage(Long projectId, ProjectExecutionPageReqVO reqVO) {
|
||||
PageResult<ProjectExecutionDO> pageResult = getExecutionPage(projectId, reqVO);
|
||||
PageResult<ProjectExecutionRespVO> voPageResult = BeanUtils.toBean(pageResult, ProjectExecutionRespVO.class);
|
||||
|
||||
@@ -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<ProjectTaskRespVO> 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);
|
||||
|
||||
@@ -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<ProjectTaskDO> 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<ProjectTaskRespVO> getTaskRespVOPage(Long projectId, Long executionId, ProjectTaskPageReqVO reqVO) {
|
||||
PageResult<ProjectTaskDO> pageResult = getTaskPage(projectId, executionId, reqVO);
|
||||
return assembleTaskRespVOPage(projectId, executionId, pageResult);
|
||||
|
||||
@@ -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<TaskAssigneeRespVO> getAssigneeList(Long projectId, Long executionId, Long taskId) {
|
||||
validateExecutionAndTaskExists(projectId, executionId, taskId);
|
||||
List<TaskAssigneeDO> 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<TaskAssigneeLogRespVO> getAssigneeLogPage(Long projectId, Long executionId, Long taskId,
|
||||
TaskAssigneeLogPageReqVO reqVO) {
|
||||
validateExecutionAndTaskExists(projectId, executionId, taskId);
|
||||
|
||||
@@ -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<TaskWorklogRespVO> getWorklogPage(Long projectId, Long executionId, Long taskId,
|
||||
TaskWorklogPageReqVO reqVO) {
|
||||
validateExecutionAndTaskExists(projectId, executionId, taskId);
|
||||
|
||||
@@ -36,8 +36,9 @@ import static org.mockito.Mockito.when;
|
||||
* {@link com.njcn.rdms.module.project.framework.security.annotation.CheckObjectPermission},且:
|
||||
* <ul>
|
||||
* <li>{@code objectId="#projectId"} 解析为 projectId(而非 executionId/taskId);</li>
|
||||
* <li>permission 为同层 QUERY 码(执行域 project:execution:query、任务域 project:task:query);</li>
|
||||
* <li>无权(checkPermission 抛异常)时方法体被拦、不执行。</li>
|
||||
* <li>permission 为同层 QUERY 码(执行域 project:execution:query、任务域 project:task:query),
|
||||
* 且因挂了 {@code visibilityFallback=true} 走可见性兜底通道 {@code checkPermissionOrVisibility}(而非 checkPermission);</li>
|
||||
* <li>无权(checkPermissionOrVisibility 抛异常)时方法体被拦、不执行。</li>
|
||||
* </ul>
|
||||
* 沿用 {@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));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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<SecurityFrameworkUtils> 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<SecurityFrameworkUtils> 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<SecurityFrameworkUtils> 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<SecurityFrameworkUtils> 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<SecurityFrameworkUtils> 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<SecurityFrameworkUtils> 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<SecurityFrameworkUtils> 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<SecurityFrameworkUtils> 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);
|
||||
|
||||
Reference in New Issue
Block a user