fix:u盾绑定
This commit is contained in:
@@ -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<UKeySignResult> 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),
|
||||
|
||||
@@ -24,4 +24,6 @@ public interface AuthSessionMapper extends BaseMapperX<AuthSessionEntity> {
|
||||
@Param("authenticatedPrincipalsJson") String authenticatedPrincipalsJson,
|
||||
@Param("now") LocalDateTime now
|
||||
);
|
||||
|
||||
int deleteAll();
|
||||
}
|
||||
|
||||
@@ -12,4 +12,11 @@ public interface RoleUkeyBindingMapper extends BaseMapperX<RoleUkeyBindingEntity
|
||||
List<RoleUkeyBindingEntity> selectActiveByRoleCodeAndUid(@Param("roleCode") String roleCode, @Param("uid") Integer uid);
|
||||
|
||||
List<RoleUkeyBindingEntity> selectActiveByRoleCode(@Param("roleCode") String roleCode);
|
||||
|
||||
List<RoleUkeyBindingEntity> selectActiveByUkeyIdentity(
|
||||
@Param("ukeySerial") String ukeySerial,
|
||||
@Param("ukeyPubkey") String ukeyPubkey
|
||||
);
|
||||
|
||||
int deleteAll();
|
||||
}
|
||||
|
||||
@@ -21,4 +21,6 @@ public interface AuthSessionRepository {
|
||||
void save(AuthSessionEntity entity);
|
||||
|
||||
void update(AuthSessionEntity entity);
|
||||
|
||||
void deleteAll();
|
||||
}
|
||||
|
||||
@@ -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<RoleUkeyBindingEntity> findById(Long id);
|
||||
|
||||
List<RoleUkeyBindingEntity> findActiveByRoleCodeAndUid(String roleCode, Integer uid);
|
||||
|
||||
List<RoleUkeyBindingEntity> findActiveByRoleCode(String roleCode);
|
||||
|
||||
List<RoleUkeyBindingEntity> findActiveByUkeyIdentity(String ukeySerial, String ukeyPubkey);
|
||||
|
||||
void save(RoleUkeyBindingEntity entity);
|
||||
|
||||
void update(RoleUkeyBindingEntity entity);
|
||||
|
||||
void deleteAll();
|
||||
}
|
||||
|
||||
@@ -51,4 +51,9 @@ public class AuthSessionRepositoryImpl implements AuthSessionRepository {
|
||||
public void update(AuthSessionEntity entity) {
|
||||
authSessionMapper.updateById(entity);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deleteAll() {
|
||||
authSessionMapper.deleteAll();
|
||||
}
|
||||
}
|
||||
|
||||
+16
@@ -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<RoleUkeyBindingEntity> findById(Long id) {
|
||||
return Optional.ofNullable(roleUkeyBindingMapper.selectById(id));
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<RoleUkeyBindingEntity> 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<RoleUkeyBindingEntity> 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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
package com.cisd.tms.modules.auth.service;
|
||||
|
||||
public interface AuthSecurityResetService {
|
||||
|
||||
void resetAfterMasterKeyInitialized();
|
||||
}
|
||||
@@ -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<AuthUserListItemResponse> 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,
|
||||
|
||||
+178
@@ -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<String, RoleAccountEntity> 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<String, AuthFullAccountEntity> 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<SeedRole> 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<SeedFullAccount> 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
|
||||
) {
|
||||
}
|
||||
}
|
||||
@@ -21,6 +21,11 @@ public class UKeySignDTO {
|
||||
@NotBlank(message = "ukeySerial不能为空")
|
||||
private String ukeySerial;
|
||||
|
||||
/**
|
||||
* 角色 UKey 绑定记录 ID。超级管理员主/备 UKey 重绑定时用于精确定位目标记录。
|
||||
*/
|
||||
private Long ukeyBindingId;
|
||||
|
||||
/**
|
||||
* 角色内固定席位编号。
|
||||
*/
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -79,4 +79,8 @@
|
||||
AND expires_at IS NOT NULL
|
||||
AND expires_at > #{now}
|
||||
</select>
|
||||
|
||||
<delete id="deleteAll">
|
||||
DELETE FROM tms_auth_session
|
||||
</delete>
|
||||
</mapper>
|
||||
|
||||
@@ -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
|
||||
</select>
|
||||
|
||||
<select id="selectActiveByRoleCode" parameterType="string" resultMap="RoleUkeyBindingResultMap">
|
||||
@@ -52,6 +52,28 @@
|
||||
FROM tms_role_ukey_binding
|
||||
WHERE role_code = #{roleCode}
|
||||
AND status = 'ACTIVE'
|
||||
ORDER BY uid ASC
|
||||
ORDER BY uid ASC, bound_at ASC, id ASC
|
||||
</select>
|
||||
|
||||
<select id="selectActiveByUkeyIdentity" resultMap="RoleUkeyBindingResultMap">
|
||||
SELECT id,
|
||||
role_code,
|
||||
uid,
|
||||
ukey_serial,
|
||||
ukey_pubkey,
|
||||
issuer_sign,
|
||||
status,
|
||||
bound_at,
|
||||
unbound_at,
|
||||
create_time,
|
||||
update_time
|
||||
FROM tms_role_ukey_binding
|
||||
WHERE status = 'ACTIVE'
|
||||
AND (ukey_serial = #{ukeySerial} OR ukey_pubkey = #{ukeyPubkey})
|
||||
ORDER BY bound_at DESC, id DESC
|
||||
</select>
|
||||
|
||||
<delete id="deleteAll">
|
||||
DELETE FROM tms_role_ukey_binding
|
||||
</delete>
|
||||
</mapper>
|
||||
|
||||
@@ -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")));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<RoleUkeyBindingEntity> 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<RoleUkeyBindingEntity> 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<RoleUkeyBindingEntity> 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<RoleUkeyBindingEntity> 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<RoleUkeyBindingEntity> store = new ArrayList<>();
|
||||
|
||||
@Override
|
||||
public Optional<RoleUkeyBindingEntity> findById(Long id) {
|
||||
return store.stream()
|
||||
.filter(entity -> id.equals(entity.getId()))
|
||||
.findFirst();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<RoleUkeyBindingEntity> findActiveByRoleCode(String roleCode) {
|
||||
return store.stream()
|
||||
@@ -400,6 +521,14 @@ class AuthAdminServiceTest {
|
||||
.toList();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<RoleUkeyBindingEntity> 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<RoleUkeyBindingEntity> all() {
|
||||
return store;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<RoleAccountEntity> 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<AuthFullAccountEntity> 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;
|
||||
}
|
||||
}
|
||||
@@ -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<RoleUkeyBindingEntity> store = new ArrayList<>();
|
||||
|
||||
@Override
|
||||
public Optional<RoleUkeyBindingEntity> findById(Long id) {
|
||||
return store.stream()
|
||||
.filter(entity -> id.equals(entity.getId()))
|
||||
.findFirst();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<RoleUkeyBindingEntity> findActiveByRoleCode(String roleCode) {
|
||||
return store.stream()
|
||||
@@ -513,6 +525,14 @@ class AuthServiceTest {
|
||||
.toList();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<RoleUkeyBindingEntity> 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user