From a6d0705e40559d2fe6d71c8dbc1efdf197577086 Mon Sep 17 00:00:00 2001 From: PIGCLOUD Date: Fri, 6 Nov 2026 14:43:04 +0800 Subject: [PATCH] fix(upms): validate department switch authorization Restrict department switching to the current user department list and reuse the localized access-denied message. Add regression coverage for valid switching and unauthorized or missing departments. Refs #298 (cherry picked from commit b897c6a34f5f0ff3a4670eb9881e058b56de376d) --- .../service/impl/SysUserServiceImpl.java | 5 + .../service/impl/SysUserDeptSwitchTests.java | 98 +++++++++++++++++++ 2 files changed, 103 insertions(+) create mode 100644 pigx-upms/pigx-upms-biz/src/test/java/com/pig4cloud/pigx/admin/service/impl/SysUserDeptSwitchTests.java diff --git a/pigx-upms/pigx-upms-biz/src/main/java/com/pig4cloud/pigx/admin/service/impl/SysUserServiceImpl.java b/pigx-upms/pigx-upms-biz/src/main/java/com/pig4cloud/pigx/admin/service/impl/SysUserServiceImpl.java index 247bf4f04..9f30ab2cb 100644 --- a/pigx-upms/pigx-upms-biz/src/main/java/com/pig4cloud/pigx/admin/service/impl/SysUserServiceImpl.java +++ b/pigx-upms/pigx-upms-biz/src/main/java/com/pig4cloud/pigx/admin/service/impl/SysUserServiceImpl.java @@ -891,6 +891,11 @@ public class SysUserServiceImpl extends ServiceImpl impl @Override @Transactional(rollbackFor = Exception.class) public R updateUserDept(Long deptId) { + // 可切换范围与当前用户部门列表保持一致,禁止通过请求参数扩大数据权限。 + if (Objects.isNull(deptId) || getUserDeptList().stream().noneMatch(dept -> deptId.equals(dept.getDeptId()))) { + return R.failed(MsgUtils.getSecurityMessage("AbstractAccessDecisionManager.accessDenied", StrUtil.EMPTY)); + } + PigxUser user = SecurityUtils.getUser(); Long userId = user.getId(); String username = user.getUsername(); diff --git a/pigx-upms/pigx-upms-biz/src/test/java/com/pig4cloud/pigx/admin/service/impl/SysUserDeptSwitchTests.java b/pigx-upms/pigx-upms-biz/src/test/java/com/pig4cloud/pigx/admin/service/impl/SysUserDeptSwitchTests.java new file mode 100644 index 000000000..49e28fd1c --- /dev/null +++ b/pigx-upms/pigx-upms-biz/src/test/java/com/pig4cloud/pigx/admin/service/impl/SysUserDeptSwitchTests.java @@ -0,0 +1,98 @@ +package com.pig4cloud.pigx.admin.service.impl; + +import com.baomidou.mybatisplus.core.MybatisConfiguration; +import com.baomidou.mybatisplus.core.conditions.Wrapper; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import com.baomidou.mybatisplus.core.metadata.TableInfoHelper; +import com.pig4cloud.pigx.admin.api.entity.SysDept; +import com.pig4cloud.pigx.admin.api.entity.SysUser; +import com.pig4cloud.pigx.common.core.constant.CacheConstants; +import com.pig4cloud.pigx.common.core.util.SpringContextHolder; +import org.springframework.context.support.StaticApplicationContext; +import org.springframework.context.support.ResourceBundleMessageSource; +import org.springframework.context.i18n.LocaleContextHolder; +import java.util.Locale; +import com.pig4cloud.pigx.common.core.constant.CommonConstants; +import com.pig4cloud.pigx.common.security.service.PigxUser; +import org.apache.ibatis.builder.MapperBuilderAssistant; +import org.junit.jupiter.api.*; +import org.mockito.ArgumentCaptor; +import org.springframework.cache.Cache; +import org.springframework.cache.CacheManager; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.test.util.ReflectionTestUtils; +import java.util.List; +import static org.assertj.core.api.Assertions.*; +import static org.mockito.Mockito.*; + +class SysUserDeptSwitchTests { + private SysUserServiceImpl service; + private Cache cache; + private Object previousContext; + private Locale previousLocale; + private StaticApplicationContext context; + + @BeforeEach + void setUp() { + previousLocale = LocaleContextHolder.getLocale(); + previousContext = ReflectionTestUtils.getField(SpringContextHolder.class, "applicationContext"); + context = new StaticApplicationContext(); + var messages = new ResourceBundleMessageSource(); + messages.setBasename("i18n/messages"); + context.getBeanFactory().registerSingleton("pigxMessageSource", messages); + var securityMessages = new ResourceBundleMessageSource(); + securityMessages.setBasename("errors/messages"); + context.getBeanFactory().registerSingleton("securityMessageSource", securityMessages); + new SpringContextHolder().setApplicationContext(context); + TableInfoHelper.initTableInfo(new MapperBuilderAssistant(new MybatisConfiguration(), "user"), SysUser.class); + service = mock(SysUserServiceImpl.class, CALLS_REAL_METHODS); + var cacheManager = mock(CacheManager.class); + cache = mock(Cache.class); + ReflectionTestUtils.setField(service, "cacheManager", cacheManager); + when(cacheManager.getCache(CacheConstants.USER_DETAILS)).thenReturn(cache); + var user = mock(PigxUser.class); + when(user.getId()).thenReturn(1001L); + when(user.getUsername()).thenReturn("dept-user"); + SecurityContextHolder.getContext().setAuthentication(new UsernamePasswordAuthenticationToken(user, null)); + var dept = new SysDept(); + dept.setDeptId(10L); + doReturn(List.of(dept)).when(service).getUserDeptList(); + doReturn(true).when(service).update(any(Wrapper.class)); + } + + @AfterEach + void tearDown() { + SecurityContextHolder.clearContext(); + LocaleContextHolder.setLocale(previousLocale); + ReflectionTestUtils.setField(SpringContextHolder.class, "applicationContext", previousContext); + context.close(); + } + + @Test + void switchesToAssociatedDepartmentAndEvictsUserCache() { + assertThat(service.updateUserDept(10L).getData()).isTrue(); + var condition = ArgumentCaptor.forClass(Wrapper.class); + verify(service).update(condition.capture()); + var update = (LambdaUpdateWrapper) condition.getValue(); + assertThat(update.getSqlSegment()).contains("user_id"); + assertThat(update.getSqlSet()).contains("dept_id"); + assertThat(update.getParamNameValuePairs().values()).containsExactlyInAnyOrder(10L, 1001L); + verify(cache).evict("dept-user"); + } + + @Test + void rejectsUnassociatedOrMissingDepartmentWithoutChangingState() { + for (Locale locale : List.of(Locale.CHINA, Locale.ENGLISH)) { + LocaleContextHolder.setLocale(locale); + for (Long deptId : new Long[] { 20L, null }) { + var result = service.updateUserDept(deptId); + assertThat(result.getCode()).isEqualTo(CommonConstants.FAIL); + assertThat(result.getMsg()).isEqualTo(Locale.CHINA.equals(locale) ? "权限不足,不允许访问" + : "Access is denied"); + } + } + verify(service, never()).update(any(Wrapper.class)); + verifyNoInteractions(cache); + } +}