From 4ecda99001e1e6c883608f7cfaf4efe6f35b09c3 Mon Sep 17 00:00:00 2001 From: waner Date: Mon, 27 Apr 2026 10:31:19 +0800 Subject: [PATCH] =?UTF-8?q?fix=EF=BC=9A=E8=A7=92=E8=89=B2=E6=94=B9?= =?UTF-8?q?=E5=AF=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../cisd/tms/common/config/WebMvcConfig.java | 1 + .../auth/controller/AuthAdminController.java | 29 ++-- .../auth/mapper/AuthFullAccountMapper.java | 2 + .../auth/mapper/RoleAccountMapper.java | 3 + .../repository/AuthFullAccountRepository.java | 2 + .../repository/RoleAccountRepository.java | 3 + .../impl/AuthFullAccountRepositoryImpl.java | 5 + .../impl/RoleAccountRepositoryImpl.java | 6 + .../auth/service/AuthAdminService.java | 5 +- .../service/impl/AuthAdminServiceImpl.java | 157 +++++++++++++++++- .../db/migration/V1__tms_schema_full.sql | 2 +- .../mapper/auth/AuthFullAccountMapper.xml | 21 +++ .../mapper/auth/RoleAccountMapper.xml | 12 ++ .../auth/controller/AuthControllerTest.java | 61 ++++--- .../InternalAuthorizationInterceptorTest.java | 2 +- .../auth/service/AuthAdminServiceTest.java | 142 +++++++++++++--- .../modules/auth/service/AuthServiceTest.java | 16 ++ .../ReplayProtectedEndpointsTest.java | 2 - 18 files changed, 405 insertions(+), 66 deletions(-) diff --git a/src/main/java/com/cisd/tms/common/config/WebMvcConfig.java b/src/main/java/com/cisd/tms/common/config/WebMvcConfig.java index 8faa830..05d5f36 100644 --- a/src/main/java/com/cisd/tms/common/config/WebMvcConfig.java +++ b/src/main/java/com/cisd/tms/common/config/WebMvcConfig.java @@ -93,6 +93,7 @@ public class WebMvcConfig implements WebMvcConfigurer { .addPathPatterns("/api/**") .excludePathPatterns( "/api/v1/device/status", + "/api/v1/masterKey/status", "/api/v1/auth/password-login", "/api/v1/auth/ukey-login", "/api/v1/auth/ukey-login/randoms", diff --git a/src/main/java/com/cisd/tms/modules/auth/controller/AuthAdminController.java b/src/main/java/com/cisd/tms/modules/auth/controller/AuthAdminController.java index f72f8ca..3b046d8 100644 --- a/src/main/java/com/cisd/tms/modules/auth/controller/AuthAdminController.java +++ b/src/main/java/com/cisd/tms/modules/auth/controller/AuthAdminController.java @@ -4,6 +4,7 @@ import com.cisd.tms.common.api.ApiResponse; import com.cisd.tms.common.enums.ErrorCode; import com.cisd.tms.common.exception.BizException; import com.cisd.tms.modules.auth.dto.AdminChangePasswordRequest; +import com.cisd.tms.modules.auth.dto.AuthUserListResponse; import com.cisd.tms.modules.auth.enums.AuthLevel; import com.cisd.tms.modules.auth.enums.RoleCode; import com.cisd.tms.modules.auth.security.RequireInternalAuth; @@ -25,25 +26,11 @@ import org.springframework.web.bind.annotation.*; @RestController @RequestMapping("/api/v1/auth") @RequiredArgsConstructor -@Tag(name = "认证管理", description = "角色启用、密码重置与 UKey 绑定接口") +@Tag(name = "认证管理", description = "角色密码重置、改密与 UKey 绑定接口") public class AuthAdminController { private final AuthAdminService authAdminService; - @PostMapping("/roles/{roleCode}/enable") - @Operation(summary = "启用角色", description = "仅允许 SUPER_ADMIN FULL 会话启用目标角色。") - @ReplayProtected - @RequireInternalAuth(role = RoleCode.SUPER_ADMIN, authLevel = AuthLevel.FULL) - @AuditedOperation(module = ModuleCode.AUTH, action = ActionType.ENABLE, summary = "启用角色") - public ApiResponse enableRole(@PathVariable("roleCode") String roleCode, HttpServletRequest request) { - authAdminService.enableRole( - (String) request.getAttribute(InternalApiAuthInterceptor.ATTR_ROLE_CODE), - (String) request.getAttribute(InternalApiAuthInterceptor.ATTR_AUTH_LEVEL), - roleCode - ); - return ApiResponse.success(); - } - @PostMapping("/roles/{roleCode}/reset-password") @Operation(summary = "重置角色密码", description = "仅允许 SUPER_ADMIN FULL 会话重置目标角色密码。") @ReplayProtected @@ -58,6 +45,18 @@ public class AuthAdminController { return ApiResponse.success(); } + @GetMapping("/users") + @Operation(summary = "查询用户管理列表", description = "按角色类型和状态分页查询角色席位账号列表。") +// @RequireInternalAuth(role = RoleCode.SUPER_ADMIN, authLevel = AuthLevel.FULL) + public ApiResponse listUsers( + @RequestParam(value = "roleCode", required = false) String roleCode, + @RequestParam(value = "status", required = false) String status, + @RequestParam(value = "page", defaultValue = "1") int page, + @RequestParam(value = "pageSize", defaultValue = "10") int pageSize + ) { + return ApiResponse.success(authAdminService.listUsers(roleCode, status, page, pageSize)); + } + @PostMapping("/roles/{roleCode}/accounts/{uid}/change-password") @Operation(summary = "管理员修改角色席位口令", description = "管理员为目标角色指定席位账号设置新口令。") @ReplayProtected diff --git a/src/main/java/com/cisd/tms/modules/auth/mapper/AuthFullAccountMapper.java b/src/main/java/com/cisd/tms/modules/auth/mapper/AuthFullAccountMapper.java index 5876ff1..abfa570 100644 --- a/src/main/java/com/cisd/tms/modules/auth/mapper/AuthFullAccountMapper.java +++ b/src/main/java/com/cisd/tms/modules/auth/mapper/AuthFullAccountMapper.java @@ -11,5 +11,7 @@ public interface AuthFullAccountMapper extends BaseMapperX selectAllAccounts(); + List selectByRoleCode(@Param("roleCode") String roleCode); } diff --git a/src/main/java/com/cisd/tms/modules/auth/mapper/RoleAccountMapper.java b/src/main/java/com/cisd/tms/modules/auth/mapper/RoleAccountMapper.java index 2d41fb7..a63f8f2 100644 --- a/src/main/java/com/cisd/tms/modules/auth/mapper/RoleAccountMapper.java +++ b/src/main/java/com/cisd/tms/modules/auth/mapper/RoleAccountMapper.java @@ -2,6 +2,7 @@ package com.cisd.tms.modules.auth.mapper; import com.cisd.tms.infrastructure.persistence.mapper.BaseMapperX; import com.cisd.tms.modules.auth.entity.RoleAccountEntity; +import java.util.List; import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Param; @@ -9,4 +10,6 @@ import org.apache.ibatis.annotations.Param; public interface RoleAccountMapper extends BaseMapperX { RoleAccountEntity selectByRoleCode(@Param("roleCode") String roleCode); + + List selectAllRoles(); } diff --git a/src/main/java/com/cisd/tms/modules/auth/repository/AuthFullAccountRepository.java b/src/main/java/com/cisd/tms/modules/auth/repository/AuthFullAccountRepository.java index 4ebd498..4afbb41 100644 --- a/src/main/java/com/cisd/tms/modules/auth/repository/AuthFullAccountRepository.java +++ b/src/main/java/com/cisd/tms/modules/auth/repository/AuthFullAccountRepository.java @@ -8,6 +8,8 @@ public interface AuthFullAccountRepository { Optional findByRoleCodeAndUid(String roleCode, Integer uid); + List findAll(); + List findByRoleCode(String roleCode); void save(AuthFullAccountEntity entity); diff --git a/src/main/java/com/cisd/tms/modules/auth/repository/RoleAccountRepository.java b/src/main/java/com/cisd/tms/modules/auth/repository/RoleAccountRepository.java index dc57254..85322b0 100644 --- a/src/main/java/com/cisd/tms/modules/auth/repository/RoleAccountRepository.java +++ b/src/main/java/com/cisd/tms/modules/auth/repository/RoleAccountRepository.java @@ -1,12 +1,15 @@ package com.cisd.tms.modules.auth.repository; import com.cisd.tms.modules.auth.entity.RoleAccountEntity; +import java.util.List; import java.util.Optional; public interface RoleAccountRepository { Optional findByRoleCode(String roleCode); + List findAll(); + void save(RoleAccountEntity entity); void update(RoleAccountEntity entity); diff --git a/src/main/java/com/cisd/tms/modules/auth/repository/impl/AuthFullAccountRepositoryImpl.java b/src/main/java/com/cisd/tms/modules/auth/repository/impl/AuthFullAccountRepositoryImpl.java index 1f30204..3d9bfec 100644 --- a/src/main/java/com/cisd/tms/modules/auth/repository/impl/AuthFullAccountRepositoryImpl.java +++ b/src/main/java/com/cisd/tms/modules/auth/repository/impl/AuthFullAccountRepositoryImpl.java @@ -21,6 +21,11 @@ public class AuthFullAccountRepositoryImpl implements AuthFullAccountRepository return Optional.ofNullable(authFullAccountMapper.selectByRoleCodeAndUid(roleCode, uid)); } + @Override + public List findAll() { + return authFullAccountMapper.selectAllAccounts(); + } + @Override public List findByRoleCode(String roleCode) { return authFullAccountMapper.selectByRoleCode(roleCode); diff --git a/src/main/java/com/cisd/tms/modules/auth/repository/impl/RoleAccountRepositoryImpl.java b/src/main/java/com/cisd/tms/modules/auth/repository/impl/RoleAccountRepositoryImpl.java index eb277b2..f6daca9 100644 --- a/src/main/java/com/cisd/tms/modules/auth/repository/impl/RoleAccountRepositoryImpl.java +++ b/src/main/java/com/cisd/tms/modules/auth/repository/impl/RoleAccountRepositoryImpl.java @@ -3,6 +3,7 @@ package com.cisd.tms.modules.auth.repository.impl; import com.cisd.tms.modules.auth.entity.RoleAccountEntity; import com.cisd.tms.modules.auth.mapper.RoleAccountMapper; import com.cisd.tms.modules.auth.repository.RoleAccountRepository; +import java.util.List; import java.util.Optional; import org.springframework.stereotype.Repository; @@ -20,6 +21,11 @@ public class RoleAccountRepositoryImpl implements RoleAccountRepository { return Optional.ofNullable(roleAccountMapper.selectByRoleCode(roleCode)); } + @Override + public List findAll() { + return roleAccountMapper.selectAllRoles(); + } + @Override public void save(RoleAccountEntity entity) { roleAccountMapper.insert(entity); diff --git a/src/main/java/com/cisd/tms/modules/auth/service/AuthAdminService.java b/src/main/java/com/cisd/tms/modules/auth/service/AuthAdminService.java index 0b3bc7e..88e83c0 100644 --- a/src/main/java/com/cisd/tms/modules/auth/service/AuthAdminService.java +++ b/src/main/java/com/cisd/tms/modules/auth/service/AuthAdminService.java @@ -1,14 +1,15 @@ package com.cisd.tms.modules.auth.service; +import com.cisd.tms.modules.auth.dto.AuthUserListResponse; import com.cisd.tms.modules.mk.dto.UKeySignDTO; import com.cisd.tms.modules.mk.dto.UKeySignResult; public interface AuthAdminService { - void enableRole(String operatorRoleCode, String operatorAuthLevel, String targetRoleCode); - void resetPassword(String operatorRoleCode, String operatorAuthLevel, String targetRoleCode); + AuthUserListResponse listUsers(String roleCode, String status, int page, int pageSize); + void changeAccountPassword( String operatorRoleCode, String operatorAuthLevel, diff --git a/src/main/java/com/cisd/tms/modules/auth/service/impl/AuthAdminServiceImpl.java b/src/main/java/com/cisd/tms/modules/auth/service/impl/AuthAdminServiceImpl.java index c312c51..8f4ee45 100644 --- a/src/main/java/com/cisd/tms/modules/auth/service/impl/AuthAdminServiceImpl.java +++ b/src/main/java/com/cisd/tms/modules/auth/service/impl/AuthAdminServiceImpl.java @@ -3,6 +3,8 @@ package com.cisd.tms.modules.auth.service.impl; import com.cisd.tms.common.enums.ErrorCode; import com.cisd.tms.common.exception.BizException; import com.cisd.tms.common.util.TraceIdUtil; +import com.cisd.tms.modules.auth.dto.AuthUserListItemResponse; +import com.cisd.tms.modules.auth.dto.AuthUserListResponse; import com.cisd.tms.modules.auth.entity.AuthFullAccountEntity; import com.cisd.tms.modules.auth.entity.RoleAccountEntity; import com.cisd.tms.modules.auth.entity.RoleUkeyBindingEntity; @@ -25,8 +27,13 @@ import com.fasterxml.jackson.databind.ObjectMapper; import java.time.Clock; import java.time.LocalDateTime; import java.time.ZoneOffset; +import java.util.ArrayList; +import java.util.Comparator; import java.util.List; +import java.util.Map; import java.util.Objects; +import java.util.Set; +import java.util.stream.Collectors; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; @@ -45,8 +52,26 @@ public class AuthAdminServiceImpl implements AuthAdminService { private final Clock clock; private final PasswordSaltGenerator passwordSaltGenerator; - @Override - public void enableRole(String operatorRoleCode, String operatorAuthLevel, String targetRoleCode) { + private void enableRoleAfterUkeyBindingIfReady(RoleCode targetRole) { + if (!hasRequiredActiveUkeyBindings(targetRole)) { + return; + } + RoleAccountEntity target = loadRole(targetRole.getCode()); + if (RoleAccountStatus.ACTIVE.name().equals(target.getStatus())) { + return; + } + activateRoleAndSeatAccounts(targetRole.getCode()); + } + + private boolean hasRequiredActiveUkeyBindings(RoleCode targetRole) { + return roleUkeyBindingRepository.findActiveByRoleCode(targetRole.getCode()).stream() + .map(RoleUkeyBindingEntity::getUid) + .filter(Objects::nonNull) + .distinct() + .count() >= targetRole.getRequiredUkeyCount(); + } + + private void activateRoleAndSeatAccounts(String targetRoleCode) { RoleAccountEntity target = loadRole(targetRoleCode); target.setStatus(RoleAccountStatus.ACTIVE.name()); roleAccountRepository.update(target); @@ -64,7 +89,6 @@ public class AuthAdminServiceImpl implements AuthAdminService { account.setLastLoginAt(null); authFullAccountRepository.update(account); } - } @Override @@ -94,6 +118,35 @@ public class AuthAdminServiceImpl implements AuthAdminService { } + @Override + public AuthUserListResponse listUsers(String roleCode, String status, int page, int pageSize) { + int normalizedPage = Math.max(page, 1); + int normalizedPageSize = Math.min(Math.max(pageSize, 1), 100); + Map rolesByCode = roleAccountRepository.findAll().stream() + .collect(Collectors.toMap(RoleAccountEntity::getRoleCode, item -> item, (left, right) -> left)); + List accounts = authFullAccountRepository.findAll(); + + String normalizedRoleFilter = normalizeRoleFilter(roleCode, rolesByCode); + String normalizedStatusFilter = normalizeStatusFilter(status); + List filteredItems = accounts.stream() + .map(account -> toUserListItem(account, rolesByCode.get(account.getRoleCode()))) + .filter(item -> normalizedRoleFilter == null || normalizedRoleFilter.equals(item.getRoleCode())) + .filter(item -> normalizedStatusFilter == null || normalizedStatusFilter.equals(item.getStatus())) + .sorted(Comparator + .comparingInt((AuthUserListItemResponse item) -> roleOrder(item.getRoleCode())) + .thenComparing(AuthUserListItemResponse::getUid, Comparator.nullsLast(Integer::compareTo))) + .toList(); + + int fromIndex = Math.min((normalizedPage - 1) * normalizedPageSize, filteredItems.size()); + int toIndex = Math.min(fromIndex + normalizedPageSize, filteredItems.size()); + AuthUserListResponse response = new AuthUserListResponse(); + response.setPage(normalizedPage); + response.setPageSize(normalizedPageSize); + response.setTotal(filteredItems.size()); + response.setItems(new ArrayList<>(filteredItems.subList(fromIndex, toIndex))); + return response; + } + @Override public void changeAccountPassword( String operatorRoleCode, @@ -118,6 +171,103 @@ public class AuthAdminServiceImpl implements AuthAdminService { authFullAccountRepository.update(account); } + private AuthUserListItemResponse toUserListItem(AuthFullAccountEntity account, RoleAccountEntity role) { + List ukeySerials = roleUkeyBindingRepository + .findActiveByRoleCodeAndUid(account.getRoleCode(), account.getUid()) + .stream() + .map(RoleUkeyBindingEntity::getUkeySerial) + .filter(Objects::nonNull) + .distinct() + .toList(); + String effectiveStatus = effectiveStatus(role, account); + AuthUserListItemResponse item = new AuthUserListItemResponse(); + item.setRoleCode(account.getRoleCode()); + item.setRoleName(role == null ? roleDisplayName(account.getRoleCode()) : role.getDisplayName()); + item.setUid(account.getUid()); + item.setUsername(account.getAccountName()); + item.setDisplayName(account.getDisplayName()); + item.setStatus(effectiveStatus); + item.setStatusName(statusName(effectiveStatus)); + item.setLastLoginAt(account.getLastLoginAt()); + item.setPasswordChangedAt(account.getPasswordChangedAt()); + item.setFailedCount(account.getFailedCount() == null ? 0 : account.getFailedCount()); + item.setUkeySerials(ukeySerials); + item.setUkeySerialNo(String.join(",", ukeySerials)); + return item; + } + + private String effectiveStatus(RoleAccountEntity role, AuthFullAccountEntity account) { + if (role != null && !RoleAccountStatus.ACTIVE.name().equals(role.getStatus())) { + return role.getStatus(); + } + return account.getStatus(); + } + + private String normalizeRoleFilter(String roleFilter, Map rolesByCode) { + if (roleFilter == null || roleFilter.isBlank()) { + return null; + } + String value = roleFilter.trim(); + for (RoleCode roleCode : RoleCode.values()) { + if (roleCode.getCode().equals(value) || roleCode.getDisplayName().equals(value)) { + return roleCode.getCode(); + } + } + return rolesByCode.values().stream() + .filter(role -> value.equals(role.getDisplayName())) + .map(RoleAccountEntity::getRoleCode) + .findFirst() + .orElse(value); + } + + private String normalizeStatusFilter(String status) { + if (status == null || status.isBlank()) { + return null; + } + String value = status.trim(); + if (Set.of("ACTIVE", "NORMAL", "正常").contains(value)) { + return RoleAccountStatus.ACTIVE.name(); + } + if (Set.of("LOCKED", "锁定").contains(value)) { + return RoleAccountStatus.LOCKED.name(); + } + if (Set.of("UNENABLED", "DISABLED", "未启用").contains(value)) { + return RoleAccountStatus.UNENABLED.name(); + } + return value; + } + + private String statusName(String status) { + if (RoleAccountStatus.ACTIVE.name().equals(status)) { + return "正常"; + } + if (RoleAccountStatus.LOCKED.name().equals(status)) { + return "锁定"; + } + if (RoleAccountStatus.UNENABLED.name().equals(status)) { + return "未启用"; + } + return status; + } + + private String roleDisplayName(String roleCode) { + return List.of(RoleCode.values()).stream() + .filter(item -> item.getCode().equals(roleCode)) + .map(RoleCode::getDisplayName) + .findFirst() + .orElse(roleCode); + } + + private int roleOrder(String roleCode) { + RoleCode[] values = RoleCode.values(); + for (int i = 0; i < values.length; i++) { + if (values[i].getCode().equals(roleCode)) { + return i; + } + } + return values.length; + } + private void bindUkey( String targetRoleCode, Integer uid, @@ -188,6 +338,7 @@ public class AuthAdminServiceImpl implements AuthAdminService { request.getPubKey(), signValue ); + enableRoleAfterUkeyBindingIfReady(targetRole); return UKeySignResult.builder() .sign(signValue) .backupPacket(backupPacket) diff --git a/src/main/resources/db/migration/V1__tms_schema_full.sql b/src/main/resources/db/migration/V1__tms_schema_full.sql index f1ab45d..f3fe8b6 100644 --- a/src/main/resources/db/migration/V1__tms_schema_full.sql +++ b/src/main/resources/db/migration/V1__tms_schema_full.sql @@ -377,7 +377,7 @@ VALUES 'SUPER_ADMIN', '超级管理员', 2, - 'UNENABLED', + 'ACTIVE', CURRENT_TIMESTAMP(3), CURRENT_TIMESTAMP(3) ), diff --git a/src/main/resources/mapper/auth/AuthFullAccountMapper.xml b/src/main/resources/mapper/auth/AuthFullAccountMapper.xml index c9c71bd..5ebb85f 100644 --- a/src/main/resources/mapper/auth/AuthFullAccountMapper.xml +++ b/src/main/resources/mapper/auth/AuthFullAccountMapper.xml @@ -46,6 +46,27 @@ LIMIT 1 + + + + diff --git a/src/test/java/com/cisd/tms/modules/auth/controller/AuthControllerTest.java b/src/test/java/com/cisd/tms/modules/auth/controller/AuthControllerTest.java index d71b77a..2da35e6 100644 --- a/src/test/java/com/cisd/tms/modules/auth/controller/AuthControllerTest.java +++ b/src/test/java/com/cisd/tms/modules/auth/controller/AuthControllerTest.java @@ -3,6 +3,8 @@ package com.cisd.tms.modules.auth.controller; import com.cisd.tms.common.exception.GlobalExceptionHandler; import com.cisd.tms.common.exception.BizException; import com.cisd.tms.modules.auth.dto.CaptchaResponse; +import com.cisd.tms.modules.auth.dto.AuthUserListItemResponse; +import com.cisd.tms.modules.auth.dto.AuthUserListResponse; import com.cisd.tms.modules.auth.dto.CurrentUserResponse; import com.cisd.tms.modules.auth.dto.LoginResponse; import com.cisd.tms.modules.auth.dto.UkeyLoginRandomResponse; @@ -223,25 +225,6 @@ class AuthControllerTest { Mockito.verify(authService).changeAccountPassword("token-change-001", 1, "12345678", "Abc1234!"); } - @Test - void shouldEnableRoleThroughAdminEndpoint() throws Exception { - AuthService authService = Mockito.mock(AuthService.class); - AuthAdminService authAdminService = Mockito.mock(AuthAdminService.class); - - MockMvc mockMvc = MockMvcBuilders - .standaloneSetup(new AuthController(authService), new AuthAdminController(authAdminService)) - .setControllerAdvice(new GlobalExceptionHandler()) - .build(); - - mockMvc.perform(post("/api/v1/auth/roles/AUDIT_ADMIN/enable") - .requestAttr(InternalApiAuthInterceptor.ATTR_ROLE_CODE, "KEY_ADMIN") - .requestAttr(InternalApiAuthInterceptor.ATTR_AUTH_LEVEL, "FULL")) - .andExpect(status().isOk()) - .andExpect(content().string(containsString("\"success\":true"))); - - Mockito.verify(authAdminService).enableRole("KEY_ADMIN", "FULL", "AUDIT_ADMIN"); - } - @Test void shouldChangeAccountPasswordThroughAdminEndpoint() throws Exception { AuthService authService = Mockito.mock(AuthService.class); @@ -268,6 +251,46 @@ class AuthControllerTest { Mockito.verify(authAdminService).changeAccountPassword("SUPER_ADMIN", "FULL", "KEY_ADMIN", 1, "12345678", "Abc1234!"); } + @Test + void shouldListUsersThroughAdminEndpoint() throws Exception { + AuthService authService = Mockito.mock(AuthService.class); + AuthAdminService authAdminService = Mockito.mock(AuthAdminService.class); + AuthUserListItemResponse item = new AuthUserListItemResponse(); + item.setRoleCode("SUPER_ADMIN"); + item.setRoleName("超级管理员"); + item.setUid(1); + item.setUsername("super-admin-full-01"); + item.setStatus("ACTIVE"); + item.setStatusName("正常"); + item.setUkeySerials(java.util.List.of("UK-PRIMARY", "UK-BACKUP")); + item.setUkeySerialNo("UK-PRIMARY,UK-BACKUP"); + AuthUserListResponse response = new AuthUserListResponse(); + response.setPage(1); + response.setPageSize(10); + response.setTotal(1); + response.setItems(java.util.List.of(item)); + Mockito.when(authAdminService.listUsers("SUPER_ADMIN", "ACTIVE", 1, 10)).thenReturn(response); + + MockMvc mockMvc = MockMvcBuilders + .standaloneSetup(new AuthController(authService), new AuthAdminController(authAdminService)) + .setControllerAdvice(new GlobalExceptionHandler()) + .build(); + + mockMvc.perform(get("/api/v1/auth/users") + .requestAttr(InternalApiAuthInterceptor.ATTR_ROLE_CODE, "SUPER_ADMIN") + .requestAttr(InternalApiAuthInterceptor.ATTR_AUTH_LEVEL, "FULL") + .param("roleCode", "SUPER_ADMIN") + .param("status", "ACTIVE") + .param("page", "1") + .param("pageSize", "10")) + .andExpect(status().isOk()) + .andExpect(content().string(containsString("\"roleCode\":\"SUPER_ADMIN\""))) + .andExpect(content().string(containsString("\"username\":\"super-admin-full-01\""))) + .andExpect(content().string(containsString("\"ukeySerialNo\":\"UK-PRIMARY,UK-BACKUP\""))); + + Mockito.verify(authAdminService).listUsers("SUPER_ADMIN", "ACTIVE", 1, 10); + } + @Test void shouldIssueSuperAdminUkeyBindingSignWithoutLoginContext() throws Exception { AuthService authService = Mockito.mock(AuthService.class); diff --git a/src/test/java/com/cisd/tms/modules/auth/security/InternalAuthorizationInterceptorTest.java b/src/test/java/com/cisd/tms/modules/auth/security/InternalAuthorizationInterceptorTest.java index dbe025c..caa6614 100644 --- a/src/test/java/com/cisd/tms/modules/auth/security/InternalAuthorizationInterceptorTest.java +++ b/src/test/java/com/cisd/tms/modules/auth/security/InternalAuthorizationInterceptorTest.java @@ -17,7 +17,7 @@ class InternalAuthorizationInterceptorTest { void shouldRejectWhenRoleDoesNotMatchRequiredRole() throws Exception { OperationAuditService operationAuditService = Mockito.mock(OperationAuditService.class); InternalAuthorizationInterceptor interceptor = new InternalAuthorizationInterceptor(new ObjectMapper(), operationAuditService); - MockHttpServletRequest request = new MockHttpServletRequest("POST", "/api/v1/auth/roles/AUDIT_ADMIN/enable"); + MockHttpServletRequest request = new MockHttpServletRequest("POST", "/api/v1/auth/roles/AUDIT_ADMIN/reset-password"); request.setAttribute(InternalApiAuthInterceptor.ATTR_ROLE_CODE, "AUDIT_ADMIN"); request.setAttribute(InternalApiAuthInterceptor.ATTR_AUTH_LEVEL, "FULL"); MockHttpServletResponse response = new MockHttpServletResponse(); diff --git a/src/test/java/com/cisd/tms/modules/auth/service/AuthAdminServiceTest.java b/src/test/java/com/cisd/tms/modules/auth/service/AuthAdminServiceTest.java index 87f5b8a..d25c0d7 100644 --- a/src/test/java/com/cisd/tms/modules/auth/service/AuthAdminServiceTest.java +++ b/src/test/java/com/cisd/tms/modules/auth/service/AuthAdminServiceTest.java @@ -35,29 +35,6 @@ class AuthAdminServiceTest { private static final Clock FIXED_CLOCK = Clock.fixed(Instant.parse("2026-03-23T02:00:00Z"), ZoneOffset.UTC); - @Test - void shouldEnableRoleAndUnifiedSeatAccounts() { - InMemoryRoleAccountRepository roles = new InMemoryRoleAccountRepository(); - InMemoryAuthFullAccountRepository accounts = new InMemoryAuthFullAccountRepository(); - RoleAccountEntity role = role(RoleCode.AUDIT_ADMIN, RoleAccountStatus.UNENABLED); - roles.save(role); - AuthFullAccountEntity account = account(RoleCode.AUDIT_ADMIN, 1, "audit-admin-01", "HASH-OLD", "SALT-OLD"); - account.setStatus(RoleAccountStatus.UNENABLED.name()); - account.setFailedCount(3); - account.setLockedUntil(LocalDateTime.of(2026, 3, 23, 3, 0)); - accounts.save(account); - - service(roles, accounts, new InMemoryRoleUkeyBindingRepository(), Mockito.mock(LmkService.class)) - .enableRole(RoleCode.SUPER_ADMIN.getCode(), AuthLevel.FULL.name(), RoleCode.AUDIT_ADMIN.getCode()); - - Assertions.assertEquals(RoleAccountStatus.ACTIVE.name(), roles.findByRoleCode(RoleCode.AUDIT_ADMIN.getCode()).orElseThrow().getStatus()); - AuthFullAccountEntity changed = accounts.findByRoleCodeAndUid(RoleCode.AUDIT_ADMIN.getCode(), 1).orElseThrow(); - Assertions.assertEquals(RoleAccountStatus.ACTIVE.name(), changed.getStatus()); - Assertions.assertTrue(Boolean.TRUE.equals(changed.getNeedChangePassword())); - Assertions.assertEquals(0, changed.getFailedCount()); - Assertions.assertNull(changed.getLockedUntil()); - } - @Test void shouldResetOnlyUnifiedSeatPasswords() { InMemoryRoleAccountRepository roles = new InMemoryRoleAccountRepository(); @@ -154,6 +131,109 @@ class AuthAdminServiceTest { Assertions.assertTrue(activeUidBindings.stream().anyMatch(item -> "UK-BACKUP".equals(item.getUkeySerial()))); } + @Test + void shouldActivateRoleWhenRequiredUkeyBindingIsIssued() { + InMemoryRoleAccountRepository roles = new InMemoryRoleAccountRepository(); + InMemoryAuthFullAccountRepository accounts = new InMemoryAuthFullAccountRepository(); + InMemoryRoleUkeyBindingRepository bindings = new InMemoryRoleUkeyBindingRepository(); + roles.save(role(RoleCode.AUDIT_ADMIN, RoleAccountStatus.UNENABLED)); + AuthFullAccountEntity account = account(RoleCode.AUDIT_ADMIN, 1, "audit-admin-01", "HASH-OLD", "SALT-OLD"); + account.setStatus(RoleAccountStatus.UNENABLED.name()); + account.setFailedCount(3); + account.setLockedUntil(LocalDateTime.of(2026, 3, 23, 3, 0)); + accounts.save(account); + LmkService lmkService = Mockito.mock(LmkService.class); + Mockito.when(lmkService.exportIkPublicKeyHex()).thenReturn("IK-PUB-001"); + Mockito.when(lmkService.signIk(Mockito.anyString())).thenReturn("ISSUE-SIGN-AUDIT"); + UKeySignDTO dto = new UKeySignDTO(); + dto.setPubKey("PUB-AUDIT"); + dto.setUkeySerial("UK-AUDIT"); + dto.setUid(1); + + service(roles, accounts, bindings, lmkService) + .issueUkeyBindingSign(RoleCode.SUPER_ADMIN.getCode(), AuthLevel.FULL.name(), RoleCode.AUDIT_ADMIN.getCode(), dto); + + Assertions.assertEquals(RoleAccountStatus.ACTIVE.name(), roles.findByRoleCode(RoleCode.AUDIT_ADMIN.getCode()).orElseThrow().getStatus()); + AuthFullAccountEntity changed = accounts.findByRoleCodeAndUid(RoleCode.AUDIT_ADMIN.getCode(), 1).orElseThrow(); + Assertions.assertEquals(RoleAccountStatus.ACTIVE.name(), changed.getStatus()); + Assertions.assertTrue(Boolean.TRUE.equals(changed.getNeedChangePassword())); + Assertions.assertEquals(0, changed.getFailedCount()); + Assertions.assertNull(changed.getLockedUntil()); + } + + @Test + void shouldNotActivateRoleUntilAllRequiredUkeySeatsAreBound() { + InMemoryRoleAccountRepository roles = new InMemoryRoleAccountRepository(); + InMemoryAuthFullAccountRepository accounts = new InMemoryAuthFullAccountRepository(); + InMemoryRoleUkeyBindingRepository bindings = new InMemoryRoleUkeyBindingRepository(); + roles.save(role(RoleCode.SUPER_ADMIN, RoleAccountStatus.UNENABLED)); + accounts.save(account(RoleCode.SUPER_ADMIN, 1, "super-admin-01", "HASH-OLD-1", "SALT-OLD-1")); + accounts.save(account(RoleCode.SUPER_ADMIN, 2, "super-admin-02", "HASH-OLD-2", "SALT-OLD-2")); + LmkService lmkService = Mockito.mock(LmkService.class); + Mockito.when(lmkService.exportIkPublicKeyHex()).thenReturn("IK-PUB-001"); + Mockito.when(lmkService.signIk(Mockito.anyString())).thenReturn("ISSUE-SIGN-SUPER"); + MasterKeyBackupPacket packet = new MasterKeyBackupPacket(); + packet.setPacketIndex(1); + Mockito.when(lmkService.buildBackupPacket(1)).thenReturn(packet); + UKeySignDTO dto = new UKeySignDTO(); + dto.setPubKey("PUB-SUPER-1"); + dto.setUkeySerial("UK-SUPER-1"); + dto.setUid(1); + + service(roles, accounts, bindings, lmkService) + .issueUkeyBindingSign(null, null, RoleCode.SUPER_ADMIN.getCode(), dto); + + Assertions.assertEquals(RoleAccountStatus.UNENABLED.name(), roles.findByRoleCode(RoleCode.SUPER_ADMIN.getCode()).orElseThrow().getStatus()); + } + + @Test + void shouldListUsersWithEffectiveStatusAndUkeySerials() { + InMemoryRoleAccountRepository roles = new InMemoryRoleAccountRepository(); + InMemoryAuthFullAccountRepository accounts = new InMemoryAuthFullAccountRepository(); + InMemoryRoleUkeyBindingRepository bindings = new InMemoryRoleUkeyBindingRepository(); + RoleAccountEntity superAdmin = role(RoleCode.SUPER_ADMIN, RoleAccountStatus.ACTIVE); + superAdmin.setDisplayName("超级管理员"); + RoleAccountEntity auditAdmin = role(RoleCode.AUDIT_ADMIN, RoleAccountStatus.UNENABLED); + roles.save(superAdmin); + roles.save(auditAdmin); + AuthFullAccountEntity first = account(RoleCode.SUPER_ADMIN, 1, "super-admin-full-01", "HASH-1", "SALT-1"); + first.setDisplayName("超级管理员UKey席位一"); + first.setLastLoginAt(LocalDateTime.of(2026, 1, 22, 11, 23, 52)); + first.setPasswordChangedAt(LocalDateTime.of(2026, 1, 21, 11, 23, 52)); + AuthFullAccountEntity second = account(RoleCode.SUPER_ADMIN, 2, "super-admin-full-02", "HASH-2", "SALT-2"); + second.setStatus(RoleAccountStatus.LOCKED.name()); + second.setFailedCount(5); + AuthFullAccountEntity audit = account(RoleCode.AUDIT_ADMIN, 1, "audit-admin-full-01", "HASH-3", "SALT-3"); + audit.setStatus(RoleAccountStatus.ACTIVE.name()); + accounts.save(first); + accounts.save(second); + accounts.save(audit); + bindings.save(binding(RoleCode.SUPER_ADMIN, 1, "UK-PRIMARY", "PUB-PRIMARY", "SIG-PRIMARY")); + bindings.save(binding(RoleCode.SUPER_ADMIN, 1, "UK-BACKUP", "PUB-BACKUP", "SIG-BACKUP")); + bindings.save(binding(RoleCode.SUPER_ADMIN, 2, "UK-LOCKED", "PUB-LOCKED", "SIG-LOCKED")); + + var response = service(roles, accounts, bindings, Mockito.mock(LmkService.class)) + .listUsers("超级管理员", "正常", 1, 10); + + Assertions.assertEquals(1, response.getPage()); + Assertions.assertEquals(10, response.getPageSize()); + Assertions.assertEquals(1, response.getTotal()); + Assertions.assertEquals("SUPER_ADMIN", response.getItems().get(0).getRoleCode()); + Assertions.assertEquals(1, response.getItems().get(0).getUid()); + Assertions.assertEquals("超级管理员", response.getItems().get(0).getRoleName()); + Assertions.assertEquals("super-admin-full-01", response.getItems().get(0).getUsername()); + Assertions.assertEquals("正常", response.getItems().get(0).getStatusName()); + Assertions.assertEquals(List.of("UK-PRIMARY", "UK-BACKUP"), response.getItems().get(0).getUkeySerials()); + Assertions.assertEquals("UK-PRIMARY,UK-BACKUP", response.getItems().get(0).getUkeySerialNo()); + + var disabledResponse = service(roles, accounts, bindings, Mockito.mock(LmkService.class)) + .listUsers(null, "未启用", 1, 10); + + Assertions.assertEquals(1, disabledResponse.getTotal()); + Assertions.assertEquals("AUDIT_ADMIN", disabledResponse.getItems().get(0).getRoleCode()); + Assertions.assertEquals("UNENABLED", disabledResponse.getItems().get(0).getStatus()); + } + private static AuthAdminService service( InMemoryRoleAccountRepository roles, InMemoryAuthFullAccountRepository accounts, @@ -247,6 +327,13 @@ class AuthAdminServiceTest { return Optional.ofNullable(store.get(roleCode)); } + @Override + public List findAll() { + return store.values().stream() + .sorted(Comparator.comparing(RoleAccountEntity::getRoleCode)) + .toList(); + } + @Override public void save(RoleAccountEntity entity) { store.put(entity.getRoleCode(), entity); @@ -274,6 +361,15 @@ class AuthAdminServiceTest { .toList(); } + @Override + public List findAll() { + return store.values().stream() + .sorted(Comparator + .comparing(AuthFullAccountEntity::getRoleCode) + .thenComparing(AuthFullAccountEntity::getUid)) + .toList(); + } + @Override public void save(AuthFullAccountEntity entity) { store.put(entity.getRoleCode() + "#" + entity.getUid(), entity); diff --git a/src/test/java/com/cisd/tms/modules/auth/service/AuthServiceTest.java b/src/test/java/com/cisd/tms/modules/auth/service/AuthServiceTest.java index 0b70b72..1337628 100644 --- a/src/test/java/com/cisd/tms/modules/auth/service/AuthServiceTest.java +++ b/src/test/java/com/cisd/tms/modules/auth/service/AuthServiceTest.java @@ -400,6 +400,13 @@ class AuthServiceTest { return Optional.ofNullable(store.get(roleCode)); } + @Override + public List findAll() { + return store.values().stream() + .sorted(Comparator.comparing(RoleAccountEntity::getRoleCode)) + .toList(); + } + @Override public void save(RoleAccountEntity entity) { store.put(entity.getRoleCode(), entity); @@ -427,6 +434,15 @@ class AuthServiceTest { .toList(); } + @Override + public List findAll() { + return store.values().stream() + .sorted(Comparator + .comparing(AuthFullAccountEntity::getRoleCode) + .thenComparing(AuthFullAccountEntity::getUid)) + .toList(); + } + @Override public void save(AuthFullAccountEntity entity) { store.put(entity.getRoleCode() + "#" + entity.getUid(), entity); diff --git a/src/test/java/com/cisd/tms/security/internal/ReplayProtectedEndpointsTest.java b/src/test/java/com/cisd/tms/security/internal/ReplayProtectedEndpointsTest.java index 277b09d..c1389bd 100644 --- a/src/test/java/com/cisd/tms/security/internal/ReplayProtectedEndpointsTest.java +++ b/src/test/java/com/cisd/tms/security/internal/ReplayProtectedEndpointsTest.java @@ -40,8 +40,6 @@ class ReplayProtectedEndpointsTest { Assertions.assertNotNull(annotation(DeviceController.class, "restart", jakarta.servlet.http.HttpServletRequest.class)); Assertions.assertNull(AnnotatedElementUtils.findMergedAnnotation(AuthAdminController.class, ReplayProtected.class)); - Assertions.assertNotNull(effectiveAnnotation(AuthAdminController.class, "enableRole", - String.class, jakarta.servlet.http.HttpServletRequest.class)); Assertions.assertNotNull(effectiveAnnotation(AuthAdminController.class, "resetPassword", String.class, jakarta.servlet.http.HttpServletRequest.class)); Assertions.assertNotNull(effectiveAnnotation(AuthAdminController.class, "issueUkeyBindingSign",