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/ukey-login",
"/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)

View File

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

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
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);
}

View File

@ -2,11 +2,10 @@ 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> findActiveByRoleCodeAndUid(String roleCode, Integer uid);
List<RoleUkeyBindingEntity> findActiveByRoleCodeAndUid(String roleCode, Integer uid);
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.repository.RoleUkeyBindingRepository;
import java.util.List;
import java.util.Optional;
import org.springframework.stereotype.Repository;
@Repository
@ -17,8 +16,8 @@ public class RoleUkeyBindingRepositoryImpl implements RoleUkeyBindingRepository
}
@Override
public Optional<RoleUkeyBindingEntity> findActiveByRoleCodeAndUid(String roleCode, Integer uid) {
return Optional.ofNullable(roleUkeyBindingMapper.selectActiveByRoleCodeAndUid(roleCode, uid));
public List<RoleUkeyBindingEntity> findActiveByRoleCodeAndUid(String roleCode, Integer uid) {
return roleUkeyBindingMapper.selectActiveByRoleCodeAndUid(roleCode, uid);
}
@Override

View File

@ -27,26 +27,6 @@ public interface AuthAdminService {
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(
String operatorRoleCode,
String operatorAuthLevel,

View File

@ -174,10 +174,7 @@ public class AuthAdminServiceImpl implements AuthAdminService {
authUserAccountRepository.update(account);
}
@Override
public void bindUkey(
String operatorRoleCode,
String operatorAuthLevel,
private void bindUkey(
String targetRoleCode,
Integer uid,
String ukeySerial,
@ -185,15 +182,11 @@ public class AuthAdminServiceImpl implements AuthAdminService {
String issuerSign
) {
RoleCode targetRole = resolveRoleCode(targetRoleCode);
if (uid == null || uid < 1 || uid > targetRole.getRequiredUkeyCount()) {
throw new BizException(ErrorCode.VALIDATE_FAILED.getCode(), "uid exceeds role ukey requirement");
}
validateRoleUid(targetRole, uid);
authFullAccountRepository.findByRoleCodeAndUid(targetRoleCode, uid)
.orElseThrow(() -> new BizException(ErrorCode.VALIDATE_FAILED.getCode(), "target role full account not found"));
RoleUkeyBindingEntity binding = roleUkeyBindingRepository
.findActiveByRoleCodeAndUid(targetRoleCode, uid)
.orElseGet(RoleUkeyBindingEntity::new);
RoleUkeyBindingEntity binding = findExistingActiveBinding(targetRoleCode, uid, ukeySerial, ukeyPubkey);
if (binding.getId() == null) {
binding.setId((long) Math.abs(Objects.hash(targetRoleCode, uid, ukeySerial, TraceIdUtil.newTraceId())));
binding.setRoleCode(targetRoleCode);
@ -208,19 +201,17 @@ public class AuthAdminServiceImpl implements AuthAdminService {
roleUkeyBindingRepository.update(binding);
}
@Override
public void bindIssuedUkey(
String operatorRoleCode,
String operatorAuthLevel,
String targetRoleCode,
private RoleUkeyBindingEntity findExistingActiveBinding(
String roleCode,
Integer uid,
String ukeySerial,
String ukeyPubkey,
String issuerSign
String ukeyPubkey
) {
resolveRoleCode(targetRoleCode);
verifyIssuerSignature(targetRoleCode, ukeyPubkey, uid, issuerSign);
bindUkey(operatorRoleCode, operatorAuthLevel, targetRoleCode, uid, ukeySerial, ukeyPubkey, issuerSign);
return roleUkeyBindingRepository.findActiveByRoleCodeAndUid(roleCode, uid).stream()
.filter(binding -> Objects.equals(binding.getUkeySerial(), ukeySerial)
|| Objects.equals(binding.getUkeyPubkey(), ukeyPubkey))
.findFirst()
.orElseGet(RoleUkeyBindingEntity::new);
}
@Override
@ -231,18 +222,28 @@ public class AuthAdminServiceImpl implements AuthAdminService {
UKeySignDTO request
) {
RoleCode targetRole = resolveRoleCode(targetRoleCode);
validateRoleUid(targetRole, request.getUid());
String authKeyPair = lmkService.exportIkPublicKeyHex();
UKeySignDTO dto = new UKeySignDTO();
dto.setPubKey(request.getPubKey());
dto.setRole(targetRole.getCode());
dto.setUid(request.getUid());
String signValue = lmkService.signIk(toIssuePayload(dto, authKeyPair));
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 is required");
}
if (RoleCode.SUPER_ADMIN == targetRole) {
backupPacket = lmkService.buildBackupPacket(request.getUid());
}
bindUkey(
targetRole.getCode(),
request.getUid(),
request.getUkeySerial(),
request.getPubKey(),
signValue
);
return UKeySignResult.builder()
.sign(signValue)
.backupPacket(backupPacket)
@ -295,6 +296,12 @@ public class AuthAdminServiceImpl implements AuthAdminService {
.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) {
return List.of(RoleCode.values()).stream()
.filter(item -> item.getCode().equals(roleCode))
@ -306,24 +313,9 @@ public class AuthAdminServiceImpl implements AuthAdminService {
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 {
UKeySignDTO dto = new UKeySignDTO();
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));
return objectMapper.writeValueAsString(UKeySignEntity.getInstance(dto, authKeyPair, roleCode));
} catch (JsonProcessingException 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());
List<RoleUkeyBindingEntity> activeBindings = roleUkeyBindingRepository.findActiveByRoleCode(roleCode.getCode());
validateUkeyCount(roleCode, activeBindings, request.getLoginFactors());
Map<Integer, RoleUkeyBindingEntity> bindingsByUid = activeBindings.stream()
.collect(Collectors.toMap(RoleUkeyBindingEntity::getUid, item -> item, (left, right) -> left, java.util.LinkedHashMap::new));
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");
}
Map<Integer, List<RoleUkeyBindingEntity>> bindingsByUid = activeBindings.stream()
.collect(Collectors.groupingBy(RoleUkeyBindingEntity::getUid));
ukeyLoginRandomService.assertIssued(
roleCode.getCode(),
request.getLoginFactors().stream().map(UkeyLoginProof::getServerRandom).toList()
@ -167,10 +161,10 @@ public class AuthServiceImpl implements AuthService {
String authKeyPair = lmkService.exportIkPublicKeyHex();
List<String> matchedSerials = new ArrayList<>();
for (UkeyLoginProof proof : request.getLoginFactors()) {
RoleUkeyBindingEntity binding = bindingsByUid.get(proof.getUid());
if (binding == null || !binding.getUkeyPubkey().equals(proof.getPubKey())) {
throw new BizException(ErrorCode.UNAUTHORIZED.getCode(), "ukey auth info does not match bound role");
}
RoleUkeyBindingEntity binding = bindingsByUid.getOrDefault(proof.getUid(), List.of()).stream()
.filter(item -> item.getUkeyPubkey().equals(proof.getPubKey()))
.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.verifyLoginSignature(proof.getPubKey(), proof.getLoginPayload(), proof.getLoginSignature());
matchedSerials.add(binding.getUkeySerial());
@ -503,16 +497,20 @@ public class AuthServiceImpl implements AuthService {
}
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()
.map(RoleUkeyBindingEntity::getUkeySerial)
.collect(Collectors.toSet());
if (!boundSerials.containsAll(uniqueSerials)) {
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);
}
@ -524,7 +522,17 @@ public class AuthServiceImpl implements AuthService {
if (proofs == null || proofs.size() != authPolicyService.requiredUkeyCount(roleCode)) {
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");
}
}
@ -532,10 +540,9 @@ public class AuthServiceImpl implements AuthService {
private String buildIssuePayload(String roleCode, UkeyLoginProof proof, String authKeyPair) {
UKeySignDTO dto = new UKeySignDTO();
dto.setPubKey(proof.getPubKey());
dto.setRole(RoleCode.valueOf(roleCode).getCode());
dto.setUid(proof.getUid());
try {
return objectMapper.writeValueAsString(UKeySignEntity.getInstance(dto, authKeyPair));
return objectMapper.writeValueAsString(UKeySignEntity.getInstance(dto, authKeyPair, RoleCode.valueOf(roleCode).getCode()));
} catch (JsonProcessingException ex) {
throw new IllegalStateException("serialize ukey issue payload failed", ex);
}

View File

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

View File

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

View File

@ -262,7 +262,8 @@ CREATE TABLE IF NOT EXISTS tms_role_ukey_binding (
unbound_at DATETIME(3) NULL,
create_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)
);

View File

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

View File

@ -267,34 +267,6 @@ class AuthControllerTest {
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
void shouldChangeFullAccountPasswordThroughAdminEndpoint() throws Exception {
AuthService authService = Mockito.mock(AuthService.class);
@ -348,15 +320,15 @@ class AuthControllerTest {
}
@Test
void shouldIssueUkeyBindingSignThroughAdminEndpoint() throws Exception {
void shouldIssueSuperAdminUkeyBindingSignWithoutLoginContext() 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-001").backupPacket(packet).build();
Mockito.when(authAdminService.issueUkeyBindingSign(
ArgumentMatchers.eq("KEY_ADMIN"),
ArgumentMatchers.eq("FULL"),
ArgumentMatchers.isNull(),
ArgumentMatchers.isNull(),
ArgumentMatchers.eq("SUPER_ADMIN"),
ArgumentMatchers.any()
)).thenReturn(response);
@ -366,14 +338,12 @@ class AuthControllerTest {
.setControllerAdvice(new GlobalExceptionHandler())
.build();
mockMvc.perform(post("/api/v1/auth/roles/SUPER_ADMIN/ukeys/issue-sign")
.requestAttr(InternalApiAuthInterceptor.ATTR_ROLE_CODE, "KEY_ADMIN")
.requestAttr(InternalApiAuthInterceptor.ATTR_AUTH_LEVEL, "FULL")
mockMvc.perform(post("/api/v1/auth/super-admin/ukeys/issue-sign")
.contentType(MediaType.APPLICATION_JSON)
.content("""
{
"pubKey": "PUB-NEW",
"role": "SUPER_ADMIN",
"ukeySerial": "UK-NEW",
"uid": 1,
"extra": "EXTRA-001"
}
@ -382,4 +352,36 @@ class AuthControllerTest {
.andExpect(content().string(containsString("\"sign\":\"ISSUE-SIGN-001\"")))
.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
void shouldReplaceActiveBindingInSameUidSeat() {
void shouldUpdateActiveBindingWhenIssueSignUsesSameSerial() {
InMemoryRoleUkeyBindingRepository bindings = new InMemoryRoleUkeyBindingRepository();
bindings.save(binding(RoleCode.SUPER_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(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(
new InMemoryRoleAccountRepository(),
@ -295,19 +299,25 @@ class AuthAdminServiceTest {
new FixedSaltSupplier("salt-001")
);
service.bindIssuedUkey(
RoleCode.KEY_ADMIN.getCode(),
AuthLevel.FULL.name(),
UKeySignDTO dto = new UKeySignDTO();
dto.setPubKey("PUB-NEW");
dto.setUkeySerial("UK-OLD");
dto.setUid(1);
service.issueUkeyBindingSign(
null,
null,
RoleCode.SUPER_ADMIN.getCode(),
1,
"UK-NEW",
"PUB-NEW",
"SIG-NEW"
dto
);
RoleUkeyBindingEntity updated = bindings.findActiveByRoleCodeAndUid(RoleCode.SUPER_ADMIN.getCode(), 1).orElseThrow();
Assertions.assertEquals("UK-NEW", updated.getUkeySerial());
RoleUkeyBindingEntity updated = bindings.findActiveByRoleCodeAndUid(RoleCode.SUPER_ADMIN.getCode(), 1).stream()
.filter(binding -> "UK-OLD".equals(binding.getUkeySerial()))
.findFirst()
.orElseThrow();
Assertions.assertEquals("UK-OLD", updated.getUkeySerial());
Assertions.assertEquals("PUB-NEW", updated.getUkeyPubkey());
Assertions.assertEquals("SIG-NEW", updated.getIssuerSign());
}
@Test
@ -321,7 +331,7 @@ class AuthAdminServiceTest {
AuthAdminService service = new AuthAdminServiceImpl(
new InMemoryRoleAccountRepository(),
fullAccountRepository(),
fullAccountRepository(binding(RoleCode.SUPER_ADMIN, 1, "UK-001", "PUB-OLD", "SIG-OLD")),
new InMemoryAuthUserAccountRepository(),
new InMemoryRoleUkeyBindingRepository(),
new FakePasswordHasher(),
@ -332,6 +342,7 @@ class AuthAdminServiceTest {
);
UKeySignDTO dto = new UKeySignDTO();
dto.setPubKey("PUB-001");
dto.setUkeySerial("UK-001");
dto.setUid(1);
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());
}
@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) {
RoleAccountEntity entity = new RoleAccountEntity();
entity.setId((long) roleCode.ordinal() + 100);
@ -504,11 +599,12 @@ class AuthAdminServiceTest {
private final Map<String, List<RoleUkeyBindingEntity>> store = new ConcurrentHashMap<>();
@Override
public Optional<RoleUkeyBindingEntity> findActiveByRoleCodeAndUid(String roleCode, Integer uid) {
public List<RoleUkeyBindingEntity> findActiveByRoleCodeAndUid(String roleCode, Integer uid) {
return store.getOrDefault(roleCode, List.of()).stream()
.filter(entity -> uid.equals(entity.getUid()))
.filter(entity -> "ACTIVE".equals(entity.getStatus()))
.findFirst();
.sorted(Comparator.comparing(RoleUkeyBindingEntity::getBoundAt).reversed())
.toList();
}
@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
void shouldLockOnlyMappedFullAccountDuringSuperAdminUkeyLoginWhenPasswordIsWrong() {
InMemoryRoleAccountRepository roleAccounts = new InMemoryRoleAccountRepository();
@ -1096,11 +1158,12 @@ class AuthServiceTest {
private final Map<String, List<RoleUkeyBindingEntity>> store = new ConcurrentHashMap<>();
@Override
public Optional<RoleUkeyBindingEntity> findActiveByRoleCodeAndUid(String roleCode, Integer uid) {
public List<RoleUkeyBindingEntity> findActiveByRoleCodeAndUid(String roleCode, Integer uid) {
return store.getOrDefault(roleCode, List.of()).stream()
.filter(entity -> uid.equals(entity.getUid()))
.filter(entity -> "ACTIVE".equals(entity.getStatus()))
.findFirst();
.sorted(Comparator.comparing(RoleUkeyBindingEntity::getBoundAt).reversed())
.toList();
}
@Override

View File

@ -41,7 +41,15 @@ class ReplayProtectedEndpointsTest {
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(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(CryptoCardController.class, ReplayProtected.class));
}