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 8079e33..1f9f237 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 @@ -1,8 +1,6 @@ package com.cisd.tms.modules.auth.controller; 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; @@ -93,7 +91,7 @@ public class AuthAdminController { } @PostMapping("/roles/{roleCode}/ukeys/issue-sign") - @Operation(summary = "签发并绑定其他管理员 UKey", description = "仅允许 SUPER_ADMIN FULL 会话登记其他管理员的 UKey。") + @Operation(summary = "签发并绑定管理员 UKey", description = "仅允许 SUPER_ADMIN FULL 会话登记或重绑定管理员 UKey;超级管理员重绑定需传 ukeyBindingId。") @ReplayProtected @RequireInternalAuth(role = RoleCode.SUPER_ADMIN, authLevel = AuthLevel.FULL) public ApiResponse issueUkeyBindingSign( @@ -101,9 +99,6 @@ public class AuthAdminController { @Valid @RequestBody UKeySignDTO request, HttpServletRequest httpRequest ) { - if (RoleCode.SUPER_ADMIN.getCode().equals(roleCode)) { - throw new BizException(ErrorCode.VALIDATE_FAILED.getCode(), "use super-admin ukey binding endpoint"); - } return ApiResponse.success(authAdminService.issueUkeyBindingSign( (String) httpRequest.getAttribute(InternalApiAuthInterceptor.ATTR_ROLE_CODE), (String) httpRequest.getAttribute(InternalApiAuthInterceptor.ATTR_AUTH_LEVEL), diff --git a/src/main/java/com/cisd/tms/modules/auth/mapper/AuthSessionMapper.java b/src/main/java/com/cisd/tms/modules/auth/mapper/AuthSessionMapper.java index aacf706..ced312d 100644 --- a/src/main/java/com/cisd/tms/modules/auth/mapper/AuthSessionMapper.java +++ b/src/main/java/com/cisd/tms/modules/auth/mapper/AuthSessionMapper.java @@ -24,4 +24,6 @@ public interface AuthSessionMapper extends BaseMapperX { @Param("authenticatedPrincipalsJson") String authenticatedPrincipalsJson, @Param("now") LocalDateTime now ); + + int deleteAll(); } diff --git a/src/main/java/com/cisd/tms/modules/auth/mapper/RoleUkeyBindingMapper.java b/src/main/java/com/cisd/tms/modules/auth/mapper/RoleUkeyBindingMapper.java index 212471d..7be6672 100644 --- a/src/main/java/com/cisd/tms/modules/auth/mapper/RoleUkeyBindingMapper.java +++ b/src/main/java/com/cisd/tms/modules/auth/mapper/RoleUkeyBindingMapper.java @@ -12,4 +12,11 @@ public interface RoleUkeyBindingMapper extends BaseMapperX selectActiveByRoleCodeAndUid(@Param("roleCode") String roleCode, @Param("uid") Integer uid); List selectActiveByRoleCode(@Param("roleCode") String roleCode); + + List selectActiveByUkeyIdentity( + @Param("ukeySerial") String ukeySerial, + @Param("ukeyPubkey") String ukeyPubkey + ); + + int deleteAll(); } diff --git a/src/main/java/com/cisd/tms/modules/auth/repository/AuthSessionRepository.java b/src/main/java/com/cisd/tms/modules/auth/repository/AuthSessionRepository.java index cac2508..be26075 100644 --- a/src/main/java/com/cisd/tms/modules/auth/repository/AuthSessionRepository.java +++ b/src/main/java/com/cisd/tms/modules/auth/repository/AuthSessionRepository.java @@ -21,4 +21,6 @@ public interface AuthSessionRepository { void save(AuthSessionEntity entity); void update(AuthSessionEntity entity); + + void deleteAll(); } diff --git a/src/main/java/com/cisd/tms/modules/auth/repository/RoleUkeyBindingRepository.java b/src/main/java/com/cisd/tms/modules/auth/repository/RoleUkeyBindingRepository.java index 42163f1..c34bba1 100644 --- a/src/main/java/com/cisd/tms/modules/auth/repository/RoleUkeyBindingRepository.java +++ b/src/main/java/com/cisd/tms/modules/auth/repository/RoleUkeyBindingRepository.java @@ -2,14 +2,21 @@ package com.cisd.tms.modules.auth.repository; import com.cisd.tms.modules.auth.entity.RoleUkeyBindingEntity; import java.util.List; +import java.util.Optional; public interface RoleUkeyBindingRepository { + Optional findById(Long id); + List findActiveByRoleCodeAndUid(String roleCode, Integer uid); List findActiveByRoleCode(String roleCode); + List findActiveByUkeyIdentity(String ukeySerial, String ukeyPubkey); + void save(RoleUkeyBindingEntity entity); void update(RoleUkeyBindingEntity entity); + + void deleteAll(); } diff --git a/src/main/java/com/cisd/tms/modules/auth/repository/impl/AuthSessionRepositoryImpl.java b/src/main/java/com/cisd/tms/modules/auth/repository/impl/AuthSessionRepositoryImpl.java index bde8c4e..d7f2f45 100644 --- a/src/main/java/com/cisd/tms/modules/auth/repository/impl/AuthSessionRepositoryImpl.java +++ b/src/main/java/com/cisd/tms/modules/auth/repository/impl/AuthSessionRepositoryImpl.java @@ -51,4 +51,9 @@ public class AuthSessionRepositoryImpl implements AuthSessionRepository { public void update(AuthSessionEntity entity) { authSessionMapper.updateById(entity); } + + @Override + public void deleteAll() { + authSessionMapper.deleteAll(); + } } diff --git a/src/main/java/com/cisd/tms/modules/auth/repository/impl/RoleUkeyBindingRepositoryImpl.java b/src/main/java/com/cisd/tms/modules/auth/repository/impl/RoleUkeyBindingRepositoryImpl.java index 56ff716..7b66b99 100644 --- a/src/main/java/com/cisd/tms/modules/auth/repository/impl/RoleUkeyBindingRepositoryImpl.java +++ b/src/main/java/com/cisd/tms/modules/auth/repository/impl/RoleUkeyBindingRepositoryImpl.java @@ -4,6 +4,7 @@ import com.cisd.tms.modules.auth.entity.RoleUkeyBindingEntity; import com.cisd.tms.modules.auth.mapper.RoleUkeyBindingMapper; import com.cisd.tms.modules.auth.repository.RoleUkeyBindingRepository; import java.util.List; +import java.util.Optional; import org.springframework.stereotype.Repository; @Repository @@ -15,6 +16,11 @@ public class RoleUkeyBindingRepositoryImpl implements RoleUkeyBindingRepository this.roleUkeyBindingMapper = roleUkeyBindingMapper; } + @Override + public Optional findById(Long id) { + return Optional.ofNullable(roleUkeyBindingMapper.selectById(id)); + } + @Override public List findActiveByRoleCodeAndUid(String roleCode, Integer uid) { return roleUkeyBindingMapper.selectActiveByRoleCodeAndUid(roleCode, uid); @@ -25,6 +31,11 @@ public class RoleUkeyBindingRepositoryImpl implements RoleUkeyBindingRepository return roleUkeyBindingMapper.selectActiveByRoleCode(roleCode); } + @Override + public List findActiveByUkeyIdentity(String ukeySerial, String ukeyPubkey) { + return roleUkeyBindingMapper.selectActiveByUkeyIdentity(ukeySerial, ukeyPubkey); + } + @Override public void save(RoleUkeyBindingEntity entity) { roleUkeyBindingMapper.insert(entity); @@ -34,4 +45,9 @@ public class RoleUkeyBindingRepositoryImpl implements RoleUkeyBindingRepository public void update(RoleUkeyBindingEntity entity) { roleUkeyBindingMapper.updateById(entity); } + + @Override + public void deleteAll() { + roleUkeyBindingMapper.deleteAll(); + } } diff --git a/src/main/java/com/cisd/tms/modules/auth/service/AuthSecurityResetService.java b/src/main/java/com/cisd/tms/modules/auth/service/AuthSecurityResetService.java new file mode 100644 index 0000000..37c9c4c --- /dev/null +++ b/src/main/java/com/cisd/tms/modules/auth/service/AuthSecurityResetService.java @@ -0,0 +1,6 @@ +package com.cisd.tms.modules.auth.service; + +public interface AuthSecurityResetService { + + void resetAfterMasterKeyInitialized(); +} 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 fa0d0b2..c7bea02 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 @@ -136,6 +136,7 @@ public class AuthAdminServiceImpl implements AuthAdminService { .sorted(Comparator .comparingInt((AuthUserListItemResponse item) -> roleOrder(item.getRoleCode())) .thenComparing(AuthUserListItemResponse::getUid, Comparator.nullsLast(Integer::compareTo)) + .thenComparingInt(this::ukeyDisplayOrder) .thenComparing(AuthUserListItemResponse::getUkeyBindingId, Comparator.nullsLast(Long::compareTo)) .thenComparing(AuthUserListItemResponse::getUkeySerialNo, Comparator.nullsLast(String::compareTo))) .toList(); @@ -180,19 +181,29 @@ public class AuthAdminServiceImpl implements AuthAdminService { .stream() .toList(); if (bindings.isEmpty()) { - return Stream.of(toUserListItem(account, role, null)); + return Stream.of(toUserListItem(account, role, null, null)); } - return bindings.stream().map(binding -> toUserListItem(account, role, binding)); + List items = new ArrayList<>(); + for (int index = 0; index < bindings.size(); index++) { + String bindingLabel = index == 0 ? "主U盾" : "备份U盾"; + items.add(toUserListItem(account, role, bindings.get(index), bindingLabel)); + } + return items.stream(); } - private AuthUserListItemResponse toUserListItem(AuthFullAccountEntity account, RoleAccountEntity role, RoleUkeyBindingEntity binding) { + private AuthUserListItemResponse toUserListItem( + AuthFullAccountEntity account, + RoleAccountEntity role, + RoleUkeyBindingEntity binding, + String bindingLabel + ) { 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.setDisplayName(bindingLabel == null ? account.getDisplayName() : account.getDisplayName() + "-" + bindingLabel); item.setStatus(effectiveStatus); item.setStatusName(statusName(effectiveStatus)); item.setLastLoginAt(account.getLastLoginAt()); @@ -203,6 +214,20 @@ public class AuthAdminServiceImpl implements AuthAdminService { return item; } + private int ukeyDisplayOrder(AuthUserListItemResponse item) { + String displayName = item.getDisplayName(); + if (displayName == null) { + return 0; + } + if (displayName.endsWith("-主U盾")) { + return 0; + } + if (displayName.endsWith("-备份U盾")) { + return 1; + } + return 0; + } + private String effectiveStatus(RoleAccountEntity role, AuthFullAccountEntity account) { if (role != null && !RoleAccountStatus.ACTIVE.name().equals(role.getStatus())) { return role.getStatus(); @@ -287,6 +312,9 @@ public class AuthAdminServiceImpl implements AuthAdminService { authFullAccountRepository.findByRoleCodeAndUid(targetRoleCode, uid) .orElseThrow(() -> new BizException(ErrorCode.VALIDATE_FAILED.getCode(), "目标角色完整账号不存在")); + if (RoleCode.SUPER_ADMIN != targetRole) { + unbindActiveSeatBindings(targetRoleCode, uid); + } RoleUkeyBindingEntity binding = findExistingActiveBinding(targetRoleCode, uid, ukeySerial, ukeyPubkey); if (binding.getId() == null) { binding.setId((long) Math.abs(Objects.hash(targetRoleCode, uid, ukeySerial, TraceIdUtil.newTraceId()))); @@ -302,6 +330,16 @@ public class AuthAdminServiceImpl implements AuthAdminService { roleUkeyBindingRepository.update(binding); } + private void rebindSuperAdminUkey( + RoleUkeyBindingEntity binding, + String ukeySerial, + String ukeyPubkey, + String issuerSign + ) { + applyBinding(binding, ukeySerial, ukeyPubkey, issuerSign); + roleUkeyBindingRepository.update(binding); + } + private RoleUkeyBindingEntity findExistingActiveBinding( String roleCode, Integer uid, @@ -315,6 +353,29 @@ public class AuthAdminServiceImpl implements AuthAdminService { .orElseGet(RoleUkeyBindingEntity::new); } + private void unbindActiveSeatBindings(String roleCode, Integer uid) { + for (RoleUkeyBindingEntity existing : roleUkeyBindingRepository.findActiveByRoleCodeAndUid(roleCode, uid)) { + existing.setStatus("UNBOUND"); + existing.setUnboundAt(now()); + roleUkeyBindingRepository.update(existing); + } + } + + private RoleUkeyBindingEntity loadSuperAdminBindingForRebind(Integer uid, Long ukeyBindingId) { + if (ukeyBindingId == null) { + throw new BizException(ErrorCode.VALIDATE_FAILED.getCode(), "超级管理员UKey重绑定必须指定ukeyBindingId"); + } + RoleUkeyBindingEntity binding = roleUkeyBindingRepository.findById(ukeyBindingId) + .orElseThrow(() -> new BizException(ErrorCode.VALIDATE_FAILED.getCode(), "超级管理员UKey绑定记录不存在")); + boolean matchesSuperAdminSeat = RoleCode.SUPER_ADMIN.getCode().equals(binding.getRoleCode()) + && Objects.equals(uid, binding.getUid()) + && "ACTIVE".equals(binding.getStatus()); + if (!matchesSuperAdminSeat) { + throw new BizException(ErrorCode.VALIDATE_FAILED.getCode(), "ukeyBindingId不属于目标超级管理员席位"); + } + return binding; + } + @Override public UKeySignResult issueUkeyBindingSign( String operatorRoleCode, @@ -324,6 +385,17 @@ public class AuthAdminServiceImpl implements AuthAdminService { ) { RoleCode targetRole = resolveRoleCode(targetRoleCode); validateRoleUid(targetRole, request.getUid()); + if (request.getUkeySerial() == null || request.getUkeySerial().isBlank()) { + throw new BizException(ErrorCode.VALIDATE_FAILED.getCode(), "ukeySerial不能为空"); + } + boolean bootstrapSuperAdmin = RoleCode.SUPER_ADMIN == targetRole && operatorRoleCode == null && operatorAuthLevel == null; + RoleUkeyBindingEntity superAdminRebindTarget = null; + if (RoleCode.SUPER_ADMIN == targetRole && !bootstrapSuperAdmin) { + superAdminRebindTarget = loadSuperAdminBindingForRebind(request.getUid(), request.getUkeyBindingId()); + rejectUkeyBoundToAnotherBinding(request.getUkeySerial(), request.getPubKey(), superAdminRebindTarget.getId()); + } else { + rejectUkeyBoundToDifferentSeat(targetRole.getCode(), request.getUid(), request.getUkeySerial(), request.getPubKey()); + } String authKeyPair = lmkService.exportIkPublicKeyHex(); UKeySignDTO dto = new UKeySignDTO(); @@ -332,19 +404,20 @@ public class AuthAdminServiceImpl implements AuthAdminService { String signValue = lmkService.signIk(toIssuePayload(dto, authKeyPair, targetRole.getCode())); MasterKeyBackupPacket backupPacket = null; - if (request.getUkeySerial() == null || request.getUkeySerial().isBlank()) { - throw new BizException(ErrorCode.VALIDATE_FAILED.getCode(), "ukeySerial不能为空"); - } if (RoleCode.SUPER_ADMIN == targetRole) { backupPacket = lmkService.buildBackupPacket(request.getUid()); } - bindUkey( - targetRole.getCode(), - request.getUid(), - request.getUkeySerial(), - request.getPubKey(), - signValue - ); + if (superAdminRebindTarget != null) { + rebindSuperAdminUkey(superAdminRebindTarget, request.getUkeySerial(), request.getPubKey(), signValue); + } else { + bindUkey( + targetRole.getCode(), + request.getUid(), + request.getUkeySerial(), + request.getPubKey(), + signValue + ); + } enableRoleAfterUkeyBindingIfReady(targetRole); return UKeySignResult.builder() .sign(signValue) @@ -352,6 +425,22 @@ public class AuthAdminServiceImpl implements AuthAdminService { .build(); } + private void rejectUkeyBoundToDifferentSeat(String roleCode, Integer uid, String ukeySerial, String ukeyPubkey) { + boolean boundToDifferentSeat = roleUkeyBindingRepository.findActiveByUkeyIdentity(ukeySerial, ukeyPubkey).stream() + .anyMatch(binding -> !Objects.equals(binding.getRoleCode(), roleCode) || !Objects.equals(binding.getUid(), uid)); + if (boundToDifferentSeat) { + throw new BizException(ErrorCode.CONFLICT.getCode(), "UKey已绑定到其他角色席位"); + } + } + + private void rejectUkeyBoundToAnotherBinding(String ukeySerial, String ukeyPubkey, Long allowedBindingId) { + boolean boundToAnotherBinding = roleUkeyBindingRepository.findActiveByUkeyIdentity(ukeySerial, ukeyPubkey).stream() + .anyMatch(binding -> !Objects.equals(binding.getId(), allowedBindingId)); + if (boundToAnotherBinding) { + throw new BizException(ErrorCode.CONFLICT.getCode(), "UKey已绑定到其他角色席位"); + } + } + private void applyBinding( RoleUkeyBindingEntity binding, String ukeySerial, diff --git a/src/main/java/com/cisd/tms/modules/auth/service/impl/AuthSecurityResetServiceImpl.java b/src/main/java/com/cisd/tms/modules/auth/service/impl/AuthSecurityResetServiceImpl.java new file mode 100644 index 0000000..a3d22ac --- /dev/null +++ b/src/main/java/com/cisd/tms/modules/auth/service/impl/AuthSecurityResetServiceImpl.java @@ -0,0 +1,178 @@ +package com.cisd.tms.modules.auth.service.impl; + +import com.cisd.tms.modules.auth.entity.AuthFullAccountEntity; +import com.cisd.tms.modules.auth.entity.RoleAccountEntity; +import com.cisd.tms.modules.auth.enums.RoleAccountStatus; +import com.cisd.tms.modules.auth.enums.RoleCode; +import com.cisd.tms.modules.auth.repository.AuthFullAccountRepository; +import com.cisd.tms.modules.auth.repository.AuthSessionRepository; +import com.cisd.tms.modules.auth.repository.RoleAccountRepository; +import com.cisd.tms.modules.auth.repository.RoleUkeyBindingRepository; +import com.cisd.tms.modules.auth.service.AuthSecurityResetService; +import java.time.Clock; +import java.time.LocalDateTime; +import java.time.ZoneOffset; +import java.util.List; +import java.util.Map; +import java.util.function.Function; +import java.util.stream.Collectors; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +public class AuthSecurityResetServiceImpl implements AuthSecurityResetService { + + private final RoleAccountRepository roleAccountRepository; + private final AuthFullAccountRepository authFullAccountRepository; + private final RoleUkeyBindingRepository roleUkeyBindingRepository; + private final AuthSessionRepository authSessionRepository; + private final Clock clock; + + public AuthSecurityResetServiceImpl( + RoleAccountRepository roleAccountRepository, + AuthFullAccountRepository authFullAccountRepository, + RoleUkeyBindingRepository roleUkeyBindingRepository, + AuthSessionRepository authSessionRepository, + Clock clock + ) { + this.roleAccountRepository = roleAccountRepository; + this.authFullAccountRepository = authFullAccountRepository; + this.roleUkeyBindingRepository = roleUkeyBindingRepository; + this.authSessionRepository = authSessionRepository; + this.clock = clock; + } + + @Override + @Transactional + public void resetAfterMasterKeyInitialized() { + roleUkeyBindingRepository.deleteAll(); + authSessionRepository.deleteAll(); + Map rolesByCode = roleAccountRepository.findAll().stream() + .collect(Collectors.toMap(RoleAccountEntity::getRoleCode, Function.identity(), (left, right) -> left)); + for (SeedRole seed : seedRoles()) { + RoleAccountEntity role = rolesByCode.getOrDefault(seed.roleCode(), new RoleAccountEntity()); + boolean existing = role.getRoleCode() != null; + if (!existing) { + role.setId(seed.id()); + } + role.setRoleCode(seed.roleCode()); + role.setDisplayName(seed.displayName()); + role.setRequiredUkeyCount(seed.requiredUkeyCount()); + role.setStatus(seed.status()); + if (existing) { + roleAccountRepository.update(role); + } else { + roleAccountRepository.save(role); + } + } + LocalDateTime current = LocalDateTime.ofInstant(clock.instant(), ZoneOffset.UTC); + Map accountsBySeat = authFullAccountRepository.findAll().stream() + .collect(Collectors.toMap( + account -> seatKey(account.getRoleCode(), account.getUid()), + Function.identity(), + (left, right) -> left + )); + for (SeedFullAccount seed : seedFullAccounts()) { + AuthFullAccountEntity account = accountsBySeat.getOrDefault(seatKey(seed.roleCode(), seed.uid()), new AuthFullAccountEntity()); + boolean existing = account.getRoleCode() != null; + if (!existing) { + account.setId(seed.id()); + } + account.setRoleCode(seed.roleCode()); + account.setUid(seed.uid()); + account.setAccountName(seed.accountName()); + account.setDisplayName(seed.displayName()); + account.setPasswordHash(seed.passwordHash()); + account.setPasswordSalt(seed.passwordSalt()); + account.setStatus(RoleAccountStatus.ACTIVE.name()); + account.setNeedChangePassword(Boolean.TRUE); + account.setPasswordChangedAt(current); + account.setFailedCount(0); + account.setLockedUntil(null); + account.setLastLoginAt(null); + account.setLastActiveAt(null); + if (existing) { + authFullAccountRepository.update(account); + } else { + authFullAccountRepository.save(account); + } + } + } + + private String seatKey(String roleCode, Integer uid) { + return roleCode + "#" + uid; + } + + private List seedRoles() { + return List.of( + new SeedRole(11001L, RoleCode.SUPER_ADMIN.getCode(), "超级管理员", 2, RoleAccountStatus.ACTIVE.name()), + new SeedRole(11002L, RoleCode.KEY_ADMIN.getCode(), "密钥管理员", 1, RoleAccountStatus.ACTIVE.name()), + new SeedRole(11003L, RoleCode.AUDIT_ADMIN.getCode(), "审计管理员", 1, RoleAccountStatus.UNENABLED.name()), + new SeedRole(11004L, RoleCode.OPS_ADMIN.getCode(), "运维管理员", 1, RoleAccountStatus.UNENABLED.name()) + ); + } + + private List seedFullAccounts() { + return List.of( + new SeedFullAccount( + 12101L, + RoleCode.SUPER_ADMIN.getCode(), + 1, + "super-admin-full-01", + "超级管理员UKey席位一", + "Viuew8WE+qziUZwGGU0x25cUpzsTF+QvQFU0uhb+yVA=", + "init-super-admin-salt-20260325" + ), + new SeedFullAccount( + 12102L, + RoleCode.SUPER_ADMIN.getCode(), + 2, + "super-admin-full-02", + "超级管理员UKey席位二", + "Viuew8WE+qziUZwGGU0x25cUpzsTF+QvQFU0uhb+yVA=", + "init-super-admin-salt-20260325" + ), + new SeedFullAccount( + 12103L, + RoleCode.KEY_ADMIN.getCode(), + 1, + "key-admin-full-01", + "密钥管理员UKey席位", + "/xqmDy3A/q9X5p7XWznIPSBabQ6bxTLAPgtHQ6Be33c=", + "init-key-admin-salt-20260325" + ), + new SeedFullAccount( + 12104L, + RoleCode.AUDIT_ADMIN.getCode(), + 1, + "audit-admin-full-01", + "审计管理员UKey席位", + "j/J1LKyUOzVEJfbbHe2dwKBKNdabRk502olk/LWIwhg=", + "init-audit-admin-salt-20260325" + ), + new SeedFullAccount( + 12105L, + RoleCode.OPS_ADMIN.getCode(), + 1, + "ops-admin-full-01", + "运维管理员UKey席位", + "2e9F4aCgsDb+AuD1LrBEqEg8yjy3ODo27UHtx06vj/A=", + "init-ops-admin-salt-20260325" + ) + ); + } + + private record SeedRole(Long id, String roleCode, String displayName, int requiredUkeyCount, String status) { + } + + private record SeedFullAccount( + Long id, + String roleCode, + int uid, + String accountName, + String displayName, + String passwordHash, + String passwordSalt + ) { + } +} diff --git a/src/main/java/com/cisd/tms/modules/mk/dto/UKeySignDTO.java b/src/main/java/com/cisd/tms/modules/mk/dto/UKeySignDTO.java index 222b546..02185b1 100644 --- a/src/main/java/com/cisd/tms/modules/mk/dto/UKeySignDTO.java +++ b/src/main/java/com/cisd/tms/modules/mk/dto/UKeySignDTO.java @@ -20,6 +20,11 @@ public class UKeySignDTO { */ @NotBlank(message = "ukeySerial不能为空") private String ukeySerial; + + /** + * 角色 UKey 绑定记录 ID。超级管理员主/备 UKey 重绑定时用于精确定位目标记录。 + */ + private Long ukeyBindingId; /** * 角色内固定席位编号。 diff --git a/src/main/java/com/cisd/tms/modules/mk/service/impl/LmkServiceImpl.java b/src/main/java/com/cisd/tms/modules/mk/service/impl/LmkServiceImpl.java index 058e979..ed7e935 100644 --- a/src/main/java/com/cisd/tms/modules/mk/service/impl/LmkServiceImpl.java +++ b/src/main/java/com/cisd/tms/modules/mk/service/impl/LmkServiceImpl.java @@ -9,6 +9,7 @@ import com.cisd.tms.integration.crypto.pcie.jna.EccRefPublicKey; import com.cisd.tms.integration.crypto.pcie.jna.EccSignature; import com.cisd.tms.integration.crypto.pcie.model.*; import com.cisd.tms.integration.crypto.pcie.service.PcieCryptoService; +import com.cisd.tms.modules.auth.service.AuthSecurityResetService; import com.cisd.tms.modules.mk.common.LMKConstant; import com.cisd.tms.modules.mk.config.MasterKeyInitProperties; import com.cisd.tms.modules.mk.dto.MasterKeyBackupMaterial; @@ -31,9 +32,9 @@ import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Set; -import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.bouncycastle.util.encoders.Hex; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; /** @@ -41,7 +42,6 @@ import org.springframework.stereotype.Service; */ @Slf4j @Service -@RequiredArgsConstructor public class LmkServiceImpl implements LmkService { // 对外接口只暴露两份备份包:packet 1 = {1,2},packet 2 = {2,3}。 @@ -67,9 +67,25 @@ public class LmkServiceImpl implements LmkService { private final PcieCryptoService pcieCryptoService; private final MasterKeyInitProperties masterKeyInitProperties; + private final AuthSecurityResetService authSecurityResetService; private final Object backupRoundLock = new Object(); private CachedBackupRound cachedBackupRound; + @Autowired + public LmkServiceImpl( + PcieCryptoService pcieCryptoService, + MasterKeyInitProperties masterKeyInitProperties, + AuthSecurityResetService authSecurityResetService + ) { + this.pcieCryptoService = pcieCryptoService; + this.masterKeyInitProperties = masterKeyInitProperties; + this.authSecurityResetService = authSecurityResetService; + } + + public LmkServiceImpl(PcieCryptoService pcieCryptoService, MasterKeyInitProperties masterKeyInitProperties) { + this(pcieCryptoService, masterKeyInitProperties, () -> { }); + } + @Override public MasterKeyStateResult initMasterKey() { if (pcieCryptoService.checkLmk()) { @@ -83,6 +99,7 @@ public class LmkServiceImpl implements LmkService { MasterKeyStateResult result = new MasterKeyStateResult(); result.setStatus(true); result.setSeedMac(Hex.toHexString(pcieCryptoService.initializeMasterKey(oldPin, newPin))); + authSecurityResetService.resetAfterMasterKeyInitialized(); return result; } diff --git a/src/main/resources/mapper/auth/AuthSessionMapper.xml b/src/main/resources/mapper/auth/AuthSessionMapper.xml index 8633676..d1450cb 100644 --- a/src/main/resources/mapper/auth/AuthSessionMapper.xml +++ b/src/main/resources/mapper/auth/AuthSessionMapper.xml @@ -79,4 +79,8 @@ AND expires_at IS NOT NULL AND expires_at > #{now} + + + DELETE FROM tms_auth_session + diff --git a/src/main/resources/mapper/auth/RoleUkeyBindingMapper.xml b/src/main/resources/mapper/auth/RoleUkeyBindingMapper.xml index 26a35c7..0bdef17 100644 --- a/src/main/resources/mapper/auth/RoleUkeyBindingMapper.xml +++ b/src/main/resources/mapper/auth/RoleUkeyBindingMapper.xml @@ -34,7 +34,7 @@ WHERE role_code = #{roleCode} AND uid = #{uid} AND status = 'ACTIVE' - ORDER BY bound_at DESC, id DESC + ORDER BY bound_at ASC, id ASC + + + + + DELETE FROM tms_role_ukey_binding + 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 e1c163c..e51d071 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 @@ -357,4 +357,40 @@ class AuthControllerTest { .andExpect(status().isOk()) .andExpect(content().string(containsString("\"sign\":\"ISSUE-SIGN-KEY-001\""))); } + + @Test + void shouldIssueSuperAdminUkeyRebindThroughAdminEndpoint() throws Exception { + AuthService authService = Mockito.mock(AuthService.class); + AuthAdminService authAdminService = Mockito.mock(AuthAdminService.class); + MasterKeyBackupPacket packet = new MasterKeyBackupPacket(); + packet.setPacketIndex(1); + UKeySignResult response = UKeySignResult.builder().sign("ISSUE-SIGN-SUPER-REBIND").backupPacket(packet).build(); + Mockito.when(authAdminService.issueUkeyBindingSign( + ArgumentMatchers.eq("SUPER_ADMIN"), + ArgumentMatchers.eq("FULL"), + ArgumentMatchers.eq("SUPER_ADMIN"), + ArgumentMatchers.any() + )).thenReturn(response); + + MockMvc mockMvc = MockMvcBuilders + .standaloneSetup(new AuthController(authService), new AuthAdminController(authAdminService)) + .setControllerAdvice(new GlobalExceptionHandler()) + .build(); + + mockMvc.perform(post("/api/v1/auth/roles/SUPER_ADMIN/ukeys/issue-sign") + .requestAttr(InternalApiAuthInterceptor.ATTR_ROLE_CODE, "SUPER_ADMIN") + .requestAttr(InternalApiAuthInterceptor.ATTR_AUTH_LEVEL, "FULL") + .contentType(MediaType.APPLICATION_JSON) + .content(""" + { + "pubKey": "PUB-SUPER-NEW", + "ukeySerial": "UK-SUPER-NEW", + "uid": 1, + "ukeyBindingId": 1001 + } + """)) + .andExpect(status().isOk()) + .andExpect(content().string(containsString("\"sign\":\"ISSUE-SIGN-SUPER-REBIND\""))) + .andExpect(content().string(containsString("\"packetIndex\":1"))); + } } 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 b8c0453..20c4e8a 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 @@ -49,9 +49,9 @@ class AuthAdminServiceTest { AuthFullAccountEntity first = accounts.findByRoleCodeAndUid(RoleCode.SUPER_ADMIN.getCode(), 1).orElseThrow(); AuthFullAccountEntity second = accounts.findByRoleCodeAndUid(RoleCode.SUPER_ADMIN.getCode(), 2).orElseThrow(); Assertions.assertEquals("salt-reset-1", first.getPasswordSalt()); - Assertions.assertEquals("HASH:12345678:salt-reset-1", first.getPasswordHash()); + Assertions.assertEquals("HASH:Sunyard@123:salt-reset-1", first.getPasswordHash()); Assertions.assertEquals("salt-reset-2", second.getPasswordSalt()); - Assertions.assertEquals("HASH:12345678:salt-reset-2", second.getPasswordHash()); + Assertions.assertEquals("HASH:Sunyard@123:salt-reset-2", second.getPasswordHash()); Assertions.assertTrue(Boolean.TRUE.equals(first.getNeedChangePassword())); Assertions.assertTrue(Boolean.TRUE.equals(second.getNeedChangePassword())); } @@ -102,7 +102,43 @@ class AuthAdminServiceTest { } @Test - void shouldIssueSuperAdminBackupUkeyWithoutReplacingExistingBindingForSameUid() { + void shouldRebindSuperAdminUkeyByBindingIdWithoutReplacingSiblingBinding() { + InMemoryRoleAccountRepository roles = new InMemoryRoleAccountRepository(); + InMemoryAuthFullAccountRepository accounts = new InMemoryAuthFullAccountRepository(); + InMemoryRoleUkeyBindingRepository bindings = new InMemoryRoleUkeyBindingRepository(); + roles.save(role(RoleCode.SUPER_ADMIN, RoleAccountStatus.ACTIVE)); + accounts.save(account(RoleCode.SUPER_ADMIN, 1, "super-admin-01", "HASH-OLD", "SALT-OLD")); + RoleUkeyBindingEntity primary = binding(RoleCode.SUPER_ADMIN, 1, "UK-PRIMARY", "PUB-PRIMARY", "SIG-PRIMARY"); + RoleUkeyBindingEntity backup = binding(RoleCode.SUPER_ADMIN, 1, "UK-BACKUP", "PUB-BACKUP", "SIG-BACKUP"); + bindings.save(primary); + bindings.save(backup); + LmkService lmkService = Mockito.mock(LmkService.class); + Mockito.when(lmkService.exportIkPublicKeyHex()).thenReturn("IK-PUB-001"); + Mockito.when(lmkService.signIk(Mockito.anyString())).thenReturn("ISSUE-SIGN-REBIND"); + MasterKeyBackupPacket packet = new MasterKeyBackupPacket(); + packet.setPacketIndex(1); + Mockito.when(lmkService.buildBackupPacket(1)).thenReturn(packet); + UKeySignDTO dto = new UKeySignDTO(); + dto.setPubKey("PUB-BACKUP-NEW"); + dto.setUkeySerial("UK-BACKUP-NEW"); + dto.setUid(1); + dto.setUkeyBindingId(backup.getId()); + + UKeySignResult result = service(roles, accounts, bindings, lmkService) + .issueUkeyBindingSign(RoleCode.SUPER_ADMIN.getCode(), AuthLevel.FULL.name(), RoleCode.SUPER_ADMIN.getCode(), dto); + + Assertions.assertEquals("ISSUE-SIGN-REBIND", result.getSign()); + Assertions.assertNotNull(result.getBackupPacket()); + List activeUidBindings = bindings.findActiveByRoleCodeAndUid(RoleCode.SUPER_ADMIN.getCode(), 1); + Assertions.assertEquals(2, activeUidBindings.size()); + Assertions.assertTrue(activeUidBindings.stream() + .anyMatch(item -> primary.getId().equals(item.getId()) && "UK-PRIMARY".equals(item.getUkeySerial()))); + Assertions.assertTrue(activeUidBindings.stream() + .anyMatch(item -> backup.getId().equals(item.getId()) && "UK-BACKUP-NEW".equals(item.getUkeySerial()))); + } + + @Test + void shouldRejectSuperAdminRebindWithoutBindingId() { InMemoryRoleAccountRepository roles = new InMemoryRoleAccountRepository(); InMemoryAuthFullAccountRepository accounts = new InMemoryAuthFullAccountRepository(); InMemoryRoleUkeyBindingRepository bindings = new InMemoryRoleUkeyBindingRepository(); @@ -110,25 +146,101 @@ class AuthAdminServiceTest { accounts.save(account(RoleCode.SUPER_ADMIN, 1, "super-admin-01", "HASH-OLD", "SALT-OLD")); bindings.save(binding(RoleCode.SUPER_ADMIN, 1, "UK-PRIMARY", "PUB-PRIMARY", "SIG-PRIMARY")); LmkService lmkService = Mockito.mock(LmkService.class); + UKeySignDTO dto = new UKeySignDTO(); + dto.setPubKey("PUB-NEW"); + dto.setUkeySerial("UK-NEW"); + dto.setUid(1); + + BizException exception = Assertions.assertThrows(BizException.class, () -> service(roles, accounts, bindings, lmkService) + .issueUkeyBindingSign(RoleCode.SUPER_ADMIN.getCode(), AuthLevel.FULL.name(), RoleCode.SUPER_ADMIN.getCode(), dto)); + + Assertions.assertEquals(ErrorCode.VALIDATE_FAILED.getCode(), exception.getCode()); + Assertions.assertEquals("超级管理员UKey重绑定必须指定ukeyBindingId", exception.getMessage()); + Mockito.verify(lmkService, Mockito.never()).buildBackupPacket(Mockito.anyInt()); + Mockito.verify(lmkService, Mockito.never()).signIk(Mockito.anyString()); + } + + @Test + void shouldRebindSingleSeatAdminByReplacingExistingBinding() { + InMemoryRoleAccountRepository roles = new InMemoryRoleAccountRepository(); + InMemoryAuthFullAccountRepository accounts = new InMemoryAuthFullAccountRepository(); + InMemoryRoleUkeyBindingRepository bindings = new InMemoryRoleUkeyBindingRepository(); + roles.save(role(RoleCode.KEY_ADMIN, RoleAccountStatus.ACTIVE)); + accounts.save(account(RoleCode.KEY_ADMIN, 1, "key-admin-01", "HASH-OLD", "SALT-OLD")); + bindings.save(binding(RoleCode.KEY_ADMIN, 1, "UK-OLD", "PUB-OLD", "SIG-OLD")); + LmkService lmkService = Mockito.mock(LmkService.class); Mockito.when(lmkService.exportIkPublicKeyHex()).thenReturn("IK-PUB-001"); - Mockito.when(lmkService.signIk(Mockito.anyString())).thenReturn("ISSUE-SIGN-BACKUP"); + Mockito.when(lmkService.signIk(Mockito.anyString())).thenReturn("ISSUE-SIGN-NEW"); + UKeySignDTO dto = new UKeySignDTO(); + dto.setPubKey("PUB-NEW"); + dto.setUkeySerial("UK-NEW"); + dto.setUid(1); + + UKeySignResult result = service(roles, accounts, bindings, lmkService) + .issueUkeyBindingSign(RoleCode.SUPER_ADMIN.getCode(), AuthLevel.FULL.name(), RoleCode.KEY_ADMIN.getCode(), dto); + + Assertions.assertEquals("ISSUE-SIGN-NEW", result.getSign()); + Assertions.assertNull(result.getBackupPacket()); + List activeBindings = bindings.findActiveByRoleCodeAndUid(RoleCode.KEY_ADMIN.getCode(), 1); + Assertions.assertEquals(1, activeBindings.size()); + Assertions.assertEquals("UK-NEW", activeBindings.get(0).getUkeySerial()); + Assertions.assertTrue(bindings.all().stream() + .anyMatch(item -> "UK-OLD".equals(item.getUkeySerial()) + && "UNBOUND".equals(item.getStatus()) + && item.getUnboundAt() != null)); + } + + @Test + void shouldBootstrapSuperAdminUkeyWithoutLoginContext() { + InMemoryRoleAccountRepository roles = new InMemoryRoleAccountRepository(); + InMemoryAuthFullAccountRepository accounts = new InMemoryAuthFullAccountRepository(); + InMemoryRoleUkeyBindingRepository bindings = new InMemoryRoleUkeyBindingRepository(); + roles.save(role(RoleCode.SUPER_ADMIN, RoleAccountStatus.ACTIVE)); + accounts.save(account(RoleCode.SUPER_ADMIN, 1, "super-admin-01", "HASH-OLD", "SALT-OLD")); + 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-BACKUP"); - dto.setUkeySerial("UK-BACKUP"); + dto.setPubKey("PUB-SUPER"); + dto.setUkeySerial("UK-SUPER"); dto.setUid(1); UKeySignResult result = service(roles, accounts, bindings, lmkService) - .issueUkeyBindingSign(RoleCode.SUPER_ADMIN.getCode(), AuthLevel.FULL.name(), RoleCode.SUPER_ADMIN.getCode(), dto); + .issueUkeyBindingSign(null, null, RoleCode.SUPER_ADMIN.getCode(), dto); - Assertions.assertEquals("ISSUE-SIGN-BACKUP", result.getSign()); + List activeBindings = bindings.findActiveByRoleCodeAndUid(RoleCode.SUPER_ADMIN.getCode(), 1); + Assertions.assertEquals(1, activeBindings.size()); + Assertions.assertEquals("UK-SUPER", activeBindings.get(0).getUkeySerial()); + Assertions.assertEquals("ISSUE-SIGN-SUPER", result.getSign()); Assertions.assertNotNull(result.getBackupPacket()); - List activeUidBindings = bindings.findActiveByRoleCodeAndUid(RoleCode.SUPER_ADMIN.getCode(), 1); - Assertions.assertEquals(2, activeUidBindings.size()); - Assertions.assertTrue(activeUidBindings.stream().anyMatch(item -> "UK-PRIMARY".equals(item.getUkeySerial()))); - Assertions.assertTrue(activeUidBindings.stream().anyMatch(item -> "UK-BACKUP".equals(item.getUkeySerial()))); + } + + @Test + void shouldRejectSameUkeyBindingToDifferentSeat() { + InMemoryRoleAccountRepository roles = new InMemoryRoleAccountRepository(); + InMemoryAuthFullAccountRepository accounts = new InMemoryAuthFullAccountRepository(); + InMemoryRoleUkeyBindingRepository bindings = new InMemoryRoleUkeyBindingRepository(); + roles.save(role(RoleCode.SUPER_ADMIN, RoleAccountStatus.ACTIVE)); + 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")); + bindings.save(binding(RoleCode.SUPER_ADMIN, 1, "UK-PRIMARY", "PUB-PRIMARY", "SIG-PRIMARY")); + LmkService lmkService = Mockito.mock(LmkService.class); + UKeySignDTO dto = new UKeySignDTO(); + dto.setPubKey("PUB-PRIMARY"); + dto.setUkeySerial("UK-PRIMARY"); + dto.setUid(2); + + BizException exception = Assertions.assertThrows(BizException.class, () -> service(roles, accounts, bindings, lmkService) + .issueUkeyBindingSign(null, null, RoleCode.SUPER_ADMIN.getCode(), dto)); + + Assertions.assertEquals(ErrorCode.CONFLICT.getCode(), exception.getCode()); + Assertions.assertEquals("UKey已绑定到其他角色席位", exception.getMessage()); + Assertions.assertTrue(bindings.findActiveByRoleCodeAndUid(RoleCode.SUPER_ADMIN.getCode(), 2).isEmpty()); + Mockito.verify(lmkService, Mockito.never()).buildBackupPacket(2); + Mockito.verify(lmkService, Mockito.never()).signIk(Mockito.anyString()); } @Test @@ -222,9 +334,11 @@ class AuthAdminServiceTest { 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("超级管理员UKey席位一-主U盾", response.getItems().get(0).getDisplayName()); Assertions.assertEquals("正常", response.getItems().get(0).getStatusName()); - Assertions.assertEquals("UK-BACKUP", response.getItems().get(0).getUkeySerialNo()); - Assertions.assertEquals("UK-PRIMARY", response.getItems().get(1).getUkeySerialNo()); + Assertions.assertEquals("UK-PRIMARY", response.getItems().get(0).getUkeySerialNo()); + Assertions.assertEquals("超级管理员UKey席位一-备份U盾", response.getItems().get(1).getDisplayName()); + Assertions.assertEquals("UK-BACKUP", response.getItems().get(1).getUkeySerialNo()); var disabledResponse = service(roles, accounts, bindings, Mockito.mock(LmkService.class)) .listUsers(null, "未启用", 1, 10); @@ -385,6 +499,13 @@ class AuthAdminServiceTest { private static class InMemoryRoleUkeyBindingRepository implements RoleUkeyBindingRepository { private final List store = new ArrayList<>(); + @Override + public Optional findById(Long id) { + return store.stream() + .filter(entity -> id.equals(entity.getId())) + .findFirst(); + } + @Override public List findActiveByRoleCode(String roleCode) { return store.stream() @@ -400,6 +521,14 @@ class AuthAdminServiceTest { .toList(); } + @Override + public List findActiveByUkeyIdentity(String ukeySerial, String ukeyPubkey) { + return store.stream() + .filter(entity -> "ACTIVE".equals(entity.getStatus())) + .filter(entity -> ukeySerial.equals(entity.getUkeySerial()) || ukeyPubkey.equals(entity.getUkeyPubkey())) + .toList(); + } + @Override public void save(RoleUkeyBindingEntity entity) { store.add(entity); @@ -410,5 +539,14 @@ class AuthAdminServiceTest { store.removeIf(item -> item.getId().equals(entity.getId())); store.add(entity); } + + @Override + public void deleteAll() { + store.clear(); + } + + private List all() { + return store; + } } } diff --git a/src/test/java/com/cisd/tms/modules/auth/service/AuthSecurityResetServiceTest.java b/src/test/java/com/cisd/tms/modules/auth/service/AuthSecurityResetServiceTest.java new file mode 100644 index 0000000..a7bf47b --- /dev/null +++ b/src/test/java/com/cisd/tms/modules/auth/service/AuthSecurityResetServiceTest.java @@ -0,0 +1,116 @@ +package com.cisd.tms.modules.auth.service; + +import com.cisd.tms.modules.auth.entity.AuthFullAccountEntity; +import com.cisd.tms.modules.auth.entity.RoleAccountEntity; +import com.cisd.tms.modules.auth.enums.RoleAccountStatus; +import com.cisd.tms.modules.auth.enums.RoleCode; +import com.cisd.tms.modules.auth.repository.AuthFullAccountRepository; +import com.cisd.tms.modules.auth.repository.AuthSessionRepository; +import com.cisd.tms.modules.auth.repository.RoleAccountRepository; +import com.cisd.tms.modules.auth.repository.RoleUkeyBindingRepository; +import com.cisd.tms.modules.auth.service.impl.AuthSecurityResetServiceImpl; +import java.time.Clock; +import java.time.Instant; +import java.time.LocalDateTime; +import java.time.ZoneOffset; +import java.util.List; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.Mockito; + +class AuthSecurityResetServiceTest { + + private static final Clock FIXED_CLOCK = Clock.fixed(Instant.parse("2026-05-13T08:00:00Z"), ZoneOffset.UTC); + + @Test + void shouldClearBindingsSessionsAndRestoreSeededRolesAndPasswords() { + RoleAccountRepository roleAccountRepository = Mockito.mock(RoleAccountRepository.class); + AuthFullAccountRepository fullAccountRepository = Mockito.mock(AuthFullAccountRepository.class); + RoleUkeyBindingRepository ukeyBindingRepository = Mockito.mock(RoleUkeyBindingRepository.class); + AuthSessionRepository authSessionRepository = Mockito.mock(AuthSessionRepository.class); + Mockito.when(fullAccountRepository.findAll()).thenReturn(List.of( + dirtyAccount(RoleCode.SUPER_ADMIN, 1), + dirtyAccount(RoleCode.SUPER_ADMIN, 2), + dirtyAccount(RoleCode.KEY_ADMIN, 1), + dirtyAccount(RoleCode.AUDIT_ADMIN, 1), + dirtyAccount(RoleCode.OPS_ADMIN, 1) + )); + Mockito.when(roleAccountRepository.findAll()).thenReturn(List.of( + dirtyRole(RoleCode.SUPER_ADMIN), + dirtyRole(RoleCode.KEY_ADMIN), + dirtyRole(RoleCode.AUDIT_ADMIN), + dirtyRole(RoleCode.OPS_ADMIN) + )); + + new AuthSecurityResetServiceImpl( + roleAccountRepository, + fullAccountRepository, + ukeyBindingRepository, + authSessionRepository, + FIXED_CLOCK + ).resetAfterMasterKeyInitialized(); + + Mockito.verify(ukeyBindingRepository).deleteAll(); + Mockito.verify(authSessionRepository).deleteAll(); + + ArgumentCaptor roleCaptor = ArgumentCaptor.forClass(RoleAccountEntity.class); + Mockito.verify(roleAccountRepository, Mockito.times(4)).update(roleCaptor.capture()); + RoleAccountEntity superAdmin = roleCaptor.getAllValues().stream() + .filter(item -> RoleCode.SUPER_ADMIN.getCode().equals(item.getRoleCode())) + .findFirst() + .orElseThrow(); + RoleAccountEntity auditAdmin = roleCaptor.getAllValues().stream() + .filter(item -> RoleCode.AUDIT_ADMIN.getCode().equals(item.getRoleCode())) + .findFirst() + .orElseThrow(); + Assertions.assertEquals("超级管理员", superAdmin.getDisplayName()); + Assertions.assertEquals(2, superAdmin.getRequiredUkeyCount()); + Assertions.assertEquals(RoleAccountStatus.ACTIVE.name(), superAdmin.getStatus()); + Assertions.assertEquals(RoleAccountStatus.UNENABLED.name(), auditAdmin.getStatus()); + + ArgumentCaptor accountCaptor = ArgumentCaptor.forClass(AuthFullAccountEntity.class); + Mockito.verify(fullAccountRepository, Mockito.times(5)).update(accountCaptor.capture()); + AuthFullAccountEntity firstSuperAdmin = accountCaptor.getAllValues().stream() + .filter(item -> RoleCode.SUPER_ADMIN.getCode().equals(item.getRoleCode()) && item.getUid() == 1) + .findFirst() + .orElseThrow(); + Assertions.assertEquals("super-admin-full-01", firstSuperAdmin.getAccountName()); + Assertions.assertEquals("超级管理员UKey席位一", firstSuperAdmin.getDisplayName()); + Assertions.assertEquals("init-super-admin-salt-20260325", firstSuperAdmin.getPasswordSalt()); + Assertions.assertEquals("Viuew8WE+qziUZwGGU0x25cUpzsTF+QvQFU0uhb+yVA=", firstSuperAdmin.getPasswordHash()); + Assertions.assertEquals(RoleAccountStatus.ACTIVE.name(), firstSuperAdmin.getStatus()); + Assertions.assertTrue(Boolean.TRUE.equals(firstSuperAdmin.getNeedChangePassword())); + Assertions.assertEquals(0, firstSuperAdmin.getFailedCount()); + Assertions.assertNull(firstSuperAdmin.getLockedUntil()); + Assertions.assertNull(firstSuperAdmin.getLastLoginAt()); + Assertions.assertNull(firstSuperAdmin.getLastActiveAt()); + Assertions.assertEquals(LocalDateTime.ofInstant(FIXED_CLOCK.instant(), ZoneOffset.UTC), firstSuperAdmin.getPasswordChangedAt()); + } + + private static RoleAccountEntity dirtyRole(RoleCode roleCode) { + RoleAccountEntity entity = new RoleAccountEntity(); + entity.setRoleCode(roleCode.getCode()); + entity.setDisplayName("DIRTY-" + roleCode.getCode()); + entity.setRequiredUkeyCount(99); + entity.setStatus(RoleAccountStatus.LOCKED.name()); + return entity; + } + + private static AuthFullAccountEntity dirtyAccount(RoleCode roleCode, int uid) { + AuthFullAccountEntity entity = new AuthFullAccountEntity(); + entity.setRoleCode(roleCode.getCode()); + entity.setUid(uid); + entity.setAccountName("dirty"); + entity.setDisplayName("dirty"); + entity.setPasswordHash("dirty"); + entity.setPasswordSalt("dirty"); + entity.setStatus(RoleAccountStatus.LOCKED.name()); + entity.setNeedChangePassword(Boolean.FALSE); + entity.setFailedCount(5); + entity.setLockedUntil(LocalDateTime.of(2026, 5, 13, 9, 0)); + entity.setLastLoginAt(LocalDateTime.of(2026, 5, 13, 9, 0)); + entity.setLastActiveAt(LocalDateTime.of(2026, 5, 13, 9, 0)); + return 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 905d745..099c314 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 @@ -493,11 +493,23 @@ class AuthServiceTest { .filter(entity -> java.util.Objects.equals(authenticatedPrincipalsJson, entity.getAuthenticatedPrincipalsJson())) .toList(); } + + @Override + public void deleteAll() { + store.clear(); + } } private static class InMemoryRoleUkeyBindingRepository implements RoleUkeyBindingRepository { private final List store = new ArrayList<>(); + @Override + public Optional findById(Long id) { + return store.stream() + .filter(entity -> id.equals(entity.getId())) + .findFirst(); + } + @Override public List findActiveByRoleCode(String roleCode) { return store.stream() @@ -513,6 +525,14 @@ class AuthServiceTest { .toList(); } + @Override + public List findActiveByUkeyIdentity(String ukeySerial, String ukeyPubkey) { + return store.stream() + .filter(entity -> "ACTIVE".equals(entity.getStatus())) + .filter(entity -> ukeySerial.equals(entity.getUkeySerial()) || ukeyPubkey.equals(entity.getUkeyPubkey())) + .toList(); + } + @Override public void save(RoleUkeyBindingEntity entity) { store.add(entity); @@ -522,5 +542,10 @@ class AuthServiceTest { public void update(RoleUkeyBindingEntity entity) { save(entity); } + + @Override + public void deleteAll() { + store.clear(); + } } } diff --git a/src/test/java/com/cisd/tms/modules/mk/service/LmkServiceTest.java b/src/test/java/com/cisd/tms/modules/mk/service/LmkServiceTest.java index ffb3b92..551dfe5 100644 --- a/src/test/java/com/cisd/tms/modules/mk/service/LmkServiceTest.java +++ b/src/test/java/com/cisd/tms/modules/mk/service/LmkServiceTest.java @@ -16,6 +16,7 @@ import com.cisd.tms.integration.crypto.pcie.model.RecoverUserKeyRequest; import com.cisd.tms.integration.crypto.pcie.service.PcieCryptoService; import com.cisd.tms.common.enums.ErrorCode; import com.cisd.tms.common.exception.BizException; +import com.cisd.tms.modules.auth.service.AuthSecurityResetService; import com.cisd.tms.modules.mk.config.MasterKeyInitProperties; import com.cisd.tms.modules.mk.dto.MasterKeyBackupPacket; import com.cisd.tms.modules.mk.dto.MasterKeyPacketIkPart; @@ -57,6 +58,22 @@ class LmkServiceTest { Assertions.assertArrayEquals("87654321".getBytes(StandardCharsets.UTF_8), newPinCaptor.getValue()); } + @Test + void shouldResetAuthSecurityStateAfterMasterKeyInitializationSucceeds() { + PcieCryptoService pcieCryptoService = Mockito.mock(PcieCryptoService.class); + AuthSecurityResetService authSecurityResetService = Mockito.mock(AuthSecurityResetService.class); + MasterKeyInitProperties properties = configuredPins("MTIzNDU2Nzg=", "ODc2NTQzMjE="); + Mockito.when(pcieCryptoService.initializeMasterKey(Mockito.any(), Mockito.any())).thenReturn(new byte[] {0x01, 0x23}); + + LmkService service = new LmkServiceImpl(pcieCryptoService, properties, authSecurityResetService); + + service.initMasterKey(); + + InOrder inOrder = Mockito.inOrder(pcieCryptoService, authSecurityResetService); + inOrder.verify(pcieCryptoService).initializeMasterKey(Mockito.any(), Mockito.any()); + inOrder.verify(authSecurityResetService).resetAfterMasterKeyInitialized(); + } + @Test void shouldPropagateInitMasterKeyFailure() { PcieCryptoService pcieCryptoService = Mockito.mock(PcieCryptoService.class);