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)
This commit is contained in:
PIGCLOUD committed 2026-11-06 15:19:40 +08:00
1 parent c0034f3799
commit a6d0705e40
2 files changed
+103

No files matched your search

@@ -891,6 +891,11 @@ public class SysUserServiceImpl extends ServiceImpl<SysUserMapper, SysUser> impl
@Override
@Transactional(rollbackFor = Exception.class)
public R<Boolean> 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();
@@ -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);
}
}