fix(auth): guard SUPER_ADMIN role mutations

Signed-off-by: dongmucat <1127093059@qq.com>
This commit is contained in:
dongmucat 2026-07-01 09:39:55 +08:00
parent 06cc523a0c
commit 3a254d7524
5 changed files with 24 additions and 6 deletions

View file

@ -319,7 +319,7 @@ Admin API 按最小权限拆分,不再统一要求 SUPER_ADMIN
|------|------|------|
| GET | `/api/v1/admin/users` | 用户列表 |
| GET | `/api/v1/admin/users/{id}` | 用户详情 |
| PUT | `/api/v1/admin/users/{id}/roles` | 修改用户角色USER_ADMIN 不可分配 SUPER_ADMIN |
| PUT | `/api/v1/admin/users/{id}/role` | 修改用户角色USER_ADMIN 不可分配 SUPER_ADMIN,也不可修改已有 SUPER_ADMIN 的角色状态 |
| POST | `/api/v1/admin/users/{id}/approve` | 审批待准入用户 |
| POST | `/api/v1/admin/users/{id}/disable` | 封禁用户 |
| POST | `/api/v1/admin/users/{id}/enable` | 解封用户 |

View file

@ -37,6 +37,8 @@ import java.util.stream.Collectors;
public class AdminUserAppService {
private static final Set<UserStatus> MANAGEABLE_STATUSES = Set.of(UserStatus.ACTIVE, UserStatus.DISABLED);
private static final String SUPER_ADMIN_ROLE = "SUPER_ADMIN";
private static final String USER_ROLE = "USER";
private final AdminUserSearchRepository adminUserSearchRepository;
private final UserAccountRepository userAccountRepository;
@ -83,15 +85,17 @@ public class AdminUserAppService {
UserAccount user = loadUser(userId);
rejectSystemAccountMutation(user);
String normalizedRoleCode = normalizeRoleCode(roleCode);
boolean targetHasSuperAdminRole = userRoleBindingRepository.findByUserId(user.getId()).stream()
.anyMatch(binding -> SUPER_ADMIN_ROLE.equals(binding.getRole().getCode()));
if ("SUPER_ADMIN".equals(normalizedRoleCode)
&& (actorPlatformRoles == null || !actorPlatformRoles.contains("SUPER_ADMIN"))) {
if ((SUPER_ADMIN_ROLE.equals(normalizedRoleCode) || targetHasSuperAdminRole)
&& (actorPlatformRoles == null || !actorPlatformRoles.contains(SUPER_ADMIN_ROLE))) {
throw new DomainForbiddenException("error.admin.user.role.superAdmin.assignDenied");
}
userRoleBindingRepository.deleteByUserId(user.getId());
if (!"USER".equals(normalizedRoleCode)) {
if (!USER_ROLE.equals(normalizedRoleCode)) {
Role role = roleRepository.findByCode(normalizedRoleCode)
.orElseThrow(() -> new DomainBadRequestException("error.admin.user.role.invalid", roleCode));
userRoleBindingRepository.save(new UserRoleBinding(user.getId(), role));

View file

@ -135,7 +135,7 @@ error.deviceAuth.deviceCode.invalid=Device code expired or invalid
error.deviceAuth.deviceCode.used=Device code has already been used
error.admin.user.notFound=User not found: {0}
error.admin.user.role.invalid=Invalid role: {0}
error.admin.user.role.superAdmin.assignDenied=Only SUPER_ADMIN can assign SUPER_ADMIN role
error.admin.user.role.superAdmin.assignDenied=Only SUPER_ADMIN can mutate SUPER_ADMIN role state
error.admin.user.systemAccount.immutable=System accounts cannot be modified from user management
error.admin.user.status.invalid=Invalid user status: {0}
error.admin.user.status.unsupported=Only ACTIVE or DISABLED status can be managed here

View file

@ -135,7 +135,7 @@ error.deviceAuth.deviceCode.invalid=设备验证码无效或已过期
error.deviceAuth.deviceCode.used=设备验证码已被使用
error.admin.user.notFound=用户不存在:{0}
error.admin.user.role.invalid=无效的角色:{0}
error.admin.user.role.superAdmin.assignDenied=只有 SUPER_ADMIN 可以分配 SUPER_ADMIN 角色
error.admin.user.role.superAdmin.assignDenied=只有 SUPER_ADMIN 可以修改 SUPER_ADMIN 角色状态
error.admin.user.systemAccount.immutable=系统账号不能在用户管理中修改
error.admin.user.status.invalid=无效的用户状态:{0}
error.admin.user.status.unsupported=这里只允许管理 ACTIVE 或 DISABLED 状态的用户

View file

@ -89,6 +89,20 @@ class AdminUserAppServiceTest {
() -> service.updateUserRole("user-1", "SUPER_ADMIN", Set.of("USER_ADMIN")));
}
@Test
void updateUserRole_nonSuperAdminCannotReplaceExistingSuperAdminRole() {
when(userAccountRepository.findById("user-1"))
.thenReturn(Optional.of(user("user-1", "alice", "alice@example.com", UserStatus.ACTIVE)));
when(userRoleBindingRepository.findByUserId("user-1"))
.thenReturn(List.of(new UserRoleBinding("user-1", role("SUPER_ADMIN"))));
assertThrows(DomainForbiddenException.class,
() -> service.updateUserRole("user-1", "USER", Set.of("USER_ADMIN")));
verify(userRoleBindingRepository, never()).deleteByUserId(any());
verify(userRoleBindingRepository, never()).save(any(UserRoleBinding.class));
}
@Test
void updateUserRole_rejectsSystemAccount() {
when(userAccountRepository.findById("builtin-skill-publisher"))