From 3a254d752477b3c8e7f9486cfbdc15d531fd7e4d Mon Sep 17 00:00:00 2001 From: dongmucat <1127093059@qq.com> Date: Wed, 1 Jul 2026 09:39:55 +0800 Subject: [PATCH] fix(auth): guard SUPER_ADMIN role mutations Signed-off-by: dongmucat <1127093059@qq.com> --- docs/06-api-design.md | 2 +- .../skillhub/service/AdminUserAppService.java | 10 +++++++--- .../src/main/resources/messages.properties | 2 +- .../src/main/resources/messages_zh.properties | 2 +- .../skillhub/service/AdminUserAppServiceTest.java | 14 ++++++++++++++ 5 files changed, 24 insertions(+), 6 deletions(-) diff --git a/docs/06-api-design.md b/docs/06-api-design.md index 673eb950..56cb5541 100644 --- a/docs/06-api-design.md +++ b/docs/06-api-design.md @@ -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` | 解封用户 | diff --git a/server/skillhub-app/src/main/java/com/iflytek/skillhub/service/AdminUserAppService.java b/server/skillhub-app/src/main/java/com/iflytek/skillhub/service/AdminUserAppService.java index b684c744..b3e1bdc7 100644 --- a/server/skillhub-app/src/main/java/com/iflytek/skillhub/service/AdminUserAppService.java +++ b/server/skillhub-app/src/main/java/com/iflytek/skillhub/service/AdminUserAppService.java @@ -37,6 +37,8 @@ import java.util.stream.Collectors; public class AdminUserAppService { private static final Set 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)); diff --git a/server/skillhub-app/src/main/resources/messages.properties b/server/skillhub-app/src/main/resources/messages.properties index 25a63127..19189122 100644 --- a/server/skillhub-app/src/main/resources/messages.properties +++ b/server/skillhub-app/src/main/resources/messages.properties @@ -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 diff --git a/server/skillhub-app/src/main/resources/messages_zh.properties b/server/skillhub-app/src/main/resources/messages_zh.properties index 99183ae5..6885dfd3 100644 --- a/server/skillhub-app/src/main/resources/messages_zh.properties +++ b/server/skillhub-app/src/main/resources/messages_zh.properties @@ -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 状态的用户 diff --git a/server/skillhub-app/src/test/java/com/iflytek/skillhub/service/AdminUserAppServiceTest.java b/server/skillhub-app/src/test/java/com/iflytek/skillhub/service/AdminUserAppServiceTest.java index 7f2cfc22..8296f940 100644 --- a/server/skillhub-app/src/test/java/com/iflytek/skillhub/service/AdminUserAppServiceTest.java +++ b/server/skillhub-app/src/test/java/com/iflytek/skillhub/service/AdminUserAppServiceTest.java @@ -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"))