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