Commit c0168a8b104fd0874d1efe6915c7d4ddc0121569
1 parent
9ecd3ed9
fix(system): 批量删除部门时整批校验数据权限
批删前检查所有部门的数据范围,任一越权则整批拒绝。 userId 由 Controller 显式传入,保留授权跨租户和忽略数据权限行为。 其他写操作保持原有行为;23 个服务测试与 9 项 MySQL 写入检查通过。 参考:https://gitee.com/zhijiantianya/ruoyi-vue-pro/pulls/1582
Showing
5 changed files
with
206 additions
and
5 deletions
yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/dept/DeptController.java
| ... | ... | @@ -21,6 +21,7 @@ import jakarta.validation.Valid; |
| 21 | 21 | import java.util.List; |
| 22 | 22 | |
| 23 | 23 | import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; |
| 24 | +import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId; | |
| 24 | 25 | |
| 25 | 26 | @Tag(name = "管理后台 - 部门") |
| 26 | 27 | @RestController |
| ... | ... | @@ -61,7 +62,7 @@ public class DeptController { |
| 61 | 62 | @Parameter(name = "ids", description = "编号列表", required = true) |
| 62 | 63 | @PreAuthorize("@ss.hasPermission('system:dept:delete')") |
| 63 | 64 | public CommonResult<Boolean> deleteDeptList(@RequestParam("ids") List<Long> ids) { |
| 64 | - deptService.deleteDeptList(ids); | |
| 65 | + deptService.deleteDeptList(ids, getLoginUserId()); | |
| 65 | 66 | return success(true); |
| 66 | 67 | } |
| 67 | 68 | ... | ... |
yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/enums/ErrorCodeConstants.java
| ... | ... | @@ -57,6 +57,7 @@ public interface ErrorCodeConstants { |
| 57 | 57 | ErrorCode DEPT_PARENT_ERROR = new ErrorCode(1_002_004_004, "不能设置自己为父部门"); |
| 58 | 58 | ErrorCode DEPT_NOT_ENABLE = new ErrorCode(1_002_004_006, "部门({})不处于开启状态,不允许选择"); |
| 59 | 59 | ErrorCode DEPT_PARENT_IS_CHILD = new ErrorCode(1_002_004_007, "不能设置自己的子部门为父部门"); |
| 60 | + ErrorCode DEPT_NOT_IN_SCOPE = new ErrorCode(1_002_004_008, "部门({})不在数据权限范围内,禁止操作"); | |
| 60 | 61 | |
| 61 | 62 | // ========== 岗位模块 1-002-005-000 ========== |
| 62 | 63 | ErrorCode POST_NOT_FOUND = new ErrorCode(1_002_005_000, "当前岗位不存在"); | ... | ... |
yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/service/dept/DeptService.java
yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/service/dept/DeptServiceImpl.java
| ... | ... | @@ -2,18 +2,22 @@ package cn.iocoder.yudao.module.system.service.dept; |
| 2 | 2 | |
| 3 | 3 | import cn.hutool.core.collection.CollUtil; |
| 4 | 4 | import cn.hutool.core.util.ObjectUtil; |
| 5 | +import cn.iocoder.yudao.framework.common.biz.system.permission.dto.DeptDataPermissionRespDTO; | |
| 5 | 6 | import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; |
| 6 | 7 | import cn.iocoder.yudao.framework.common.util.object.BeanUtils; |
| 7 | 8 | import cn.iocoder.yudao.framework.datapermission.core.annotation.DataPermission; |
| 9 | +import cn.iocoder.yudao.framework.datapermission.core.aop.DataPermissionContextHolder; | |
| 8 | 10 | import cn.iocoder.yudao.module.system.controller.admin.dept.vo.dept.DeptListReqVO; |
| 9 | 11 | import cn.iocoder.yudao.module.system.controller.admin.dept.vo.dept.DeptSaveReqVO; |
| 10 | 12 | import cn.iocoder.yudao.module.system.dal.dataobject.dept.DeptDO; |
| 11 | 13 | import cn.iocoder.yudao.module.system.dal.mysql.dept.DeptMapper; |
| 12 | 14 | import cn.iocoder.yudao.module.system.dal.redis.RedisKeyConstants; |
| 15 | +import cn.iocoder.yudao.module.system.service.permission.PermissionService; | |
| 13 | 16 | import com.google.common.annotations.VisibleForTesting; |
| 14 | 17 | import lombok.extern.slf4j.Slf4j; |
| 15 | 18 | import org.springframework.cache.annotation.CacheEvict; |
| 16 | 19 | import org.springframework.cache.annotation.Cacheable; |
| 20 | +import org.springframework.context.annotation.Lazy; | |
| 17 | 21 | import org.springframework.stereotype.Service; |
| 18 | 22 | import org.springframework.validation.annotation.Validated; |
| 19 | 23 | |
| ... | ... | @@ -22,6 +26,7 @@ import java.util.*; |
| 22 | 26 | |
| 23 | 27 | import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; |
| 24 | 28 | import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet; |
| 29 | +import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.skipPermissionCheck; | |
| 25 | 30 | import static cn.iocoder.yudao.module.system.enums.ErrorCodeConstants.*; |
| 26 | 31 | |
| 27 | 32 | /** |
| ... | ... | @@ -37,6 +42,10 @@ public class DeptServiceImpl implements DeptService { |
| 37 | 42 | @Resource |
| 38 | 43 | private DeptMapper deptMapper; |
| 39 | 44 | |
| 45 | + @Resource | |
| 46 | + @Lazy // 延迟,避免循环依赖报错 | |
| 47 | + private PermissionService permissionService; | |
| 48 | + | |
| 40 | 49 | @Override |
| 41 | 50 | @CacheEvict(cacheNames = RedisKeyConstants.DEPT_CHILDREN_ID_LIST, |
| 42 | 51 | allEntries = true) // allEntries 清空所有缓存,因为操作一个部门,涉及到多个缓存 |
| ... | ... | @@ -91,7 +100,9 @@ public class DeptServiceImpl implements DeptService { |
| 91 | 100 | @Override |
| 92 | 101 | @CacheEvict(cacheNames = RedisKeyConstants.DEPT_CHILDREN_ID_LIST, |
| 93 | 102 | allEntries = true) // allEntries 清空所有缓存,因为操作一个部门,涉及到多个缓存 |
| 94 | - public void deleteDeptList(List<Long> ids) { | |
| 103 | + public void deleteDeptList(List<Long> ids, Long userId) { | |
| 104 | + // 校验数据权限 | |
| 105 | + validateDeptListDataScope(ids, userId); | |
| 95 | 106 | // 校验是否有子部门 |
| 96 | 107 | for (Long id : ids) { |
| 97 | 108 | if (deptMapper.selectCountByParentId(id) > 0) { |
| ... | ... | @@ -103,6 +114,41 @@ public class DeptServiceImpl implements DeptService { |
| 103 | 114 | deptMapper.deleteByIds(ids); |
| 104 | 115 | } |
| 105 | 116 | |
| 117 | + /** | |
| 118 | + * 校验部门们是否都在用户的数据权限范围内,任一不在则整体拒绝 | |
| 119 | + * | |
| 120 | + * 原因:数据权限插件只会删除范围内的部门,导致批量删除时“部分成功” | |
| 121 | + * 相关 PR:https://gitee.com/zhijiantianya/ruoyi-vue-pro/pulls/1582 | |
| 122 | + * | |
| 123 | + * @param ids 部门编号数组 | |
| 124 | + * @param userId 用户编号 | |
| 125 | + */ | |
| 126 | + @VisibleForTesting | |
| 127 | + void validateDeptListDataScope(Collection<Long> ids, Long userId) { | |
| 128 | + if (CollUtil.isEmpty(ids)) { | |
| 129 | + return; | |
| 130 | + } | |
| 131 | + // 特殊:跨租户访问、忽略数据权限时,与数据权限插件保持一致,无需校验 | |
| 132 | + if (skipPermissionCheck()) { | |
| 133 | + return; | |
| 134 | + } | |
| 135 | + DataPermission dataPermission = DataPermissionContextHolder.get(); | |
| 136 | + if (dataPermission != null && !dataPermission.enable()) { | |
| 137 | + return; | |
| 138 | + } | |
| 139 | + | |
| 140 | + // 校验每个部门:与 DeptDataPermissionRule 对部门表的过滤保持一致 | |
| 141 | + DeptDataPermissionRespDTO deptDataPermission = permissionService.getDeptDataPermission(userId); | |
| 142 | + if (Boolean.TRUE.equals(deptDataPermission.getAll())) { | |
| 143 | + return; | |
| 144 | + } | |
| 145 | + for (Long id : ids) { | |
| 146 | + if (!CollUtil.contains(deptDataPermission.getDeptIds(), id)) { | |
| 147 | + throw exception(DEPT_NOT_IN_SCOPE, id); | |
| 148 | + } | |
| 149 | + } | |
| 150 | + } | |
| 151 | + | |
| 106 | 152 | @VisibleForTesting |
| 107 | 153 | void validateDeptExists(Long id) { |
| 108 | 154 | if (id == null) { | ... | ... |
yudao-module-system/src/test/java/cn/iocoder/yudao/module/system/service/dept/DeptServiceImplTest.java
| 1 | 1 | package cn.iocoder.yudao.module.system.service.dept; |
| 2 | 2 | |
| 3 | +import cn.iocoder.yudao.framework.common.biz.system.permission.dto.DeptDataPermissionRespDTO; | |
| 3 | 4 | import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; |
| 5 | +import cn.iocoder.yudao.framework.common.enums.UserTypeEnum; | |
| 4 | 6 | import cn.iocoder.yudao.framework.common.util.object.ObjectUtils; |
| 7 | +import cn.iocoder.yudao.framework.datapermission.core.util.DataPermissionUtils; | |
| 8 | +import cn.iocoder.yudao.framework.security.core.LoginUser; | |
| 9 | +import cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils; | |
| 5 | 10 | import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest; |
| 6 | 11 | import cn.iocoder.yudao.module.system.controller.admin.dept.vo.dept.DeptListReqVO; |
| 7 | 12 | import cn.iocoder.yudao.module.system.controller.admin.dept.vo.dept.DeptSaveReqVO; |
| 8 | 13 | import cn.iocoder.yudao.module.system.dal.dataobject.dept.DeptDO; |
| 9 | 14 | import cn.iocoder.yudao.module.system.dal.mysql.dept.DeptMapper; |
| 15 | +import cn.iocoder.yudao.module.system.service.permission.PermissionService; | |
| 10 | 16 | import org.junit.jupiter.api.Test; |
| 11 | 17 | import org.springframework.context.annotation.Import; |
| 18 | +import org.springframework.mock.web.MockHttpServletRequest; | |
| 19 | +import org.springframework.security.core.context.SecurityContextHolder; | |
| 20 | +import org.springframework.test.context.bean.override.mockito.MockitoBean; | |
| 12 | 21 | |
| 13 | 22 | import jakarta.annotation.Resource; |
| 14 | 23 | import java.util.Arrays; |
| 15 | 24 | import java.util.List; |
| 16 | 25 | import java.util.Set; |
| 17 | 26 | |
| 27 | +import static cn.iocoder.yudao.framework.common.util.collection.SetUtils.asSet; | |
| 18 | 28 | import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertPojoEquals; |
| 19 | 29 | import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertServiceException; |
| 20 | 30 | import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.*; |
| 21 | 31 | import static cn.iocoder.yudao.module.system.enums.ErrorCodeConstants.*; |
| 22 | 32 | import static java.util.Collections.singletonList; |
| 23 | 33 | import static org.junit.jupiter.api.Assertions.*; |
| 34 | +import static org.mockito.ArgumentMatchers.anyLong; | |
| 35 | +import static org.mockito.ArgumentMatchers.eq; | |
| 36 | +import static org.mockito.Mockito.*; | |
| 24 | 37 | |
| 25 | 38 | /** |
| 26 | 39 | * {@link DeptServiceImpl} 的单元测试类 |
| ... | ... | @@ -35,6 +48,9 @@ public class DeptServiceImplTest extends BaseDbUnitTest { |
| 35 | 48 | @Resource |
| 36 | 49 | private DeptMapper deptMapper; |
| 37 | 50 | |
| 51 | + @MockitoBean | |
| 52 | + private PermissionService permissionService; | |
| 53 | + | |
| 38 | 54 | @Test |
| 39 | 55 | public void testCreateDept() { |
| 40 | 56 | // 准备参数 |
| ... | ... | @@ -113,12 +129,19 @@ public class DeptServiceImplTest extends BaseDbUnitTest { |
| 113 | 129 | deptMapper.insert(deptDO2); |
| 114 | 130 | // 准备参数 |
| 115 | 131 | List<Long> ids = Arrays.asList(deptDO1.getId(), deptDO2.getId()); |
| 132 | + Long userId = randomLongId(); | |
| 133 | + // mock 方法(数据权限包含全部部门) | |
| 134 | + DeptDataPermissionRespDTO deptDataPermission = new DeptDataPermissionRespDTO(); | |
| 135 | + deptDataPermission.setDeptIds(asSet(deptDO1.getId(), deptDO2.getId())); | |
| 136 | + when(permissionService.getDeptDataPermission(eq(userId))).thenReturn(deptDataPermission); | |
| 116 | 137 | |
| 117 | 138 | // 调用 |
| 118 | - deptService.deleteDeptList(ids); | |
| 139 | + deptService.deleteDeptList(ids, userId); | |
| 119 | 140 | // 校验数据不存在了 |
| 120 | 141 | assertNull(deptMapper.selectById(deptDO1.getId())); |
| 121 | 142 | assertNull(deptMapper.selectById(deptDO2.getId())); |
| 143 | + // 校验使用传入的用户编号,获取数据权限 | |
| 144 | + verify(permissionService).getDeptDataPermission(eq(userId)); | |
| 122 | 145 | } |
| 123 | 146 | |
| 124 | 147 | @Test |
| ... | ... | @@ -136,9 +159,138 @@ public class DeptServiceImplTest extends BaseDbUnitTest { |
| 136 | 159 | |
| 137 | 160 | // 准备参数(包含有子部门的 parentDept) |
| 138 | 161 | List<Long> ids = Arrays.asList(parentDept.getId(), anotherDept.getId()); |
| 162 | + Long userId = randomLongId(); | |
| 163 | + // mock 方法(数据权限为全部) | |
| 164 | + DeptDataPermissionRespDTO deptDataPermission = new DeptDataPermissionRespDTO(); | |
| 165 | + deptDataPermission.setAll(true); | |
| 166 | + when(permissionService.getDeptDataPermission(eq(userId))).thenReturn(deptDataPermission); | |
| 167 | + | |
| 168 | + // 调用, 并断言异常 | |
| 169 | + assertServiceException(() -> deptService.deleteDeptList(ids, userId), DEPT_EXITS_CHILDREN); | |
| 170 | + } | |
| 171 | + | |
| 172 | + @Test | |
| 173 | + public void testDeleteDeptList_notInScope() { | |
| 174 | + // mock 数据 | |
| 175 | + DeptDO deptDO1 = randomPojo(DeptDO.class); | |
| 176 | + deptMapper.insert(deptDO1); | |
| 177 | + DeptDO deptDO2 = randomPojo(DeptDO.class); | |
| 178 | + deptMapper.insert(deptDO2); | |
| 179 | + // 准备参数 | |
| 180 | + List<Long> ids = Arrays.asList(deptDO1.getId(), deptDO2.getId()); | |
| 181 | + Long userId = randomLongId(); | |
| 182 | + // mock 方法(数据权限只包含 deptDO1) | |
| 183 | + DeptDataPermissionRespDTO deptDataPermission = new DeptDataPermissionRespDTO(); | |
| 184 | + deptDataPermission.setDeptIds(asSet(deptDO1.getId())); | |
| 185 | + when(permissionService.getDeptDataPermission(eq(userId))).thenReturn(deptDataPermission); | |
| 139 | 186 | |
| 140 | 187 | // 调用, 并断言异常 |
| 141 | - assertServiceException(() -> deptService.deleteDeptList(ids), DEPT_EXITS_CHILDREN); | |
| 188 | + assertServiceException(() -> deptService.deleteDeptList(ids, userId), DEPT_NOT_IN_SCOPE, deptDO2.getId()); | |
| 189 | + // 校验整体拒绝,范围内的部门也未被删除 | |
| 190 | + assertNotNull(deptMapper.selectById(deptDO1.getId())); | |
| 191 | + assertNotNull(deptMapper.selectById(deptDO2.getId())); | |
| 192 | + } | |
| 193 | + | |
| 194 | + @Test | |
| 195 | + public void testDeleteDeptList_selfScope() { | |
| 196 | + // mock 数据 | |
| 197 | + DeptDO deptDO = randomPojo(DeptDO.class); | |
| 198 | + deptMapper.insert(deptDO); | |
| 199 | + // 准备参数 | |
| 200 | + List<Long> ids = singletonList(deptDO.getId()); | |
| 201 | + Long userId = randomLongId(); | |
| 202 | + // mock 方法(数据权限为仅本人,部门表无对应的用户字段,无法操作任何部门) | |
| 203 | + DeptDataPermissionRespDTO deptDataPermission = new DeptDataPermissionRespDTO(); | |
| 204 | + deptDataPermission.setSelf(true); | |
| 205 | + when(permissionService.getDeptDataPermission(eq(userId))).thenReturn(deptDataPermission); | |
| 206 | + | |
| 207 | + // 调用, 并断言异常 | |
| 208 | + assertServiceException(() -> deptService.deleteDeptList(ids, userId), DEPT_NOT_IN_SCOPE, deptDO.getId()); | |
| 209 | + assertNotNull(deptMapper.selectById(deptDO.getId())); | |
| 210 | + } | |
| 211 | + | |
| 212 | + @Test | |
| 213 | + public void testDeleteDeptList_allScope() { | |
| 214 | + // mock 数据 | |
| 215 | + DeptDO deptDO1 = randomPojo(DeptDO.class); | |
| 216 | + deptMapper.insert(deptDO1); | |
| 217 | + DeptDO deptDO2 = randomPojo(DeptDO.class); | |
| 218 | + deptMapper.insert(deptDO2); | |
| 219 | + // 准备参数 | |
| 220 | + List<Long> ids = Arrays.asList(deptDO1.getId(), deptDO2.getId()); | |
| 221 | + Long userId = randomLongId(); | |
| 222 | + // mock 方法(数据权限为全部,deptIds 为空) | |
| 223 | + DeptDataPermissionRespDTO deptDataPermission = new DeptDataPermissionRespDTO(); | |
| 224 | + deptDataPermission.setAll(true); | |
| 225 | + when(permissionService.getDeptDataPermission(eq(userId))).thenReturn(deptDataPermission); | |
| 226 | + | |
| 227 | + // 调用 | |
| 228 | + deptService.deleteDeptList(ids, userId); | |
| 229 | + // 校验数据不存在了 | |
| 230 | + assertNull(deptMapper.selectById(deptDO1.getId())); | |
| 231 | + assertNull(deptMapper.selectById(deptDO2.getId())); | |
| 232 | + } | |
| 233 | + | |
| 234 | + @Test | |
| 235 | + public void testDeleteDeptList_duplicateIds() { | |
| 236 | + // mock 数据 | |
| 237 | + DeptDO deptDO1 = randomPojo(DeptDO.class); | |
| 238 | + deptMapper.insert(deptDO1); | |
| 239 | + DeptDO deptDO2 = randomPojo(DeptDO.class); | |
| 240 | + deptMapper.insert(deptDO2); | |
| 241 | + // 准备参数(deptDO1 重复) | |
| 242 | + List<Long> ids = Arrays.asList(deptDO1.getId(), deptDO1.getId(), deptDO2.getId()); | |
| 243 | + Long userId = randomLongId(); | |
| 244 | + // mock 方法 | |
| 245 | + DeptDataPermissionRespDTO deptDataPermission = new DeptDataPermissionRespDTO(); | |
| 246 | + deptDataPermission.setDeptIds(asSet(deptDO1.getId(), deptDO2.getId())); | |
| 247 | + when(permissionService.getDeptDataPermission(eq(userId))).thenReturn(deptDataPermission); | |
| 248 | + | |
| 249 | + // 调用 | |
| 250 | + deptService.deleteDeptList(ids, userId); | |
| 251 | + // 校验数据不存在了 | |
| 252 | + assertNull(deptMapper.selectById(deptDO1.getId())); | |
| 253 | + assertNull(deptMapper.selectById(deptDO2.getId())); | |
| 254 | + } | |
| 255 | + | |
| 256 | + @Test | |
| 257 | + public void testDeleteDeptList_visitTenant() { | |
| 258 | + // mock 数据 | |
| 259 | + DeptDO deptDO = randomPojo(DeptDO.class); | |
| 260 | + deptMapper.insert(deptDO); | |
| 261 | + // 准备参数 | |
| 262 | + List<Long> ids = singletonList(deptDO.getId()); | |
| 263 | + Long userId = randomLongId(); | |
| 264 | + // mock 登录用户(跨租户访问) | |
| 265 | + LoginUser loginUser = new LoginUser().setId(userId).setUserType(UserTypeEnum.ADMIN.getValue()) | |
| 266 | + .setTenantId(1L).setVisitTenantId(2L); | |
| 267 | + SecurityFrameworkUtils.setLoginUser(loginUser, new MockHttpServletRequest()); | |
| 268 | + | |
| 269 | + try { | |
| 270 | + // 调用 | |
| 271 | + deptService.deleteDeptList(ids, userId); | |
| 272 | + } finally { | |
| 273 | + SecurityContextHolder.clearContext(); | |
| 274 | + } | |
| 275 | + // 校验数据不存在了,且未获取目标租户的数据权限 | |
| 276 | + assertNull(deptMapper.selectById(deptDO.getId())); | |
| 277 | + verify(permissionService, never()).getDeptDataPermission(anyLong()); | |
| 278 | + } | |
| 279 | + | |
| 280 | + @Test | |
| 281 | + public void testDeleteDeptList_ignoreDataPermission() { | |
| 282 | + // mock 数据 | |
| 283 | + DeptDO deptDO = randomPojo(DeptDO.class); | |
| 284 | + deptMapper.insert(deptDO); | |
| 285 | + // 准备参数 | |
| 286 | + List<Long> ids = singletonList(deptDO.getId()); | |
| 287 | + Long userId = randomLongId(); | |
| 288 | + | |
| 289 | + // 调用(忽略数据权限) | |
| 290 | + DataPermissionUtils.executeIgnore(() -> deptService.deleteDeptList(ids, userId)); | |
| 291 | + // 校验数据不存在了 | |
| 292 | + assertNull(deptMapper.selectById(deptDO.getId())); | |
| 293 | + verify(permissionService, never()).getDeptDataPermission(anyLong()); | |
| 142 | 294 | } |
| 143 | 295 | |
| 144 | 296 | @Test | ... | ... |