fix:角色授权修改

This commit is contained in:
waner 2026-04-25 14:56:32 +08:00
parent 779052142b
commit 7bc364f367
17 changed files with 321 additions and 225 deletions

View File

@ -96,7 +96,8 @@ public class WebMvcConfig implements WebMvcConfigurer {
"/api/v1/auth/password-login", "/api/v1/auth/password-login",
"/api/v1/auth/ukey-login", "/api/v1/auth/ukey-login",
"/api/v1/auth/ukey-login/randoms", "/api/v1/auth/ukey-login/randoms",
"/api/v1/auth/captcha" "/api/v1/auth/captcha",
"/api/v1/auth/super-admin/ukeys/issue-sign"
); );
registry.addInterceptor(internalApiReplayInterceptor) registry.addInterceptor(internalApiReplayInterceptor)

View File

@ -1,8 +1,9 @@
package com.cisd.tms.modules.auth.controller; package com.cisd.tms.modules.auth.controller;
import com.cisd.tms.common.api.ApiResponse; 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.AdminChangePasswordRequest;
import com.cisd.tms.modules.auth.dto.UkeyBindRequest;
import com.cisd.tms.modules.auth.enums.AuthLevel; import com.cisd.tms.modules.auth.enums.AuthLevel;
import com.cisd.tms.modules.auth.enums.RoleCode; import com.cisd.tms.modules.auth.enums.RoleCode;
import com.cisd.tms.modules.auth.security.RequireInternalAuth; import com.cisd.tms.modules.auth.security.RequireInternalAuth;
@ -24,14 +25,14 @@ import org.springframework.web.bind.annotation.*;
@RestController @RestController
@RequestMapping("/api/v1/auth") @RequestMapping("/api/v1/auth")
@RequiredArgsConstructor @RequiredArgsConstructor
@ReplayProtected
@Tag(name = "认证管理", description = "角色启用、密码重置与 UKey 绑定接口") @Tag(name = "认证管理", description = "角色启用、密码重置与 UKey 绑定接口")
public class AuthAdminController { public class AuthAdminController {
private final AuthAdminService authAdminService; private final AuthAdminService authAdminService;
@PostMapping("/roles/{roleCode}/enable") @PostMapping("/roles/{roleCode}/enable")
@Operation(summary = "启用角色", description = "仅允许 KEY_ADMIN FULL 会话启用目标角色。") @Operation(summary = "启用角色", description = "仅允许 SUPER_ADMIN FULL 会话启用目标角色。")
@ReplayProtected
@RequireInternalAuth(role = RoleCode.SUPER_ADMIN, authLevel = AuthLevel.FULL) @RequireInternalAuth(role = RoleCode.SUPER_ADMIN, authLevel = AuthLevel.FULL)
@AuditedOperation(module = ModuleCode.AUTH, action = ActionType.ENABLE, summary = "启用角色") @AuditedOperation(module = ModuleCode.AUTH, action = ActionType.ENABLE, summary = "启用角色")
public ApiResponse<Void> enableRole(@PathVariable("roleCode") String roleCode, HttpServletRequest request) { public ApiResponse<Void> enableRole(@PathVariable("roleCode") String roleCode, HttpServletRequest request) {
@ -44,7 +45,8 @@ public class AuthAdminController {
} }
@PostMapping("/roles/{roleCode}/reset-password") @PostMapping("/roles/{roleCode}/reset-password")
@Operation(summary = "重置角色密码", description = "仅允许 KEY_ADMIN FULL 会话重置目标角色密码。") @Operation(summary = "重置角色密码", description = "仅允许 SUPER_ADMIN FULL 会话重置目标角色密码。")
@ReplayProtected
@RequireInternalAuth(role = RoleCode.SUPER_ADMIN, authLevel = AuthLevel.FULL) @RequireInternalAuth(role = RoleCode.SUPER_ADMIN, authLevel = AuthLevel.FULL)
@AuditedOperation(module = ModuleCode.AUTH, action = ActionType.RESET, summary = "重置角色密码") @AuditedOperation(module = ModuleCode.AUTH, action = ActionType.RESET, summary = "重置角色密码")
public ApiResponse<Void> resetPassword(@PathVariable("roleCode") String roleCode, HttpServletRequest request) { public ApiResponse<Void> resetPassword(@PathVariable("roleCode") String roleCode, HttpServletRequest request) {
@ -58,6 +60,7 @@ public class AuthAdminController {
@PostMapping("/roles/{roleCode}/full-accounts/{uid}/change-password") @PostMapping("/roles/{roleCode}/full-accounts/{uid}/change-password")
@Operation(summary = "管理员修改 FULL 账户口令", description = "管理员为目标角色指定 UKey 席位账号设置新口令。") @Operation(summary = "管理员修改 FULL 账户口令", description = "管理员为目标角色指定 UKey 席位账号设置新口令。")
@ReplayProtected
// @RequireInternalAuth(role = RoleCode.SUPER_ADMIN, authLevel = AuthLevel.FULL) // @RequireInternalAuth(role = RoleCode.SUPER_ADMIN, authLevel = AuthLevel.FULL)
@AuditedOperation(module = ModuleCode.AUTH, action = ActionType.UPDATE, summary = "管理员修改 FULL 账户口令") @AuditedOperation(module = ModuleCode.AUTH, action = ActionType.UPDATE, summary = "管理员修改 FULL 账户口令")
public ApiResponse<Void> changeFullAccountPassword( public ApiResponse<Void> changeFullAccountPassword(
@ -79,6 +82,7 @@ public class AuthAdminController {
@PostMapping("/roles/{roleCode}/limited-accounts/{username}/change-password") @PostMapping("/roles/{roleCode}/limited-accounts/{username}/change-password")
@Operation(summary = "管理员修改 LIMITED 账户口令", description = "管理员为目标角色指定独立用户账号设置新口令。") @Operation(summary = "管理员修改 LIMITED 账户口令", description = "管理员为目标角色指定独立用户账号设置新口令。")
@ReplayProtected
// @RequireInternalAuth(role = RoleCode.SUPER_ADMIN, authLevel = AuthLevel.FULL) // @RequireInternalAuth(role = RoleCode.SUPER_ADMIN, authLevel = AuthLevel.FULL)
@AuditedOperation(module = ModuleCode.AUTH, action = ActionType.UPDATE, summary = "管理员修改 LIMITED 账户口令") @AuditedOperation(module = ModuleCode.AUTH, action = ActionType.UPDATE, summary = "管理员修改 LIMITED 账户口令")
public ApiResponse<Void> changeLimitedAccountPassword( public ApiResponse<Void> changeLimitedAccountPassword(
@ -98,35 +102,31 @@ public class AuthAdminController {
return ApiResponse.success(); return ApiResponse.success();
} }
@PostMapping("/roles/{roleCode}/ukeys/bind") @PostMapping("/super-admin/ukeys/issue-sign")
@Operation(summary = "绑定角色 UKey", description = "仅允许 KEY_ADMIN FULL 会话登记目标角色的 UKey 绑定信息。") @Operation(summary = "签发并绑定超级管理员 UKey", description = "系统初始化后登记超级管理员 UKey不要求已有登录态。")
@RequireInternalAuth(role = RoleCode.SUPER_ADMIN, authLevel = AuthLevel.FULL) public ApiResponse<UKeySignResult> issueSuperAdminUkeyBindingSign(
@AuditedOperation(module = ModuleCode.AUTH, action = ActionType.BIND, summary = "绑定角色 UKey") @Valid @RequestBody UKeySignDTO request
public ApiResponse<Void> bindUkey(
@PathVariable("roleCode") String roleCode,
@Valid @RequestBody UkeyBindRequest request,
HttpServletRequest httpRequest
) { ) {
authAdminService.bindIssuedUkey( return ApiResponse.success(authAdminService.issueUkeyBindingSign(
(String) httpRequest.getAttribute(InternalApiAuthInterceptor.ATTR_ROLE_CODE), null,
(String) httpRequest.getAttribute(InternalApiAuthInterceptor.ATTR_AUTH_LEVEL), null,
roleCode, RoleCode.SUPER_ADMIN.getCode(),
request.getUid(), request
request.getUkeySerial(), ));
request.getPubKey(),
request.getIssuerSignature()
);
return ApiResponse.success();
} }
@PostMapping("/roles/{roleCode}/ukeys/issue-sign") @PostMapping("/roles/{roleCode}/ukeys/issue-sign")
@Operation(summary = "生成 UKey 发行签名", description = "按旧绑定流程为目标角色 UKey 材料生成发行签名。") @Operation(summary = "签发并绑定其他管理员 UKey", description = "仅允许 SUPER_ADMIN FULL 会话登记其他管理员的 UKey。")
// @RequireInternalAuth(role = RoleCode.SUPER_ADMIN, authLevel = AuthLevel.LIMITED) @ReplayProtected
@RequireInternalAuth(role = RoleCode.SUPER_ADMIN, authLevel = AuthLevel.FULL)
public ApiResponse<UKeySignResult> issueUkeyBindingSign( public ApiResponse<UKeySignResult> issueUkeyBindingSign(
@PathVariable("roleCode") String roleCode, @PathVariable("roleCode") String roleCode,
@RequestBody UKeySignDTO request, @Valid @RequestBody UKeySignDTO request,
HttpServletRequest httpRequest 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( return ApiResponse.success(authAdminService.issueUkeyBindingSign(
(String) httpRequest.getAttribute(InternalApiAuthInterceptor.ATTR_ROLE_CODE), (String) httpRequest.getAttribute(InternalApiAuthInterceptor.ATTR_ROLE_CODE),
(String) httpRequest.getAttribute(InternalApiAuthInterceptor.ATTR_AUTH_LEVEL), (String) httpRequest.getAttribute(InternalApiAuthInterceptor.ATTR_AUTH_LEVEL),

View File

@ -1,57 +0,0 @@
package com.cisd.tms.modules.auth.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.NotBlank;
@Schema(description = "UKey 绑定请求")
public class UkeyBindRequest {
@NotNull(message = "uid is required")
@Schema(description = "角色内固定席位编号", example = "1")
private Integer uid;
@NotBlank(message = "ukeySerial is required")
@Schema(description = "UKey 序列号", example = "UK-001")
private String ukeySerial;
@NotBlank(message = "pubKey is required")
@Schema(description = "UKey 公钥")
private String pubKey;
@NotBlank(message = "issuerSignature is required")
@Schema(description = "认证公钥签名值")
private String issuerSignature;
public Integer getUid() {
return uid;
}
public void setUid(Integer uid) {
this.uid = uid;
}
public String getUkeySerial() {
return ukeySerial;
}
public void setUkeySerial(String ukeySerial) {
this.ukeySerial = ukeySerial;
}
public String getPubKey() {
return pubKey;
}
public void setPubKey(String pubKey) {
this.pubKey = pubKey;
}
public String getIssuerSignature() {
return issuerSignature;
}
public void setIssuerSignature(String issuerSignature) {
this.issuerSignature = issuerSignature;
}
}

View File

@ -9,7 +9,7 @@ import org.apache.ibatis.annotations.Param;
@Mapper @Mapper
public interface RoleUkeyBindingMapper extends BaseMapperX<RoleUkeyBindingEntity> { public interface RoleUkeyBindingMapper extends BaseMapperX<RoleUkeyBindingEntity> {
RoleUkeyBindingEntity selectActiveByRoleCodeAndUid(@Param("roleCode") String roleCode, @Param("uid") Integer uid); List<RoleUkeyBindingEntity> selectActiveByRoleCodeAndUid(@Param("roleCode") String roleCode, @Param("uid") Integer uid);
List<RoleUkeyBindingEntity> selectActiveByRoleCode(@Param("roleCode") String roleCode); List<RoleUkeyBindingEntity> selectActiveByRoleCode(@Param("roleCode") String roleCode);
} }

View File

@ -2,11 +2,10 @@ package com.cisd.tms.modules.auth.repository;
import com.cisd.tms.modules.auth.entity.RoleUkeyBindingEntity; import com.cisd.tms.modules.auth.entity.RoleUkeyBindingEntity;
import java.util.List; import java.util.List;
import java.util.Optional;
public interface RoleUkeyBindingRepository { public interface RoleUkeyBindingRepository {
Optional<RoleUkeyBindingEntity> findActiveByRoleCodeAndUid(String roleCode, Integer uid); List<RoleUkeyBindingEntity> findActiveByRoleCodeAndUid(String roleCode, Integer uid);
List<RoleUkeyBindingEntity> findActiveByRoleCode(String roleCode); List<RoleUkeyBindingEntity> findActiveByRoleCode(String roleCode);

View File

@ -4,7 +4,6 @@ import com.cisd.tms.modules.auth.entity.RoleUkeyBindingEntity;
import com.cisd.tms.modules.auth.mapper.RoleUkeyBindingMapper; import com.cisd.tms.modules.auth.mapper.RoleUkeyBindingMapper;
import com.cisd.tms.modules.auth.repository.RoleUkeyBindingRepository; import com.cisd.tms.modules.auth.repository.RoleUkeyBindingRepository;
import java.util.List; import java.util.List;
import java.util.Optional;
import org.springframework.stereotype.Repository; import org.springframework.stereotype.Repository;
@Repository @Repository
@ -17,8 +16,8 @@ public class RoleUkeyBindingRepositoryImpl implements RoleUkeyBindingRepository
} }
@Override @Override
public Optional<RoleUkeyBindingEntity> findActiveByRoleCodeAndUid(String roleCode, Integer uid) { public List<RoleUkeyBindingEntity> findActiveByRoleCodeAndUid(String roleCode, Integer uid) {
return Optional.ofNullable(roleUkeyBindingMapper.selectActiveByRoleCodeAndUid(roleCode, uid)); return roleUkeyBindingMapper.selectActiveByRoleCodeAndUid(roleCode, uid);
} }
@Override @Override

View File

@ -27,26 +27,6 @@ public interface AuthAdminService {
String newPassword String newPassword
); );
void bindUkey(
String operatorRoleCode,
String operatorAuthLevel,
String targetRoleCode,
Integer uid,
String ukeySerial,
String ukeyPubkey,
String issuerSign
);
void bindIssuedUkey(
String operatorRoleCode,
String operatorAuthLevel,
String targetRoleCode,
Integer uid,
String ukeySerial,
String ukeyPubkey,
String issuerSign
);
UKeySignResult issueUkeyBindingSign( UKeySignResult issueUkeyBindingSign(
String operatorRoleCode, String operatorRoleCode,
String operatorAuthLevel, String operatorAuthLevel,

View File

@ -174,10 +174,7 @@ public class AuthAdminServiceImpl implements AuthAdminService {
authUserAccountRepository.update(account); authUserAccountRepository.update(account);
} }
@Override private void bindUkey(
public void bindUkey(
String operatorRoleCode,
String operatorAuthLevel,
String targetRoleCode, String targetRoleCode,
Integer uid, Integer uid,
String ukeySerial, String ukeySerial,
@ -185,15 +182,11 @@ public class AuthAdminServiceImpl implements AuthAdminService {
String issuerSign String issuerSign
) { ) {
RoleCode targetRole = resolveRoleCode(targetRoleCode); RoleCode targetRole = resolveRoleCode(targetRoleCode);
if (uid == null || uid < 1 || uid > targetRole.getRequiredUkeyCount()) { validateRoleUid(targetRole, uid);
throw new BizException(ErrorCode.VALIDATE_FAILED.getCode(), "uid exceeds role ukey requirement");
}
authFullAccountRepository.findByRoleCodeAndUid(targetRoleCode, uid) authFullAccountRepository.findByRoleCodeAndUid(targetRoleCode, uid)
.orElseThrow(() -> new BizException(ErrorCode.VALIDATE_FAILED.getCode(), "target role full account not found")); .orElseThrow(() -> new BizException(ErrorCode.VALIDATE_FAILED.getCode(), "target role full account not found"));
RoleUkeyBindingEntity binding = roleUkeyBindingRepository RoleUkeyBindingEntity binding = findExistingActiveBinding(targetRoleCode, uid, ukeySerial, ukeyPubkey);
.findActiveByRoleCodeAndUid(targetRoleCode, uid)
.orElseGet(RoleUkeyBindingEntity::new);
if (binding.getId() == null) { if (binding.getId() == null) {
binding.setId((long) Math.abs(Objects.hash(targetRoleCode, uid, ukeySerial, TraceIdUtil.newTraceId()))); binding.setId((long) Math.abs(Objects.hash(targetRoleCode, uid, ukeySerial, TraceIdUtil.newTraceId())));
binding.setRoleCode(targetRoleCode); binding.setRoleCode(targetRoleCode);
@ -208,19 +201,17 @@ public class AuthAdminServiceImpl implements AuthAdminService {
roleUkeyBindingRepository.update(binding); roleUkeyBindingRepository.update(binding);
} }
@Override private RoleUkeyBindingEntity findExistingActiveBinding(
public void bindIssuedUkey( String roleCode,
String operatorRoleCode,
String operatorAuthLevel,
String targetRoleCode,
Integer uid, Integer uid,
String ukeySerial, String ukeySerial,
String ukeyPubkey, String ukeyPubkey
String issuerSign
) { ) {
resolveRoleCode(targetRoleCode); return roleUkeyBindingRepository.findActiveByRoleCodeAndUid(roleCode, uid).stream()
verifyIssuerSignature(targetRoleCode, ukeyPubkey, uid, issuerSign); .filter(binding -> Objects.equals(binding.getUkeySerial(), ukeySerial)
bindUkey(operatorRoleCode, operatorAuthLevel, targetRoleCode, uid, ukeySerial, ukeyPubkey, issuerSign); || Objects.equals(binding.getUkeyPubkey(), ukeyPubkey))
.findFirst()
.orElseGet(RoleUkeyBindingEntity::new);
} }
@Override @Override
@ -231,18 +222,28 @@ public class AuthAdminServiceImpl implements AuthAdminService {
UKeySignDTO request UKeySignDTO request
) { ) {
RoleCode targetRole = resolveRoleCode(targetRoleCode); RoleCode targetRole = resolveRoleCode(targetRoleCode);
validateRoleUid(targetRole, request.getUid());
String authKeyPair = lmkService.exportIkPublicKeyHex(); String authKeyPair = lmkService.exportIkPublicKeyHex();
UKeySignDTO dto = new UKeySignDTO(); UKeySignDTO dto = new UKeySignDTO();
dto.setPubKey(request.getPubKey()); dto.setPubKey(request.getPubKey());
dto.setRole(targetRole.getCode());
dto.setUid(request.getUid()); dto.setUid(request.getUid());
String signValue = lmkService.signIk(toIssuePayload(dto, authKeyPair)); String signValue = lmkService.signIk(toIssuePayload(dto, authKeyPair, targetRole.getCode()));
MasterKeyBackupPacket backupPacket = null; MasterKeyBackupPacket backupPacket = null;
if (request.getUkeySerial() == null || request.getUkeySerial().isBlank()) {
throw new BizException(ErrorCode.VALIDATE_FAILED.getCode(), "ukeySerial is required");
}
if (RoleCode.SUPER_ADMIN == targetRole) { if (RoleCode.SUPER_ADMIN == targetRole) {
backupPacket = lmkService.buildBackupPacket(request.getUid()); backupPacket = lmkService.buildBackupPacket(request.getUid());
} }
bindUkey(
targetRole.getCode(),
request.getUid(),
request.getUkeySerial(),
request.getPubKey(),
signValue
);
return UKeySignResult.builder() return UKeySignResult.builder()
.sign(signValue) .sign(signValue)
.backupPacket(backupPacket) .backupPacket(backupPacket)
@ -295,6 +296,12 @@ public class AuthAdminServiceImpl implements AuthAdminService {
.orElseThrow(() -> new BizException(ErrorCode.VALIDATE_FAILED.getCode(), "target role account not found")); .orElseThrow(() -> new BizException(ErrorCode.VALIDATE_FAILED.getCode(), "target role account not found"));
} }
private void validateRoleUid(RoleCode targetRole, Integer uid) {
if (uid == null || uid < 1 || uid > targetRole.getRequiredUkeyCount()) {
throw new BizException(ErrorCode.VALIDATE_FAILED.getCode(), "uid exceeds role ukey requirement");
}
}
private RoleCode resolveRoleCode(String roleCode) { private RoleCode resolveRoleCode(String roleCode) {
return List.of(RoleCode.values()).stream() return List.of(RoleCode.values()).stream()
.filter(item -> item.getCode().equals(roleCode)) .filter(item -> item.getCode().equals(roleCode))
@ -306,24 +313,9 @@ public class AuthAdminServiceImpl implements AuthAdminService {
return LocalDateTime.ofInstant(clock.instant(), ZoneOffset.UTC); return LocalDateTime.ofInstant(clock.instant(), ZoneOffset.UTC);
} }
private void verifyIssuerSignature(String targetRoleCode, String ukeyPubkey, Integer uid, String issuerSign) { private String toIssuePayload(UKeySignDTO dto, String authKeyPair, String roleCode) {
try { try {
UKeySignDTO dto = new UKeySignDTO(); return objectMapper.writeValueAsString(UKeySignEntity.getInstance(dto, authKeyPair, roleCode));
dto.setPubKey(ukeyPubkey);
dto.setRole(resolveRoleCode(targetRoleCode).getCode());
dto.setUid(uid);
String authKeyPair = lmkService.exportIkPublicKeyHex();
lmkService.verifyIk(toIssuePayload(dto, authKeyPair), issuerSign);
} catch (BizException ex) {
throw ex;
} catch (RuntimeException ex) {
throw new BizException(ErrorCode.UNAUTHORIZED.getCode(), "ukey issuer signature verification failed");
}
}
private String toIssuePayload(UKeySignDTO dto, String authKeyPair) {
try {
return objectMapper.writeValueAsString(UKeySignEntity.getInstance(dto, authKeyPair));
} catch (JsonProcessingException ex) { } catch (JsonProcessingException ex) {
throw new IllegalStateException("serialize ukey issue payload failed", ex); throw new IllegalStateException("serialize ukey issue payload failed", ex);
} }

View File

@ -152,14 +152,8 @@ public class AuthServiceImpl implements AuthService {
RoleCode roleCode = RoleCode.valueOf(request.getRoleCode()); RoleCode roleCode = RoleCode.valueOf(request.getRoleCode());
List<RoleUkeyBindingEntity> activeBindings = roleUkeyBindingRepository.findActiveByRoleCode(roleCode.getCode()); List<RoleUkeyBindingEntity> activeBindings = roleUkeyBindingRepository.findActiveByRoleCode(roleCode.getCode());
validateUkeyCount(roleCode, activeBindings, request.getLoginFactors()); validateUkeyCount(roleCode, activeBindings, request.getLoginFactors());
Map<Integer, RoleUkeyBindingEntity> bindingsByUid = activeBindings.stream() Map<Integer, List<RoleUkeyBindingEntity>> bindingsByUid = activeBindings.stream()
.collect(Collectors.toMap(RoleUkeyBindingEntity::getUid, item -> item, (left, right) -> left, java.util.LinkedHashMap::new)); .collect(Collectors.groupingBy(RoleUkeyBindingEntity::getUid));
Set<Integer> requestUids = request.getLoginFactors().stream()
.map(UkeyLoginProof::getUid)
.collect(Collectors.toSet());
if (requestUids.size() != request.getLoginFactors().size() || !bindingsByUid.keySet().containsAll(requestUids)) {
throw new BizException(ErrorCode.UNAUTHORIZED.getCode(), "ukey auth info does not match bound role");
}
ukeyLoginRandomService.assertIssued( ukeyLoginRandomService.assertIssued(
roleCode.getCode(), roleCode.getCode(),
request.getLoginFactors().stream().map(UkeyLoginProof::getServerRandom).toList() request.getLoginFactors().stream().map(UkeyLoginProof::getServerRandom).toList()
@ -167,10 +161,10 @@ public class AuthServiceImpl implements AuthService {
String authKeyPair = lmkService.exportIkPublicKeyHex(); String authKeyPair = lmkService.exportIkPublicKeyHex();
List<String> matchedSerials = new ArrayList<>(); List<String> matchedSerials = new ArrayList<>();
for (UkeyLoginProof proof : request.getLoginFactors()) { for (UkeyLoginProof proof : request.getLoginFactors()) {
RoleUkeyBindingEntity binding = bindingsByUid.get(proof.getUid()); RoleUkeyBindingEntity binding = bindingsByUid.getOrDefault(proof.getUid(), List.of()).stream()
if (binding == null || !binding.getUkeyPubkey().equals(proof.getPubKey())) { .filter(item -> item.getUkeyPubkey().equals(proof.getPubKey()))
throw new BizException(ErrorCode.UNAUTHORIZED.getCode(), "ukey auth info does not match bound role"); .findFirst()
} .orElseThrow(() -> new BizException(ErrorCode.UNAUTHORIZED.getCode(), "ukey auth info does not match bound role"));
compatUkeyVerifier.verifyIssuedBinding(buildIssuePayload(request.getRoleCode(), proof, authKeyPair), proof.getIssueSignature()); compatUkeyVerifier.verifyIssuedBinding(buildIssuePayload(request.getRoleCode(), proof, authKeyPair), proof.getIssueSignature());
compatUkeyVerifier.verifyLoginSignature(proof.getPubKey(), proof.getLoginPayload(), proof.getLoginSignature()); compatUkeyVerifier.verifyLoginSignature(proof.getPubKey(), proof.getLoginPayload(), proof.getLoginSignature());
matchedSerials.add(binding.getUkeySerial()); matchedSerials.add(binding.getUkeySerial());
@ -503,16 +497,20 @@ public class AuthServiceImpl implements AuthService {
} }
List<RoleUkeyBindingEntity> bindings = roleUkeyBindingRepository.findActiveByRoleCode(roleAccount.getRoleCode()); List<RoleUkeyBindingEntity> bindings = roleUkeyBindingRepository.findActiveByRoleCode(roleAccount.getRoleCode());
if (bindings.size() != requiredUkeyCount) {
throw new BizException(ErrorCode.UNAUTHORIZED.getCode(), "ukey count does not satisfy role requirement");
}
Set<String> boundSerials = bindings.stream() Set<String> boundSerials = bindings.stream()
.map(RoleUkeyBindingEntity::getUkeySerial) .map(RoleUkeyBindingEntity::getUkeySerial)
.collect(Collectors.toSet()); .collect(Collectors.toSet());
if (!boundSerials.containsAll(uniqueSerials)) { if (!boundSerials.containsAll(uniqueSerials)) {
throw new BizException(ErrorCode.UNAUTHORIZED.getCode(), "ukey verification failed"); throw new BizException(ErrorCode.UNAUTHORIZED.getCode(), "ukey verification failed");
} }
long matchedUidCount = bindings.stream()
.filter(binding -> uniqueSerials.contains(binding.getUkeySerial()))
.map(RoleUkeyBindingEntity::getUid)
.distinct()
.count();
if (matchedUidCount != requiredUkeyCount) {
throw new BizException(ErrorCode.UNAUTHORIZED.getCode(), "ukey count does not satisfy role requirement");
}
return authPolicyService.resolveAuthLevel(AuthMethod.UKEY); return authPolicyService.resolveAuthLevel(AuthMethod.UKEY);
} }
@ -524,7 +522,17 @@ public class AuthServiceImpl implements AuthService {
if (proofs == null || proofs.size() != authPolicyService.requiredUkeyCount(roleCode)) { if (proofs == null || proofs.size() != authPolicyService.requiredUkeyCount(roleCode)) {
throw new BizException(ErrorCode.UNAUTHORIZED.getCode(), "ukey count does not satisfy role requirement"); throw new BizException(ErrorCode.UNAUTHORIZED.getCode(), "ukey count does not satisfy role requirement");
} }
if (activeBindings.size() != authPolicyService.requiredUkeyCount(roleCode)) { Set<Integer> requestUids = proofs.stream()
.map(UkeyLoginProof::getUid)
.collect(Collectors.toSet());
int requiredCount = authPolicyService.requiredUkeyCount(roleCode);
if (requestUids.size() != requiredCount) {
throw new BizException(ErrorCode.UNAUTHORIZED.getCode(), "ukey count does not satisfy role requirement");
}
Set<Integer> activeUids = activeBindings.stream()
.map(RoleUkeyBindingEntity::getUid)
.collect(Collectors.toSet());
if (!activeUids.containsAll(requestUids)) {
throw new BizException(ErrorCode.UNAUTHORIZED.getCode(), "ukey count does not satisfy role requirement"); throw new BizException(ErrorCode.UNAUTHORIZED.getCode(), "ukey count does not satisfy role requirement");
} }
} }
@ -532,10 +540,9 @@ public class AuthServiceImpl implements AuthService {
private String buildIssuePayload(String roleCode, UkeyLoginProof proof, String authKeyPair) { private String buildIssuePayload(String roleCode, UkeyLoginProof proof, String authKeyPair) {
UKeySignDTO dto = new UKeySignDTO(); UKeySignDTO dto = new UKeySignDTO();
dto.setPubKey(proof.getPubKey()); dto.setPubKey(proof.getPubKey());
dto.setRole(RoleCode.valueOf(roleCode).getCode());
dto.setUid(proof.getUid()); dto.setUid(proof.getUid());
try { try {
return objectMapper.writeValueAsString(UKeySignEntity.getInstance(dto, authKeyPair)); return objectMapper.writeValueAsString(UKeySignEntity.getInstance(dto, authKeyPair, RoleCode.valueOf(roleCode).getCode()));
} catch (JsonProcessingException ex) { } catch (JsonProcessingException ex) {
throw new IllegalStateException("serialize ukey issue payload failed", ex); throw new IllegalStateException("serialize ukey issue payload failed", ex);
} }

View File

@ -1,5 +1,7 @@
package com.cisd.tms.modules.mk.dto; package com.cisd.tms.modules.mk.dto;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import lombok.Data; import lombok.Data;
/** /**
@ -10,16 +12,19 @@ public class UKeySignDTO {
/** /**
* UKey 公钥 * UKey 公钥
*/ */
@NotBlank(message = "pubKey is required")
private String pubKey; private String pubKey;
/** /**
* 签名的角色 * UKey 序列号发行签名接口会直接完成绑定因此必传
*/ */
private String role; @NotBlank(message = "ukeySerial is required")
private String ukeySerial;
/** /**
* 角色内固定席位编号 * 角色内固定席位编号
*/ */
@NotNull(message = "uid is required")
private Integer uid; private Integer uid;
} }

View File

@ -27,11 +27,11 @@ public class UKeySignEntity {
*/ */
private Integer uid; private Integer uid;
public static UKeySignEntity getInstance(UKeySignDTO uKeySignDTO,String authKeyPair){ public static UKeySignEntity getInstance(UKeySignDTO uKeySignDTO, String authKeyPair, String role) {
return UKeySignEntity.builder() return UKeySignEntity.builder()
.pubKey(uKeySignDTO.getPubKey()) .pubKey(uKeySignDTO.getPubKey())
.authKeyPair(authKeyPair) .authKeyPair(authKeyPair)
.role(uKeySignDTO.getRole()) .role(role)
.uid(uKeySignDTO.getUid()) .uid(uKeySignDTO.getUid())
.build(); .build();
} }

View File

@ -262,7 +262,8 @@ CREATE TABLE IF NOT EXISTS tms_role_ukey_binding (
unbound_at DATETIME(3) NULL, unbound_at DATETIME(3) NULL,
create_time DATETIME(3) NOT NULL, create_time DATETIME(3) NOT NULL,
update_time DATETIME(3) NOT NULL, update_time DATETIME(3) NOT NULL,
UNIQUE KEY uk_tms_role_ukey_binding_role_uid (role_code, uid), UNIQUE KEY uk_tms_role_ukey_binding_role_uid_serial (role_code, uid, ukey_serial),
KEY idx_tms_role_ukey_binding_role_uid_status (role_code, uid, status),
KEY idx_tms_role_ukey_binding_role_status (role_code, status) KEY idx_tms_role_ukey_binding_role_status (role_code, status)
); );

View File

@ -34,7 +34,7 @@
WHERE role_code = #{roleCode} WHERE role_code = #{roleCode}
AND uid = #{uid} AND uid = #{uid}
AND status = 'ACTIVE' AND status = 'ACTIVE'
LIMIT 1 ORDER BY bound_at DESC, id DESC
</select> </select>
<select id="selectActiveByRoleCode" parameterType="string" resultMap="RoleUkeyBindingResultMap"> <select id="selectActiveByRoleCode" parameterType="string" resultMap="RoleUkeyBindingResultMap">

View File

@ -267,34 +267,6 @@ class AuthControllerTest {
Mockito.verify(authAdminService).enableRole("KEY_ADMIN", "FULL", "AUDIT_ADMIN"); Mockito.verify(authAdminService).enableRole("KEY_ADMIN", "FULL", "AUDIT_ADMIN");
} }
@Test
void shouldBindUkeyThroughAdminEndpoint() throws Exception {
AuthService authService = Mockito.mock(AuthService.class);
AuthAdminService authAdminService = Mockito.mock(AuthAdminService.class);
MockMvc mockMvc = MockMvcBuilders
.standaloneSetup(new AuthController(authService), new AuthAdminController(authAdminService))
.setControllerAdvice(new GlobalExceptionHandler())
.build();
mockMvc.perform(post("/api/v1/auth/roles/SUPER_ADMIN/ukeys/bind")
.requestAttr(InternalApiAuthInterceptor.ATTR_ROLE_CODE, "KEY_ADMIN")
.requestAttr(InternalApiAuthInterceptor.ATTR_AUTH_LEVEL, "FULL")
.contentType(MediaType.APPLICATION_JSON)
.content("""
{
"uid": 1,
"ukeySerial": "UK-NEW",
"pubKey": "PUB-NEW",
"issuerSignature": "SIG-NEW"
}
"""))
.andExpect(status().isOk())
.andExpect(content().string(containsString("\"success\":true")));
Mockito.verify(authAdminService).bindIssuedUkey("KEY_ADMIN", "FULL", "SUPER_ADMIN", 1, "UK-NEW", "PUB-NEW", "SIG-NEW");
}
@Test @Test
void shouldChangeFullAccountPasswordThroughAdminEndpoint() throws Exception { void shouldChangeFullAccountPasswordThroughAdminEndpoint() throws Exception {
AuthService authService = Mockito.mock(AuthService.class); AuthService authService = Mockito.mock(AuthService.class);
@ -348,15 +320,15 @@ class AuthControllerTest {
} }
@Test @Test
void shouldIssueUkeyBindingSignThroughAdminEndpoint() throws Exception { void shouldIssueSuperAdminUkeyBindingSignWithoutLoginContext() throws Exception {
AuthService authService = Mockito.mock(AuthService.class); AuthService authService = Mockito.mock(AuthService.class);
AuthAdminService authAdminService = Mockito.mock(AuthAdminService.class); AuthAdminService authAdminService = Mockito.mock(AuthAdminService.class);
MasterKeyBackupPacket packet = new MasterKeyBackupPacket(); MasterKeyBackupPacket packet = new MasterKeyBackupPacket();
packet.setPacketIndex(1); packet.setPacketIndex(1);
UKeySignResult response = UKeySignResult.builder().sign("ISSUE-SIGN-001").backupPacket(packet).build(); UKeySignResult response = UKeySignResult.builder().sign("ISSUE-SIGN-001").backupPacket(packet).build();
Mockito.when(authAdminService.issueUkeyBindingSign( Mockito.when(authAdminService.issueUkeyBindingSign(
ArgumentMatchers.eq("KEY_ADMIN"), ArgumentMatchers.isNull(),
ArgumentMatchers.eq("FULL"), ArgumentMatchers.isNull(),
ArgumentMatchers.eq("SUPER_ADMIN"), ArgumentMatchers.eq("SUPER_ADMIN"),
ArgumentMatchers.any() ArgumentMatchers.any()
)).thenReturn(response); )).thenReturn(response);
@ -366,14 +338,12 @@ class AuthControllerTest {
.setControllerAdvice(new GlobalExceptionHandler()) .setControllerAdvice(new GlobalExceptionHandler())
.build(); .build();
mockMvc.perform(post("/api/v1/auth/roles/SUPER_ADMIN/ukeys/issue-sign") mockMvc.perform(post("/api/v1/auth/super-admin/ukeys/issue-sign")
.requestAttr(InternalApiAuthInterceptor.ATTR_ROLE_CODE, "KEY_ADMIN")
.requestAttr(InternalApiAuthInterceptor.ATTR_AUTH_LEVEL, "FULL")
.contentType(MediaType.APPLICATION_JSON) .contentType(MediaType.APPLICATION_JSON)
.content(""" .content("""
{ {
"pubKey": "PUB-NEW", "pubKey": "PUB-NEW",
"role": "SUPER_ADMIN", "ukeySerial": "UK-NEW",
"uid": 1, "uid": 1,
"extra": "EXTRA-001" "extra": "EXTRA-001"
} }
@ -382,4 +352,36 @@ class AuthControllerTest {
.andExpect(content().string(containsString("\"sign\":\"ISSUE-SIGN-001\""))) .andExpect(content().string(containsString("\"sign\":\"ISSUE-SIGN-001\"")))
.andExpect(content().string(containsString("\"packetIndex\":1"))); .andExpect(content().string(containsString("\"packetIndex\":1")));
} }
@Test
void shouldIssueNormalAdminUkeyBindingSignThroughAdminEndpoint() throws Exception {
AuthService authService = Mockito.mock(AuthService.class);
AuthAdminService authAdminService = Mockito.mock(AuthAdminService.class);
UKeySignResult response = UKeySignResult.builder().sign("ISSUE-SIGN-KEY-001").build();
Mockito.when(authAdminService.issueUkeyBindingSign(
ArgumentMatchers.eq("SUPER_ADMIN"),
ArgumentMatchers.eq("FULL"),
ArgumentMatchers.eq("KEY_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/KEY_ADMIN/ukeys/issue-sign")
.requestAttr(InternalApiAuthInterceptor.ATTR_ROLE_CODE, "SUPER_ADMIN")
.requestAttr(InternalApiAuthInterceptor.ATTR_AUTH_LEVEL, "FULL")
.contentType(MediaType.APPLICATION_JSON)
.content("""
{
"pubKey": "PUB-KEY",
"ukeySerial": "UK-KEY",
"uid": 1
}
"""))
.andExpect(status().isOk())
.andExpect(content().string(containsString("\"sign\":\"ISSUE-SIGN-KEY-001\"")));
}
} }

View File

@ -277,11 +277,15 @@ class AuthAdminServiceTest {
} }
@Test @Test
void shouldReplaceActiveBindingInSameUidSeat() { void shouldUpdateActiveBindingWhenIssueSignUsesSameSerial() {
InMemoryRoleUkeyBindingRepository bindings = new InMemoryRoleUkeyBindingRepository(); InMemoryRoleUkeyBindingRepository bindings = new InMemoryRoleUkeyBindingRepository();
bindings.save(binding(RoleCode.SUPER_ADMIN, 1, "UK-OLD", "PUB-OLD", "SIG-OLD")); bindings.save(binding(RoleCode.SUPER_ADMIN, 1, "UK-OLD", "PUB-OLD", "SIG-OLD"));
LmkService lmkService = Mockito.mock(LmkService.class); LmkService lmkService = Mockito.mock(LmkService.class);
Mockito.when(lmkService.exportIkPublicKeyHex()).thenReturn("IK-PUB-001"); Mockito.when(lmkService.exportIkPublicKeyHex()).thenReturn("IK-PUB-001");
Mockito.when(lmkService.signIk(org.mockito.ArgumentMatchers.anyString())).thenReturn("SIG-NEW");
com.cisd.tms.modules.mk.dto.MasterKeyBackupPacket packet = new com.cisd.tms.modules.mk.dto.MasterKeyBackupPacket();
packet.setPacketIndex(1);
Mockito.when(lmkService.buildBackupPacket(1)).thenReturn(packet);
AuthAdminService service = new AuthAdminServiceImpl( AuthAdminService service = new AuthAdminServiceImpl(
new InMemoryRoleAccountRepository(), new InMemoryRoleAccountRepository(),
@ -295,19 +299,25 @@ class AuthAdminServiceTest {
new FixedSaltSupplier("salt-001") new FixedSaltSupplier("salt-001")
); );
service.bindIssuedUkey( UKeySignDTO dto = new UKeySignDTO();
RoleCode.KEY_ADMIN.getCode(), dto.setPubKey("PUB-NEW");
AuthLevel.FULL.name(), dto.setUkeySerial("UK-OLD");
dto.setUid(1);
service.issueUkeyBindingSign(
null,
null,
RoleCode.SUPER_ADMIN.getCode(), RoleCode.SUPER_ADMIN.getCode(),
1, dto
"UK-NEW",
"PUB-NEW",
"SIG-NEW"
); );
RoleUkeyBindingEntity updated = bindings.findActiveByRoleCodeAndUid(RoleCode.SUPER_ADMIN.getCode(), 1).orElseThrow(); RoleUkeyBindingEntity updated = bindings.findActiveByRoleCodeAndUid(RoleCode.SUPER_ADMIN.getCode(), 1).stream()
Assertions.assertEquals("UK-NEW", updated.getUkeySerial()); .filter(binding -> "UK-OLD".equals(binding.getUkeySerial()))
.findFirst()
.orElseThrow();
Assertions.assertEquals("UK-OLD", updated.getUkeySerial());
Assertions.assertEquals("PUB-NEW", updated.getUkeyPubkey()); Assertions.assertEquals("PUB-NEW", updated.getUkeyPubkey());
Assertions.assertEquals("SIG-NEW", updated.getIssuerSign());
} }
@Test @Test
@ -321,7 +331,7 @@ class AuthAdminServiceTest {
AuthAdminService service = new AuthAdminServiceImpl( AuthAdminService service = new AuthAdminServiceImpl(
new InMemoryRoleAccountRepository(), new InMemoryRoleAccountRepository(),
fullAccountRepository(), fullAccountRepository(binding(RoleCode.SUPER_ADMIN, 1, "UK-001", "PUB-OLD", "SIG-OLD")),
new InMemoryAuthUserAccountRepository(), new InMemoryAuthUserAccountRepository(),
new InMemoryRoleUkeyBindingRepository(), new InMemoryRoleUkeyBindingRepository(),
new FakePasswordHasher(), new FakePasswordHasher(),
@ -332,6 +342,7 @@ class AuthAdminServiceTest {
); );
UKeySignDTO dto = new UKeySignDTO(); UKeySignDTO dto = new UKeySignDTO();
dto.setPubKey("PUB-001"); dto.setPubKey("PUB-001");
dto.setUkeySerial("UK-001");
dto.setUid(1); dto.setUid(1);
UKeySignResult result = service.issueUkeyBindingSign(RoleCode.KEY_ADMIN.getCode(), AuthLevel.FULL.name(), RoleCode.SUPER_ADMIN.getCode(), dto); UKeySignResult result = service.issueUkeyBindingSign(RoleCode.KEY_ADMIN.getCode(), AuthLevel.FULL.name(), RoleCode.SUPER_ADMIN.getCode(), dto);
@ -341,6 +352,90 @@ class AuthAdminServiceTest {
Assertions.assertEquals(1, result.getBackupPacket().getPacketIndex()); Assertions.assertEquals(1, result.getBackupPacket().getPacketIndex());
} }
@Test
void shouldIssueAndBindNormalRoleUkeyInOneStep() {
InMemoryRoleUkeyBindingRepository bindings = new InMemoryRoleUkeyBindingRepository();
LmkService lmkService = Mockito.mock(LmkService.class);
Mockito.when(lmkService.exportIkPublicKeyHex()).thenReturn("IK-PUB-001");
Mockito.when(lmkService.signIk(org.mockito.ArgumentMatchers.anyString())).thenReturn("ISSUE-SIGN-KEY-001");
AuthAdminService service = new AuthAdminServiceImpl(
new InMemoryRoleAccountRepository(),
fullAccountRepository(binding(RoleCode.KEY_ADMIN, 1, "UK-KEY-001", "PUB-OLD", "SIG-OLD")),
new InMemoryAuthUserAccountRepository(),
bindings,
new FakePasswordHasher(),
lmkService,
new com.fasterxml.jackson.databind.ObjectMapper(),
FIXED_CLOCK,
new FixedSaltSupplier("salt-001")
);
UKeySignDTO dto = new UKeySignDTO();
dto.setPubKey("PUB-KEY-001");
dto.setUkeySerial("UK-KEY-001");
dto.setUid(1);
UKeySignResult result = service.issueUkeyBindingSign(
RoleCode.SUPER_ADMIN.getCode(),
AuthLevel.FULL.name(),
RoleCode.KEY_ADMIN.getCode(),
dto
);
Assertions.assertEquals("ISSUE-SIGN-KEY-001", result.getSign());
Assertions.assertNull(result.getBackupPacket());
RoleUkeyBindingEntity binding = bindings.findActiveByRoleCodeAndUid(RoleCode.KEY_ADMIN.getCode(), 1).stream()
.filter(item -> "UK-KEY-001".equals(item.getUkeySerial()))
.findFirst()
.orElseThrow();
Assertions.assertEquals("PUB-KEY-001", binding.getUkeyPubkey());
Assertions.assertEquals("ISSUE-SIGN-KEY-001", binding.getIssuerSign());
}
@Test
void shouldIssueSuperAdminBackupUkeyWithoutReplacingExistingBindingForSameUid() {
InMemoryRoleUkeyBindingRepository bindings = new InMemoryRoleUkeyBindingRepository();
bindings.save(binding(RoleCode.SUPER_ADMIN, 1, "UK-PRIMARY", "PUB-PRIMARY", "SIG-PRIMARY"));
LmkService lmkService = Mockito.mock(LmkService.class);
Mockito.when(lmkService.exportIkPublicKeyHex()).thenReturn("IK-PUB-001");
Mockito.when(lmkService.signIk(org.mockito.ArgumentMatchers.anyString())).thenReturn("ISSUE-SIGN-BACKUP");
com.cisd.tms.modules.mk.dto.MasterKeyBackupPacket packet = new com.cisd.tms.modules.mk.dto.MasterKeyBackupPacket();
packet.setPacketIndex(1);
Mockito.when(lmkService.buildBackupPacket(1)).thenReturn(packet);
AuthAdminService service = new AuthAdminServiceImpl(
new InMemoryRoleAccountRepository(),
fullAccountRepository(binding(RoleCode.SUPER_ADMIN, 1, "UK-PRIMARY", "PUB-PRIMARY", "SIG-PRIMARY")),
new InMemoryAuthUserAccountRepository(),
bindings,
new FakePasswordHasher(),
lmkService,
new com.fasterxml.jackson.databind.ObjectMapper(),
FIXED_CLOCK,
new FixedSaltSupplier("salt-001")
);
UKeySignDTO dto = new UKeySignDTO();
dto.setPubKey("PUB-BACKUP");
dto.setUkeySerial("UK-BACKUP");
dto.setUid(1);
UKeySignResult result = service.issueUkeyBindingSign(
RoleCode.KEY_ADMIN.getCode(),
AuthLevel.FULL.name(),
RoleCode.SUPER_ADMIN.getCode(),
dto
);
Assertions.assertEquals("ISSUE-SIGN-BACKUP", result.getSign());
List<RoleUkeyBindingEntity> activeUidBindings = bindings.findActiveByRoleCodeAndUid(RoleCode.SUPER_ADMIN.getCode(), 1);
Assertions.assertEquals(2, activeUidBindings.size());
Assertions.assertTrue(activeUidBindings.stream().anyMatch(binding -> "UK-PRIMARY".equals(binding.getUkeySerial())
&& "PUB-PRIMARY".equals(binding.getUkeyPubkey())));
Assertions.assertTrue(activeUidBindings.stream().anyMatch(binding -> "UK-BACKUP".equals(binding.getUkeySerial())
&& "PUB-BACKUP".equals(binding.getUkeyPubkey())
&& "ISSUE-SIGN-BACKUP".equals(binding.getIssuerSign())));
}
private static RoleAccountEntity role(RoleCode roleCode, RoleAccountStatus status) { private static RoleAccountEntity role(RoleCode roleCode, RoleAccountStatus status) {
RoleAccountEntity entity = new RoleAccountEntity(); RoleAccountEntity entity = new RoleAccountEntity();
entity.setId((long) roleCode.ordinal() + 100); entity.setId((long) roleCode.ordinal() + 100);
@ -504,11 +599,12 @@ class AuthAdminServiceTest {
private final Map<String, List<RoleUkeyBindingEntity>> store = new ConcurrentHashMap<>(); private final Map<String, List<RoleUkeyBindingEntity>> store = new ConcurrentHashMap<>();
@Override @Override
public Optional<RoleUkeyBindingEntity> findActiveByRoleCodeAndUid(String roleCode, Integer uid) { public List<RoleUkeyBindingEntity> findActiveByRoleCodeAndUid(String roleCode, Integer uid) {
return store.getOrDefault(roleCode, List.of()).stream() return store.getOrDefault(roleCode, List.of()).stream()
.filter(entity -> uid.equals(entity.getUid())) .filter(entity -> uid.equals(entity.getUid()))
.filter(entity -> "ACTIVE".equals(entity.getStatus())) .filter(entity -> "ACTIVE".equals(entity.getStatus()))
.findFirst(); .sorted(Comparator.comparing(RoleUkeyBindingEntity::getBoundAt).reversed())
.toList();
} }
@Override @Override

View File

@ -694,6 +694,68 @@ class AuthServiceTest {
); );
} }
@Test
void shouldLoginSuperAdminWithBackupUkeysBoundToSameUidSeats() {
InMemoryRoleAccountRepository roleAccounts = new InMemoryRoleAccountRepository();
InMemoryAuthUserAccountRepository userAccounts = new InMemoryAuthUserAccountRepository();
InMemoryAuthFullAccountRepository fullAccounts = new InMemoryAuthFullAccountRepository();
InMemoryAuthSessionRepository sessions = new InMemoryAuthSessionRepository();
InMemoryRoleUkeyBindingRepository ukeyBindings = new InMemoryRoleUkeyBindingRepository();
roleAccounts.save(activeRole(RoleCode.SUPER_ADMIN));
fullAccounts.save(activeFullAccount(RoleCode.SUPER_ADMIN, 1, "super-admin-full-01", "11111111", "SALT-S1", 0, null));
fullAccounts.save(activeFullAccount(RoleCode.SUPER_ADMIN, 2, "super-admin-full-02", "22222222", "SALT-S2", 0, null));
ukeyBindings.save(activeBinding(RoleCode.SUPER_ADMIN, 1, "UK-1", "PUB-1"));
ukeyBindings.save(activeBinding(RoleCode.SUPER_ADMIN, 1, "UK-1-BACKUP", "PUB-1-BACKUP"));
ukeyBindings.save(activeBinding(RoleCode.SUPER_ADMIN, 2, "UK-2", "PUB-2"));
ukeyBindings.save(activeBinding(RoleCode.SUPER_ADMIN, 2, "UK-2-BACKUP", "PUB-2-BACKUP"));
LmkService lmkService = org.mockito.Mockito.mock(LmkService.class);
UkeyLoginRandomService randomService = org.mockito.Mockito.mock(UkeyLoginRandomService.class);
CompatUkeyVerifier verifier = org.mockito.Mockito.mock(CompatUkeyVerifier.class);
org.mockito.Mockito.when(lmkService.getMasterKeyStatus()).thenReturn(MasterKeyStatus.NORMAL.getDetail("MAC-001"));
org.mockito.Mockito.when(lmkService.exportIkPublicKeyHex()).thenReturn("IK-PUB-001");
AuthService service = new AuthServiceImpl(
roleAccounts,
new InMemoryLegacyAuthUserRepository(),
userAccounts,
fullAccounts,
sessions,
ukeyBindings,
new FakePasswordHasher(),
lmkService,
randomService,
verifier,
new InMemoryCaptchaService(),
new ObjectMapper(),
FIXED_CLOCK,
() -> "token-full-super-backup-001",
() -> "salt-full-super-backup-001",
new AuthPolicyServiceImpl()
);
UkeyLoginRequest request = new UkeyLoginRequest();
request.setRoleCode(RoleCode.SUPER_ADMIN.getCode());
request.setLoginFactors(List.of(
proofWithPassword("PUB-1-BACKUP", 1, "11111111", "RB-1", "ISSUE-1B", "LOGIN-DATA-1B", "LOGIN-SIGN-1B"),
proofWithPassword("PUB-2-BACKUP", 2, "22222222", "RB-2", "ISSUE-2B", "LOGIN-DATA-2B", "LOGIN-SIGN-2B")
));
LoginResponse response = service.ukeyLogin(request);
Assertions.assertEquals("token-full-super-backup-001", response.getToken());
Assertions.assertEquals(AuthLevel.FULL.name(), response.getAuthLevel());
org.mockito.Mockito.verify(randomService).assertIssued(RoleCode.SUPER_ADMIN.getCode(), List.of("RB-1", "RB-2"));
org.mockito.Mockito.verify(verifier).verifyIssuedBinding(
"{\"pubKey\":\"PUB-1-BACKUP\",\"authKeyPair\":\"IK-PUB-001\",\"role\":\"SUPER_ADMIN\",\"uid\":1}",
"ISSUE-1B"
);
org.mockito.Mockito.verify(verifier).verifyIssuedBinding(
"{\"pubKey\":\"PUB-2-BACKUP\",\"authKeyPair\":\"IK-PUB-001\",\"role\":\"SUPER_ADMIN\",\"uid\":2}",
"ISSUE-2B"
);
}
@Test @Test
void shouldLockOnlyMappedFullAccountDuringSuperAdminUkeyLoginWhenPasswordIsWrong() { void shouldLockOnlyMappedFullAccountDuringSuperAdminUkeyLoginWhenPasswordIsWrong() {
InMemoryRoleAccountRepository roleAccounts = new InMemoryRoleAccountRepository(); InMemoryRoleAccountRepository roleAccounts = new InMemoryRoleAccountRepository();
@ -1096,11 +1158,12 @@ class AuthServiceTest {
private final Map<String, List<RoleUkeyBindingEntity>> store = new ConcurrentHashMap<>(); private final Map<String, List<RoleUkeyBindingEntity>> store = new ConcurrentHashMap<>();
@Override @Override
public Optional<RoleUkeyBindingEntity> findActiveByRoleCodeAndUid(String roleCode, Integer uid) { public List<RoleUkeyBindingEntity> findActiveByRoleCodeAndUid(String roleCode, Integer uid) {
return store.getOrDefault(roleCode, List.of()).stream() return store.getOrDefault(roleCode, List.of()).stream()
.filter(entity -> uid.equals(entity.getUid())) .filter(entity -> uid.equals(entity.getUid()))
.filter(entity -> "ACTIVE".equals(entity.getStatus())) .filter(entity -> "ACTIVE".equals(entity.getStatus()))
.findFirst(); .sorted(Comparator.comparing(RoleUkeyBindingEntity::getBoundAt).reversed())
.toList();
} }
@Override @Override

View File

@ -41,7 +41,15 @@ class ReplayProtectedEndpointsTest {
String.class, com.cisd.tms.modules.auth.dto.ChangePasswordRequest.class, jakarta.servlet.http.HttpServletRequest.class)); String.class, com.cisd.tms.modules.auth.dto.ChangePasswordRequest.class, jakarta.servlet.http.HttpServletRequest.class));
Assertions.assertNotNull(annotation(DeviceController.class, "restart", jakarta.servlet.http.HttpServletRequest.class)); Assertions.assertNotNull(annotation(DeviceController.class, "restart", jakarta.servlet.http.HttpServletRequest.class));
Assertions.assertNotNull(AnnotatedElementUtils.findMergedAnnotation(AuthAdminController.class, ReplayProtected.class)); Assertions.assertNull(AnnotatedElementUtils.findMergedAnnotation(AuthAdminController.class, ReplayProtected.class));
Assertions.assertNotNull(effectiveAnnotation(AuthAdminController.class, "enableRole",
String.class, jakarta.servlet.http.HttpServletRequest.class));
Assertions.assertNotNull(effectiveAnnotation(AuthAdminController.class, "resetPassword",
String.class, jakarta.servlet.http.HttpServletRequest.class));
Assertions.assertNotNull(effectiveAnnotation(AuthAdminController.class, "issueUkeyBindingSign",
String.class, com.cisd.tms.modules.mk.dto.UKeySignDTO.class, jakarta.servlet.http.HttpServletRequest.class));
Assertions.assertNull(effectiveAnnotation(AuthAdminController.class, "issueSuperAdminUkeyBindingSign",
com.cisd.tms.modules.mk.dto.UKeySignDTO.class));
Assertions.assertNotNull(AnnotatedElementUtils.findMergedAnnotation(TimeConfigController.class, ReplayProtected.class)); Assertions.assertNotNull(AnnotatedElementUtils.findMergedAnnotation(TimeConfigController.class, ReplayProtected.class));
Assertions.assertNotNull(AnnotatedElementUtils.findMergedAnnotation(CryptoCardController.class, ReplayProtected.class)); Assertions.assertNotNull(AnnotatedElementUtils.findMergedAnnotation(CryptoCardController.class, ReplayProtected.class));
} }