角色和登录改动

This commit is contained in:
waner
2026-04-15 11:18:01 +08:00
parent 622702b0b0
commit 9bea50466f
34 changed files with 1078 additions and 693 deletions
@@ -2,8 +2,9 @@ package com.cisd.tms.modules.auth.controller;
import com.cisd.tms.common.api.ApiResponse;
import com.cisd.tms.modules.auth.dto.UkeyBindRequest;
import com.cisd.tms.modules.auth.security.RequireAuthLevel;
import com.cisd.tms.modules.auth.security.RequireRole;
import com.cisd.tms.modules.auth.enums.AuthLevel;
import com.cisd.tms.modules.auth.enums.RoleCode;
import com.cisd.tms.modules.auth.security.RequireInternalAuth;
import com.cisd.tms.modules.auth.service.AuthAdminService;
import com.cisd.tms.modules.mk.dto.UKeySignDTO;
import com.cisd.tms.modules.mk.dto.UKeySignResult;
@@ -31,9 +32,8 @@ public class AuthAdminController {
private final AuthAdminService authAdminService;
@PostMapping("/roles/{roleCode}/enable")
@Operation(summary = "启用角色", description = "仅允许 SUPER_ADMIN FULL 会话启用目标角色。")
@RequireRole("SUPER_ADMIN")
@RequireAuthLevel("FULL")
@Operation(summary = "启用角色", description = "仅允许 KEY_ADMIN FULL 会话启用目标角色。")
@RequireInternalAuth(role = RoleCode.SUPER_ADMIN, authLevel = AuthLevel.FULL)
public ApiResponse<Void> enableRole(@PathVariable("roleCode") String roleCode, HttpServletRequest request) {
authAdminService.enableRole(
(String) request.getAttribute(InternalApiAuthInterceptor.ATTR_ROLE_CODE),
@@ -44,9 +44,8 @@ public class AuthAdminController {
}
@PostMapping("/roles/{roleCode}/reset-password")
@Operation(summary = "重置角色密码", description = "仅允许 SUPER_ADMIN FULL 会话重置目标角色密码。")
@RequireRole("SUPER_ADMIN")
@RequireAuthLevel("FULL")
@Operation(summary = "重置角色密码", description = "仅允许 KEY_ADMIN FULL 会话重置目标角色密码。")
@RequireInternalAuth(role = RoleCode.SUPER_ADMIN, authLevel = AuthLevel.FULL)
public ApiResponse<Void> resetPassword(@PathVariable("roleCode") String roleCode, HttpServletRequest request) {
authAdminService.resetPassword(
(String) request.getAttribute(InternalApiAuthInterceptor.ATTR_ROLE_CODE),
@@ -57,9 +56,8 @@ public class AuthAdminController {
}
@PostMapping("/roles/{roleCode}/ukeys/bind")
@Operation(summary = "绑定角色 UKey", description = "仅允许 SUPER_ADMIN FULL 会话登记目标角色的 UKey 绑定信息。")
@RequireRole("SUPER_ADMIN")
@RequireAuthLevel("FULL")
@Operation(summary = "绑定角色 UKey", description = "仅允许 KEY_ADMIN FULL 会话登记目标角色的 UKey 绑定信息。")
@RequireInternalAuth(role = RoleCode.SUPER_ADMIN, authLevel = AuthLevel.FULL)
public ApiResponse<Void> bindUkey(
@PathVariable("roleCode") String roleCode,
@Valid @RequestBody UkeyBindRequest request,
@@ -69,11 +67,9 @@ public class AuthAdminController {
(String) httpRequest.getAttribute(InternalApiAuthInterceptor.ATTR_ROLE_CODE),
(String) httpRequest.getAttribute(InternalApiAuthInterceptor.ATTR_AUTH_LEVEL),
roleCode,
request.getSlotNo(),
request.getUid(),
request.getUkeySerial(),
request.getPubKey(),
request.getUid(),
request.getRid(),
request.getIssuerSignature()
);
return ApiResponse.success();
@@ -81,8 +77,7 @@ public class AuthAdminController {
@PostMapping("/roles/{roleCode}/ukeys/issue-sign")
@Operation(summary = "生成 UKey 发行签名", description = "按旧绑定流程为目标角色 UKey 材料生成发行签名。")
// @RequireRole("SUPER_ADMIN")
@RequireAuthLevel("FULL")
@RequireInternalAuth(role = RoleCode.SUPER_ADMIN, authLevel = AuthLevel.LIMITED)
public ApiResponse<UKeySignResult> issueUkeyBindingSign(
@PathVariable("roleCode") String roleCode,
@RequestBody UKeySignDTO request,
@@ -11,9 +11,11 @@ public class LoginRequest {
@Schema(description = "角色编码", example = "KEY_ADMIN")
private String roleCode;
@NotBlank(message = "password is required")
@Schema(description = "登录密码", example = "P@ssw0rd")
private String password;
@Schema(description = "参与角色口令校验的账号列表")
private List<PasswordLoginAccountRequest> accounts;
@Schema(description = "角色口令")
private String rolePassword;
@Schema(description = "UKey 序列号列表,完整登录时需传入角色要求数量的序列号")
private List<@NotBlank(message = "ukey serial must not be blank") String> ukeySerials;
@@ -26,12 +28,20 @@ public class LoginRequest {
this.roleCode = roleCode;
}
public String getPassword() {
return password;
public List<PasswordLoginAccountRequest> getAccounts() {
return accounts;
}
public void setPassword(String password) {
this.password = password;
public void setAccounts(List<PasswordLoginAccountRequest> accounts) {
this.accounts = accounts;
}
public String getRolePassword() {
return rolePassword;
}
public void setRolePassword(String rolePassword) {
this.rolePassword = rolePassword;
}
public List<String> getUkeySerials() {
@@ -0,0 +1,32 @@
package com.cisd.tms.modules.auth.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
@Schema(description = "角色口令校验账号")
public class PasswordLoginAccountRequest {
@NotBlank(message = "username is required")
@Schema(description = "账号名", example = "super-admin-01")
private String username;
@NotBlank(message = "password is required")
@Schema(description = "账号口令", example = "12345678")
private String password;
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
}
@@ -1,7 +1,10 @@
package com.cisd.tms.modules.auth.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotEmpty;
import java.util.List;
@Schema(description = "口令登录请求")
public class PasswordLoginRequest {
@@ -13,9 +16,10 @@ public class PasswordLoginRequest {
@Schema(description = "角色编码", example = "AUDIT_ADMIN")
private String roleCode;
@NotBlank(message = "password is required")
@Schema(description = "登录口令", example = "12345678")
private String password;
@Valid
@NotEmpty(message = "accounts is required")
@Schema(description = "参与本次角色口令校验的账号列表")
private List<PasswordLoginAccountRequest> accounts;
@NotBlank(message = "captchaCode is required")
@Schema(description = "图形验证码", example = "ABCD")
@@ -33,12 +37,12 @@ public class PasswordLoginRequest {
this.roleCode = roleCode;
}
public String getPassword() {
return password;
public List<PasswordLoginAccountRequest> getAccounts() {
return accounts;
}
public void setPassword(String password) {
this.password = password;
public void setAccounts(List<PasswordLoginAccountRequest> accounts) {
this.accounts = accounts;
}
public String getCaptchaCode() {
@@ -1,15 +1,15 @@
package com.cisd.tms.modules.auth.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.NotBlank;
@Schema(description = "UKey 绑定请求")
public class UkeyBindRequest {
@NotNull(message = "slotNo is required")
@Schema(description = "角色 UKey 槽位号", example = "1")
private Integer slotNo;
@NotNull(message = "uid is required")
@Schema(description = "角色内固定席位编号", example = "1")
private Integer uid;
@NotBlank(message = "ukeySerial is required")
@Schema(description = "UKey 序列号", example = "UK-001")
@@ -19,24 +19,16 @@ public class UkeyBindRequest {
@Schema(description = "UKey 公钥")
private String pubKey;
@NotBlank(message = "uid is required")
@Schema(description = "UKey 认证信息 uid", example = "1")
private String uid;
@NotBlank(message = "rid is required")
@Schema(description = "UKey 认证信息 rid", example = "RID-001")
private String rid;
@NotBlank(message = "issuerSignature is required")
@Schema(description = "认证公钥签名值")
private String issuerSignature;
public Integer getSlotNo() {
return slotNo;
public Integer getUid() {
return uid;
}
public void setSlotNo(Integer slotNo) {
this.slotNo = slotNo;
public void setUid(Integer uid) {
this.uid = uid;
}
public String getUkeySerial() {
@@ -62,20 +54,4 @@ public class UkeyBindRequest {
public void setIssuerSignature(String issuerSignature) {
this.issuerSignature = issuerSignature;
}
public String getUid() {
return uid;
}
public void setUid(String uid) {
this.uid = uid;
}
public String getRid() {
return rid;
}
public void setRid(String rid) {
this.rid = rid;
}
}
@@ -2,6 +2,7 @@ package com.cisd.tms.modules.auth.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
@Schema(description = "单个 UKey 登录证明")
public class UkeyLoginProof {
@@ -9,11 +10,8 @@ public class UkeyLoginProof {
@NotBlank(message = "pubKey is required")
private String pubKey;
@NotBlank(message = "uid is required")
private String uid;
@NotBlank(message = "rid is required")
private String rid;
@NotNull(message = "uid is required")
private Integer uid;
/**
* 对应旧系统下发的 rb,命名改为更明确的服务端随机数。
@@ -38,22 +36,14 @@ public class UkeyLoginProof {
this.pubKey = pubKey;
}
public String getUid() {
public Integer getUid() {
return uid;
}
public void setUid(String uid) {
public void setUid(Integer uid) {
this.uid = uid;
}
public String getRid() {
return rid;
}
public void setRid(String rid) {
this.rid = rid;
}
public String getServerRandom() {
return serverRandom;
}
@@ -13,9 +13,9 @@ public class UkeyLoginRequest {
@Schema(description = "角色编码", example = "KEY_ADMIN")
private String roleCode;
@NotBlank(message = "password is required")
@NotBlank(message = "rolePassword is required")
@Schema(description = "角色口令", example = "12345678")
private String password;
private String rolePassword;
@Valid
@NotEmpty(message = "ukeyProofs is required")
@@ -30,12 +30,12 @@ public class UkeyLoginRequest {
this.roleCode = roleCode;
}
public String getPassword() {
return password;
public String getRolePassword() {
return rolePassword;
}
public void setPassword(String password) {
this.password = password;
public void setRolePassword(String rolePassword) {
this.rolePassword = rolePassword;
}
public List<UkeyLoginProof> getUkeyProofs() {
@@ -15,6 +15,10 @@ public class AuthSessionEntity extends BaseEntity {
* 当前会话所属角色编码。
*/
private String roleCode;
/**
* 本次角色会话在后端侧记录的认证账号列表。
*/
private String authenticatedUsersJson;
/**
* 当前会话的认证方式,区分口令登录和 UKey 登录。
*/
@@ -44,6 +48,14 @@ public class AuthSessionEntity extends BaseEntity {
this.roleCode = roleCode;
}
public String getAuthenticatedUsersJson() {
return authenticatedUsersJson;
}
public void setAuthenticatedUsersJson(String authenticatedUsersJson) {
this.authenticatedUsersJson = authenticatedUsersJson;
}
public String getAuthMethod() {
return authMethod;
}
@@ -0,0 +1,100 @@
package com.cisd.tms.modules.auth.entity;
import com.baomidou.mybatisplus.annotation.TableName;
import com.cisd.tms.infrastructure.persistence.entity.BaseEntity;
import java.time.LocalDateTime;
@TableName("tms_auth_user_account")
public class AuthUserAccountEntity extends BaseEntity {
private String username;
private String displayName;
private String roleCode;
private String passwordHash;
private String passwordSalt;
private String status;
private Integer failedCount;
private LocalDateTime lockedUntil;
private LocalDateTime lastLoginAt;
private LocalDateTime lastActiveAt;
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getDisplayName() {
return displayName;
}
public void setDisplayName(String displayName) {
this.displayName = displayName;
}
public String getRoleCode() {
return roleCode;
}
public void setRoleCode(String roleCode) {
this.roleCode = roleCode;
}
public String getPasswordHash() {
return passwordHash;
}
public void setPasswordHash(String passwordHash) {
this.passwordHash = passwordHash;
}
public String getPasswordSalt() {
return passwordSalt;
}
public void setPasswordSalt(String passwordSalt) {
this.passwordSalt = passwordSalt;
}
public String getStatus() {
return status;
}
public void setStatus(String status) {
this.status = status;
}
public Integer getFailedCount() {
return failedCount;
}
public void setFailedCount(Integer failedCount) {
this.failedCount = failedCount;
}
public LocalDateTime getLockedUntil() {
return lockedUntil;
}
public void setLockedUntil(LocalDateTime lockedUntil) {
this.lockedUntil = lockedUntil;
}
public LocalDateTime getLastLoginAt() {
return lastLoginAt;
}
public void setLastLoginAt(LocalDateTime lastLoginAt) {
this.lastLoginAt = lastLoginAt;
}
public LocalDateTime getLastActiveAt() {
return lastActiveAt;
}
public void setLastActiveAt(LocalDateTime lastActiveAt) {
this.lastActiveAt = lastActiveAt;
}
}
@@ -12,17 +12,9 @@ public class RoleUkeyBindingEntity extends BaseEntity {
*/
private String roleCode;
/**
* 角色下的 UKey 槽位,从 1 开始,和旧系统卡位模型保持一致。
* 角色内固定席位编号,从 1 开始。
*/
private Integer slotNo;
/**
* 旧系统 auth 文件中的 uid,登录时要参与认证匹配。
*/
private String uid;
/**
* 旧系统 auth 文件中的 rid,登录时要参与角色槽位校验。
*/
private String rid;
private Integer uid;
private String ukeySerial;
private String ukeyPubkey;
private String issuerSign;
@@ -38,30 +30,14 @@ public class RoleUkeyBindingEntity extends BaseEntity {
this.roleCode = roleCode;
}
public Integer getSlotNo() {
return slotNo;
}
public void setSlotNo(Integer slotNo) {
this.slotNo = slotNo;
}
public String getUid() {
public Integer getUid() {
return uid;
}
public void setUid(String uid) {
public void setUid(Integer uid) {
this.uid = uid;
}
public String getRid() {
return rid;
}
public void setRid(String rid) {
this.rid = rid;
}
public String getUkeySerial() {
return ukeySerial;
}
@@ -0,0 +1,15 @@
package com.cisd.tms.modules.auth.mapper;
import com.cisd.tms.infrastructure.persistence.mapper.BaseMapperX;
import com.cisd.tms.modules.auth.entity.AuthUserAccountEntity;
import java.util.List;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
@Mapper
public interface AuthUserAccountMapper extends BaseMapperX<AuthUserAccountEntity> {
AuthUserAccountEntity selectByUsername(@Param("username") String username);
List<AuthUserAccountEntity> selectByRoleCode(@Param("roleCode") String roleCode);
}
@@ -9,7 +9,7 @@ import org.apache.ibatis.annotations.Param;
@Mapper
public interface RoleUkeyBindingMapper extends BaseMapperX<RoleUkeyBindingEntity> {
RoleUkeyBindingEntity selectActiveByRoleCodeAndSlotNo(@Param("roleCode") String roleCode, @Param("slotNo") Integer slotNo);
RoleUkeyBindingEntity selectActiveByRoleCodeAndUid(@Param("roleCode") String roleCode, @Param("uid") Integer uid);
List<RoleUkeyBindingEntity> selectActiveByRoleCode(@Param("roleCode") String roleCode);
}
@@ -0,0 +1,16 @@
package com.cisd.tms.modules.auth.repository;
import com.cisd.tms.modules.auth.entity.AuthUserAccountEntity;
import java.util.List;
import java.util.Optional;
public interface AuthUserAccountRepository {
Optional<AuthUserAccountEntity> findByUsername(String username);
List<AuthUserAccountEntity> findByRoleCode(String roleCode);
void save(AuthUserAccountEntity entity);
void update(AuthUserAccountEntity entity);
}
@@ -6,7 +6,7 @@ import java.util.Optional;
public interface RoleUkeyBindingRepository {
Optional<RoleUkeyBindingEntity> findActiveByRoleCodeAndSlotNo(String roleCode, Integer slotNo);
Optional<RoleUkeyBindingEntity> findActiveByRoleCodeAndUid(String roleCode, Integer uid);
List<RoleUkeyBindingEntity> findActiveByRoleCode(String roleCode);
@@ -0,0 +1,38 @@
package com.cisd.tms.modules.auth.repository.impl;
import com.cisd.tms.modules.auth.entity.AuthUserAccountEntity;
import com.cisd.tms.modules.auth.mapper.AuthUserAccountMapper;
import com.cisd.tms.modules.auth.repository.AuthUserAccountRepository;
import java.util.List;
import java.util.Optional;
import org.springframework.stereotype.Repository;
@Repository
public class AuthUserAccountRepositoryImpl implements AuthUserAccountRepository {
private final AuthUserAccountMapper authUserAccountMapper;
public AuthUserAccountRepositoryImpl(AuthUserAccountMapper authUserAccountMapper) {
this.authUserAccountMapper = authUserAccountMapper;
}
@Override
public Optional<AuthUserAccountEntity> findByUsername(String username) {
return Optional.ofNullable(authUserAccountMapper.selectByUsername(username));
}
@Override
public List<AuthUserAccountEntity> findByRoleCode(String roleCode) {
return authUserAccountMapper.selectByRoleCode(roleCode);
}
@Override
public void save(AuthUserAccountEntity entity) {
authUserAccountMapper.insert(entity);
}
@Override
public void update(AuthUserAccountEntity entity) {
authUserAccountMapper.updateById(entity);
}
}
@@ -17,8 +17,8 @@ public class RoleUkeyBindingRepositoryImpl implements RoleUkeyBindingRepository
}
@Override
public Optional<RoleUkeyBindingEntity> findActiveByRoleCodeAndSlotNo(String roleCode, Integer slotNo) {
return Optional.ofNullable(roleUkeyBindingMapper.selectActiveByRoleCodeAndSlotNo(roleCode, slotNo));
public Optional<RoleUkeyBindingEntity> findActiveByRoleCodeAndUid(String roleCode, Integer uid) {
return Optional.ofNullable(roleUkeyBindingMapper.selectActiveByRoleCodeAndUid(roleCode, uid));
}
@Override
@@ -1,5 +1,7 @@
package com.cisd.tms.modules.auth.security;
import com.cisd.tms.modules.auth.enums.AuthLevel;
import com.cisd.tms.modules.auth.enums.RoleCode;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@@ -7,7 +9,9 @@ import java.lang.annotation.Target;
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface RequireAuthLevel {
public @interface RequireInternalAuth {
String value();
RoleCode role();
AuthLevel authLevel();
}
@@ -1,13 +0,0 @@
package com.cisd.tms.modules.auth.security;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface RequireRole {
String value();
}
@@ -13,11 +13,9 @@ public interface AuthAdminService {
String operatorRoleCode,
String operatorAuthLevel,
String targetRoleCode,
Integer slotNo,
Integer uid,
String ukeySerial,
String ukeyPubkey,
String uid,
String rid,
String issuerSign
);
@@ -25,11 +23,9 @@ public interface AuthAdminService {
String operatorRoleCode,
String operatorAuthLevel,
String targetRoleCode,
Integer slotNo,
Integer uid,
String ukeySerial,
String ukeyPubkey,
String uid,
String rid,
String issuerSign
);
@@ -3,11 +3,12 @@ package com.cisd.tms.modules.auth.service.impl;
import com.cisd.tms.common.enums.ErrorCode;
import com.cisd.tms.common.exception.BizException;
import com.cisd.tms.common.util.TraceIdUtil;
import com.cisd.tms.modules.auth.entity.AuthUserAccountEntity;
import com.cisd.tms.modules.auth.entity.RoleAccountEntity;
import com.cisd.tms.modules.auth.entity.RoleUkeyBindingEntity;
import com.cisd.tms.modules.auth.enums.AuthLevel;
import com.cisd.tms.modules.auth.enums.RoleAccountStatus;
import com.cisd.tms.modules.auth.enums.RoleCode;
import com.cisd.tms.modules.auth.repository.AuthUserAccountRepository;
import com.cisd.tms.modules.auth.repository.RoleAccountRepository;
import com.cisd.tms.modules.auth.repository.RoleUkeyBindingRepository;
import com.cisd.tms.modules.auth.service.AuthAdminService;
@@ -35,6 +36,7 @@ public class AuthAdminServiceImpl implements AuthAdminService {
static final String DEFAULT_PASSWORD = "12345678";
private final RoleAccountRepository roleAccountRepository;
private final AuthUserAccountRepository authUserAccountRepository;
private final RoleUkeyBindingRepository roleUkeyBindingRepository;
private final PasswordHasher passwordHasher;
private final LmkService lmkService;
@@ -44,7 +46,6 @@ public class AuthAdminServiceImpl implements AuthAdminService {
@Override
public void enableRole(String operatorRoleCode, String operatorAuthLevel, String targetRoleCode) {
requireKeyAdminFull(operatorRoleCode, operatorAuthLevel);
RoleAccountEntity target = loadRole(targetRoleCode);
target.setStatus(RoleAccountStatus.ACTIVE.name());
target.setNeedChangePassword(Boolean.TRUE);
@@ -55,16 +56,31 @@ public class AuthAdminServiceImpl implements AuthAdminService {
@Override
public void resetPassword(String operatorRoleCode, String operatorAuthLevel, String targetRoleCode) {
requireKeyAdminFull(operatorRoleCode, operatorAuthLevel);
RoleAccountEntity target = loadRole(targetRoleCode);
String newSalt = passwordSaltGenerator.nextSalt();
target.setPasswordSalt(newSalt);
target.setPasswordHash(passwordHasher.hash(DEFAULT_PASSWORD, newSalt));
String roleSalt = passwordSaltGenerator.nextSalt();
target.setPasswordSalt(roleSalt);
target.setPasswordHash(passwordHasher.hash(DEFAULT_PASSWORD, roleSalt));
target.setStatus(RoleAccountStatus.ACTIVE.name());
target.setNeedChangePassword(Boolean.TRUE);
target.setFailedCount(0);
target.setLockedUntil(null);
roleAccountRepository.update(target);
List<AuthUserAccountEntity> accounts = authUserAccountRepository.findByRoleCode(targetRoleCode);
if (accounts.isEmpty()) {
throw new BizException(ErrorCode.VALIDATE_FAILED.getCode(), "target role user accounts not found");
}
for (AuthUserAccountEntity account : accounts) {
String newSalt = passwordSaltGenerator.nextSalt();
account.setPasswordSalt(newSalt);
account.setPasswordHash(passwordHasher.hash(DEFAULT_PASSWORD, newSalt));
account.setStatus(RoleAccountStatus.ACTIVE.name());
account.setFailedCount(0);
account.setLockedUntil(null);
account.setLastActiveAt(null);
account.setLastLoginAt(null);
authUserAccountRepository.update(account);
}
}
@Override
@@ -72,33 +88,30 @@ public class AuthAdminServiceImpl implements AuthAdminService {
String operatorRoleCode,
String operatorAuthLevel,
String targetRoleCode,
Integer slotNo,
Integer uid,
String ukeySerial,
String ukeyPubkey,
String uid,
String rid,
String issuerSign
) {
requireKeyAdminFull(operatorRoleCode, operatorAuthLevel);
RoleCode targetRole = resolveRoleCode(targetRoleCode);
if (slotNo == null || slotNo < 1 || slotNo > targetRole.getRequiredUkeyCount()) {
throw new BizException(ErrorCode.VALIDATE_FAILED.getCode(), "slotNo exceeds role ukey requirement");
if (uid == null || uid < 1 || uid > targetRole.getRequiredUkeyCount()) {
throw new BizException(ErrorCode.VALIDATE_FAILED.getCode(), "uid exceeds role ukey requirement");
}
RoleUkeyBindingEntity binding = roleUkeyBindingRepository
.findActiveByRoleCodeAndSlotNo(targetRoleCode, slotNo)
.findActiveByRoleCodeAndUid(targetRoleCode, uid)
.orElseGet(RoleUkeyBindingEntity::new);
if (binding.getId() == null) {
binding.setId((long) Math.abs(Objects.hash(targetRoleCode, slotNo, ukeySerial, TraceIdUtil.newTraceId())));
binding.setId((long) Math.abs(Objects.hash(targetRoleCode, uid, ukeySerial, TraceIdUtil.newTraceId())));
binding.setRoleCode(targetRoleCode);
binding.setSlotNo(slotNo);
binding.setUid(uid);
binding.setStatus("ACTIVE");
applyBinding(binding, ukeySerial, ukeyPubkey, uid, rid, issuerSign);
applyBinding(binding, ukeySerial, ukeyPubkey, issuerSign);
roleUkeyBindingRepository.save(binding);
return;
}
applyBinding(binding, ukeySerial, ukeyPubkey, uid, rid, issuerSign);
applyBinding(binding, ukeySerial, ukeyPubkey, issuerSign);
roleUkeyBindingRepository.update(binding);
}
@@ -107,17 +120,14 @@ public class AuthAdminServiceImpl implements AuthAdminService {
String operatorRoleCode,
String operatorAuthLevel,
String targetRoleCode,
Integer slotNo,
Integer uid,
String ukeySerial,
String ukeyPubkey,
String uid,
String rid,
String issuerSign
) {
requireKeyAdminFull(operatorRoleCode, operatorAuthLevel);
resolveRoleCode(targetRoleCode);
verifyIssuerSignature(targetRoleCode, ukeyPubkey, uid, rid, issuerSign);
bindUkey(operatorRoleCode, operatorAuthLevel, targetRoleCode, slotNo, ukeySerial, ukeyPubkey, uid, rid, issuerSign);
verifyIssuerSignature(targetRoleCode, ukeyPubkey, uid, issuerSign);
bindUkey(operatorRoleCode, operatorAuthLevel, targetRoleCode, uid, ukeySerial, ukeyPubkey, issuerSign);
}
@Override
@@ -127,7 +137,6 @@ public class AuthAdminServiceImpl implements AuthAdminService {
String targetRoleCode,
UKeySignDTO request
) {
requireKeyAdminFull(operatorRoleCode, operatorAuthLevel);
RoleCode targetRole = resolveRoleCode(targetRoleCode);
String authKeyPair = lmkService.exportIkPublicKeyHex();
@@ -135,12 +144,11 @@ public class AuthAdminServiceImpl implements AuthAdminService {
dto.setPubKey(request.getPubKey());
dto.setRole(targetRole.getCode());
dto.setUid(request.getUid());
dto.setRid(request.getRid());
String signValue = lmkService.signIk(toIssuePayload(dto, authKeyPair));
MasterKeyBackupPacket backupPacket = null;
if (RoleCode.SUPER_ADMIN == targetRole) {
backupPacket = lmkService.buildBackupPacket(Integer.parseInt(request.getUid()));
backupPacket = lmkService.buildBackupPacket(request.getUid());
}
return UKeySignResult.builder()
.sign(signValue)
@@ -152,26 +160,16 @@ public class AuthAdminServiceImpl implements AuthAdminService {
RoleUkeyBindingEntity binding,
String ukeySerial,
String ukeyPubkey,
String uid,
String rid,
String issuerSign
) {
binding.setUkeySerial(ukeySerial);
binding.setUkeyPubkey(ukeyPubkey);
binding.setUid(uid);
binding.setRid(rid);
binding.setIssuerSign(issuerSign);
binding.setStatus("ACTIVE");
binding.setBoundAt(now());
binding.setUnboundAt(null);
}
private void requireKeyAdminFull(String operatorRoleCode, String operatorAuthLevel) {
if (!RoleCode.KEY_ADMIN.getCode().equals(operatorRoleCode) || !AuthLevel.FULL.name().equals(operatorAuthLevel)) {
throw new BizException(ErrorCode.UNAUTHORIZED.getCode(), "operation requires KEY_ADMIN FULL session");
}
}
private RoleAccountEntity loadRole(String roleCode) {
resolveRoleCode(roleCode);
return roleAccountRepository.findByRoleCode(roleCode)
@@ -189,13 +187,12 @@ public class AuthAdminServiceImpl implements AuthAdminService {
return LocalDateTime.ofInstant(clock.instant(), ZoneOffset.UTC);
}
private void verifyIssuerSignature(String targetRoleCode, String ukeyPubkey, String uid, String rid, String issuerSign) {
private void verifyIssuerSignature(String targetRoleCode, String ukeyPubkey, Integer uid, String issuerSign) {
try {
UKeySignDTO dto = new UKeySignDTO();
dto.setPubKey(ukeyPubkey);
dto.setRole(resolveRoleCode(targetRoleCode).getCode());
dto.setUid(uid);
dto.setRid(rid);
String authKeyPair = lmkService.exportIkPublicKeyHex();
lmkService.verifyIk(toIssuePayload(dto, authKeyPair), issuerSign);
} catch (BizException ex) {
@@ -6,12 +6,14 @@ import com.cisd.tms.modules.auth.dto.CaptchaResponse;
import com.cisd.tms.modules.auth.dto.CurrentUserResponse;
import com.cisd.tms.modules.auth.dto.LoginRequest;
import com.cisd.tms.modules.auth.dto.LoginResponse;
import com.cisd.tms.modules.auth.dto.PasswordLoginAccountRequest;
import com.cisd.tms.modules.auth.dto.PasswordLoginRequest;
import com.cisd.tms.modules.auth.dto.UkeyLoginProof;
import com.cisd.tms.modules.auth.dto.UkeyLoginRandomRequest;
import com.cisd.tms.modules.auth.dto.UkeyLoginRandomResponse;
import com.cisd.tms.modules.auth.dto.UkeyLoginRequest;
import com.cisd.tms.modules.auth.entity.AuthSessionEntity;
import com.cisd.tms.modules.auth.entity.AuthUserAccountEntity;
import com.cisd.tms.modules.auth.entity.AuthUserEntity;
import com.cisd.tms.modules.auth.entity.RoleAccountEntity;
import com.cisd.tms.modules.auth.entity.RoleUkeyBindingEntity;
@@ -20,10 +22,18 @@ import com.cisd.tms.modules.auth.enums.AuthMethod;
import com.cisd.tms.modules.auth.enums.RoleAccountStatus;
import com.cisd.tms.modules.auth.enums.RoleCode;
import com.cisd.tms.modules.auth.repository.AuthSessionRepository;
import com.cisd.tms.modules.auth.repository.AuthUserAccountRepository;
import com.cisd.tms.modules.auth.repository.AuthUserRepository;
import com.cisd.tms.modules.auth.repository.RoleAccountRepository;
import com.cisd.tms.modules.auth.repository.RoleUkeyBindingRepository;
import com.cisd.tms.modules.auth.service.*;
import com.cisd.tms.modules.auth.service.AuthPolicyService;
import com.cisd.tms.modules.auth.service.AuthService;
import com.cisd.tms.modules.auth.service.CaptchaService;
import com.cisd.tms.modules.auth.service.CompatUkeyVerifier;
import com.cisd.tms.modules.auth.service.PasswordHasher;
import com.cisd.tms.modules.auth.service.PasswordSaltGenerator;
import com.cisd.tms.modules.auth.service.SessionTokenGenerator;
import com.cisd.tms.modules.auth.service.UkeyLoginRandomService;
import com.cisd.tms.modules.mk.dto.UKeySignDTO;
import com.cisd.tms.modules.mk.dto.UKeySignEntity;
import com.cisd.tms.modules.mk.enums.MasterKeyStatus;
@@ -34,6 +44,7 @@ import java.time.Clock;
import java.time.LocalDateTime;
import java.time.OffsetDateTime;
import java.time.ZoneOffset;
import java.util.ArrayList;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
@@ -51,6 +62,7 @@ public class AuthServiceImpl implements AuthService {
private final RoleAccountRepository roleAccountRepository;
private final AuthUserRepository authUserRepository;
private final AuthUserAccountRepository authUserAccountRepository;
private final AuthSessionRepository authSessionRepository;
private final RoleUkeyBindingRepository roleUkeyBindingRepository;
private final PasswordHasher passwordHasher;
@@ -70,14 +82,22 @@ public class AuthServiceImpl implements AuthService {
.orElseThrow(() -> new BizException(ErrorCode.UNAUTHORIZED.getCode(), "role account not found"));
validateRoleStatus(roleAccount);
if (!passwordHasher.matches(request.getPassword(), roleAccount.getPasswordSalt(), roleAccount.getPasswordHash())) {
onPasswordFailed(roleAccount);
}
resetFailureState(roleAccount);
AuthMethod authMethod = resolveAuthMethod(request.getUkeySerials());
List<AuthUserAccountEntity> validatedAccounts = new ArrayList<>();
if (AuthMethod.PASSWORD == authMethod) {
validatedAccounts = validateRoleAccounts(roleAccount, request.getAccounts());
resetValidatedAccounts(validatedAccounts);
} else {
validateRolePassword(roleAccount, request.getRolePassword());
resetRoleFailureState(roleAccount);
}
AuthLevel authLevel = resolveAuthLevel(roleAccount, request.getUkeySerials());
AuthSessionEntity session = buildSession(roleAccount.getRoleCode(), authMethod, authLevel);
AuthSessionEntity session = buildSession(
roleAccount.getRoleCode(),
authMethod,
authLevel,
validatedAccounts.isEmpty() ? null : validatedAccounts.stream().map(AuthUserAccountEntity::getUsername).toList()
);
authSessionRepository.save(session);
LoginResponse response = new LoginResponse();
@@ -85,7 +105,7 @@ public class AuthServiceImpl implements AuthService {
response.setAuthLevel(authLevel.name());
response.setToken(session.getSessionToken());
response.setExpiresAt(OffsetDateTime.of(session.getExpiresAt(), ZoneOffset.UTC).toString());
response.setNeedChangePassword(Boolean.TRUE.equals(roleAccount.getNeedChangePassword()));
response.setNeedChangePassword(needsPasswordChange(roleAccount));
return response;
}
@@ -95,7 +115,7 @@ public class AuthServiceImpl implements AuthService {
captchaService.verify(request.getCaptchaId(), request.getCaptchaCode());
LoginRequest loginRequest = new LoginRequest();
loginRequest.setRoleCode(request.getRoleCode());
loginRequest.setPassword(request.getPassword());
loginRequest.setAccounts(request.getAccounts());
return login(loginRequest);
}
@@ -114,12 +134,12 @@ public class AuthServiceImpl implements AuthService {
RoleCode roleCode = RoleCode.valueOf(request.getRoleCode());
List<RoleUkeyBindingEntity> activeBindings = roleUkeyBindingRepository.findActiveByRoleCode(roleCode.getCode());
validateUkeyCount(roleCode, activeBindings, request.getUkeyProofs());
Map<String, RoleUkeyBindingEntity> bindingsByPubKey = activeBindings.stream()
.collect(Collectors.toMap(RoleUkeyBindingEntity::getUkeyPubkey, item -> item, (left, right) -> left, java.util.LinkedHashMap::new));
Set<String> requestPubKeys = request.getUkeyProofs().stream()
.map(UkeyLoginProof::getPubKey)
Map<Integer, RoleUkeyBindingEntity> bindingsByUid = activeBindings.stream()
.collect(Collectors.toMap(RoleUkeyBindingEntity::getUid, item -> item, (left, right) -> left, java.util.LinkedHashMap::new));
Set<Integer> requestUids = request.getUkeyProofs().stream()
.map(UkeyLoginProof::getUid)
.collect(Collectors.toSet());
if (requestPubKeys.size() != request.getUkeyProofs().size() || !bindingsByPubKey.keySet().containsAll(requestPubKeys)) {
if (requestUids.size() != request.getUkeyProofs().size() || !bindingsByUid.keySet().containsAll(requestUids)) {
throw new BizException(ErrorCode.UNAUTHORIZED.getCode(), "ukey auth info does not match bound role");
}
ukeyLoginRandomService.assertIssued(
@@ -127,16 +147,19 @@ public class AuthServiceImpl implements AuthService {
request.getUkeyProofs().stream().map(UkeyLoginProof::getServerRandom).toList()
);
String authKeyPair = lmkService.exportIkPublicKeyHex();
List<String> matchedSerials = new java.util.ArrayList<>();
List<String> matchedSerials = new ArrayList<>();
for (UkeyLoginProof proof : request.getUkeyProofs()) {
RoleUkeyBindingEntity binding = bindingsByPubKey.get(proof.getPubKey());
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");
}
compatUkeyVerifier.verifyIssuedBinding(buildIssuePayload(request.getRoleCode(), proof, authKeyPair), proof.getIssueSignature());
compatUkeyVerifier.verifyLoginSignature(proof.getPubKey(), proof.getLoginPayload(), proof.getLoginSignature());
matchedSerials.add(binding.getUkeySerial());
}
LoginRequest loginRequest = new LoginRequest();
loginRequest.setRoleCode(request.getRoleCode());
loginRequest.setPassword(request.getPassword());
loginRequest.setRolePassword(request.getRolePassword());
loginRequest.setUkeySerials(matchedSerials);
return login(loginRequest);
}
@@ -164,23 +187,24 @@ public class AuthServiceImpl implements AuthService {
if (session != null) {
response.setAuthLevel(session.getAuthLevel());
}
response.setNeedChangePassword(Boolean.TRUE.equals(roleAccount.getNeedChangePassword()));
response.setNeedChangePassword(needsPasswordChange(roleAccount));
return response;
}
AuthUserEntity entity = authUserRepository.findByUsername(normalized).orElse(null);
CurrentUserResponse response = new CurrentUserResponse();
if (entity == null) {
response.setUsername(normalized);
response.setDisplayName("TMS Administrator");
response.setRole("ADMIN");
response.setNeedChangePassword(Boolean.FALSE);
return response;
}
response.setUsername(entity.getUsername());
response.setDisplayName(entity.getDisplayName());
response.setRole(entity.getRole());
response.setNeedChangePassword(Boolean.FALSE);
return response;
}
@@ -197,34 +221,107 @@ public class AuthServiceImpl implements AuthService {
AuthSessionEntity session = requireActiveSession(sessionToken);
RoleAccountEntity roleAccount = roleAccountRepository.findByRoleCode(session.getRoleCode())
.orElseThrow(() -> new BizException(ErrorCode.UNAUTHORIZED.getCode(), "role account not found"));
validateRoleStatus(roleAccount);
if (!passwordHasher.matches(currentPassword, roleAccount.getPasswordSalt(), roleAccount.getPasswordHash())) {
throw new BizException(ErrorCode.UNAUTHORIZED.getCode(), "current password is incorrect");
onRolePasswordFailed(roleAccount);
}
String newSalt = passwordSaltGenerator.nextSalt();
roleAccount.setPasswordSalt(newSalt);
roleAccount.setPasswordHash(passwordHasher.hash(newPassword, newSalt));
String salt = passwordSaltGenerator.nextSalt();
LocalDateTime current = now();
roleAccount.setPasswordSalt(salt);
roleAccount.setPasswordHash(passwordHasher.hash(newPassword, salt));
roleAccount.setNeedChangePassword(Boolean.FALSE);
roleAccount.setFailedCount(0);
roleAccount.setLockedUntil(null);
roleAccount.setStatus(RoleAccountStatus.ACTIVE.name());
roleAccount.setLastActiveAt(current);
roleAccountRepository.update(roleAccount);
session.setLastActiveAt(current);
session.setExpiresAt(current.plusMinutes(IDLE_TIMEOUT_MINUTES));
authSessionRepository.update(session);
}
private void validateRoleStatus(RoleAccountEntity roleAccount) {
if (RoleAccountStatus.UNENABLED.name().equals(roleAccount.getStatus())) {
throw new BizException(ErrorCode.UNAUTHORIZED.getCode(), "role is not enabled");
}
if (RoleAccountStatus.LOCKED.name().equals(roleAccount.getStatus()) && isStillLocked(roleAccount)) {
if (RoleAccountStatus.LOCKED.name().equals(roleAccount.getStatus())
&& (roleAccount.getLockedUntil() == null || roleAccount.getLockedUntil().isAfter(now()))) {
throw new BizException(ErrorCode.UNAUTHORIZED.getCode(), "role account is locked");
}
}
private boolean isStillLocked(RoleAccountEntity roleAccount) {
return roleAccount.getLockedUntil() == null || roleAccount.getLockedUntil().isAfter(now());
private void validateRolePassword(RoleAccountEntity roleAccount, String rolePassword) {
if (rolePassword == null || rolePassword.isBlank()) {
throw new BizException(ErrorCode.UNAUTHORIZED.getCode(), "role password is required");
}
if (!passwordHasher.matches(rolePassword, roleAccount.getPasswordSalt(), roleAccount.getPasswordHash())) {
onRolePasswordFailed(roleAccount);
}
}
private void onPasswordFailed(RoleAccountEntity roleAccount) {
private List<AuthUserAccountEntity> validateRoleAccounts(
RoleAccountEntity roleAccount,
List<PasswordLoginAccountRequest> accounts
) {
int expectedCount = requiredPasswordAccountCount(roleAccount.getRoleCode());
if (accounts == null || accounts.size() != expectedCount) {
throw new BizException(
ErrorCode.UNAUTHORIZED.getCode(),
roleAccount.getRoleCode() + " requires exactly " + expectedCount + " account passwords"
);
}
Set<String> uniqueUsernames = new LinkedHashSet<>();
List<AuthUserAccountEntity> validatedAccounts = new ArrayList<>();
for (PasswordLoginAccountRequest account : accounts) {
if (!uniqueUsernames.add(account.getUsername())) {
throw new BizException(ErrorCode.UNAUTHORIZED.getCode(), "duplicate account username submitted");
}
AuthUserAccountEntity userAccount = authUserAccountRepository.findByUsername(account.getUsername())
.orElseThrow(() -> new BizException(ErrorCode.UNAUTHORIZED.getCode(), "user account not found"));
validateUserAccountRole(roleAccount.getRoleCode(), userAccount);
validateUserAccountStatus(userAccount);
if (!passwordHasher.matches(account.getPassword(), userAccount.getPasswordSalt(), userAccount.getPasswordHash())) {
onPasswordFailed(userAccount);
}
validatedAccounts.add(userAccount);
}
return validatedAccounts;
}
private void validateUserAccountRole(String expectedRoleCode, AuthUserAccountEntity userAccount) {
if (!expectedRoleCode.equals(userAccount.getRoleCode())) {
throw new BizException(ErrorCode.UNAUTHORIZED.getCode(), "user account does not belong to target role");
}
}
private void validateUserAccountStatus(AuthUserAccountEntity userAccount) {
if (RoleAccountStatus.UNENABLED.name().equals(userAccount.getStatus())) {
throw new BizException(ErrorCode.UNAUTHORIZED.getCode(), "user account is not enabled");
}
if (RoleAccountStatus.LOCKED.name().equals(userAccount.getStatus())
&& (userAccount.getLockedUntil() == null || userAccount.getLockedUntil().isAfter(now()))) {
throw new BizException(ErrorCode.UNAUTHORIZED.getCode(), "user account is locked");
}
}
private void onPasswordFailed(AuthUserAccountEntity userAccount) {
int failedCount = userAccount.getFailedCount() == null ? 0 : userAccount.getFailedCount();
failedCount++;
userAccount.setFailedCount(failedCount);
if (failedCount >= MAX_FAILED_ATTEMPTS) {
userAccount.setStatus(RoleAccountStatus.LOCKED.name());
userAccount.setLockedUntil(now().plusMinutes(IDLE_TIMEOUT_MINUTES));
authUserAccountRepository.update(userAccount);
throw new BizException(ErrorCode.UNAUTHORIZED.getCode(), "user account is locked");
}
authUserAccountRepository.update(userAccount);
throw new BizException(ErrorCode.UNAUTHORIZED.getCode(), "password is incorrect");
}
private void onRolePasswordFailed(RoleAccountEntity roleAccount) {
int failedCount = roleAccount.getFailedCount() == null ? 0 : roleAccount.getFailedCount();
failedCount++;
roleAccount.setFailedCount(failedCount);
@@ -238,7 +335,19 @@ public class AuthServiceImpl implements AuthService {
throw new BizException(ErrorCode.UNAUTHORIZED.getCode(), "password is incorrect");
}
private void resetFailureState(RoleAccountEntity roleAccount) {
private void resetValidatedAccounts(List<AuthUserAccountEntity> validatedAccounts) {
LocalDateTime current = now();
for (AuthUserAccountEntity account : validatedAccounts) {
account.setFailedCount(0);
account.setLockedUntil(null);
account.setStatus(RoleAccountStatus.ACTIVE.name());
account.setLastLoginAt(current);
account.setLastActiveAt(current);
authUserAccountRepository.update(account);
}
}
private void resetRoleFailureState(RoleAccountEntity roleAccount) {
roleAccount.setFailedCount(0);
roleAccount.setLockedUntil(null);
roleAccount.setStatus(RoleAccountStatus.ACTIVE.name());
@@ -247,6 +356,10 @@ public class AuthServiceImpl implements AuthService {
roleAccountRepository.update(roleAccount);
}
private int requiredPasswordAccountCount(String roleCode) {
return RoleCode.valueOf(roleCode).getRequiredUkeyCount();
}
private void ensureMasterKeyReady() {
MasterKeyStatus.StatusDetail status = lmkService.getMasterKeyStatus();
if (status == null || status.getCode() == MasterKeyStatus.ABNORMAL.getCode()) {
@@ -264,18 +377,19 @@ public class AuthServiceImpl implements AuthService {
}
Set<String> uniqueSerials = new LinkedHashSet<>(ukeySerials);
if (uniqueSerials.size() != roleAccount.getRequiredUkeyCount()) {
int requiredUkeyCount = authPolicyService.requiredUkeyCount(RoleCode.valueOf(roleAccount.getRoleCode()));
if (uniqueSerials.size() != requiredUkeyCount) {
throw new BizException(ErrorCode.UNAUTHORIZED.getCode(), "ukey count does not satisfy role requirement");
}
List<RoleUkeyBindingEntity> bindings = roleUkeyBindingRepository.findActiveByRoleCode(roleAccount.getRoleCode());
if (bindings.size() != roleAccount.getRequiredUkeyCount()) {
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(java.util.stream.Collectors.toSet());
.collect(Collectors.toSet());
if (!boundSerials.containsAll(uniqueSerials)) {
throw new BizException(ErrorCode.UNAUTHORIZED.getCode(), "ukey verification failed");
}
@@ -300,7 +414,6 @@ public class AuthServiceImpl implements AuthService {
dto.setPubKey(proof.getPubKey());
dto.setRole(RoleCode.valueOf(roleCode).getCode());
dto.setUid(proof.getUid());
dto.setRid(proof.getRid());
try {
return objectMapper.writeValueAsString(UKeySignEntity.getInstance(dto, authKeyPair));
} catch (JsonProcessingException ex) {
@@ -308,10 +421,16 @@ public class AuthServiceImpl implements AuthService {
}
}
private AuthSessionEntity buildSession(String roleCode, AuthMethod authMethod, AuthLevel authLevel) {
private AuthSessionEntity buildSession(
String roleCode,
AuthMethod authMethod,
AuthLevel authLevel,
List<String> authenticatedUsers
) {
LocalDateTime issuedAt = now();
AuthSessionEntity entity = new AuthSessionEntity();
entity.setRoleCode(roleCode);
entity.setAuthenticatedUsersJson(writeAuthenticatedUsers(authenticatedUsers));
entity.setAuthMethod(authMethod.name());
entity.setAuthLevel(authLevel.name());
entity.setSessionToken(sessionTokenGenerator.nextToken());
@@ -321,10 +440,25 @@ public class AuthServiceImpl implements AuthService {
return entity;
}
private String writeAuthenticatedUsers(List<String> authenticatedUsers) {
if (authenticatedUsers == null) {
return null;
}
try {
return objectMapper.writeValueAsString(authenticatedUsers);
} catch (JsonProcessingException ex) {
throw new IllegalStateException("serialize authenticated users failed", ex);
}
}
private LocalDateTime now() {
return LocalDateTime.ofInstant(clock.instant(), ZoneOffset.UTC);
}
private boolean needsPasswordChange(RoleAccountEntity roleAccount) {
return Boolean.TRUE.equals(roleAccount.getNeedChangePassword());
}
private AuthSessionEntity requireActiveSession(String sessionToken) {
AuthSessionEntity session = authSessionRepository.findBySessionToken(sessionToken)
.orElseThrow(() -> new BizException(ErrorCode.UNAUTHORIZED.getCode(), "session not found"));
@@ -8,6 +8,7 @@ import java.nio.file.Path;
import java.time.Duration;
import java.time.LocalDateTime;
import java.util.function.Supplier;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@Component
@@ -18,6 +19,7 @@ public class RunnerStallDetector {
private final ResourceBackupProperties resourceBackupProperties;
private final Supplier<LocalDateTime> nowSupplier;
@Autowired
public RunnerStallDetector(
ResourceRestoreTaskRepository resourceRestoreTaskRepository,
ResourceRestoreStateFileStore resourceRestoreStateFileStore,
@@ -83,8 +83,8 @@ public class MasterKeyRestoreDTO {
this.iDevPriKeyLen = iDevPriKeyLen;
}
public Map<String,String> getComponents() {
Map<String,String> componentMap = new HashMap<>();
public Map<Integer,String> getComponents() {
Map<Integer,String> componentMap = new HashMap<>();
for(UKeySignVerifyDTO uKeySignVerifyDTO : uKeySignVerifyDTOList){
if(null == uKeySignVerifyDTO.getKeyComponent()){
throw new IllegalArgumentException("UKey无主密钥分量,UID:" + uKeySignVerifyDTO.getuKeySignDTO().getUid());
@@ -9,11 +9,6 @@ public class UKey {
*/
private String uid;
/**
* 角色 ID
*/
private String rid;
/**
* 设备公钥 HEX(32 字节 x + 32 字节 y)
*/
@@ -18,13 +18,8 @@ public class UKeySignDTO {
private String role;
/**
* UKey 中的认证信息
* 角色内固定席位编号。
*/
private String uid;
/**
* UKey 中的认证信息
*/
private String rid;
private Integer uid;
}
@@ -25,11 +25,7 @@ public class UKeySignEntity {
/**
* 登录时传输
*/
private String uid;
/**
* 登录时传输
*/
private String rid;
private Integer uid;
public static UKeySignEntity getInstance(UKeySignDTO uKeySignDTO,String authKeyPair){
return UKeySignEntity.builder()
@@ -37,7 +33,6 @@ public class UKeySignEntity {
.authKeyPair(authKeyPair)
.role(uKeySignDTO.getRole())
.uid(uKeySignDTO.getUid())
.rid(uKeySignDTO.getRid())
.build();
}
}
@@ -260,9 +260,7 @@ CREATE TABLE IF NOT EXISTS tms_role_account (
CREATE TABLE IF NOT EXISTS tms_role_ukey_binding (
id BIGINT PRIMARY KEY,
role_code VARCHAR(64) NOT NULL,
slot_no INT NOT NULL,
uid VARCHAR(64) NULL,
rid VARCHAR(64) NULL,
uid INT NOT NULL,
ukey_serial VARCHAR(128) NOT NULL,
ukey_pubkey TEXT NOT NULL,
issuer_sign TEXT NULL,
@@ -271,14 +269,33 @@ 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_slot (role_code, slot_no),
UNIQUE KEY uk_tms_role_ukey_binding_role_uid (role_code, uid),
KEY idx_tms_role_ukey_binding_role_status (role_code, status)
);
CREATE TABLE IF NOT EXISTS tms_auth_user_account (
id BIGINT PRIMARY KEY,
username VARCHAR(64) NOT NULL,
display_name VARCHAR(128) NOT NULL,
role_code VARCHAR(64) NOT NULL,
password_hash VARCHAR(256) NOT NULL,
password_salt VARCHAR(128) NOT NULL,
status VARCHAR(32) NOT NULL,
failed_count INT NOT NULL DEFAULT 0,
locked_until DATETIME(3) NULL,
last_login_at DATETIME(3) NULL,
last_active_at DATETIME(3) NULL,
create_time DATETIME(3) NOT NULL,
update_time DATETIME(3) NOT NULL,
UNIQUE KEY uk_tms_auth_user_account_username (username),
KEY idx_tms_auth_user_account_role_code (role_code)
);
CREATE TABLE IF NOT EXISTS tms_auth_session (
id BIGINT PRIMARY KEY,
session_token VARCHAR(128) NOT NULL,
role_code VARCHAR(64) NOT NULL,
authenticated_users_json JSON NULL,
auth_method VARCHAR(32) NOT NULL,
auth_level VARCHAR(32) NOT NULL,
issued_at DATETIME(3) NOT NULL,
@@ -378,7 +395,7 @@ VALUES
11001,
'SUPER_ADMIN',
'超级管理员',
3,
2,
'Viuew8WE+qziUZwGGU0x25cUpzsTF+QvQFU0uhb+yVA=',
'init-super-admin-salt-20260325',
'UNENABLED',
@@ -394,7 +411,7 @@ VALUES
11002,
'KEY_ADMIN',
'密钥管理员',
2,
1,
'/xqmDy3A/q9X5p7XWznIPSBabQ6bxTLAPgtHQ6Be33c=',
'init-key-admin-salt-20260325',
'ACTIVE',
@@ -439,6 +456,98 @@ VALUES
CURRENT_TIMESTAMP(3)
);
INSERT IGNORE INTO tms_auth_user_account (
id,
username,
display_name,
role_code,
password_hash,
password_salt,
status,
failed_count,
locked_until,
last_login_at,
last_active_at,
create_time,
update_time
)
VALUES
(
12001,
'super-admin-01',
'超级管理员账号一',
'SUPER_ADMIN',
'Viuew8WE+qziUZwGGU0x25cUpzsTF+QvQFU0uhb+yVA=',
'init-super-admin-salt-20260325',
'ACTIVE',
0,
NULL,
NULL,
NULL,
CURRENT_TIMESTAMP(3),
CURRENT_TIMESTAMP(3)
),
(
12002,
'super-admin-02',
'超级管理员账号二',
'SUPER_ADMIN',
'Viuew8WE+qziUZwGGU0x25cUpzsTF+QvQFU0uhb+yVA=',
'init-super-admin-salt-20260325',
'ACTIVE',
0,
NULL,
NULL,
NULL,
CURRENT_TIMESTAMP(3),
CURRENT_TIMESTAMP(3)
),
(
12003,
'key-admin-01',
'密钥管理员账号',
'KEY_ADMIN',
'/xqmDy3A/q9X5p7XWznIPSBabQ6bxTLAPgtHQ6Be33c=',
'init-key-admin-salt-20260325',
'ACTIVE',
0,
NULL,
NULL,
NULL,
CURRENT_TIMESTAMP(3),
CURRENT_TIMESTAMP(3)
),
(
12004,
'audit-admin-01',
'审计管理员账号',
'AUDIT_ADMIN',
'j/J1LKyUOzVEJfbbHe2dwKBKNdabRk502olk/LWIwhg=',
'init-audit-admin-salt-20260325',
'ACTIVE',
0,
NULL,
NULL,
NULL,
CURRENT_TIMESTAMP(3),
CURRENT_TIMESTAMP(3)
),
(
12005,
'ops-admin-01',
'运维管理员账号',
'OPS_ADMIN',
'2e9F4aCgsDb+AuD1LrBEqEg8yjy3ODo27UHtx06vj/A=',
'init-ops-admin-salt-20260325',
'ACTIVE',
0,
NULL,
NULL,
NULL,
CURRENT_TIMESTAMP(3),
CURRENT_TIMESTAMP(3)
);
-- -----------------------------------------------------------------------------
-- IP白名单表
@@ -8,6 +8,7 @@
<id property="id" column="id"/>
<result property="sessionToken" column="session_token"/>
<result property="roleCode" column="role_code"/>
<result property="authenticatedUsersJson" column="authenticated_users_json"/>
<result property="authMethod" column="auth_method"/>
<result property="authLevel" column="auth_level"/>
<result property="issuedAt" column="issued_at"/>
@@ -22,6 +23,7 @@
SELECT id,
session_token,
role_code,
authenticated_users_json,
auth_method,
auth_level,
issued_at,
@@ -0,0 +1,60 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.cisd.tms.modules.auth.mapper.AuthUserAccountMapper">
<resultMap id="AuthUserAccountResultMap" type="com.cisd.tms.modules.auth.entity.AuthUserAccountEntity">
<id property="id" column="id"/>
<result property="username" column="username"/>
<result property="displayName" column="display_name"/>
<result property="roleCode" column="role_code"/>
<result property="passwordHash" column="password_hash"/>
<result property="passwordSalt" column="password_salt"/>
<result property="status" column="status"/>
<result property="failedCount" column="failed_count"/>
<result property="lockedUntil" column="locked_until"/>
<result property="lastLoginAt" column="last_login_at"/>
<result property="lastActiveAt" column="last_active_at"/>
<result property="createTime" column="create_time"/>
<result property="updateTime" column="update_time"/>
</resultMap>
<select id="selectByUsername" parameterType="string" resultMap="AuthUserAccountResultMap">
SELECT id,
username,
display_name,
role_code,
password_hash,
password_salt,
status,
failed_count,
locked_until,
last_login_at,
last_active_at,
create_time,
update_time
FROM tms_auth_user_account
WHERE username = #{username}
LIMIT 1
</select>
<select id="selectByRoleCode" parameterType="string" resultMap="AuthUserAccountResultMap">
SELECT id,
username,
display_name,
role_code,
password_hash,
password_salt,
status,
failed_count,
locked_until,
last_login_at,
last_active_at,
create_time,
update_time
FROM tms_auth_user_account
WHERE role_code = #{roleCode}
ORDER BY username ASC
</select>
</mapper>
@@ -7,9 +7,7 @@
<resultMap id="RoleUkeyBindingResultMap" type="com.cisd.tms.modules.auth.entity.RoleUkeyBindingEntity">
<id property="id" column="id"/>
<result property="roleCode" column="role_code"/>
<result property="slotNo" column="slot_no"/>
<result property="uid" column="uid"/>
<result property="rid" column="rid"/>
<result property="ukeySerial" column="ukey_serial"/>
<result property="ukeyPubkey" column="ukey_pubkey"/>
<result property="issuerSign" column="issuer_sign"/>
@@ -20,12 +18,10 @@
<result property="updateTime" column="update_time"/>
</resultMap>
<select id="selectActiveByRoleCodeAndSlotNo" resultMap="RoleUkeyBindingResultMap">
<select id="selectActiveByRoleCodeAndUid" resultMap="RoleUkeyBindingResultMap">
SELECT id,
role_code,
slot_no,
uid,
rid,
ukey_serial,
ukey_pubkey,
issuer_sign,
@@ -36,7 +32,7 @@
update_time
FROM tms_role_ukey_binding
WHERE role_code = #{roleCode}
AND slot_no = #{slotNo}
AND uid = #{uid}
AND status = 'ACTIVE'
LIMIT 1
</select>
@@ -44,9 +40,7 @@
<select id="selectActiveByRoleCode" parameterType="string" resultMap="RoleUkeyBindingResultMap">
SELECT id,
role_code,
slot_no,
uid,
rid,
ukey_serial,
ukey_pubkey,
issuer_sign,
@@ -58,6 +52,6 @@
FROM tms_role_ukey_binding
WHERE role_code = #{roleCode}
AND status = 'ACTIVE'
ORDER BY slot_no ASC
ORDER BY uid ASC
</select>
</mapper>
@@ -70,8 +70,8 @@ class TmsApplicationTests {
org.junit.jupiter.api.Assertions.assertTrue(sql.contains("CREATE TABLE IF NOT EXISTS tms_role_ukey_binding"));
org.junit.jupiter.api.Assertions.assertTrue(sql.contains("CREATE TABLE IF NOT EXISTS tms_auth_session"));
org.junit.jupiter.api.Assertions.assertTrue(sql.contains("auth_method VARCHAR(32) NOT NULL"));
org.junit.jupiter.api.Assertions.assertTrue(sql.contains("uid VARCHAR(64) NULL"));
org.junit.jupiter.api.Assertions.assertTrue(sql.contains("rid VARCHAR(64) NULL"));
org.junit.jupiter.api.Assertions.assertTrue(sql.contains("uid INT NOT NULL"));
org.junit.jupiter.api.Assertions.assertFalse(sql.contains("rid VARCHAR(64) NULL"));
org.junit.jupiter.api.Assertions.assertTrue(sql.contains("CREATE TABLE IF NOT EXISTS tms_auth_challenge"));
org.junit.jupiter.api.Assertions.assertTrue(sql.contains("CREATE TABLE IF NOT EXISTS tms_auth_audit_log"));
org.junit.jupiter.api.Assertions.assertTrue(sql.contains("'KEY_ADMIN'"));
@@ -1,6 +1,7 @@
package com.cisd.tms.modules.auth.controller;
import com.cisd.tms.common.exception.GlobalExceptionHandler;
import com.cisd.tms.common.exception.BizException;
import com.cisd.tms.modules.auth.dto.CaptchaResponse;
import com.cisd.tms.modules.auth.dto.CurrentUserResponse;
import com.cisd.tms.modules.auth.dto.LoginResponse;
@@ -45,7 +46,12 @@ class AuthControllerTest {
.content("""
{
"roleCode": "AUDIT_ADMIN",
"password": "12345678",
"accounts": [
{
"username": "audit-admin-01",
"password": "12345678"
}
],
"captchaCode": "ABCD",
"captchaId": "captcha-001"
}
@@ -101,12 +107,11 @@ class AuthControllerTest {
.content("""
{
"roleCode": "KEY_ADMIN",
"password": "12345678",
"rolePassword": "12345678",
"ukeyProofs": [
{
"pubKey": "PUB-1",
"uid": "4",
"rid": "4",
"uid": 1,
"serverRandom": "RB-1",
"issueSignature": "ISSUE-1",
"loginPayload": "LOGIN-DATA-1",
@@ -114,8 +119,7 @@ class AuthControllerTest {
},
{
"pubKey": "PUB-2",
"uid": "5",
"rid": "5",
"uid": 2,
"serverRandom": "RB-2",
"issueSignature": "ISSUE-2",
"loginPayload": "LOGIN-DATA-2",
@@ -158,7 +162,7 @@ class AuthControllerTest {
response.setDisplayName("审计管理员");
response.setRole("AUDIT_ADMIN");
response.setAuthLevel("LIMITED");
response.setNeedChangePassword(false);
response.setNeedChangePassword(true);
Mockito.when(authService.me("AUDIT_ADMIN", "token-me-001")).thenReturn(response);
MockMvc mockMvc = MockMvcBuilders
@@ -171,7 +175,8 @@ class AuthControllerTest {
.requestAttr(InternalApiAuthInterceptor.ATTR_ROLE_CODE, "AUDIT_ADMIN"))
.andExpect(status().isOk())
.andExpect(content().string(containsString("\"role\":\"AUDIT_ADMIN\"")))
.andExpect(content().string(containsString("\"authLevel\":\"LIMITED\"")));
.andExpect(content().string(containsString("\"authLevel\":\"LIMITED\"")))
.andExpect(content().string(containsString("\"needChangePassword\":true")));
}
@Test
@@ -252,18 +257,16 @@ class AuthControllerTest {
.contentType(MediaType.APPLICATION_JSON)
.content("""
{
"slotNo": 1,
"uid": 1,
"ukeySerial": "UK-NEW",
"pubKey": "PUB-NEW",
"uid": "1",
"rid": "RID-001",
"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", "1", "RID-001", "SIG-NEW");
Mockito.verify(authAdminService).bindIssuedUkey("KEY_ADMIN", "FULL", "SUPER_ADMIN", 1, "UK-NEW", "PUB-NEW", "SIG-NEW");
}
@Test
@@ -293,8 +296,7 @@ class AuthControllerTest {
{
"pubKey": "PUB-NEW",
"role": "SUPER_ADMIN",
"uid": "1",
"rid": "RID-001",
"uid": 1,
"extra": "EXTRA-001"
}
"""))
@@ -1,11 +1,12 @@
package com.cisd.tms.modules.auth.service;
import com.cisd.tms.common.exception.BizException;
import com.cisd.tms.modules.auth.entity.AuthUserAccountEntity;
import com.cisd.tms.modules.auth.entity.RoleAccountEntity;
import com.cisd.tms.modules.auth.entity.RoleUkeyBindingEntity;
import com.cisd.tms.modules.auth.enums.AuthLevel;
import com.cisd.tms.modules.auth.enums.RoleAccountStatus;
import com.cisd.tms.modules.auth.enums.RoleCode;
import com.cisd.tms.modules.auth.repository.AuthUserAccountRepository;
import com.cisd.tms.modules.auth.repository.RoleAccountRepository;
import com.cisd.tms.modules.auth.repository.RoleUkeyBindingRepository;
import com.cisd.tms.modules.auth.service.impl.AuthAdminServiceImpl;
@@ -35,10 +36,11 @@ class AuthAdminServiceTest {
@Test
void shouldEnableRoleWhenOperatorIsKeyAdminWithFullSession() {
InMemoryRoleAccountRepository roleAccounts = new InMemoryRoleAccountRepository();
roleAccounts.save(role(RoleCode.AUDIT_ADMIN, RoleAccountStatus.UNENABLED, "OLD-HASH", "OLD-SALT", true));
roleAccounts.save(role(RoleCode.AUDIT_ADMIN, RoleAccountStatus.UNENABLED));
AuthAdminService service = newAuthAdminService(
roleAccounts,
new InMemoryAuthUserAccountRepository(),
new InMemoryRoleUkeyBindingRepository(),
FIXED_CLOCK,
new FixedSaltSupplier("salt-001")
@@ -48,61 +50,58 @@ class AuthAdminServiceTest {
RoleAccountEntity updated = roleAccounts.findByRoleCode(RoleCode.AUDIT_ADMIN.getCode()).orElseThrow();
Assertions.assertEquals(RoleAccountStatus.ACTIVE.name(), updated.getStatus());
Assertions.assertTrue(updated.getNeedChangePassword());
}
@Test
void shouldRejectEnableRoleWhenOperatorIsNotKeyAdminFull() {
AuthAdminService service = newAuthAdminService(
new InMemoryRoleAccountRepository(),
new InMemoryRoleUkeyBindingRepository(),
FIXED_CLOCK,
new FixedSaltSupplier("salt-001")
);
BizException exception = Assertions.assertThrows(
BizException.class,
() -> service.enableRole(RoleCode.KEY_ADMIN.getCode(), AuthLevel.LIMITED.name(), RoleCode.AUDIT_ADMIN.getCode())
);
Assertions.assertEquals("operation requires KEY_ADMIN FULL session", exception.getMessage());
}
@Test
void shouldResetPasswordUnlockRoleAndRequirePasswordChange() {
void shouldResetPasswordForAllAccountsUnderRole() {
InMemoryRoleAccountRepository roleAccounts = new InMemoryRoleAccountRepository();
RoleAccountEntity target = role(RoleCode.OPS_ADMIN, RoleAccountStatus.LOCKED, "OLD-HASH", "OLD-SALT", false);
target.setFailedCount(5);
target.setLockedUntil(LocalDateTime.of(2026, 3, 23, 3, 30));
roleAccounts.save(target);
InMemoryAuthUserAccountRepository userAccounts = new InMemoryAuthUserAccountRepository();
roleAccounts.save(role(RoleCode.SUPER_ADMIN, RoleAccountStatus.ACTIVE, "OLD-ROLE-HASH", "OLD-ROLE-SALT"));
userAccounts.save(user("super-admin-01", RoleCode.SUPER_ADMIN, "OLD-HASH-1", "OLD-SALT-1"));
userAccounts.save(user("super-admin-02", RoleCode.SUPER_ADMIN, "OLD-HASH-2", "OLD-SALT-2"));
userAccounts.findByUsername("super-admin-01").orElseThrow().setFailedCount(3);
userAccounts.findByUsername("super-admin-01").orElseThrow().setLockedUntil(LocalDateTime.of(2026, 3, 23, 3, 30));
userAccounts.findByUsername("super-admin-02").orElseThrow().setFailedCount(5);
userAccounts.findByUsername("super-admin-02").orElseThrow().setLockedUntil(LocalDateTime.of(2026, 3, 23, 3, 40));
AuthAdminService service = newAuthAdminService(
roleAccounts,
userAccounts,
new InMemoryRoleUkeyBindingRepository(),
FIXED_CLOCK,
new FixedSaltSupplier("salt-002")
new FixedSaltSupplier("salt-role-001", "salt-002", "salt-003")
);
service.resetPassword(RoleCode.KEY_ADMIN.getCode(), AuthLevel.FULL.name(), RoleCode.OPS_ADMIN.getCode());
service.resetPassword(RoleCode.KEY_ADMIN.getCode(), AuthLevel.FULL.name(), RoleCode.SUPER_ADMIN.getCode());
RoleAccountEntity updated = roleAccounts.findByRoleCode(RoleCode.OPS_ADMIN.getCode()).orElseThrow();
Assertions.assertEquals(RoleAccountStatus.ACTIVE.name(), updated.getStatus());
Assertions.assertEquals(0, updated.getFailedCount());
Assertions.assertNull(updated.getLockedUntil());
Assertions.assertEquals("salt-002", updated.getPasswordSalt());
Assertions.assertEquals("HASH:12345678:salt-002", updated.getPasswordHash());
Assertions.assertTrue(updated.getNeedChangePassword());
RoleAccountEntity role = roleAccounts.findByRoleCode(RoleCode.SUPER_ADMIN.getCode()).orElseThrow();
AuthUserAccountEntity first = userAccounts.findByUsername("super-admin-01").orElseThrow();
AuthUserAccountEntity second = userAccounts.findByUsername("super-admin-02").orElseThrow();
Assertions.assertEquals(RoleAccountStatus.ACTIVE.name(), role.getStatus());
Assertions.assertEquals("salt-role-001", role.getPasswordSalt());
Assertions.assertEquals("HASH:12345678:salt-role-001", role.getPasswordHash());
Assertions.assertEquals(RoleAccountStatus.ACTIVE.name(), first.getStatus());
Assertions.assertEquals(RoleAccountStatus.ACTIVE.name(), second.getStatus());
Assertions.assertEquals(0, first.getFailedCount());
Assertions.assertEquals(0, second.getFailedCount());
Assertions.assertNull(first.getLockedUntil());
Assertions.assertNull(second.getLockedUntil());
Assertions.assertEquals("salt-002", first.getPasswordSalt());
Assertions.assertEquals("HASH:12345678:salt-002", first.getPasswordHash());
Assertions.assertEquals("salt-003", second.getPasswordSalt());
Assertions.assertEquals("HASH:12345678:salt-003", second.getPasswordHash());
}
@Test
void shouldReplaceActiveBindingInSameSlot() {
void shouldReplaceActiveBindingInSameUidSeat() {
InMemoryRoleUkeyBindingRepository bindings = new InMemoryRoleUkeyBindingRepository();
bindings.save(binding(RoleCode.SUPER_ADMIN, 1, "UK-OLD", "PUB-OLD", "SIG-OLD"));
LmkService lmkService = org.mockito.Mockito.mock(LmkService.class);
org.mockito.Mockito.when(lmkService.exportIkPublicKeyHex()).thenReturn("IK-PUB-001");
LmkService lmkService = Mockito.mock(LmkService.class);
Mockito.when(lmkService.exportIkPublicKeyHex()).thenReturn("IK-PUB-001");
AuthAdminService service = new AuthAdminServiceImpl(
new InMemoryRoleAccountRepository(),
new InMemoryAuthUserAccountRepository(),
bindings,
new FakePasswordHasher(),
lmkService,
@@ -118,53 +117,12 @@ class AuthAdminServiceTest {
1,
"UK-NEW",
"PUB-NEW",
"1",
"RID-001",
"SIG-NEW"
);
RoleUkeyBindingEntity updated = bindings.findActiveByRoleCodeAndSlotNo(RoleCode.SUPER_ADMIN.getCode(), 1).orElseThrow();
RoleUkeyBindingEntity updated = bindings.findActiveByRoleCodeAndUid(RoleCode.SUPER_ADMIN.getCode(), 1).orElseThrow();
Assertions.assertEquals("UK-NEW", updated.getUkeySerial());
Assertions.assertEquals("PUB-NEW", updated.getUkeyPubkey());
Assertions.assertEquals("1", updated.getUid());
Assertions.assertEquals("RID-001", updated.getRid());
Assertions.assertEquals("SIG-NEW", updated.getIssuerSign());
Assertions.assertEquals(LocalDateTime.of(2026, 3, 23, 3, 0), updated.getBoundAt());
org.mockito.Mockito.verify(lmkService).verifyIk(
"{\"pubKey\":\"PUB-NEW\",\"authKeyPair\":\"IK-PUB-001\",\"role\":\"SUPER_ADMIN\",\"uid\":\"1\",\"rid\":\"RID-001\"}",
"SIG-NEW"
);
}
@Test
void shouldRejectBindingWhenSlotExceedsRoleRequirement() {
LmkService lmkService = org.mockito.Mockito.mock(LmkService.class);
AuthAdminService service = new AuthAdminServiceImpl(
new InMemoryRoleAccountRepository(),
new InMemoryRoleUkeyBindingRepository(),
new FakePasswordHasher(),
lmkService,
new com.fasterxml.jackson.databind.ObjectMapper(),
FIXED_CLOCK,
new FixedSaltSupplier("salt-001")
);
BizException exception = Assertions.assertThrows(
BizException.class,
() -> service.bindIssuedUkey(
RoleCode.KEY_ADMIN.getCode(),
AuthLevel.FULL.name(),
RoleCode.AUDIT_ADMIN.getCode(),
2,
"UK-001",
"PUB-001",
"1",
"RID-001",
"SIG-001"
)
);
Assertions.assertEquals("slotNo exceeds role ukey requirement", exception.getMessage());
}
@Test
@@ -178,6 +136,7 @@ class AuthAdminServiceTest {
AuthAdminService service = new AuthAdminServiceImpl(
new InMemoryRoleAccountRepository(),
new InMemoryAuthUserAccountRepository(),
new InMemoryRoleUkeyBindingRepository(),
new FakePasswordHasher(),
lmkService,
@@ -187,80 +146,57 @@ class AuthAdminServiceTest {
);
UKeySignDTO dto = new UKeySignDTO();
dto.setPubKey("PUB-001");
dto.setRole("ignored");
dto.setUid("1");
dto.setRid("RID-001");
dto.setUid(1);
UKeySignResult result = service.issueUkeyBindingSign(RoleCode.KEY_ADMIN.getCode(), AuthLevel.FULL.name(), RoleCode.SUPER_ADMIN.getCode(), dto);
Assertions.assertEquals("ISSUE-SIGN-001", result.getSign());
Assertions.assertNotNull(result.getBackupPacket());
Assertions.assertEquals(1, result.getBackupPacket().getPacketIndex());
Mockito.verify(lmkService).signIk(
"{\"pubKey\":\"PUB-001\",\"authKeyPair\":\"IK-PUB-001\",\"role\":\"SUPER_ADMIN\",\"uid\":\"1\",\"rid\":\"RID-001\"}"
);
Mockito.verify(lmkService).buildBackupPacket(1);
}
@Test
void shouldIssueBindingSignAndPreserveExtraForNonSuperAdmin() {
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-002");
AuthAdminService service = new AuthAdminServiceImpl(
new InMemoryRoleAccountRepository(),
new InMemoryRoleUkeyBindingRepository(),
new FakePasswordHasher(),
lmkService,
new com.fasterxml.jackson.databind.ObjectMapper(),
FIXED_CLOCK,
new FixedSaltSupplier("salt-001")
);
UKeySignDTO dto = new UKeySignDTO();
dto.setPubKey("PUB-002");
dto.setRole("ignored");
dto.setUid("1");
dto.setRid("RID-002");
// dto.setExtra("EXTRA-KEEP");
UKeySignResult result = service.issueUkeyBindingSign(RoleCode.KEY_ADMIN.getCode(), AuthLevel.FULL.name(), RoleCode.AUDIT_ADMIN.getCode(), dto);
Assertions.assertEquals("ISSUE-SIGN-002", result.getSign());
Assertions.assertNull(result.getBackupPacket());
}
private static RoleAccountEntity role(
RoleCode roleCode,
RoleAccountStatus status,
String passwordHash,
String passwordSalt,
boolean needChangePassword
) {
private static RoleAccountEntity role(RoleCode roleCode, RoleAccountStatus status) {
RoleAccountEntity entity = new RoleAccountEntity();
entity.setId((long) roleCode.ordinal() + 100);
entity.setRoleCode(roleCode.getCode());
entity.setDisplayName(roleCode.getDisplayName());
entity.setRequiredUkeyCount(roleCode.getRequiredUkeyCount());
entity.setStatus(status.name());
return entity;
}
private static RoleAccountEntity role(RoleCode roleCode, RoleAccountStatus status, String passwordHash, String passwordSalt) {
RoleAccountEntity entity = role(roleCode, status);
entity.setPasswordHash(passwordHash);
entity.setPasswordSalt(passwordSalt);
entity.setStatus(status.name());
entity.setNeedChangePassword(needChangePassword);
entity.setFailedCount(0);
return entity;
}
private static AuthUserAccountEntity user(String username, RoleCode roleCode, String passwordHash, String passwordSalt) {
AuthUserAccountEntity entity = new AuthUserAccountEntity();
entity.setId((long) (username.hashCode() & Integer.MAX_VALUE));
entity.setUsername(username);
entity.setDisplayName(username);
entity.setRoleCode(roleCode.getCode());
entity.setPasswordHash(passwordHash);
entity.setPasswordSalt(passwordSalt);
entity.setStatus(RoleAccountStatus.ACTIVE.name());
entity.setFailedCount(0);
return entity;
}
private static RoleUkeyBindingEntity binding(
RoleCode roleCode,
int slotNo,
int uid,
String serial,
String pubkey,
String issuerSign
) {
RoleUkeyBindingEntity entity = new RoleUkeyBindingEntity();
entity.setId((long) slotNo);
entity.setId((long) uid);
entity.setRoleCode(roleCode.getCode());
entity.setSlotNo(slotNo);
entity.setUid(uid);
entity.setUkeySerial(serial);
entity.setUkeyPubkey(pubkey);
entity.setIssuerSign(issuerSign);
@@ -314,13 +250,40 @@ class AuthAdminServiceTest {
}
}
private static class InMemoryAuthUserAccountRepository implements AuthUserAccountRepository {
private final Map<String, AuthUserAccountEntity> store = new ConcurrentHashMap<>();
@Override
public Optional<AuthUserAccountEntity> findByUsername(String username) {
return Optional.ofNullable(store.get(username));
}
@Override
public List<AuthUserAccountEntity> findByRoleCode(String roleCode) {
return store.values().stream()
.filter(entity -> roleCode.equals(entity.getRoleCode()))
.sorted(Comparator.comparing(AuthUserAccountEntity::getUsername))
.toList();
}
@Override
public void save(AuthUserAccountEntity entity) {
store.put(entity.getUsername(), entity);
}
@Override
public void update(AuthUserAccountEntity entity) {
store.put(entity.getUsername(), entity);
}
}
private static class InMemoryRoleUkeyBindingRepository implements RoleUkeyBindingRepository {
private final Map<String, List<RoleUkeyBindingEntity>> store = new ConcurrentHashMap<>();
@Override
public Optional<RoleUkeyBindingEntity> findActiveByRoleCodeAndSlotNo(String roleCode, Integer slotNo) {
public Optional<RoleUkeyBindingEntity> findActiveByRoleCodeAndUid(String roleCode, Integer uid) {
return store.getOrDefault(roleCode, List.of()).stream()
.filter(entity -> slotNo.equals(entity.getSlotNo()))
.filter(entity -> uid.equals(entity.getUid()))
.filter(entity -> "ACTIVE".equals(entity.getStatus()))
.findFirst();
}
@@ -329,7 +292,7 @@ class AuthAdminServiceTest {
public List<RoleUkeyBindingEntity> findActiveByRoleCode(String roleCode) {
return store.getOrDefault(roleCode, List.of()).stream()
.filter(entity -> "ACTIVE".equals(entity.getStatus()))
.sorted(Comparator.comparing(RoleUkeyBindingEntity::getSlotNo))
.sorted(Comparator.comparing(RoleUkeyBindingEntity::getUid))
.toList();
}
@@ -348,15 +311,17 @@ class AuthAdminServiceTest {
private static AuthAdminService newAuthAdminService(
InMemoryRoleAccountRepository roleAccounts,
InMemoryAuthUserAccountRepository userAccounts,
InMemoryRoleUkeyBindingRepository bindings,
Clock clock,
PasswordSaltGenerator saltGenerator
) {
return new AuthAdminServiceImpl(
roleAccounts,
userAccounts,
bindings,
new FakePasswordHasher(),
org.mockito.Mockito.mock(LmkService.class),
Mockito.mock(LmkService.class),
new com.fasterxml.jackson.databind.ObjectMapper(),
clock,
saltGenerator
@@ -2,8 +2,10 @@ package com.cisd.tms.modules.auth.service;
import com.cisd.tms.common.exception.BizException;
import com.cisd.tms.modules.auth.dto.CaptchaResponse;
import com.cisd.tms.modules.auth.dto.CurrentUserResponse;
import com.cisd.tms.modules.auth.dto.LoginRequest;
import com.cisd.tms.modules.auth.dto.LoginResponse;
import com.cisd.tms.modules.auth.dto.PasswordLoginAccountRequest;
import com.cisd.tms.modules.auth.dto.PasswordLoginRequest;
import com.cisd.tms.modules.auth.dto.UkeyLoginProof;
import com.cisd.tms.modules.auth.dto.UkeyLoginRandomRequest;
@@ -11,20 +13,23 @@ import com.cisd.tms.modules.auth.dto.UkeyLoginRandomResponse;
import com.cisd.tms.modules.auth.dto.UkeyLoginRequest;
import com.cisd.tms.modules.auth.entity.AuthSessionEntity;
import com.cisd.tms.modules.auth.entity.AuthUserEntity;
import com.cisd.tms.modules.auth.entity.AuthUserAccountEntity;
import com.cisd.tms.modules.auth.entity.RoleAccountEntity;
import com.cisd.tms.modules.auth.entity.RoleUkeyBindingEntity;
import com.cisd.tms.modules.auth.enums.AuthLevel;
import com.cisd.tms.modules.auth.enums.AuthMethod;
import com.cisd.tms.modules.auth.enums.RoleAccountStatus;
import com.cisd.tms.modules.auth.enums.RoleCode;
import com.cisd.tms.modules.auth.repository.AuthUserRepository;
import com.cisd.tms.modules.auth.repository.AuthSessionRepository;
import com.cisd.tms.modules.auth.repository.AuthUserAccountRepository;
import com.cisd.tms.modules.auth.repository.AuthUserRepository;
import com.cisd.tms.modules.auth.repository.RoleAccountRepository;
import com.cisd.tms.modules.auth.repository.RoleUkeyBindingRepository;
import com.cisd.tms.modules.auth.service.impl.AuthPolicyServiceImpl;
import com.cisd.tms.modules.auth.service.impl.AuthServiceImpl;
import com.cisd.tms.modules.mk.enums.MasterKeyStatus;
import com.cisd.tms.modules.mk.service.LmkService;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.time.Clock;
import java.time.Instant;
import java.time.LocalDateTime;
@@ -35,7 +40,6 @@ import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
@@ -44,177 +48,134 @@ class AuthServiceTest {
private static final Clock FIXED_CLOCK = Clock.fixed(Instant.parse("2026-03-23T02:00:00Z"), ZoneOffset.UTC);
@Test
void shouldCreateLimitedSessionForPasswordOnlyLogin() {
void shouldCreateLimitedSessionForSingleAccountRoleLogin() {
InMemoryRoleAccountRepository roleAccounts = new InMemoryRoleAccountRepository();
InMemoryAuthUserAccountRepository userAccounts = new InMemoryAuthUserAccountRepository();
InMemoryAuthSessionRepository sessions = new InMemoryAuthSessionRepository();
InMemoryRoleUkeyBindingRepository ukeyBindings = new InMemoryRoleUkeyBindingRepository();
roleAccounts.save(activeRole(RoleCode.AUDIT_ADMIN, "HASH:12345678:SALT-A", "SALT-A", 0, true));
roleAccounts.save(activeRole(RoleCode.AUDIT_ADMIN, true));
userAccounts.save(activeUserAccount("audit-admin-01", RoleCode.AUDIT_ADMIN, "12345678", "SALT-A", 0, null));
AuthService service = newAuthService(
roleAccounts,
userAccounts,
sessions,
ukeyBindings,
new InMemoryRoleUkeyBindingRepository(),
FIXED_CLOCK,
() -> "token-limited-001"
);
LoginRequest request = new LoginRequest();
request.setRoleCode(RoleCode.AUDIT_ADMIN.getCode());
request.setPassword("12345678");
request.setAccounts(List.of(account("audit-admin-01", "12345678")));
LoginResponse response = service.login(request);
Assertions.assertEquals(RoleCode.AUDIT_ADMIN.getCode(), response.getRoleCode());
Assertions.assertEquals(AuthLevel.LIMITED.name(), response.getAuthLevel());
Assertions.assertEquals("token-limited-001", response.getToken());
Assertions.assertEquals("2026-03-23T02:10Z", response.getExpiresAt());
Assertions.assertTrue(response.getNeedChangePassword());
Assertions.assertEquals(1, sessions.count());
AuthSessionEntity session = sessions.findBySessionToken("token-limited-001").orElseThrow();
Assertions.assertEquals(AuthLevel.LIMITED.name(), session.getAuthLevel());
Assertions.assertEquals(AuthMethod.PASSWORD.name(), session.getAuthMethod());
Assertions.assertEquals(LocalDateTime.of(2026, 3, 23, 2, 0), session.getIssuedAt());
Assertions.assertEquals(LocalDateTime.of(2026, 3, 23, 2, 10), session.getExpiresAt());
Assertions.assertEquals("[\"audit-admin-01\"]", session.getAuthenticatedUsersJson());
}
@Test
void shouldCreateFullSessionWhenEnoughUkeysAreProvided() {
void shouldRequireTwoAccountsForSuperAdminPasswordLogin() {
InMemoryRoleAccountRepository roleAccounts = new InMemoryRoleAccountRepository();
InMemoryAuthSessionRepository sessions = new InMemoryAuthSessionRepository();
InMemoryRoleUkeyBindingRepository ukeyBindings = new InMemoryRoleUkeyBindingRepository();
roleAccounts.save(activeRole(RoleCode.KEY_ADMIN, "HASH:12345678:SALT-K", "SALT-K", 0, false));
ukeyBindings.save(activeBinding(RoleCode.KEY_ADMIN, 1, "UK-1"));
AuthService service = newAuthService(
roleAccounts,
sessions,
ukeyBindings,
FIXED_CLOCK,
() -> "token-full-001"
);
LoginRequest request = new LoginRequest();
request.setRoleCode(RoleCode.KEY_ADMIN.getCode());
request.setPassword("12345678");
request.setUkeySerials(List.of("UK-1"));
LoginResponse response = service.login(request);
Assertions.assertEquals(AuthLevel.FULL.name(), response.getAuthLevel());
Assertions.assertEquals("token-full-001", response.getToken());
Assertions.assertFalse(response.getNeedChangePassword());
AuthSessionEntity session = sessions.findBySessionToken("token-full-001").orElseThrow();
Assertions.assertEquals(AuthMethod.UKEY.name(), session.getAuthMethod());
}
@Test
void shouldRejectLoginWhenRoleIsUnenabled() {
InMemoryRoleAccountRepository roleAccounts = new InMemoryRoleAccountRepository();
roleAccounts.save(role(RoleCode.OPS_ADMIN, RoleAccountStatus.UNENABLED, "HASH:12345678:SALT-O", "SALT-O", 0, true, null));
InMemoryAuthUserAccountRepository userAccounts = new InMemoryAuthUserAccountRepository();
roleAccounts.save(activeRole(RoleCode.SUPER_ADMIN));
userAccounts.save(activeUserAccount("super-admin-01", RoleCode.SUPER_ADMIN, "11111111", "SALT-S1", 0, null));
userAccounts.save(activeUserAccount("super-admin-02", RoleCode.SUPER_ADMIN, "22222222", "SALT-S2", 0, null));
AuthService service = newAuthService(
roleAccounts,
userAccounts,
new InMemoryAuthSessionRepository(),
new InMemoryRoleUkeyBindingRepository(),
FIXED_CLOCK,
() -> "unused"
);
LoginRequest request = new LoginRequest();
request.setRoleCode(RoleCode.OPS_ADMIN.getCode());
request.setPassword("12345678");
BizException exception = Assertions.assertThrows(BizException.class, () -> service.login(request));
Assertions.assertEquals("role is not enabled", exception.getMessage());
}
@Test
void shouldIncrementFailedCountAndLockRoleAfterFiveWrongPasswords() {
InMemoryRoleAccountRepository roleAccounts = new InMemoryRoleAccountRepository();
RoleAccountEntity role = activeRole(RoleCode.AUDIT_ADMIN, "HASH:12345678:SALT-A", "SALT-A", 4, false);
roleAccounts.save(role);
AuthService service = newAuthService(
roleAccounts,
new InMemoryAuthSessionRepository(),
new InMemoryRoleUkeyBindingRepository(),
FIXED_CLOCK,
() -> "unused"
);
LoginRequest request = new LoginRequest();
request.setRoleCode(RoleCode.AUDIT_ADMIN.getCode());
request.setPassword("wrong-password");
BizException exception = Assertions.assertThrows(BizException.class, () -> service.login(request));
Assertions.assertEquals("role account is locked", exception.getMessage());
RoleAccountEntity updated = roleAccounts.findByRoleCode(RoleCode.AUDIT_ADMIN.getCode()).orElseThrow();
Assertions.assertEquals(RoleAccountStatus.LOCKED.name(), updated.getStatus());
Assertions.assertEquals(5, updated.getFailedCount());
Assertions.assertEquals(LocalDateTime.of(2026, 3, 23, 2, 10), updated.getLockedUntil());
}
@Test
void shouldRejectFullLoginWhenUkeyCountDoesNotMeetRoleRequirement() {
InMemoryRoleAccountRepository roleAccounts = new InMemoryRoleAccountRepository();
InMemoryRoleUkeyBindingRepository ukeyBindings = new InMemoryRoleUkeyBindingRepository();
roleAccounts.save(activeRole(RoleCode.SUPER_ADMIN, "HASH:12345678:SALT-S", "SALT-S", 0, false));
ukeyBindings.save(activeBinding(RoleCode.SUPER_ADMIN, 1, "UK-1"));
ukeyBindings.save(activeBinding(RoleCode.SUPER_ADMIN, 2, "UK-2"));
AuthService service = newAuthService(
roleAccounts,
new InMemoryAuthSessionRepository(),
ukeyBindings,
FIXED_CLOCK,
() -> "unused"
);
LoginRequest request = new LoginRequest();
request.setRoleCode(RoleCode.SUPER_ADMIN.getCode());
request.setPassword("12345678");
request.setUkeySerials(List.of("UK-1"));
request.setAccounts(List.of(account("super-admin-01", "11111111")));
BizException exception = Assertions.assertThrows(BizException.class, () -> service.login(request));
Assertions.assertEquals("ukey count does not satisfy role requirement", exception.getMessage());
Assertions.assertEquals("SUPER_ADMIN requires exactly 2 account passwords", exception.getMessage());
}
@Test
void shouldReturnCurrentFixedRoleInfoForMe() {
void shouldLockOnlySpecificUserAccountAfterFiveWrongPasswords() {
InMemoryRoleAccountRepository roleAccounts = new InMemoryRoleAccountRepository();
InMemoryAuthUserAccountRepository userAccounts = new InMemoryAuthUserAccountRepository();
roleAccounts.save(activeRole(RoleCode.SUPER_ADMIN));
userAccounts.save(activeUserAccount("super-admin-01", RoleCode.SUPER_ADMIN, "11111111", "SALT-S1", 4, null));
userAccounts.save(activeUserAccount("super-admin-02", RoleCode.SUPER_ADMIN, "22222222", "SALT-S2", 0, null));
AuthService service = newAuthService(
roleAccounts,
userAccounts,
new InMemoryAuthSessionRepository(),
new InMemoryRoleUkeyBindingRepository(),
FIXED_CLOCK,
() -> "unused"
);
LoginRequest request = new LoginRequest();
request.setRoleCode(RoleCode.SUPER_ADMIN.getCode());
request.setAccounts(List.of(
account("super-admin-01", "bad-password"),
account("super-admin-02", "22222222")
));
BizException exception = Assertions.assertThrows(BizException.class, () -> service.login(request));
Assertions.assertEquals("user account is locked", exception.getMessage());
AuthUserAccountEntity locked = userAccounts.findByUsername("super-admin-01").orElseThrow();
AuthUserAccountEntity untouched = userAccounts.findByUsername("super-admin-02").orElseThrow();
Assertions.assertEquals(RoleAccountStatus.LOCKED.name(), locked.getStatus());
Assertions.assertEquals(5, locked.getFailedCount());
Assertions.assertEquals(LocalDateTime.of(2026, 3, 23, 2, 10), locked.getLockedUntil());
Assertions.assertEquals(0, untouched.getFailedCount());
Assertions.assertEquals(RoleAccountStatus.ACTIVE.name(), untouched.getStatus());
}
@Test
void shouldKeepMeEndpointRoleScoped() {
InMemoryRoleAccountRepository roleAccounts = new InMemoryRoleAccountRepository();
InMemoryAuthSessionRepository sessions = new InMemoryAuthSessionRepository();
roleAccounts.save(activeRole(RoleCode.AUDIT_ADMIN, "HASH:12345678:SALT-A", "SALT-A", 0, false));
roleAccounts.save(activeRole(RoleCode.AUDIT_ADMIN, true));
sessions.save(session("token-me-001", RoleCode.AUDIT_ADMIN.getCode(), AuthLevel.LIMITED.name()));
AuthService service = newAuthService(
roleAccounts,
new InMemoryAuthUserAccountRepository(),
sessions,
new InMemoryRoleUkeyBindingRepository(),
FIXED_CLOCK,
() -> "unused"
);
var response = service.me(RoleCode.AUDIT_ADMIN.getCode(), "token-me-001");
CurrentUserResponse response = service.me(RoleCode.AUDIT_ADMIN.getCode(), "token-me-001");
Assertions.assertEquals(RoleCode.AUDIT_ADMIN.getCode(), response.getUsername());
Assertions.assertEquals(RoleCode.AUDIT_ADMIN.getDisplayName(), response.getDisplayName());
Assertions.assertEquals(RoleCode.AUDIT_ADMIN.getCode(), response.getRole());
Assertions.assertEquals(AuthLevel.LIMITED.name(), response.getAuthLevel());
Assertions.assertFalse(response.getNeedChangePassword());
Assertions.assertTrue(response.getNeedChangePassword());
}
@Test
void shouldChangePasswordAndClearNeedChangePassword() {
void shouldChangeCurrentRolePasswordForActiveSession() {
InMemoryRoleAccountRepository roleAccounts = new InMemoryRoleAccountRepository();
InMemoryAuthSessionRepository sessions = new InMemoryAuthSessionRepository();
roleAccounts.save(activeRole(RoleCode.KEY_ADMIN, "HASH:12345678:SALT-K", "SALT-K", 0, true));
roleAccounts.save(activeRole(RoleCode.KEY_ADMIN, "12345678", "ROLE-SALT-K", 0, null, true));
sessions.save(session("token-change-001", RoleCode.KEY_ADMIN.getCode(), AuthLevel.FULL.name()));
AuthService service = newAuthService(
roleAccounts,
new InMemoryAuthUserAccountRepository(),
sessions,
new InMemoryRoleUkeyBindingRepository(),
FIXED_CLOCK,
@@ -223,106 +184,21 @@ class AuthServiceTest {
service.changePassword("token-change-001", "12345678", "87654321");
RoleAccountEntity updated = roleAccounts.findByRoleCode(RoleCode.KEY_ADMIN.getCode()).orElseThrow();
Assertions.assertFalse(updated.getNeedChangePassword());
Assertions.assertNotEquals("SALT-K", updated.getPasswordSalt());
Assertions.assertEquals("HASH:87654321:" + updated.getPasswordSalt(), updated.getPasswordHash());
}
@Test
void shouldLogoutSession() {
InMemoryRoleAccountRepository roleAccounts = new InMemoryRoleAccountRepository();
InMemoryAuthSessionRepository sessions = new InMemoryAuthSessionRepository();
roleAccounts.save(activeRole(RoleCode.OPS_ADMIN, "HASH:12345678:SALT-O", "SALT-O", 0, false));
sessions.save(session("token-logout-001", RoleCode.OPS_ADMIN.getCode(), AuthLevel.LIMITED.name()));
AuthService service = newAuthService(
roleAccounts,
sessions,
new InMemoryRoleUkeyBindingRepository(),
FIXED_CLOCK,
() -> "unused"
);
service.logout("token-logout-001");
AuthSessionEntity session = sessions.findBySessionToken("token-logout-001").orElseThrow();
Assertions.assertEquals(LocalDateTime.of(2026, 3, 23, 2, 0), session.getLogoutAt());
Assertions.assertEquals(LocalDateTime.of(2026, 3, 23, 2, 0), session.getExpiresAt());
}
@Test
void shouldIssueRoleScopedRandomsForUkeyLogin() {
UkeyLoginRandomService randomService = org.mockito.Mockito.mock(UkeyLoginRandomService.class);
org.mockito.Mockito.when(randomService.issue(RoleCode.SUPER_ADMIN.getCode(), 2)).thenReturn(List.of("RB-1", "RB-2"));
AuthService service = new AuthServiceImpl(
new InMemoryRoleAccountRepository(),
new InMemoryLegacyAuthUserRepository(),
new InMemoryAuthSessionRepository(),
new InMemoryRoleUkeyBindingRepository(),
new FakePasswordHasher(),
org.mockito.Mockito.mock(LmkService.class),
randomService,
org.mockito.Mockito.mock(CompatUkeyVerifier.class),
new InMemoryCaptchaService(),
new ObjectMapper(),
FIXED_CLOCK,
() -> "unused",
() -> "salt-unused",
new AuthPolicyServiceImpl()
);
UkeyLoginRandomRequest request = new UkeyLoginRandomRequest();
request.setRoleCode(RoleCode.SUPER_ADMIN.getCode());
UkeyLoginRandomResponse response = service.issueUkeyLoginRandoms(request);
Assertions.assertEquals(RoleCode.SUPER_ADMIN.getCode(), response.getRoleCode());
Assertions.assertEquals(List.of("RB-1", "RB-2"), response.getRandoms());
}
@Test
void shouldRejectPasswordLoginWhenMasterKeyIsNotReady() {
InMemoryRoleAccountRepository roleAccounts = new InMemoryRoleAccountRepository();
roleAccounts.save(activeRole(RoleCode.AUDIT_ADMIN, "HASH:12345678:SALT-A", "SALT-A", 0, false));
LmkService lmkService = org.mockito.Mockito.mock(LmkService.class);
org.mockito.Mockito.when(lmkService.getMasterKeyStatus()).thenReturn(MasterKeyStatus.ABNORMAL.getDetail());
InMemoryCaptchaService captchaService = new InMemoryCaptchaService();
String captchaId = captchaService.issue("ABCD").getCaptchaId();
AuthService service = new AuthServiceImpl(
roleAccounts,
new InMemoryLegacyAuthUserRepository(),
new InMemoryAuthSessionRepository(),
new InMemoryRoleUkeyBindingRepository(),
new FakePasswordHasher(),
lmkService,
org.mockito.Mockito.mock(UkeyLoginRandomService.class),
org.mockito.Mockito.mock(CompatUkeyVerifier.class),
captchaService,
new ObjectMapper(),
FIXED_CLOCK,
() -> "unused",
() -> "salt-unused",
new AuthPolicyServiceImpl()
);
PasswordLoginRequest request = new PasswordLoginRequest();
request.setRoleCode(RoleCode.AUDIT_ADMIN.getCode());
request.setPassword("12345678");
request.setCaptchaId(captchaId);
request.setCaptchaCode("ABCD");
BizException exception = Assertions.assertThrows(BizException.class, () -> service.passwordLogin(request));
Assertions.assertEquals("master key is not ready", exception.getMessage());
RoleAccountEntity changed = roleAccounts.findByRoleCode(RoleCode.KEY_ADMIN.getCode()).orElseThrow();
Assertions.assertEquals("salt-test", changed.getPasswordSalt());
Assertions.assertEquals("HASH:87654321:salt-test", changed.getPasswordHash());
Assertions.assertEquals(0, changed.getFailedCount());
Assertions.assertNull(changed.getLockedUntil());
Assertions.assertFalse(Boolean.TRUE.equals(changed.getNeedChangePassword()));
Assertions.assertEquals(LocalDateTime.of(2026, 3, 23, 2, 0), changed.getLastActiveAt());
}
@Test
void shouldIssueCaptchaAndAcceptPasswordLoginWithValidCaptcha() {
InMemoryRoleAccountRepository roleAccounts = new InMemoryRoleAccountRepository();
roleAccounts.save(activeRole(RoleCode.OPS_ADMIN, "HASH:12345678:SALT-O", "SALT-O", 0, false));
InMemoryAuthUserAccountRepository userAccounts = new InMemoryAuthUserAccountRepository();
roleAccounts.save(activeRole(RoleCode.OPS_ADMIN));
userAccounts.save(activeUserAccount("ops-admin-01", RoleCode.OPS_ADMIN, "12345678", "SALT-O", 0, null));
LmkService lmkService = org.mockito.Mockito.mock(LmkService.class);
org.mockito.Mockito.when(lmkService.getMasterKeyStatus()).thenReturn(MasterKeyStatus.NORMAL.getDetail("MAC-001"));
InMemoryCaptchaService captchaService = new InMemoryCaptchaService();
@@ -330,6 +206,7 @@ class AuthServiceTest {
AuthService service = new AuthServiceImpl(
roleAccounts,
new InMemoryLegacyAuthUserRepository(),
userAccounts,
new InMemoryAuthSessionRepository(),
new InMemoryRoleUkeyBindingRepository(),
new FakePasswordHasher(),
@@ -348,9 +225,9 @@ class AuthServiceTest {
PasswordLoginRequest request = new PasswordLoginRequest();
request.setRoleCode(RoleCode.OPS_ADMIN.getCode());
request.setPassword("12345678");
request.setCaptchaId(captcha.getCaptchaId());
request.setCaptchaCode("ABCD");
request.setAccounts(List.of(account("ops-admin-01", "12345678")));
LoginResponse response = service.passwordLogin(request);
@@ -359,12 +236,14 @@ class AuthServiceTest {
}
@Test
void shouldVerifyIssuedUkeysRandomsAndLoginSignsBeforeCreatingFullSession() {
void shouldVerifyUkeyProofsBeforeCreatingFullSession() {
InMemoryRoleAccountRepository roleAccounts = new InMemoryRoleAccountRepository();
InMemoryAuthUserAccountRepository userAccounts = new InMemoryAuthUserAccountRepository();
InMemoryAuthSessionRepository sessions = new InMemoryAuthSessionRepository();
InMemoryRoleUkeyBindingRepository ukeyBindings = new InMemoryRoleUkeyBindingRepository();
roleAccounts.save(activeRole(RoleCode.KEY_ADMIN, "HASH:12345678:SALT-K", "SALT-K", 0, false));
ukeyBindings.save(activeBinding(RoleCode.KEY_ADMIN, 1, "UK-1", "PUB-1", "4", "4"));
roleAccounts.save(activeRole(RoleCode.KEY_ADMIN, "12345678", "ROLE-SALT-K"));
userAccounts.save(activeUserAccount("key-admin-01", RoleCode.KEY_ADMIN, "12345678", "SALT-K", 0, null));
ukeyBindings.save(activeBinding(RoleCode.KEY_ADMIN, 1, "UK-1", "PUB-1"));
LmkService lmkService = org.mockito.Mockito.mock(LmkService.class);
UkeyLoginRandomService randomService = org.mockito.Mockito.mock(UkeyLoginRandomService.class);
@@ -376,6 +255,7 @@ class AuthServiceTest {
AuthService service = new AuthServiceImpl(
roleAccounts,
new InMemoryLegacyAuthUserRepository(),
userAccounts,
sessions,
ukeyBindings,
new FakePasswordHasher(),
@@ -392,13 +272,14 @@ class AuthServiceTest {
UkeyLoginRandomRequest randomRequest = new UkeyLoginRandomRequest();
randomRequest.setRoleCode(RoleCode.KEY_ADMIN.getCode());
service.issueUkeyLoginRandoms(randomRequest);
UkeyLoginRandomResponse ignored = service.issueUkeyLoginRandoms(randomRequest);
Assertions.assertNotNull(ignored);
UkeyLoginRequest request = new UkeyLoginRequest();
request.setRoleCode(RoleCode.KEY_ADMIN.getCode());
request.setPassword("12345678");
request.setRolePassword("12345678");
request.setUkeyProofs(List.of(
proof("PUB-1", "4", "4", "RB-1", "ISSUE-1", "LOGIN-DATA-1", "LOGIN-SIGN-1")
proof("PUB-1", 1, "RB-1", "ISSUE-1", "LOGIN-DATA-1", "LOGIN-SIGN-1")
));
LoginResponse response = service.ukeyLogin(request);
@@ -407,70 +288,207 @@ class AuthServiceTest {
Assertions.assertEquals(AuthLevel.FULL.name(), response.getAuthLevel());
org.mockito.Mockito.verify(randomService).assertIssued(RoleCode.KEY_ADMIN.getCode(), List.of("RB-1"));
org.mockito.Mockito.verify(verifier).verifyIssuedBinding(
"{\"pubKey\":\"PUB-1\",\"authKeyPair\":\"IK-PUB-001\",\"role\":\"KEY_ADMIN\",\"uid\":\"4\",\"rid\":\"4\"}",
"{\"pubKey\":\"PUB-1\",\"authKeyPair\":\"IK-PUB-001\",\"role\":\"KEY_ADMIN\",\"uid\":1}",
"ISSUE-1"
);
org.mockito.Mockito.verify(verifier).verifyLoginSignature("PUB-1", "LOGIN-DATA-1", "LOGIN-SIGN-1");
}
private static RoleAccountEntity activeRole(
RoleCode roleCode,
String passwordHash,
String salt,
int failedCount,
boolean needChangePassword
) {
return role(roleCode, RoleAccountStatus.ACTIVE, passwordHash, salt, failedCount, needChangePassword, null);
@Test
void shouldLockRolePasswordDuringUkeyLoginWhenRolePasswordIsWrong() {
InMemoryRoleAccountRepository roleAccounts = new InMemoryRoleAccountRepository();
InMemoryAuthUserAccountRepository userAccounts = new InMemoryAuthUserAccountRepository();
InMemoryRoleUkeyBindingRepository ukeyBindings = new InMemoryRoleUkeyBindingRepository();
roleAccounts.save(activeRole(RoleCode.KEY_ADMIN, "12345678", "ROLE-SALT-K", 4, null));
userAccounts.save(activeUserAccount("key-admin-01", RoleCode.KEY_ADMIN, "12345678", "SALT-K", 0, null));
ukeyBindings.save(activeBinding(RoleCode.KEY_ADMIN, 1, "UK-1", "PUB-1"));
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,
new InMemoryAuthSessionRepository(),
ukeyBindings,
new FakePasswordHasher(),
lmkService,
randomService,
verifier,
new InMemoryCaptchaService(),
new ObjectMapper(),
FIXED_CLOCK,
() -> "unused",
() -> "salt-full-ukey-003",
new AuthPolicyServiceImpl()
);
UkeyLoginRequest request = new UkeyLoginRequest();
request.setRoleCode(RoleCode.KEY_ADMIN.getCode());
request.setRolePassword("bad-role-password");
request.setUkeyProofs(List.of(
proof("PUB-1", 1, "RB-1", "ISSUE-1", "LOGIN-DATA-1", "LOGIN-SIGN-1")
));
BizException exception = Assertions.assertThrows(BizException.class, () -> service.ukeyLogin(request));
Assertions.assertEquals("role account is locked", exception.getMessage());
RoleAccountEntity updated = roleAccounts.findByRoleCode(RoleCode.KEY_ADMIN.getCode()).orElseThrow();
Assertions.assertEquals(5, updated.getFailedCount());
Assertions.assertEquals(RoleAccountStatus.LOCKED.name(), updated.getStatus());
Assertions.assertEquals(LocalDateTime.of(2026, 3, 23, 2, 10), updated.getLockedUntil());
}
private static RoleAccountEntity role(
RoleCode roleCode,
RoleAccountStatus status,
String passwordHash,
String salt,
int failedCount,
boolean needChangePassword,
LocalDateTime lockedUntil
) {
private static RoleAccountEntity activeRole(RoleCode roleCode) {
return activeRole(roleCode, false);
}
private static RoleAccountEntity activeRole(RoleCode roleCode, boolean needChangePassword) {
RoleAccountEntity entity = new RoleAccountEntity();
entity.setId((long) roleCode.ordinal() + 1);
entity.setRoleCode(roleCode.getCode());
entity.setDisplayName(roleCode.getDisplayName());
entity.setRequiredUkeyCount(roleCode.getRequiredUkeyCount());
entity.setPasswordHash(passwordHash);
entity.setPasswordSalt(salt);
entity.setStatus(status.name());
entity.setStatus(RoleAccountStatus.ACTIVE.name());
entity.setNeedChangePassword(needChangePassword);
return entity;
}
private static RoleAccountEntity activeRole(RoleCode roleCode, String rolePassword, String salt) {
return activeRole(roleCode, rolePassword, salt, 0, null, false);
}
private static RoleAccountEntity activeRole(
RoleCode roleCode,
String rolePassword,
String salt,
int failedCount,
LocalDateTime lockedUntil
) {
return activeRole(roleCode, rolePassword, salt, failedCount, lockedUntil, false);
}
private static RoleAccountEntity activeRole(
RoleCode roleCode,
String rolePassword,
String salt,
int failedCount,
LocalDateTime lockedUntil,
boolean needChangePassword
) {
RoleAccountEntity entity = activeRole(roleCode, needChangePassword);
entity.setPasswordHash("HASH:" + rolePassword + ":" + salt);
entity.setPasswordSalt(salt);
entity.setFailedCount(failedCount);
entity.setLockedUntil(lockedUntil);
return entity;
}
private static RoleUkeyBindingEntity activeBinding(RoleCode roleCode, int slotNo, String serial) {
RoleUkeyBindingEntity entity = new RoleUkeyBindingEntity();
entity.setId((long) slotNo);
private static AuthUserAccountEntity activeUserAccount(
String username,
RoleCode roleCode,
String password,
String salt,
int failedCount,
LocalDateTime lockedUntil
) {
AuthUserAccountEntity entity = new AuthUserAccountEntity();
entity.setId((long) (username.hashCode() & Integer.MAX_VALUE));
entity.setUsername(username);
entity.setDisplayName(username);
entity.setRoleCode(roleCode.getCode());
entity.setSlotNo(slotNo);
entity.setUkeySerial(serial);
entity.setStatus("ACTIVE");
entity.setPasswordSalt(salt);
entity.setPasswordHash("HASH:" + password + ":" + salt);
entity.setStatus(RoleAccountStatus.ACTIVE.name());
entity.setFailedCount(failedCount);
entity.setLockedUntil(lockedUntil);
return entity;
}
private static PasswordLoginAccountRequest account(String username, String password) {
PasswordLoginAccountRequest request = new PasswordLoginAccountRequest();
request.setUsername(username);
request.setPassword(password);
return request;
}
private static RoleUkeyBindingEntity activeBinding(
RoleCode roleCode,
int slotNo,
int uid,
String serial,
String pubKey,
String uid,
String rid
String pubKey
) {
RoleUkeyBindingEntity entity = activeBinding(roleCode, slotNo, serial);
entity.setUkeyPubkey(pubKey);
RoleUkeyBindingEntity entity = new RoleUkeyBindingEntity();
entity.setId((long) uid);
entity.setRoleCode(roleCode.getCode());
entity.setUid(uid);
entity.setRid(rid);
entity.setUkeySerial(serial);
entity.setUkeyPubkey(pubKey);
entity.setStatus("ACTIVE");
return entity;
}
private static UkeyLoginProof proof(
String pubKey,
int uid,
String serverRandom,
String issueSignature,
String loginPayload,
String loginSignature
) {
UkeyLoginProof proof = new UkeyLoginProof();
proof.setPubKey(pubKey);
proof.setUid(uid);
proof.setServerRandom(serverRandom);
proof.setIssueSignature(issueSignature);
proof.setLoginPayload(loginPayload);
proof.setLoginSignature(loginSignature);
return proof;
}
private static AuthSessionEntity session(String token, String roleCode, String authLevel) {
AuthSessionEntity entity = new AuthSessionEntity();
entity.setId((long) (token.hashCode() & Integer.MAX_VALUE));
entity.setSessionToken(token);
entity.setRoleCode(roleCode);
entity.setAuthLevel(authLevel);
entity.setIssuedAt(LocalDateTime.of(2026, 3, 23, 1, 50));
entity.setLastActiveAt(LocalDateTime.of(2026, 3, 23, 1, 58));
entity.setExpiresAt(LocalDateTime.of(2026, 3, 23, 2, 8));
return entity;
}
private static AuthService newAuthService(
InMemoryRoleAccountRepository roleAccounts,
InMemoryAuthUserAccountRepository userAccounts,
InMemoryAuthSessionRepository sessions,
InMemoryRoleUkeyBindingRepository ukeyBindings,
Clock clock,
SessionTokenGenerator tokenGenerator
) {
return new AuthServiceImpl(
roleAccounts,
new InMemoryLegacyAuthUserRepository(),
userAccounts,
sessions,
ukeyBindings,
new FakePasswordHasher(),
org.mockito.Mockito.mock(LmkService.class),
org.mockito.Mockito.mock(UkeyLoginRandomService.class),
org.mockito.Mockito.mock(CompatUkeyVerifier.class),
new InMemoryCaptchaService(),
new ObjectMapper(),
clock,
tokenGenerator,
() -> "salt-test",
new AuthPolicyServiceImpl()
);
}
private static class FakePasswordHasher implements PasswordHasher {
@Override
@@ -503,6 +521,33 @@ class AuthServiceTest {
}
}
private static class InMemoryAuthUserAccountRepository implements AuthUserAccountRepository {
private final Map<String, AuthUserAccountEntity> store = new ConcurrentHashMap<>();
@Override
public Optional<AuthUserAccountEntity> findByUsername(String username) {
return Optional.ofNullable(store.get(username));
}
@Override
public List<AuthUserAccountEntity> findByRoleCode(String roleCode) {
return store.values().stream()
.filter(entity -> roleCode.equals(entity.getRoleCode()))
.sorted(Comparator.comparing(AuthUserAccountEntity::getUsername))
.toList();
}
@Override
public void save(AuthUserAccountEntity entity) {
store.put(entity.getUsername(), entity);
}
@Override
public void update(AuthUserAccountEntity entity) {
store.put(entity.getUsername(), entity);
}
}
private static class InMemoryLegacyAuthUserRepository implements AuthUserRepository {
@Override
@@ -528,19 +573,15 @@ class AuthServiceTest {
public void update(AuthSessionEntity entity) {
store.put(entity.getSessionToken(), entity);
}
private int count() {
return store.size();
}
}
private static class InMemoryRoleUkeyBindingRepository implements RoleUkeyBindingRepository {
private final Map<String, List<RoleUkeyBindingEntity>> store = new ConcurrentHashMap<>();
@Override
public Optional<RoleUkeyBindingEntity> findActiveByRoleCodeAndSlotNo(String roleCode, Integer slotNo) {
public Optional<RoleUkeyBindingEntity> findActiveByRoleCodeAndUid(String roleCode, Integer uid) {
return store.getOrDefault(roleCode, List.of()).stream()
.filter(entity -> slotNo.equals(entity.getSlotNo()))
.filter(entity -> uid.equals(entity.getUid()))
.filter(entity -> "ACTIVE".equals(entity.getStatus()))
.findFirst();
}
@@ -549,7 +590,7 @@ class AuthServiceTest {
public List<RoleUkeyBindingEntity> findActiveByRoleCode(String roleCode) {
return store.getOrDefault(roleCode, List.of()).stream()
.filter(entity -> "ACTIVE".equals(entity.getStatus()))
.sorted(Comparator.comparing(RoleUkeyBindingEntity::getSlotNo))
.sorted(Comparator.comparing(RoleUkeyBindingEntity::getUid))
.toList();
}
@@ -566,38 +607,6 @@ class AuthServiceTest {
}
}
private static AuthSessionEntity session(String token, String roleCode, String authLevel) {
AuthSessionEntity entity = new AuthSessionEntity();
entity.setId((long) (token.hashCode() & Integer.MAX_VALUE));
entity.setSessionToken(token);
entity.setRoleCode(roleCode);
entity.setAuthLevel(authLevel);
entity.setIssuedAt(LocalDateTime.of(2026, 3, 23, 1, 50));
entity.setLastActiveAt(LocalDateTime.of(2026, 3, 23, 1, 58));
entity.setExpiresAt(LocalDateTime.of(2026, 3, 23, 2, 8));
return entity;
}
private static UkeyLoginProof proof(
String pubKey,
String uid,
String rid,
String serverRandom,
String issueSignature,
String loginPayload,
String loginSignature
) {
UkeyLoginProof proof = new UkeyLoginProof();
proof.setPubKey(pubKey);
proof.setUid(uid);
proof.setRid(rid);
proof.setServerRandom(serverRandom);
proof.setIssueSignature(issueSignature);
proof.setLoginPayload(loginPayload);
proof.setLoginSignature(loginSignature);
return proof;
}
private static class InMemoryCaptchaService implements CaptchaService {
private final Map<String, String> store = new ConcurrentHashMap<>();
@@ -623,29 +632,4 @@ class AuthServiceTest {
}
}
}
private static AuthService newAuthService(
InMemoryRoleAccountRepository roleAccounts,
InMemoryAuthSessionRepository sessions,
InMemoryRoleUkeyBindingRepository ukeyBindings,
Clock clock,
SessionTokenGenerator tokenGenerator
) {
return new AuthServiceImpl(
roleAccounts,
new InMemoryLegacyAuthUserRepository(),
sessions,
ukeyBindings,
new FakePasswordHasher(),
org.mockito.Mockito.mock(LmkService.class),
org.mockito.Mockito.mock(UkeyLoginRandomService.class),
org.mockito.Mockito.mock(CompatUkeyVerifier.class),
new InMemoryCaptchaService(),
new ObjectMapper(),
clock,
tokenGenerator,
() -> "salt-test",
new AuthPolicyServiceImpl()
);
}
}