角色和登录改动

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