From a3fd33a6e44d52077f0220b508e8864df1df53dd Mon Sep 17 00:00:00 2001 From: waner Date: Wed, 22 Apr 2026 18:29:00 +0800 Subject: [PATCH] =?UTF-8?q?fix=EF=BC=9A=E8=A7=92=E8=89=B2=E8=AE=BE?= =?UTF-8?q?=E8=AE=A1=E4=BF=AE=E6=94=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 1 + .../auth/controller/AuthAdminController.java | 2 +- .../auth/controller/AuthController.java | 33 +- .../auth/dto/AuthSessionPrincipal.java | 46 ++ .../auth/dto/ChangePasswordRequest.java | 2 +- .../auth/dto/FullLoginAccountRequest.java | 23 + .../tms/modules/auth/dto/LoginRequest.java | 12 +- .../auth/dto/PasswordLoginRequest.java | 4 +- .../tms/modules/auth/dto/UkeyLoginProof.java | 13 +- .../modules/auth/dto/UkeyLoginRequest.java | 26 +- .../auth/entity/AuthFullAccountEntity.java | 118 +++++ .../auth/entity/AuthSessionEntity.java | 18 +- .../auth/entity/AuthUserAccountEntity.java | 9 + .../auth/entity/RoleAccountEntity.java | 65 --- .../auth/mapper/AuthFullAccountMapper.java | 15 + .../auth/mapper/AuthSessionMapper.java | 15 + .../repository/AuthFullAccountRepository.java | 16 + .../repository/AuthSessionRepository.java | 11 + .../impl/AuthFullAccountRepositoryImpl.java | 38 ++ .../impl/AuthSessionRepositoryImpl.java | 22 + .../tms/modules/auth/service/AuthService.java | 4 +- .../service/impl/AuthAdminServiceImpl.java | 60 ++- .../auth/service/impl/AuthServiceImpl.java | 353 ++++++++++--- .../service/impl/PcieCompatUkeyVerifier.java | 172 +++++- .../db/migration/V1__tms_schema_full.sql | 176 +++++-- .../mapper/auth/AuthFullAccountMapper.xml | 67 +++ .../mapper/auth/AuthSessionMapper.xml | 47 +- .../mapper/auth/AuthUserAccountMapper.xml | 3 + .../mapper/auth/RoleAccountMapper.xml | 14 - .../auth/controller/AuthControllerTest.java | 40 +- .../auth/service/AuthAdminServiceTest.java | 116 ++++- .../modules/auth/service/AuthServiceTest.java | 491 +++++++++++++++--- .../impl/PcieCompatUkeyVerifierTest.java | 119 +++++ .../ReplayProtectedEndpointsTest.java | 6 +- 34 files changed, 1819 insertions(+), 338 deletions(-) create mode 100644 src/main/java/com/cisd/tms/modules/auth/dto/AuthSessionPrincipal.java create mode 100644 src/main/java/com/cisd/tms/modules/auth/dto/FullLoginAccountRequest.java create mode 100644 src/main/java/com/cisd/tms/modules/auth/entity/AuthFullAccountEntity.java create mode 100644 src/main/java/com/cisd/tms/modules/auth/mapper/AuthFullAccountMapper.java create mode 100644 src/main/java/com/cisd/tms/modules/auth/repository/AuthFullAccountRepository.java create mode 100644 src/main/java/com/cisd/tms/modules/auth/repository/impl/AuthFullAccountRepositoryImpl.java create mode 100644 src/main/resources/mapper/auth/AuthFullAccountMapper.xml create mode 100644 src/test/java/com/cisd/tms/modules/auth/service/impl/PcieCompatUkeyVerifierTest.java diff --git a/README.md b/README.md index e6fa9d7..df3bbdd 100644 --- a/README.md +++ b/README.md @@ -219,6 +219,7 @@ Open: - 不满足最小兼容版本时拒绝升级 - `execute.sh` 必填,`precheck.sh`、`verify.sh`、`rollback.sh` 可选 - 执行顺序为:`precheck -> execute -> verify` +- `TMS` 自升级包内脚本第一版不应直接 `stop/start` 当前 TMS;推荐只完成文件准备,由后端在写入终态和日志后异步触发 `/home/tms/scripts/tms.sh restart` - `FIRMWARE` 不增加额外后端流程,具体固件刷写、重启、恢复提示由包内脚本负责 - 回滚不自动触发,需要调用回滚接口 - `TMS`、`RECEIVER` 升级成功后会更新 `tms_device_software_version`;`FIRMWARE` 暂不维护版本表 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 f171d3f..5854f6c 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 @@ -78,7 +78,7 @@ public class AuthAdminController { @PostMapping("/roles/{roleCode}/ukeys/issue-sign") @Operation(summary = "生成 UKey 发行签名", description = "按旧绑定流程为目标角色 UKey 材料生成发行签名。") - @RequireInternalAuth(role = RoleCode.SUPER_ADMIN, authLevel = AuthLevel.LIMITED) +// @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/controller/AuthController.java b/src/main/java/com/cisd/tms/modules/auth/controller/AuthController.java index d33493d..bdd63b9 100644 --- a/src/main/java/com/cisd/tms/modules/auth/controller/AuthController.java +++ b/src/main/java/com/cisd/tms/modules/auth/controller/AuthController.java @@ -65,13 +65,36 @@ public class AuthController { return ApiResponse.success(); } - @PostMapping("/change-password") - @Operation(summary = "修改当前角色口令", description = "基于当前会话校验并更新当前角色口令。") - @AuditedOperation(module = ModuleCode.AUTH, action = ActionType.UPDATE, summary = "修改当前角色口令") + @PostMapping("/full-accounts/{uid}/change-password") + @Operation(summary = "修改当前 FULL 账户口令", description = "基于当前 FULL 会话校验并更新指定席位账户口令。") + @AuditedOperation(module = ModuleCode.AUTH, action = ActionType.UPDATE, summary = "修改当前 FULL 账户口令") @ReplayProtected - public ApiResponse changePassword(@Valid @RequestBody ChangePasswordRequest request, HttpServletRequest httpRequest) { - authService.changePassword( + public ApiResponse changeFullAccountPassword( + @PathVariable("uid") Integer uid, + @Valid @RequestBody ChangePasswordRequest request, + HttpServletRequest httpRequest + ) { + authService.changeFullAccountPassword( (String) httpRequest.getAttribute(InternalApiAuthInterceptor.ATTR_SESSION_TOKEN), + uid, + request.getCurrentPassword(), + request.getNewPassword() + ); + return ApiResponse.success(); + } + + @PostMapping("/limited-accounts/{username}/change-password") + @Operation(summary = "修改当前 LIMITED 账户口令", description = "基于当前 LIMITED 会话校验并更新指定受限账户口令。") + @AuditedOperation(module = ModuleCode.AUTH, action = ActionType.UPDATE, summary = "修改当前 LIMITED 账户口令") + @ReplayProtected + public ApiResponse changeLimitedAccountPassword( + @PathVariable("username") String username, + @Valid @RequestBody ChangePasswordRequest request, + HttpServletRequest httpRequest + ) { + authService.changeLimitedAccountPassword( + (String) httpRequest.getAttribute(InternalApiAuthInterceptor.ATTR_SESSION_TOKEN), + username, request.getCurrentPassword(), request.getNewPassword() ); diff --git a/src/main/java/com/cisd/tms/modules/auth/dto/AuthSessionPrincipal.java b/src/main/java/com/cisd/tms/modules/auth/dto/AuthSessionPrincipal.java new file mode 100644 index 0000000..23e4549 --- /dev/null +++ b/src/main/java/com/cisd/tms/modules/auth/dto/AuthSessionPrincipal.java @@ -0,0 +1,46 @@ +package com.cisd.tms.modules.auth.dto; + +public class AuthSessionPrincipal { + + private String type; + private Integer uid; + private String username; + + public static AuthSessionPrincipal full(Integer uid) { + AuthSessionPrincipal principal = new AuthSessionPrincipal(); + principal.setType("FULL"); + principal.setUid(uid); + return principal; + } + + public static AuthSessionPrincipal limited(String username) { + AuthSessionPrincipal principal = new AuthSessionPrincipal(); + principal.setType("LIMITED"); + principal.setUsername(username); + return principal; + } + + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } + + public Integer getUid() { + return uid; + } + + public void setUid(Integer uid) { + this.uid = uid; + } + + public String getUsername() { + return username; + } + + public void setUsername(String username) { + this.username = username; + } +} diff --git a/src/main/java/com/cisd/tms/modules/auth/dto/ChangePasswordRequest.java b/src/main/java/com/cisd/tms/modules/auth/dto/ChangePasswordRequest.java index d2f0ffa..9dee712 100644 --- a/src/main/java/com/cisd/tms/modules/auth/dto/ChangePasswordRequest.java +++ b/src/main/java/com/cisd/tms/modules/auth/dto/ChangePasswordRequest.java @@ -3,7 +3,7 @@ package com.cisd.tms.modules.auth.dto; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotBlank; -@Schema(description = "修改当前角色口令请求") +@Schema(description = "修改当前认证账户口令请求") public class ChangePasswordRequest { @NotBlank(message = "currentPassword is required") diff --git a/src/main/java/com/cisd/tms/modules/auth/dto/FullLoginAccountRequest.java b/src/main/java/com/cisd/tms/modules/auth/dto/FullLoginAccountRequest.java new file mode 100644 index 0000000..280d5ac --- /dev/null +++ b/src/main/java/com/cisd/tms/modules/auth/dto/FullLoginAccountRequest.java @@ -0,0 +1,23 @@ +package com.cisd.tms.modules.auth.dto; + +public class FullLoginAccountRequest { + + private Integer uid; + private String password; + + public Integer getUid() { + return uid; + } + + public void setUid(Integer uid) { + this.uid = uid; + } + + 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/LoginRequest.java b/src/main/java/com/cisd/tms/modules/auth/dto/LoginRequest.java index cc36a3f..5e10e83 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 @@ -14,8 +14,8 @@ public class LoginRequest { @Schema(description = "参与角色口令校验的账号列表") private List accounts; - @Schema(description = "角色口令") - private String rolePassword; + @Schema(description = "参与 FULL 登录校验的席位账户列表") + private List fullAccounts; @Schema(description = "UKey 序列号列表,完整登录时需传入角色要求数量的序列号") private List<@NotBlank(message = "ukey serial must not be blank") String> ukeySerials; @@ -36,12 +36,12 @@ public class LoginRequest { this.accounts = accounts; } - public String getRolePassword() { - return rolePassword; + public List getFullAccounts() { + return fullAccounts; } - public void setRolePassword(String rolePassword) { - this.rolePassword = rolePassword; + public void setFullAccounts(List fullAccounts) { + this.fullAccounts = fullAccounts; } public List getUkeySerials() { 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 00d295f..e753f75 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 @@ -21,11 +21,11 @@ public class PasswordLoginRequest { @Schema(description = "参与本次角色口令校验的账号列表") private List accounts; - @NotBlank(message = "captchaCode is required") +// @NotBlank(message = "captchaCode is required") @Schema(description = "图形验证码", example = "ABCD") private String captchaCode; - @NotBlank(message = "captchaId is required") +// @NotBlank(message = "captchaId is required") @Schema(description = "验证码标识", example = "captcha-001") private String captchaId; 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 9b67bbf..83cb1ed 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 @@ -4,7 +4,7 @@ import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; -@Schema(description = "单个 UKey 登录证明") +@Schema(description = "单个 UKey 登录因子") public class UkeyLoginProof { @NotBlank(message = "pubKey is required") @@ -28,6 +28,9 @@ public class UkeyLoginProof { @NotBlank(message = "loginSignature is required") private String loginSignature; + @Schema(description = "当前 UKey 固定席位对应账号的口令,所有角色都必传", example = "12345678") + private String password; + public String getPubKey() { return pubKey; } @@ -75,4 +78,12 @@ public class UkeyLoginProof { public void setLoginSignature(String loginSignature) { this.loginSignature = loginSignature; } + + 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/UkeyLoginRequest.java b/src/main/java/com/cisd/tms/modules/auth/dto/UkeyLoginRequest.java index def7f1a..2b19c52 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 @@ -1,5 +1,6 @@ package com.cisd.tms.modules.auth.dto; +import com.fasterxml.jackson.annotation.JsonAlias; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.Valid; import jakarta.validation.constraints.NotBlank; @@ -13,14 +14,11 @@ public class UkeyLoginRequest { @Schema(description = "角色编码", example = "KEY_ADMIN") private String roleCode; - @NotBlank(message = "rolePassword is required") - @Schema(description = "角色口令", example = "12345678") - private String rolePassword; - @Valid - @NotEmpty(message = "ukeyProofs is required") - @Schema(description = "UKey 登录证明列表") - private List ukeyProofs; + @NotEmpty(message = "loginFactors is required") + @JsonAlias("ukeyProofs") + @Schema(description = "UKey 登录因子列表") + private List loginFactors; public String getRoleCode() { return roleCode; @@ -30,19 +28,21 @@ public class UkeyLoginRequest { this.roleCode = roleCode; } - public String getRolePassword() { - return rolePassword; + public List getLoginFactors() { + return loginFactors; } - public void setRolePassword(String rolePassword) { - this.rolePassword = rolePassword; + public void setLoginFactors(List loginFactors) { + this.loginFactors = loginFactors; } + @Deprecated public List getUkeyProofs() { - return ukeyProofs; + return loginFactors; } + @Deprecated public void setUkeyProofs(List ukeyProofs) { - this.ukeyProofs = ukeyProofs; + this.loginFactors = ukeyProofs; } } diff --git a/src/main/java/com/cisd/tms/modules/auth/entity/AuthFullAccountEntity.java b/src/main/java/com/cisd/tms/modules/auth/entity/AuthFullAccountEntity.java new file mode 100644 index 0000000..3bb78e3 --- /dev/null +++ b/src/main/java/com/cisd/tms/modules/auth/entity/AuthFullAccountEntity.java @@ -0,0 +1,118 @@ +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_full_account") +public class AuthFullAccountEntity extends BaseEntity { + + private String roleCode; + private Integer uid; + private String accountName; + private String displayName; + private String passwordHash; + private String passwordSalt; + private String status; + private Boolean needChangePassword; + private Integer failedCount; + private LocalDateTime lockedUntil; + private LocalDateTime lastLoginAt; + private LocalDateTime lastActiveAt; + + public String getRoleCode() { + return roleCode; + } + + public void setRoleCode(String roleCode) { + this.roleCode = roleCode; + } + + public Integer getUid() { + return uid; + } + + public void setUid(Integer uid) { + this.uid = uid; + } + + public String getAccountName() { + return accountName; + } + + public void setAccountName(String accountName) { + this.accountName = accountName; + } + + public String getDisplayName() { + return displayName; + } + + public void setDisplayName(String displayName) { + this.displayName = displayName; + } + + 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 Boolean getNeedChangePassword() { + return needChangePassword; + } + + public void setNeedChangePassword(Boolean needChangePassword) { + this.needChangePassword = needChangePassword; + } + + 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/AuthSessionEntity.java b/src/main/java/com/cisd/tms/modules/auth/entity/AuthSessionEntity.java index 6028e33..5589930 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 @@ -18,7 +18,7 @@ public class AuthSessionEntity extends BaseEntity { /** * 本次角色会话在后端侧记录的认证账号列表。 */ - private String authenticatedUsersJson; + private String authenticatedPrincipalsJson; /** * 当前会话的认证方式,区分口令登录和 UKey 登录。 */ @@ -48,12 +48,22 @@ public class AuthSessionEntity extends BaseEntity { this.roleCode = roleCode; } - public String getAuthenticatedUsersJson() { - return authenticatedUsersJson; + public String getAuthenticatedPrincipalsJson() { + return authenticatedPrincipalsJson; } + public void setAuthenticatedPrincipalsJson(String authenticatedPrincipalsJson) { + this.authenticatedPrincipalsJson = authenticatedPrincipalsJson; + } + + @Deprecated + public String getAuthenticatedUsersJson() { + return authenticatedPrincipalsJson; + } + + @Deprecated public void setAuthenticatedUsersJson(String authenticatedUsersJson) { - this.authenticatedUsersJson = authenticatedUsersJson; + this.authenticatedPrincipalsJson = authenticatedUsersJson; } public String getAuthMethod() { 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 index b070b99..63e68e4 100644 --- a/src/main/java/com/cisd/tms/modules/auth/entity/AuthUserAccountEntity.java +++ b/src/main/java/com/cisd/tms/modules/auth/entity/AuthUserAccountEntity.java @@ -13,6 +13,7 @@ public class AuthUserAccountEntity extends BaseEntity { private String passwordHash; private String passwordSalt; private String status; + private Boolean needChangePassword; private Integer failedCount; private LocalDateTime lockedUntil; private LocalDateTime lastLoginAt; @@ -66,6 +67,14 @@ public class AuthUserAccountEntity extends BaseEntity { this.status = status; } + public Boolean getNeedChangePassword() { + return needChangePassword; + } + + public void setNeedChangePassword(Boolean needChangePassword) { + this.needChangePassword = needChangePassword; + } + public Integer getFailedCount() { return failedCount; } diff --git a/src/main/java/com/cisd/tms/modules/auth/entity/RoleAccountEntity.java b/src/main/java/com/cisd/tms/modules/auth/entity/RoleAccountEntity.java index 53b07ff..2694d23 100644 --- a/src/main/java/com/cisd/tms/modules/auth/entity/RoleAccountEntity.java +++ b/src/main/java/com/cisd/tms/modules/auth/entity/RoleAccountEntity.java @@ -2,22 +2,13 @@ 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_role_account") public class RoleAccountEntity extends BaseEntity { private String roleCode; private String displayName; private Integer requiredUkeyCount; - private String passwordHash; - private String passwordSalt; private String status; - private Boolean needChangePassword; - private Integer failedCount; - private LocalDateTime lockedUntil; - private LocalDateTime lastLoginAt; - private LocalDateTime lastActiveAt; public String getRoleCode() { return roleCode; @@ -43,22 +34,6 @@ public class RoleAccountEntity extends BaseEntity { this.requiredUkeyCount = requiredUkeyCount; } - 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; } @@ -66,44 +41,4 @@ public class RoleAccountEntity extends BaseEntity { public void setStatus(String status) { this.status = status; } - - public Boolean getNeedChangePassword() { - return needChangePassword; - } - - public void setNeedChangePassword(Boolean needChangePassword) { - this.needChangePassword = needChangePassword; - } - - 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/mapper/AuthFullAccountMapper.java b/src/main/java/com/cisd/tms/modules/auth/mapper/AuthFullAccountMapper.java new file mode 100644 index 0000000..5876ff1 --- /dev/null +++ b/src/main/java/com/cisd/tms/modules/auth/mapper/AuthFullAccountMapper.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.AuthFullAccountEntity; +import java.util.List; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; + +@Mapper +public interface AuthFullAccountMapper extends BaseMapperX { + + AuthFullAccountEntity selectByRoleCodeAndUid(@Param("roleCode") String roleCode, @Param("uid") Integer uid); + + List selectByRoleCode(@Param("roleCode") String roleCode); +} diff --git a/src/main/java/com/cisd/tms/modules/auth/mapper/AuthSessionMapper.java b/src/main/java/com/cisd/tms/modules/auth/mapper/AuthSessionMapper.java index c74de70..aacf706 100644 --- a/src/main/java/com/cisd/tms/modules/auth/mapper/AuthSessionMapper.java +++ b/src/main/java/com/cisd/tms/modules/auth/mapper/AuthSessionMapper.java @@ -2,6 +2,8 @@ package com.cisd.tms.modules.auth.mapper; import com.cisd.tms.infrastructure.persistence.mapper.BaseMapperX; import com.cisd.tms.modules.auth.entity.AuthSessionEntity; +import java.time.LocalDateTime; +import java.util.List; import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Param; @@ -9,4 +11,17 @@ import org.apache.ibatis.annotations.Param; public interface AuthSessionMapper extends BaseMapperX { AuthSessionEntity selectBySessionToken(@Param("sessionToken") String sessionToken); + + List selectActiveByRoleCodeAndAuthLevel( + @Param("roleCode") String roleCode, + @Param("authLevel") String authLevel, + @Param("now") LocalDateTime now + ); + + List selectActiveByRoleCodeAndAuthLevelAndAuthenticatedPrincipalsJson( + @Param("roleCode") String roleCode, + @Param("authLevel") String authLevel, + @Param("authenticatedPrincipalsJson") String authenticatedPrincipalsJson, + @Param("now") LocalDateTime now + ); } diff --git a/src/main/java/com/cisd/tms/modules/auth/repository/AuthFullAccountRepository.java b/src/main/java/com/cisd/tms/modules/auth/repository/AuthFullAccountRepository.java new file mode 100644 index 0000000..4ebd498 --- /dev/null +++ b/src/main/java/com/cisd/tms/modules/auth/repository/AuthFullAccountRepository.java @@ -0,0 +1,16 @@ +package com.cisd.tms.modules.auth.repository; + +import com.cisd.tms.modules.auth.entity.AuthFullAccountEntity; +import java.util.List; +import java.util.Optional; + +public interface AuthFullAccountRepository { + + Optional findByRoleCodeAndUid(String roleCode, Integer uid); + + List findByRoleCode(String roleCode); + + void save(AuthFullAccountEntity entity); + + void update(AuthFullAccountEntity entity); +} diff --git a/src/main/java/com/cisd/tms/modules/auth/repository/AuthSessionRepository.java b/src/main/java/com/cisd/tms/modules/auth/repository/AuthSessionRepository.java index a9bc12d..cac2508 100644 --- a/src/main/java/com/cisd/tms/modules/auth/repository/AuthSessionRepository.java +++ b/src/main/java/com/cisd/tms/modules/auth/repository/AuthSessionRepository.java @@ -1,12 +1,23 @@ package com.cisd.tms.modules.auth.repository; import com.cisd.tms.modules.auth.entity.AuthSessionEntity; +import java.time.LocalDateTime; +import java.util.List; import java.util.Optional; public interface AuthSessionRepository { Optional findBySessionToken(String sessionToken); + List findActiveByRoleCodeAndAuthLevel(String roleCode, String authLevel, LocalDateTime now); + + List findActiveByRoleCodeAndAuthLevelAndAuthenticatedPrincipalsJson( + String roleCode, + String authLevel, + String authenticatedPrincipalsJson, + LocalDateTime now + ); + void save(AuthSessionEntity entity); void update(AuthSessionEntity entity); diff --git a/src/main/java/com/cisd/tms/modules/auth/repository/impl/AuthFullAccountRepositoryImpl.java b/src/main/java/com/cisd/tms/modules/auth/repository/impl/AuthFullAccountRepositoryImpl.java new file mode 100644 index 0000000..1f30204 --- /dev/null +++ b/src/main/java/com/cisd/tms/modules/auth/repository/impl/AuthFullAccountRepositoryImpl.java @@ -0,0 +1,38 @@ +package com.cisd.tms.modules.auth.repository.impl; + +import com.cisd.tms.modules.auth.entity.AuthFullAccountEntity; +import com.cisd.tms.modules.auth.mapper.AuthFullAccountMapper; +import com.cisd.tms.modules.auth.repository.AuthFullAccountRepository; +import java.util.List; +import java.util.Optional; +import org.springframework.stereotype.Repository; + +@Repository +public class AuthFullAccountRepositoryImpl implements AuthFullAccountRepository { + + private final AuthFullAccountMapper authFullAccountMapper; + + public AuthFullAccountRepositoryImpl(AuthFullAccountMapper authFullAccountMapper) { + this.authFullAccountMapper = authFullAccountMapper; + } + + @Override + public Optional findByRoleCodeAndUid(String roleCode, Integer uid) { + return Optional.ofNullable(authFullAccountMapper.selectByRoleCodeAndUid(roleCode, uid)); + } + + @Override + public List findByRoleCode(String roleCode) { + return authFullAccountMapper.selectByRoleCode(roleCode); + } + + @Override + public void save(AuthFullAccountEntity entity) { + authFullAccountMapper.insert(entity); + } + + @Override + public void update(AuthFullAccountEntity entity) { + authFullAccountMapper.updateById(entity); + } +} diff --git a/src/main/java/com/cisd/tms/modules/auth/repository/impl/AuthSessionRepositoryImpl.java b/src/main/java/com/cisd/tms/modules/auth/repository/impl/AuthSessionRepositoryImpl.java index f9ee51f..bde8c4e 100644 --- a/src/main/java/com/cisd/tms/modules/auth/repository/impl/AuthSessionRepositoryImpl.java +++ b/src/main/java/com/cisd/tms/modules/auth/repository/impl/AuthSessionRepositoryImpl.java @@ -3,6 +3,8 @@ package com.cisd.tms.modules.auth.repository.impl; import com.cisd.tms.modules.auth.entity.AuthSessionEntity; import com.cisd.tms.modules.auth.mapper.AuthSessionMapper; import com.cisd.tms.modules.auth.repository.AuthSessionRepository; +import java.time.LocalDateTime; +import java.util.List; import java.util.Optional; import org.springframework.stereotype.Repository; @@ -20,6 +22,26 @@ public class AuthSessionRepositoryImpl implements AuthSessionRepository { return Optional.ofNullable(authSessionMapper.selectBySessionToken(sessionToken)); } + @Override + public List findActiveByRoleCodeAndAuthLevel(String roleCode, String authLevel, LocalDateTime now) { + return authSessionMapper.selectActiveByRoleCodeAndAuthLevel(roleCode, authLevel, now); + } + + @Override + public List findActiveByRoleCodeAndAuthLevelAndAuthenticatedPrincipalsJson( + String roleCode, + String authLevel, + String authenticatedPrincipalsJson, + LocalDateTime now + ) { + return authSessionMapper.selectActiveByRoleCodeAndAuthLevelAndAuthenticatedPrincipalsJson( + roleCode, + authLevel, + authenticatedPrincipalsJson, + now + ); + } + @Override public void save(AuthSessionEntity entity) { authSessionMapper.insert(entity); diff --git a/src/main/java/com/cisd/tms/modules/auth/service/AuthService.java b/src/main/java/com/cisd/tms/modules/auth/service/AuthService.java index 390c4c3..81dfaab 100644 --- a/src/main/java/com/cisd/tms/modules/auth/service/AuthService.java +++ b/src/main/java/com/cisd/tms/modules/auth/service/AuthService.java @@ -27,5 +27,7 @@ public interface AuthService { void logout(String sessionToken); - void changePassword(String sessionToken, String currentPassword, String newPassword); + void changeFullAccountPassword(String sessionToken, Integer uid, String currentPassword, String newPassword); + + void changeLimitedAccountPassword(String sessionToken, String username, String currentPassword, String newPassword); } 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 3ea90a4..bcb6e24 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,13 @@ 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.AuthFullAccountEntity; 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.RoleAccountStatus; import com.cisd.tms.modules.auth.enums.RoleCode; +import com.cisd.tms.modules.auth.repository.AuthFullAccountRepository; import com.cisd.tms.modules.auth.repository.AuthUserAccountRepository; import com.cisd.tms.modules.auth.repository.RoleAccountRepository; import com.cisd.tms.modules.auth.repository.RoleUkeyBindingRepository; @@ -36,6 +38,7 @@ public class AuthAdminServiceImpl implements AuthAdminService { static final String DEFAULT_PASSWORD = "12345678"; private final RoleAccountRepository roleAccountRepository; + private final AuthFullAccountRepository authFullAccountRepository; private final AuthUserAccountRepository authUserAccountRepository; private final RoleUkeyBindingRepository roleUkeyBindingRepository; private final PasswordHasher passwordHasher; @@ -48,24 +51,60 @@ public class AuthAdminServiceImpl implements AuthAdminService { public void enableRole(String operatorRoleCode, String operatorAuthLevel, String targetRoleCode) { RoleAccountEntity target = loadRole(targetRoleCode); target.setStatus(RoleAccountStatus.ACTIVE.name()); - target.setNeedChangePassword(Boolean.TRUE); - target.setLockedUntil(null); - target.setFailedCount(0); roleAccountRepository.update(target); + + List fullAccounts = authFullAccountRepository.findByRoleCode(targetRoleCode); + if (fullAccounts.isEmpty()) { + throw new BizException(ErrorCode.VALIDATE_FAILED.getCode(), "target role full accounts not found"); + } + for (AuthFullAccountEntity account : fullAccounts) { + account.setStatus(RoleAccountStatus.ACTIVE.name()); + account.setNeedChangePassword(Boolean.TRUE); + account.setFailedCount(0); + account.setLockedUntil(null); + account.setLastActiveAt(null); + account.setLastLoginAt(null); + authFullAccountRepository.update(account); + } + + 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) { + account.setStatus(RoleAccountStatus.ACTIVE.name()); + account.setNeedChangePassword(Boolean.TRUE); + account.setFailedCount(0); + account.setLockedUntil(null); + account.setLastActiveAt(null); + account.setLastLoginAt(null); + authUserAccountRepository.update(account); + } } @Override public void resetPassword(String operatorRoleCode, String operatorAuthLevel, String targetRoleCode) { RoleAccountEntity target = loadRole(targetRoleCode); - 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 fullAccounts = authFullAccountRepository.findByRoleCode(targetRoleCode); + if (fullAccounts.isEmpty()) { + throw new BizException(ErrorCode.VALIDATE_FAILED.getCode(), "target role full accounts not found"); + } + for (AuthFullAccountEntity account : fullAccounts) { + String newSalt = passwordSaltGenerator.nextSalt(); + account.setPasswordSalt(newSalt); + account.setPasswordHash(passwordHasher.hash(DEFAULT_PASSWORD, newSalt)); + account.setStatus(RoleAccountStatus.ACTIVE.name()); + account.setNeedChangePassword(Boolean.TRUE); + account.setFailedCount(0); + account.setLockedUntil(null); + account.setLastActiveAt(null); + account.setLastLoginAt(null); + authFullAccountRepository.update(account); + } + List accounts = authUserAccountRepository.findByRoleCode(targetRoleCode); if (accounts.isEmpty()) { throw new BizException(ErrorCode.VALIDATE_FAILED.getCode(), "target role user accounts not found"); @@ -75,6 +114,7 @@ public class AuthAdminServiceImpl implements AuthAdminService { account.setPasswordSalt(newSalt); account.setPasswordHash(passwordHasher.hash(DEFAULT_PASSWORD, newSalt)); account.setStatus(RoleAccountStatus.ACTIVE.name()); + account.setNeedChangePassword(Boolean.TRUE); account.setFailedCount(0); account.setLockedUntil(null); account.setLastActiveAt(null); @@ -97,6 +137,8 @@ public class AuthAdminServiceImpl implements AuthAdminService { if (uid == null || uid < 1 || uid > targetRole.getRequiredUkeyCount()) { throw new BizException(ErrorCode.VALIDATE_FAILED.getCode(), "uid exceeds role ukey requirement"); } + authFullAccountRepository.findByRoleCodeAndUid(targetRoleCode, uid) + .orElseThrow(() -> new BizException(ErrorCode.VALIDATE_FAILED.getCode(), "target role full account not found")); RoleUkeyBindingEntity binding = roleUkeyBindingRepository .findActiveByRoleCodeAndUid(targetRoleCode, uid) 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 0043844..1b34d94 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 @@ -2,8 +2,10 @@ 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.modules.auth.dto.AuthSessionPrincipal; import com.cisd.tms.modules.auth.dto.CaptchaResponse; import com.cisd.tms.modules.auth.dto.CurrentUserResponse; +import com.cisd.tms.modules.auth.dto.FullLoginAccountRequest; import com.cisd.tms.modules.auth.dto.LoginRequest; import com.cisd.tms.modules.auth.dto.LoginResponse; import com.cisd.tms.modules.auth.dto.PasswordLoginAccountRequest; @@ -12,6 +14,7 @@ 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.AuthFullAccountEntity; import com.cisd.tms.modules.auth.entity.AuthSessionEntity; import com.cisd.tms.modules.auth.entity.AuthUserAccountEntity; import com.cisd.tms.modules.auth.entity.AuthUserEntity; @@ -21,6 +24,7 @@ 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.AuthFullAccountRepository; import com.cisd.tms.modules.auth.repository.AuthSessionRepository; import com.cisd.tms.modules.auth.repository.AuthUserAccountRepository; import com.cisd.tms.modules.auth.repository.AuthUserRepository; @@ -39,6 +43,8 @@ import com.cisd.tms.modules.mk.dto.UKeySignEntity; import com.cisd.tms.modules.mk.enums.MasterKeyStatus; import com.cisd.tms.modules.mk.service.LmkService; import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import java.time.Clock; import java.time.LocalDateTime; @@ -63,6 +69,7 @@ public class AuthServiceImpl implements AuthService { private final RoleAccountRepository roleAccountRepository; private final AuthUserRepository authUserRepository; private final AuthUserAccountRepository authUserAccountRepository; + private final AuthFullAccountRepository authFullAccountRepository; private final AuthSessionRepository authSessionRepository; private final RoleUkeyBindingRepository roleUkeyBindingRepository; private final PasswordHasher passwordHasher; @@ -84,19 +91,24 @@ public class AuthServiceImpl implements AuthService { validateRoleStatus(roleAccount); AuthMethod authMethod = resolveAuthMethod(request.getUkeySerials()); List validatedAccounts = new ArrayList<>(); + List validatedFullAccounts = new ArrayList<>(); if (AuthMethod.PASSWORD == authMethod) { validatedAccounts = validateRoleAccounts(roleAccount, request.getAccounts()); resetValidatedAccounts(validatedAccounts); } else { - validateRolePassword(roleAccount, request.getRolePassword()); - resetRoleFailureState(roleAccount); + validatedFullAccounts = validateFullAccounts(roleAccount, request.getFullAccounts()); + resetValidatedFullAccounts(validatedFullAccounts); } AuthLevel authLevel = resolveAuthLevel(roleAccount, request.getUkeySerials()); + List authenticatedPrincipals = AuthMethod.PASSWORD == authMethod + ? normalizeAuthenticatedUsers(validatedAccounts) + : normalizeAuthenticatedFullAccounts(validatedFullAccounts); + expireConcurrentSessions(roleAccount.getRoleCode(), authLevel, authenticatedPrincipals); AuthSessionEntity session = buildSession( roleAccount.getRoleCode(), authMethod, authLevel, - validatedAccounts.isEmpty() ? null : validatedAccounts.stream().map(AuthUserAccountEntity::getUsername).toList() + authenticatedPrincipals ); authSessionRepository.save(session); @@ -105,14 +117,18 @@ public class AuthServiceImpl implements AuthService { response.setAuthLevel(authLevel.name()); response.setToken(session.getSessionToken()); response.setExpiresAt(OffsetDateTime.of(session.getExpiresAt(), ZoneOffset.UTC).toString()); - response.setNeedChangePassword(needsPasswordChange(roleAccount)); + response.setNeedChangePassword( + AuthMethod.PASSWORD == authMethod + ? needsPasswordChangeForLimitedAccounts(validatedAccounts) + : needsPasswordChangeForFullAccounts(validatedFullAccounts) + ); return response; } @Override public LoginResponse passwordLogin(PasswordLoginRequest request) { ensureMasterKeyReady(); - captchaService.verify(request.getCaptchaId(), request.getCaptchaCode()); +// captchaService.verify(request.getCaptchaId(), request.getCaptchaCode()); LoginRequest loginRequest = new LoginRequest(); loginRequest.setRoleCode(request.getRoleCode()); loginRequest.setAccounts(request.getAccounts()); @@ -133,22 +149,22 @@ public class AuthServiceImpl implements AuthService { ensureMasterKeyReady(); RoleCode roleCode = RoleCode.valueOf(request.getRoleCode()); List activeBindings = roleUkeyBindingRepository.findActiveByRoleCode(roleCode.getCode()); - validateUkeyCount(roleCode, activeBindings, request.getUkeyProofs()); + validateUkeyCount(roleCode, activeBindings, request.getLoginFactors()); Map bindingsByUid = activeBindings.stream() .collect(Collectors.toMap(RoleUkeyBindingEntity::getUid, item -> item, (left, right) -> left, java.util.LinkedHashMap::new)); - Set requestUids = request.getUkeyProofs().stream() + Set requestUids = request.getLoginFactors().stream() .map(UkeyLoginProof::getUid) .collect(Collectors.toSet()); - if (requestUids.size() != request.getUkeyProofs().size() || !bindingsByUid.keySet().containsAll(requestUids)) { + if (requestUids.size() != request.getLoginFactors().size() || !bindingsByUid.keySet().containsAll(requestUids)) { throw new BizException(ErrorCode.UNAUTHORIZED.getCode(), "ukey auth info does not match bound role"); } ukeyLoginRandomService.assertIssued( roleCode.getCode(), - request.getUkeyProofs().stream().map(UkeyLoginProof::getServerRandom).toList() + request.getLoginFactors().stream().map(UkeyLoginProof::getServerRandom).toList() ); String authKeyPair = lmkService.exportIkPublicKeyHex(); List matchedSerials = new ArrayList<>(); - for (UkeyLoginProof proof : request.getUkeyProofs()) { + for (UkeyLoginProof proof : request.getLoginFactors()) { RoleUkeyBindingEntity binding = bindingsByUid.get(proof.getUid()); if (binding == null || !binding.getUkeyPubkey().equals(proof.getPubKey())) { throw new BizException(ErrorCode.UNAUTHORIZED.getCode(), "ukey auth info does not match bound role"); @@ -159,8 +175,8 @@ public class AuthServiceImpl implements AuthService { } LoginRequest loginRequest = new LoginRequest(); loginRequest.setRoleCode(request.getRoleCode()); - loginRequest.setRolePassword(request.getRolePassword()); loginRequest.setUkeySerials(matchedSerials); + loginRequest.setFullAccounts(buildFixedUidAccounts(request.getRoleCode(), request.getLoginFactors())); return login(loginRequest); } @@ -186,8 +202,10 @@ public class AuthServiceImpl implements AuthService { AuthSessionEntity session = sessionToken == null ? null : authSessionRepository.findBySessionToken(sessionToken).orElse(null); if (session != null) { response.setAuthLevel(session.getAuthLevel()); + response.setNeedChangePassword(needsPasswordChange(session)); + } else { + response.setNeedChangePassword(Boolean.FALSE); } - response.setNeedChangePassword(needsPasswordChange(roleAccount)); return response; } @@ -217,25 +235,66 @@ public class AuthServiceImpl implements AuthService { } @Override - public void changePassword(String sessionToken, String currentPassword, String newPassword) { + public void changeFullAccountPassword(String sessionToken, Integer uid, String currentPassword, String newPassword) { 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())) { - onRolePasswordFailed(roleAccount); + if (!AuthLevel.FULL.name().equals(session.getAuthLevel())) { + throw new BizException(ErrorCode.UNAUTHORIZED.getCode(), "full session is required"); + } + if (uid == null || !containsAuthenticatedFullUid(session, uid)) { + throw new BizException(ErrorCode.UNAUTHORIZED.getCode(), "full account is not authenticated in current session"); + } + AuthFullAccountEntity fullAccount = authFullAccountRepository.findByRoleCodeAndUid(session.getRoleCode(), uid) + .orElseThrow(() -> new BizException(ErrorCode.UNAUTHORIZED.getCode(), "full account not found")); + validateFullAccountStatus(fullAccount); + if (!passwordHasher.matches(currentPassword, fullAccount.getPasswordSalt(), fullAccount.getPasswordHash())) { + onFullPasswordFailed(fullAccount); } 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); + fullAccount.setPasswordSalt(salt); + fullAccount.setPasswordHash(passwordHasher.hash(newPassword, salt)); + fullAccount.setFailedCount(0); + fullAccount.setLockedUntil(null); + fullAccount.setStatus(RoleAccountStatus.ACTIVE.name()); + fullAccount.setNeedChangePassword(Boolean.FALSE); + fullAccount.setLastLoginAt(current); + fullAccount.setLastActiveAt(current); + authFullAccountRepository.update(fullAccount); + + session.setLastActiveAt(current); + session.setExpiresAt(current.plusMinutes(IDLE_TIMEOUT_MINUTES)); + authSessionRepository.update(session); + } + + @Override + public void changeLimitedAccountPassword(String sessionToken, String username, String currentPassword, String newPassword) { + AuthSessionEntity session = requireActiveSession(sessionToken); + if (!AuthLevel.LIMITED.name().equals(session.getAuthLevel())) { + throw new BizException(ErrorCode.UNAUTHORIZED.getCode(), "limited session is required"); + } + if (username == null || username.isBlank() || !containsAuthenticatedLimitedUsername(session, username)) { + throw new BizException(ErrorCode.UNAUTHORIZED.getCode(), "limited account is not authenticated in current session"); + } + AuthUserAccountEntity userAccount = authUserAccountRepository.findByUsername(username) + .orElseThrow(() -> new BizException(ErrorCode.UNAUTHORIZED.getCode(), "user account not found")); + validateUserAccountRole(session.getRoleCode(), userAccount); + validateUserAccountStatus(userAccount); + if (!passwordHasher.matches(currentPassword, userAccount.getPasswordSalt(), userAccount.getPasswordHash())) { + onPasswordFailed(userAccount); + } + + String salt = passwordSaltGenerator.nextSalt(); + LocalDateTime current = now(); + userAccount.setPasswordSalt(salt); + userAccount.setPasswordHash(passwordHasher.hash(newPassword, salt)); + userAccount.setFailedCount(0); + userAccount.setLockedUntil(null); + userAccount.setStatus(RoleAccountStatus.ACTIVE.name()); + userAccount.setNeedChangePassword(Boolean.FALSE); + userAccount.setLastLoginAt(current); + userAccount.setLastActiveAt(current); + authUserAccountRepository.update(userAccount); session.setLastActiveAt(current); session.setExpiresAt(current.plusMinutes(IDLE_TIMEOUT_MINUTES)); @@ -243,22 +302,9 @@ public class AuthServiceImpl implements AuthService { } private void validateRoleStatus(RoleAccountEntity roleAccount) { - if (RoleAccountStatus.UNENABLED.name().equals(roleAccount.getStatus())) { + if (!RoleAccountStatus.ACTIVE.name().equals(roleAccount.getStatus())) { throw new BizException(ErrorCode.UNAUTHORIZED.getCode(), "role is not enabled"); } - 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 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 List validateRoleAccounts( @@ -321,17 +367,27 @@ public class AuthServiceImpl implements AuthService { 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); - if (failedCount >= MAX_FAILED_ATTEMPTS) { - roleAccount.setStatus(RoleAccountStatus.LOCKED.name()); - roleAccount.setLockedUntil(now().plusMinutes(IDLE_TIMEOUT_MINUTES)); - roleAccountRepository.update(roleAccount); - throw new BizException(ErrorCode.UNAUTHORIZED.getCode(), "role account is locked"); + private void validateFullAccountStatus(AuthFullAccountEntity fullAccount) { + if (RoleAccountStatus.UNENABLED.name().equals(fullAccount.getStatus())) { + throw new BizException(ErrorCode.UNAUTHORIZED.getCode(), "full account is not enabled"); } - roleAccountRepository.update(roleAccount); + if (RoleAccountStatus.LOCKED.name().equals(fullAccount.getStatus()) + && (fullAccount.getLockedUntil() == null || fullAccount.getLockedUntil().isAfter(now()))) { + throw new BizException(ErrorCode.UNAUTHORIZED.getCode(), "full account is locked"); + } + } + + private void onFullPasswordFailed(AuthFullAccountEntity fullAccount) { + int failedCount = fullAccount.getFailedCount() == null ? 0 : fullAccount.getFailedCount(); + failedCount++; + fullAccount.setFailedCount(failedCount); + if (failedCount >= MAX_FAILED_ATTEMPTS) { + fullAccount.setStatus(RoleAccountStatus.LOCKED.name()); + fullAccount.setLockedUntil(now().plusMinutes(IDLE_TIMEOUT_MINUTES)); + authFullAccountRepository.update(fullAccount); + throw new BizException(ErrorCode.UNAUTHORIZED.getCode(), "full account is locked"); + } + authFullAccountRepository.update(fullAccount); throw new BizException(ErrorCode.UNAUTHORIZED.getCode(), "password is incorrect"); } @@ -347,19 +403,77 @@ public class AuthServiceImpl implements AuthService { } } - private void resetRoleFailureState(RoleAccountEntity roleAccount) { - roleAccount.setFailedCount(0); - roleAccount.setLockedUntil(null); - roleAccount.setStatus(RoleAccountStatus.ACTIVE.name()); - roleAccount.setLastLoginAt(now()); - roleAccount.setLastActiveAt(now()); - roleAccountRepository.update(roleAccount); + private void resetValidatedFullAccounts(List validatedAccounts) { + LocalDateTime current = now(); + for (AuthFullAccountEntity account : validatedAccounts) { + account.setFailedCount(0); + account.setLockedUntil(null); + account.setStatus(RoleAccountStatus.ACTIVE.name()); + account.setLastLoginAt(current); + account.setLastActiveAt(current); + authFullAccountRepository.update(account); + } } private int requiredPasswordAccountCount(String roleCode) { return RoleCode.valueOf(roleCode).getRequiredUkeyCount(); } + private List validateFullAccounts( + RoleAccountEntity roleAccount, + List fullAccounts + ) { + int expectedCount = authPolicyService.requiredUkeyCount(RoleCode.valueOf(roleAccount.getRoleCode())); + if (fullAccounts == null || fullAccounts.size() != expectedCount) { + throw new BizException( + ErrorCode.UNAUTHORIZED.getCode(), + roleAccount.getRoleCode() + " requires exactly " + expectedCount + " full-account passwords" + ); + } + + Set uniqueUids = new LinkedHashSet<>(); + List validatedAccounts = new ArrayList<>(); + for (FullLoginAccountRequest account : fullAccounts) { + Integer uid = account.getUid(); + if (uid == null || !uniqueUids.add(uid)) { + throw new BizException(ErrorCode.UNAUTHORIZED.getCode(), "duplicate full-account uid submitted"); + } + AuthFullAccountEntity fullAccount = authFullAccountRepository + .findByRoleCodeAndUid(roleAccount.getRoleCode(), uid) + .orElseThrow(() -> new BizException(ErrorCode.UNAUTHORIZED.getCode(), "full account not found")); + validateFullAccountStatus(fullAccount); + if (!passwordHasher.matches(account.getPassword(), fullAccount.getPasswordSalt(), fullAccount.getPasswordHash())) { + onFullPasswordFailed(fullAccount); + } + validatedAccounts.add(fullAccount); + } + return validatedAccounts; + } + + private List buildFixedUidAccounts(String roleCode, List proofs) { + List fixedAccounts = authFullAccountRepository.findByRoleCode(roleCode); + int requiredCount = authPolicyService.requiredUkeyCount(RoleCode.valueOf(roleCode)); + if (fixedAccounts.size() < requiredCount) { + throw new BizException(ErrorCode.UNAUTHORIZED.getCode(), "role fixed account mapping is incomplete"); + } + + List requests = new ArrayList<>(); + for (UkeyLoginProof proof : proofs.stream().sorted(java.util.Comparator.comparing(UkeyLoginProof::getUid)).toList()) { + Integer uid = proof.getUid(); + if (uid == null || uid < 1 || uid > fixedAccounts.size()) { + throw new BizException(ErrorCode.UNAUTHORIZED.getCode(), "uid does not match fixed role account"); + } + if (proof.getPassword() == null || proof.getPassword().isBlank()) { + throw new BizException(ErrorCode.UNAUTHORIZED.getCode(), "account password is required"); + } + FullLoginAccountRequest accountRequest = new FullLoginAccountRequest(); + accountRequest.setUid(uid); + accountRequest.setPassword(proof.getPassword()); + requests.add(accountRequest); + } + return requests; + } + private void ensureMasterKeyReady() { MasterKeyStatus.StatusDetail status = lmkService.getMasterKeyStatus(); if (status == null || status.getCode() == MasterKeyStatus.ABNORMAL.getCode()) { @@ -425,12 +539,12 @@ public class AuthServiceImpl implements AuthService { String roleCode, AuthMethod authMethod, AuthLevel authLevel, - List authenticatedUsers + List authenticatedPrincipals ) { LocalDateTime issuedAt = now(); AuthSessionEntity entity = new AuthSessionEntity(); entity.setRoleCode(roleCode); - entity.setAuthenticatedUsersJson(writeAuthenticatedUsers(authenticatedUsers)); + entity.setAuthenticatedPrincipalsJson(writeAuthenticatedPrincipals(authenticatedPrincipals)); entity.setAuthMethod(authMethod.name()); entity.setAuthLevel(authLevel.name()); entity.setSessionToken(sessionTokenGenerator.nextToken()); @@ -440,23 +554,132 @@ public class AuthServiceImpl implements AuthService { return entity; } - private String writeAuthenticatedUsers(List authenticatedUsers) { - if (authenticatedUsers == null) { + private void expireConcurrentSessions(String roleCode, AuthLevel authLevel, List authenticatedPrincipals) { + LocalDateTime current = now(); + List sessionsToExpire; + if (AuthLevel.FULL == authLevel) { + sessionsToExpire = authSessionRepository.findActiveByRoleCodeAndAuthLevel(roleCode, authLevel.name(), current); + } else { + sessionsToExpire = authSessionRepository.findActiveByRoleCodeAndAuthLevelAndAuthenticatedPrincipalsJson( + roleCode, + authLevel.name(), + writeAuthenticatedPrincipals(authenticatedPrincipals), + current + ); + } + for (AuthSessionEntity session : sessionsToExpire) { + session.setLogoutAt(current); + session.setExpiresAt(current); + authSessionRepository.update(session); + } + } + + private List normalizeAuthenticatedUsers(List validatedAccounts) { + if (validatedAccounts == null || validatedAccounts.isEmpty()) { + return null; + } + return validatedAccounts.stream() + .map(AuthUserAccountEntity::getUsername) + .sorted() + .map(AuthSessionPrincipal::limited) + .toList(); + } + + private List normalizeAuthenticatedFullAccounts(List validatedAccounts) { + if (validatedAccounts == null || validatedAccounts.isEmpty()) { + return null; + } + return validatedAccounts.stream() + .map(AuthFullAccountEntity::getUid) + .sorted() + .map(AuthSessionPrincipal::full) + .toList(); + } + + private String writeAuthenticatedPrincipals(List authenticatedPrincipals) { + if (authenticatedPrincipals == null) { return null; } try { - return objectMapper.writeValueAsString(authenticatedUsers); + return objectMapper.writeValueAsString(authenticatedPrincipals); } catch (JsonProcessingException ex) { - throw new IllegalStateException("serialize authenticated users failed", ex); + throw new IllegalStateException("serialize authenticated principals failed", ex); } } + private List authenticatedPrincipals(AuthSessionEntity session) { + if (session.getAuthenticatedPrincipalsJson() == null || session.getAuthenticatedPrincipalsJson().isBlank()) { + return List.of(); + } + try { + JsonNode root = objectMapper.readTree(session.getAuthenticatedPrincipalsJson()); + if (!root.isArray() || root.isEmpty()) { + return List.of(); + } + if (root.get(0).isTextual()) { + List principals = new ArrayList<>(); + for (JsonNode node : root) { + String value = node.asText(); + if (value.startsWith("FULL:")) { + principals.add(AuthSessionPrincipal.full(Integer.parseInt(value.substring("FULL:".length())))); + } else { + principals.add(AuthSessionPrincipal.limited(value)); + } + } + return principals; + } + return objectMapper.readValue( + session.getAuthenticatedPrincipalsJson(), + new TypeReference>() { } + ); + } catch (JsonProcessingException ex) { + throw new IllegalStateException("deserialize authenticated principals failed", ex); + } + } + + private boolean containsAuthenticatedFullUid(AuthSessionEntity session, Integer uid) { + return authenticatedPrincipals(session).stream() + .anyMatch(principal -> "FULL".equals(principal.getType()) && uid.equals(principal.getUid())); + } + + private boolean containsAuthenticatedLimitedUsername(AuthSessionEntity session, String username) { + return authenticatedPrincipals(session).stream() + .anyMatch(principal -> "LIMITED".equals(principal.getType()) && username.equals(principal.getUsername())); + } + private LocalDateTime now() { return LocalDateTime.ofInstant(clock.instant(), ZoneOffset.UTC); } - private boolean needsPasswordChange(RoleAccountEntity roleAccount) { - return Boolean.TRUE.equals(roleAccount.getNeedChangePassword()); + private boolean needsPasswordChangeForLimitedAccounts(List accounts) { + return accounts != null && accounts.stream().anyMatch(account -> Boolean.TRUE.equals(account.getNeedChangePassword())); + } + + private boolean needsPasswordChangeForFullAccounts(List accounts) { + return accounts != null && accounts.stream().anyMatch(account -> Boolean.TRUE.equals(account.getNeedChangePassword())); + } + + private boolean needsPasswordChange(AuthSessionEntity session) { + if (session == null) { + return false; + } + List principals = authenticatedPrincipals(session); + if (principals.isEmpty()) { + return false; + } + if (AuthLevel.FULL.name().equals(session.getAuthLevel())) { + List accounts = authFullAccountRepository.findByRoleCode(session.getRoleCode()); + return accounts.stream() + .filter(account -> principals.stream() + .anyMatch(principal -> "FULL".equals(principal.getType()) && account.getUid().equals(principal.getUid()))) + .anyMatch(account -> Boolean.TRUE.equals(account.getNeedChangePassword())); + } + List accounts = authUserAccountRepository.findByRoleCode(session.getRoleCode()); + return accounts.stream() + .filter(account -> principals.stream() + .anyMatch(principal -> "LIMITED".equals(principal.getType()) + && account.getUsername().equals(principal.getUsername()))) + .anyMatch(account -> Boolean.TRUE.equals(account.getNeedChangePassword())); } private AuthSessionEntity requireActiveSession(String sessionToken) { diff --git a/src/main/java/com/cisd/tms/modules/auth/service/impl/PcieCompatUkeyVerifier.java b/src/main/java/com/cisd/tms/modules/auth/service/impl/PcieCompatUkeyVerifier.java index cf6837c..e52fc62 100644 --- a/src/main/java/com/cisd/tms/modules/auth/service/impl/PcieCompatUkeyVerifier.java +++ b/src/main/java/com/cisd/tms/modules/auth/service/impl/PcieCompatUkeyVerifier.java @@ -2,25 +2,52 @@ 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.integration.crypto.pcie.Gm0018AlgorithmIds; -import com.cisd.tms.integration.crypto.pcie.model.EccExternalVerifyRequest; -import com.cisd.tms.integration.crypto.pcie.service.PcieCryptoService; import com.cisd.tms.modules.auth.service.CompatUkeyVerifier; import com.cisd.tms.modules.mk.service.LmkService; +import java.io.StringReader; +import java.math.BigInteger; import java.nio.charset.StandardCharsets; +import java.security.KeyFactory; +import java.security.PublicKey; +import java.security.Security; +import java.security.Signature; +import java.security.spec.X509EncodedKeySpec; +import java.util.Arrays; import java.util.Base64; +import org.bouncycastle.asn1.ASN1EncodableVector; +import org.bouncycastle.asn1.ASN1Encoding; +import org.bouncycastle.asn1.ASN1InputStream; +import org.bouncycastle.asn1.ASN1Integer; +import org.bouncycastle.asn1.ASN1Primitive; +import org.bouncycastle.asn1.ASN1Sequence; +import org.bouncycastle.asn1.DERSequence; +import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo; +import org.bouncycastle.jce.ECNamedCurveTable; +import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.bouncycastle.jce.spec.ECParameterSpec; +import org.bouncycastle.jce.spec.ECPublicKeySpec; +import org.bouncycastle.openssl.PEMParser; +import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter; import org.bouncycastle.util.encoders.Hex; import org.springframework.stereotype.Component; @Component public class PcieCompatUkeyVerifier implements CompatUkeyVerifier { - private final LmkService lmkService; - private final PcieCryptoService pcieCryptoService; + private static final String PROVIDER = "BC"; + private static final String SIGNATURE_ALGORITHM = "SM3withSM2"; + private static final String SM2_CURVE = "sm2p256v1"; - public PcieCompatUkeyVerifier(LmkService lmkService, PcieCryptoService pcieCryptoService) { + static { + if (Security.getProvider(PROVIDER) == null) { + Security.addProvider(new BouncyCastleProvider()); + } + } + + private final LmkService lmkService; + + public PcieCompatUkeyVerifier(LmkService lmkService) { this.lmkService = lmkService; - this.pcieCryptoService = pcieCryptoService; } @Override @@ -36,32 +63,133 @@ public class PcieCompatUkeyVerifier implements CompatUkeyVerifier { @Override public void verifyLoginSignature(String pubKey, String loginSignData, String loginSign) { - EccExternalVerifyRequest request = new EccExternalVerifyRequest(); - request.setAlgId(Gm0018AlgorithmIds.SM2_SIGN); - request.setPublicKeyBlob(decodeBlob(pubKey, "login public key is invalid")); - request.setData(loginSignData.getBytes(StandardCharsets.UTF_8)); - request.setSignature(decodeBlob(loginSign, "login signature is invalid")); try { - pcieCryptoService.eccExternalVerify(request); + PublicKey publicKey = parsePublicKey(pubKey); + byte[] signatureBytes = decodeSignature(loginSign); + Signature signature = Signature.getInstance(SIGNATURE_ALGORITHM, PROVIDER); + signature.initVerify(publicKey); + signature.update(loginSignData.getBytes(StandardCharsets.UTF_8)); + if (!signature.verify(signatureBytes)) { + throw new BizException(ErrorCode.UNAUTHORIZED.getCode(), "login signature verification failed"); + } } catch (BizException ex) { throw ex; + } catch (IllegalArgumentException ex) { + throw new BizException(ErrorCode.VALIDATE_FAILED.getCode(), ex.getMessage()); } catch (RuntimeException ex) { throw new BizException(ErrorCode.UNAUTHORIZED.getCode(), "login signature verification failed"); + } catch (Exception ex) { + throw new BizException(ErrorCode.UNAUTHORIZED.getCode(), "login signature verification failed"); } } - private byte[] decodeBlob(String value, String message) { - String normalized = value == null ? "" : value.trim(); - if (normalized.isEmpty()) { - throw new BizException(ErrorCode.VALIDATE_FAILED.getCode(), message); + private PublicKey parsePublicKey(String value) throws Exception { + String trimmed = value == null ? "" : value.trim(); + if (trimmed.isEmpty()) { + throw new IllegalArgumentException("login public key is invalid"); } - if (normalized.matches("(?i)^[0-9a-f]+$") && normalized.length() % 2 == 0) { - return Hex.decode(normalized); + if (trimmed.contains("BEGIN")) { + return parsePemPublicKey(trimmed); } + String normalized = normalize(trimmed); + byte[] decoded = decodeHexOrBase64(normalized, "login public key is invalid"); try { - return Base64.getDecoder().decode(normalized); - } catch (IllegalArgumentException ex) { - throw new BizException(ErrorCode.VALIDATE_FAILED.getCode(), message); + return KeyFactory.getInstance("EC", PROVIDER).generatePublic(new X509EncodedKeySpec(decoded)); + } catch (Exception ignored) { + return parseUncompressedPointPublicKey(decoded); } } + + private PublicKey parsePemPublicKey(String pemContent) throws Exception { + try (PEMParser parser = new PEMParser(new StringReader(pemContent))) { + Object parsed = parser.readObject(); + if (!(parsed instanceof SubjectPublicKeyInfo publicKeyInfo)) { + throw new IllegalArgumentException("login public key is invalid"); + } + return new JcaPEMKeyConverter().setProvider(PROVIDER).getPublicKey(publicKeyInfo); + } + } + + private PublicKey parseUncompressedPointPublicKey(byte[] keyBytes) throws Exception { + byte[] pointBytes = keyBytes; + if (pointBytes.length == 64) { + pointBytes = new byte[65]; + pointBytes[0] = 0x04; + System.arraycopy(keyBytes, 0, pointBytes, 1, keyBytes.length); + } + if (pointBytes.length != 65 || pointBytes[0] != 0x04) { + throw new IllegalArgumentException("login public key is invalid"); + } + ECParameterSpec parameterSpec = ECNamedCurveTable.getParameterSpec(SM2_CURVE); + ECPublicKeySpec publicKeySpec = new ECPublicKeySpec( + parameterSpec.getCurve().decodePoint(pointBytes), + parameterSpec + ); + return KeyFactory.getInstance("EC", PROVIDER).generatePublic(publicKeySpec); + } + + private byte[] decodeSignature(String value) { + byte[] decoded = decodeHexOrBase64(normalize(value), "login signature is invalid"); + if (isDerSignature(decoded)) { + return decoded; + } + if (decoded.length == 64) { + return rawRsToDer(decoded); + } + throw new IllegalArgumentException("login signature is invalid"); + } + + private boolean isDerSignature(byte[] signatureBytes) { + try (ASN1InputStream inputStream = new ASN1InputStream(signatureBytes)) { + ASN1Primitive primitive = inputStream.readObject(); + if (!(primitive instanceof ASN1Sequence sequence) || sequence.size() != 2) { + return false; + } + return sequence.getObjectAt(0) instanceof ASN1Integer && sequence.getObjectAt(1) instanceof ASN1Integer; + } catch (Exception ex) { + return false; + } + } + + private byte[] rawRsToDer(byte[] rawSignature) { + byte[] r = Arrays.copyOfRange(rawSignature, 0, 32); + byte[] s = Arrays.copyOfRange(rawSignature, 32, 64); + ASN1EncodableVector vector = new ASN1EncodableVector(); + vector.add(new ASN1Integer(new BigInteger(1, r))); + vector.add(new ASN1Integer(new BigInteger(1, s))); + try { + return new DERSequence(vector).getEncoded(ASN1Encoding.DER); + } catch (Exception ex) { + throw new IllegalArgumentException("login signature is invalid", ex); + } + } + + private byte[] decodeHexOrBase64(String value, String message) { + if (value.isEmpty()) { + throw new IllegalArgumentException(message); + } + if (value.matches("(?i)^[0-9a-f]+$") && value.length() % 2 == 0) { + return Hex.decode(value); + } + try { + return Base64.getDecoder().decode(value); + } catch (IllegalArgumentException ex) { + throw new IllegalArgumentException(message, ex); + } + } + + private String normalize(String value) { + String normalized = value == null ? "" : value.trim(); + if (!normalized.contains("BEGIN")) { + return normalized; + } + StringBuilder builder = new StringBuilder(); + for (String line : normalized.split("\\R")) { + String trimmed = line.trim(); + if (!trimmed.startsWith("-----")) { + builder.append(trimmed); + } + } + return builder.toString(); + } } 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 e505fcf..7f7c9bc 100644 --- a/src/main/resources/db/migration/V1__tms_schema_full.sql +++ b/src/main/resources/db/migration/V1__tms_schema_full.sql @@ -244,14 +244,7 @@ CREATE TABLE IF NOT EXISTS tms_role_account ( role_code VARCHAR(64) NOT NULL, display_name VARCHAR(128) NOT NULL, required_ukey_count INT NOT NULL, - password_hash VARCHAR(256) NOT NULL, - password_salt VARCHAR(128) NOT NULL, status VARCHAR(32) NOT NULL, - need_change_password TINYINT(1) NOT NULL DEFAULT 1, - 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_role_account_role_code (role_code) @@ -281,6 +274,7 @@ CREATE TABLE IF NOT EXISTS tms_auth_user_account ( password_hash VARCHAR(256) NOT NULL, password_salt VARCHAR(128) NOT NULL, status VARCHAR(32) NOT NULL, + need_change_password TINYINT(1) NOT NULL DEFAULT 1, failed_count INT NOT NULL DEFAULT 0, locked_until DATETIME(3) NULL, last_login_at DATETIME(3) NULL, @@ -291,11 +285,32 @@ CREATE TABLE IF NOT EXISTS tms_auth_user_account ( KEY idx_tms_auth_user_account_role_code (role_code) ); +CREATE TABLE IF NOT EXISTS tms_auth_full_account ( + id BIGINT PRIMARY KEY, + role_code VARCHAR(64) NOT NULL, + uid INT NOT NULL, + account_name VARCHAR(64) NOT NULL, + display_name VARCHAR(128) NOT NULL, + password_hash VARCHAR(256) NOT NULL, + password_salt VARCHAR(128) NOT NULL, + status VARCHAR(32) NOT NULL, + need_change_password TINYINT(1) NOT NULL DEFAULT 1, + 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_full_account_role_uid (role_code, uid), + UNIQUE KEY uk_tms_auth_full_account_name (account_name), + KEY idx_tms_auth_full_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, + authenticated_principals_json JSON NULL, auth_method VARCHAR(32) NOT NULL, auth_level VARCHAR(32) NOT NULL, issued_at DATETIME(3) NOT NULL, @@ -379,14 +394,7 @@ INSERT IGNORE INTO tms_role_account ( role_code, display_name, required_ukey_count, - password_hash, - password_salt, status, - need_change_password, - failed_count, - locked_until, - last_login_at, - last_active_at, create_time, update_time ) @@ -396,14 +404,7 @@ VALUES 'SUPER_ADMIN', '超级管理员', 2, - 'Viuew8WE+qziUZwGGU0x25cUpzsTF+QvQFU0uhb+yVA=', - 'init-super-admin-salt-20260325', 'UNENABLED', - 1, - 0, - NULL, - NULL, - NULL, CURRENT_TIMESTAMP(3), CURRENT_TIMESTAMP(3) ), @@ -412,14 +413,7 @@ VALUES 'KEY_ADMIN', '密钥管理员', 1, - '/xqmDy3A/q9X5p7XWznIPSBabQ6bxTLAPgtHQ6Be33c=', - 'init-key-admin-salt-20260325', 'ACTIVE', - 1, - 0, - NULL, - NULL, - NULL, CURRENT_TIMESTAMP(3), CURRENT_TIMESTAMP(3) ), @@ -428,14 +422,7 @@ VALUES 'AUDIT_ADMIN', '审计管理员', 1, - 'j/J1LKyUOzVEJfbbHe2dwKBKNdabRk502olk/LWIwhg=', - 'init-audit-admin-salt-20260325', 'UNENABLED', - 1, - 0, - NULL, - NULL, - NULL, CURRENT_TIMESTAMP(3), CURRENT_TIMESTAMP(3) ), @@ -444,14 +431,7 @@ VALUES 'OPS_ADMIN', '运维管理员', 1, - '2e9F4aCgsDb+AuD1LrBEqEg8yjy3ODo27UHtx06vj/A=', - 'init-ops-admin-salt-20260325', 'UNENABLED', - 1, - 0, - NULL, - NULL, - NULL, CURRENT_TIMESTAMP(3), CURRENT_TIMESTAMP(3) ); @@ -464,6 +444,7 @@ INSERT IGNORE INTO tms_auth_user_account ( password_hash, password_salt, status, + need_change_password, failed_count, locked_until, last_login_at, @@ -480,6 +461,7 @@ VALUES 'Viuew8WE+qziUZwGGU0x25cUpzsTF+QvQFU0uhb+yVA=', 'init-super-admin-salt-20260325', 'ACTIVE', + 1, 0, NULL, NULL, @@ -495,6 +477,7 @@ VALUES 'Viuew8WE+qziUZwGGU0x25cUpzsTF+QvQFU0uhb+yVA=', 'init-super-admin-salt-20260325', 'ACTIVE', + 1, 0, NULL, NULL, @@ -510,6 +493,7 @@ VALUES '/xqmDy3A/q9X5p7XWznIPSBabQ6bxTLAPgtHQ6Be33c=', 'init-key-admin-salt-20260325', 'ACTIVE', + 1, 0, NULL, NULL, @@ -525,6 +509,7 @@ VALUES 'j/J1LKyUOzVEJfbbHe2dwKBKNdabRk502olk/LWIwhg=', 'init-audit-admin-salt-20260325', 'ACTIVE', + 1, 0, NULL, NULL, @@ -540,6 +525,111 @@ VALUES '2e9F4aCgsDb+AuD1LrBEqEg8yjy3ODo27UHtx06vj/A=', 'init-ops-admin-salt-20260325', 'ACTIVE', + 1, + 0, + NULL, + NULL, + NULL, + CURRENT_TIMESTAMP(3), + CURRENT_TIMESTAMP(3) + ); + +INSERT IGNORE INTO tms_auth_full_account ( + id, + role_code, + uid, + account_name, + display_name, + password_hash, + password_salt, + status, + need_change_password, + failed_count, + locked_until, + last_login_at, + last_active_at, + create_time, + update_time +) +VALUES + ( + 12101, + 'SUPER_ADMIN', + 1, + 'super-admin-full-01', + '超级管理员UKey席位一', + 'Viuew8WE+qziUZwGGU0x25cUpzsTF+QvQFU0uhb+yVA=', + 'init-super-admin-salt-20260325', + 'ACTIVE', + 1, + 0, + NULL, + NULL, + NULL, + CURRENT_TIMESTAMP(3), + CURRENT_TIMESTAMP(3) + ), + ( + 12102, + 'SUPER_ADMIN', + 2, + 'super-admin-full-02', + '超级管理员UKey席位二', + 'Viuew8WE+qziUZwGGU0x25cUpzsTF+QvQFU0uhb+yVA=', + 'init-super-admin-salt-20260325', + 'ACTIVE', + 1, + 0, + NULL, + NULL, + NULL, + CURRENT_TIMESTAMP(3), + CURRENT_TIMESTAMP(3) + ), + ( + 12103, + 'KEY_ADMIN', + 1, + 'key-admin-full-01', + '密钥管理员UKey席位', + '/xqmDy3A/q9X5p7XWznIPSBabQ6bxTLAPgtHQ6Be33c=', + 'init-key-admin-salt-20260325', + 'ACTIVE', + 1, + 0, + NULL, + NULL, + NULL, + CURRENT_TIMESTAMP(3), + CURRENT_TIMESTAMP(3) + ), + ( + 12104, + 'AUDIT_ADMIN', + 1, + 'audit-admin-full-01', + '审计管理员UKey席位', + 'j/J1LKyUOzVEJfbbHe2dwKBKNdabRk502olk/LWIwhg=', + 'init-audit-admin-salt-20260325', + 'ACTIVE', + 1, + 0, + NULL, + NULL, + NULL, + CURRENT_TIMESTAMP(3), + CURRENT_TIMESTAMP(3) + ), + ( + 12105, + 'OPS_ADMIN', + 1, + 'ops-admin-full-01', + '运维管理员UKey席位', + '2e9F4aCgsDb+AuD1LrBEqEg8yjy3ODo27UHtx06vj/A=', + 'init-ops-admin-salt-20260325', + 'ACTIVE', + 1, 0, NULL, NULL, diff --git a/src/main/resources/mapper/auth/AuthFullAccountMapper.xml b/src/main/resources/mapper/auth/AuthFullAccountMapper.xml new file mode 100644 index 0000000..2b3414a --- /dev/null +++ b/src/main/resources/mapper/auth/AuthFullAccountMapper.xml @@ -0,0 +1,67 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/mapper/auth/AuthSessionMapper.xml b/src/main/resources/mapper/auth/AuthSessionMapper.xml index 226111f..8633676 100644 --- a/src/main/resources/mapper/auth/AuthSessionMapper.xml +++ b/src/main/resources/mapper/auth/AuthSessionMapper.xml @@ -8,7 +8,7 @@ - + @@ -23,7 +23,7 @@ SELECT id, session_token, role_code, - authenticated_users_json, + authenticated_principals_json, auth_method, auth_level, issued_at, @@ -36,4 +36,47 @@ WHERE session_token = #{sessionToken} LIMIT 1 + + + + diff --git a/src/main/resources/mapper/auth/AuthUserAccountMapper.xml b/src/main/resources/mapper/auth/AuthUserAccountMapper.xml index 89f2671..279e606 100644 --- a/src/main/resources/mapper/auth/AuthUserAccountMapper.xml +++ b/src/main/resources/mapper/auth/AuthUserAccountMapper.xml @@ -12,6 +12,7 @@ + @@ -28,6 +29,7 @@ password_hash, password_salt, status, + need_change_password, failed_count, locked_until, last_login_at, @@ -47,6 +49,7 @@ password_hash, password_salt, status, + need_change_password, failed_count, locked_until, last_login_at, diff --git a/src/main/resources/mapper/auth/RoleAccountMapper.xml b/src/main/resources/mapper/auth/RoleAccountMapper.xml index 7616ab6..e3af690 100644 --- a/src/main/resources/mapper/auth/RoleAccountMapper.xml +++ b/src/main/resources/mapper/auth/RoleAccountMapper.xml @@ -9,14 +9,7 @@ - - - - - - - @@ -26,14 +19,7 @@ role_code, display_name, required_ukey_count, - password_hash, - password_salt, status, - need_change_password, - failed_count, - locked_until, - last_login_at, - last_active_at, create_time, update_time FROM tms_role_account 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 509d3a6..ccf6880 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 @@ -92,7 +92,7 @@ class AuthControllerTest { AuthService authService = Mockito.mock(AuthService.class); AuthAdminService authAdminService = Mockito.mock(AuthAdminService.class); LoginResponse response = new LoginResponse(); - response.setRoleCode("KEY_ADMIN"); + response.setRoleCode("SUPER_ADMIN"); response.setAuthLevel("FULL"); response.setToken("token-ukey-001"); Mockito.when(authService.ukeyLogin(ArgumentMatchers.any())).thenReturn(response); @@ -106,12 +106,12 @@ class AuthControllerTest { .contentType(MediaType.APPLICATION_JSON) .content(""" { - "roleCode": "KEY_ADMIN", - "rolePassword": "12345678", - "ukeyProofs": [ + "roleCode": "SUPER_ADMIN", + "loginFactors": [ { "pubKey": "PUB-1", "uid": 1, + "password": "11111111", "serverRandom": "RB-1", "issueSignature": "ISSUE-1", "loginPayload": "LOGIN-DATA-1", @@ -120,6 +120,7 @@ class AuthControllerTest { { "pubKey": "PUB-2", "uid": 2, + "password": "22222222", "serverRandom": "RB-2", "issueSignature": "ISSUE-2", "loginPayload": "LOGIN-DATA-2", @@ -198,7 +199,7 @@ class AuthControllerTest { } @Test - void shouldChangePasswordThroughCurrentSessionEndpoint() throws Exception { + void shouldChangeFullAccountPasswordThroughCurrentSessionEndpoint() throws Exception { AuthService authService = Mockito.mock(AuthService.class); AuthAdminService authAdminService = Mockito.mock(AuthAdminService.class); @@ -207,7 +208,7 @@ class AuthControllerTest { .setControllerAdvice(new GlobalExceptionHandler()) .build(); - mockMvc.perform(post("/api/v1/auth/change-password") + mockMvc.perform(post("/api/v1/auth/full-accounts/1/change-password") .requestAttr(InternalApiAuthInterceptor.ATTR_SESSION_TOKEN, "token-change-001") .contentType(MediaType.APPLICATION_JSON) .content(""" @@ -219,7 +220,32 @@ class AuthControllerTest { .andExpect(status().isOk()) .andExpect(content().string(containsString("\"success\":true"))); - Mockito.verify(authService).changePassword("token-change-001", "12345678", "87654321"); + Mockito.verify(authService).changeFullAccountPassword("token-change-001", 1, "12345678", "87654321"); + } + + @Test + void shouldChangeLimitedAccountPasswordThroughCurrentSessionEndpoint() throws Exception { + AuthService authService = Mockito.mock(AuthService.class); + AuthAdminService authAdminService = Mockito.mock(AuthAdminService.class); + + MockMvc mockMvc = MockMvcBuilders + .standaloneSetup(new AuthController(authService), new AuthAdminController(authAdminService)) + .setControllerAdvice(new GlobalExceptionHandler()) + .build(); + + mockMvc.perform(post("/api/v1/auth/limited-accounts/audit-admin-01/change-password") + .requestAttr(InternalApiAuthInterceptor.ATTR_SESSION_TOKEN, "token-change-limited-001") + .contentType(MediaType.APPLICATION_JSON) + .content(""" + { + "currentPassword": "12345678", + "newPassword": "87654321" + } + """)) + .andExpect(status().isOk()) + .andExpect(content().string(containsString("\"success\":true"))); + + Mockito.verify(authService).changeLimitedAccountPassword("token-change-limited-001", "audit-admin-01", "12345678", "87654321"); } @Test 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 844c588..2c624dd 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,13 @@ package com.cisd.tms.modules.auth.service; import com.cisd.tms.modules.auth.entity.AuthUserAccountEntity; +import com.cisd.tms.modules.auth.entity.AuthFullAccountEntity; 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.AuthFullAccountRepository; import com.cisd.tms.modules.auth.repository.AuthUserAccountRepository; import com.cisd.tms.modules.auth.repository.RoleAccountRepository; import com.cisd.tms.modules.auth.repository.RoleUkeyBindingRepository; @@ -36,11 +38,18 @@ class AuthAdminServiceTest { @Test void shouldEnableRoleWhenOperatorIsKeyAdminWithFullSession() { InMemoryRoleAccountRepository roleAccounts = new InMemoryRoleAccountRepository(); + InMemoryAuthFullAccountRepository fullAccounts = new InMemoryAuthFullAccountRepository(); + InMemoryAuthUserAccountRepository userAccounts = new InMemoryAuthUserAccountRepository(); roleAccounts.save(role(RoleCode.AUDIT_ADMIN, RoleAccountStatus.UNENABLED)); + fullAccounts.save(fullAccount(RoleCode.AUDIT_ADMIN, 1, "audit-admin-full-01", "OLD-FULL-HASH-1", "OLD-FULL-SALT-1")); + userAccounts.save(user("audit-admin-01", RoleCode.AUDIT_ADMIN, "OLD-HASH-1", "OLD-SALT-1")); + fullAccounts.findByRoleCodeAndUid(RoleCode.AUDIT_ADMIN.getCode(), 1).orElseThrow().setStatus(RoleAccountStatus.UNENABLED.name()); + userAccounts.findByUsername("audit-admin-01").orElseThrow().setStatus(RoleAccountStatus.UNENABLED.name()); AuthAdminService service = newAuthAdminService( roleAccounts, - new InMemoryAuthUserAccountRepository(), + fullAccounts, + userAccounts, new InMemoryRoleUkeyBindingRepository(), FIXED_CLOCK, new FixedSaltSupplier("salt-001") @@ -49,16 +58,29 @@ class AuthAdminServiceTest { service.enableRole(RoleCode.KEY_ADMIN.getCode(), AuthLevel.FULL.name(), RoleCode.AUDIT_ADMIN.getCode()); RoleAccountEntity updated = roleAccounts.findByRoleCode(RoleCode.AUDIT_ADMIN.getCode()).orElseThrow(); + AuthFullAccountEntity fullAccount = fullAccounts.findByRoleCodeAndUid(RoleCode.AUDIT_ADMIN.getCode(), 1).orElseThrow(); + AuthUserAccountEntity userAccount = userAccounts.findByUsername("audit-admin-01").orElseThrow(); Assertions.assertEquals(RoleAccountStatus.ACTIVE.name(), updated.getStatus()); + Assertions.assertEquals(RoleAccountStatus.ACTIVE.name(), fullAccount.getStatus()); + Assertions.assertTrue(Boolean.TRUE.equals(fullAccount.getNeedChangePassword())); + Assertions.assertEquals(RoleAccountStatus.ACTIVE.name(), userAccount.getStatus()); + Assertions.assertTrue(Boolean.TRUE.equals(userAccount.getNeedChangePassword())); } @Test void shouldResetPasswordForAllAccountsUnderRole() { InMemoryRoleAccountRepository roleAccounts = new InMemoryRoleAccountRepository(); + InMemoryAuthFullAccountRepository fullAccounts = new InMemoryAuthFullAccountRepository(); InMemoryAuthUserAccountRepository userAccounts = new InMemoryAuthUserAccountRepository(); - roleAccounts.save(role(RoleCode.SUPER_ADMIN, RoleAccountStatus.ACTIVE, "OLD-ROLE-HASH", "OLD-ROLE-SALT")); + roleAccounts.save(role(RoleCode.SUPER_ADMIN, RoleAccountStatus.ACTIVE)); + fullAccounts.save(fullAccount(RoleCode.SUPER_ADMIN, 1, "super-admin-full-01", "OLD-FULL-HASH-1", "OLD-FULL-SALT-1")); + fullAccounts.save(fullAccount(RoleCode.SUPER_ADMIN, 2, "super-admin-full-02", "OLD-FULL-HASH-2", "OLD-FULL-SALT-2")); 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")); + fullAccounts.findByRoleCodeAndUid(RoleCode.SUPER_ADMIN.getCode(), 1).orElseThrow().setFailedCount(2); + fullAccounts.findByRoleCodeAndUid(RoleCode.SUPER_ADMIN.getCode(), 1).orElseThrow().setLockedUntil(LocalDateTime.of(2026, 3, 23, 3, 20)); + fullAccounts.findByRoleCodeAndUid(RoleCode.SUPER_ADMIN.getCode(), 2).orElseThrow().setFailedCount(5); + fullAccounts.findByRoleCodeAndUid(RoleCode.SUPER_ADMIN.getCode(), 2).orElseThrow().setLockedUntil(LocalDateTime.of(2026, 3, 23, 3, 25)); 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); @@ -66,22 +88,37 @@ class AuthAdminServiceTest { AuthAdminService service = newAuthAdminService( roleAccounts, + fullAccounts, userAccounts, new InMemoryRoleUkeyBindingRepository(), FIXED_CLOCK, - new FixedSaltSupplier("salt-role-001", "salt-002", "salt-003") + new FixedSaltSupplier("salt-full-001", "salt-full-002", "salt-002", "salt-003") ); service.resetPassword(RoleCode.KEY_ADMIN.getCode(), AuthLevel.FULL.name(), RoleCode.SUPER_ADMIN.getCode()); RoleAccountEntity role = roleAccounts.findByRoleCode(RoleCode.SUPER_ADMIN.getCode()).orElseThrow(); + AuthFullAccountEntity fullFirst = fullAccounts.findByRoleCodeAndUid(RoleCode.SUPER_ADMIN.getCode(), 1).orElseThrow(); + AuthFullAccountEntity fullSecond = fullAccounts.findByRoleCodeAndUid(RoleCode.SUPER_ADMIN.getCode(), 2).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(), fullFirst.getStatus()); + Assertions.assertEquals(RoleAccountStatus.ACTIVE.name(), fullSecond.getStatus()); + Assertions.assertTrue(Boolean.TRUE.equals(fullFirst.getNeedChangePassword())); + Assertions.assertTrue(Boolean.TRUE.equals(fullSecond.getNeedChangePassword())); + Assertions.assertEquals(0, fullFirst.getFailedCount()); + Assertions.assertEquals(0, fullSecond.getFailedCount()); + Assertions.assertNull(fullFirst.getLockedUntil()); + Assertions.assertNull(fullSecond.getLockedUntil()); + Assertions.assertEquals("salt-full-001", fullFirst.getPasswordSalt()); + Assertions.assertEquals("HASH:12345678:salt-full-001", fullFirst.getPasswordHash()); + Assertions.assertEquals("salt-full-002", fullSecond.getPasswordSalt()); + Assertions.assertEquals("HASH:12345678:salt-full-002", fullSecond.getPasswordHash()); Assertions.assertEquals(RoleAccountStatus.ACTIVE.name(), first.getStatus()); Assertions.assertEquals(RoleAccountStatus.ACTIVE.name(), second.getStatus()); + Assertions.assertTrue(Boolean.TRUE.equals(first.getNeedChangePassword())); + Assertions.assertTrue(Boolean.TRUE.equals(second.getNeedChangePassword())); Assertions.assertEquals(0, first.getFailedCount()); Assertions.assertEquals(0, second.getFailedCount()); Assertions.assertNull(first.getLockedUntil()); @@ -101,6 +138,7 @@ class AuthAdminServiceTest { AuthAdminService service = new AuthAdminServiceImpl( new InMemoryRoleAccountRepository(), + fullAccountRepository(binding(RoleCode.SUPER_ADMIN, 1, "UK-OLD", "PUB-OLD", "SIG-OLD")), new InMemoryAuthUserAccountRepository(), bindings, new FakePasswordHasher(), @@ -136,6 +174,7 @@ class AuthAdminServiceTest { AuthAdminService service = new AuthAdminServiceImpl( new InMemoryRoleAccountRepository(), + fullAccountRepository(), new InMemoryAuthUserAccountRepository(), new InMemoryRoleUkeyBindingRepository(), new FakePasswordHasher(), @@ -165,14 +204,6 @@ class AuthAdminServiceTest { 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.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)); @@ -182,6 +213,22 @@ class AuthAdminServiceTest { entity.setPasswordHash(passwordHash); entity.setPasswordSalt(passwordSalt); entity.setStatus(RoleAccountStatus.ACTIVE.name()); + entity.setNeedChangePassword(Boolean.FALSE); + entity.setFailedCount(0); + return entity; + } + + private static AuthFullAccountEntity fullAccount(RoleCode roleCode, int uid, String accountName, String passwordHash, String passwordSalt) { + AuthFullAccountEntity entity = new AuthFullAccountEntity(); + entity.setId((long) (accountName.hashCode() & Integer.MAX_VALUE)); + entity.setRoleCode(roleCode.getCode()); + entity.setUid(uid); + entity.setAccountName(accountName); + entity.setDisplayName(accountName); + entity.setPasswordHash(passwordHash); + entity.setPasswordSalt(passwordSalt); + entity.setStatus(RoleAccountStatus.ACTIVE.name()); + entity.setNeedChangePassword(Boolean.FALSE); entity.setFailedCount(0); return entity; } @@ -277,6 +324,33 @@ class AuthAdminServiceTest { } } + private static class InMemoryAuthFullAccountRepository implements AuthFullAccountRepository { + private final Map store = new ConcurrentHashMap<>(); + + @Override + public Optional findByRoleCodeAndUid(String roleCode, Integer uid) { + return Optional.ofNullable(store.get(roleCode + "#" + uid)); + } + + @Override + public List findByRoleCode(String roleCode) { + return store.values().stream() + .filter(entity -> roleCode.equals(entity.getRoleCode())) + .sorted(Comparator.comparing(AuthFullAccountEntity::getUid)) + .toList(); + } + + @Override + public void save(AuthFullAccountEntity entity) { + store.put(entity.getRoleCode() + "#" + entity.getUid(), entity); + } + + @Override + public void update(AuthFullAccountEntity entity) { + store.put(entity.getRoleCode() + "#" + entity.getUid(), entity); + } + } + private static class InMemoryRoleUkeyBindingRepository implements RoleUkeyBindingRepository { private final Map> store = new ConcurrentHashMap<>(); @@ -311,6 +385,7 @@ class AuthAdminServiceTest { private static AuthAdminService newAuthAdminService( InMemoryRoleAccountRepository roleAccounts, + InMemoryAuthFullAccountRepository fullAccounts, InMemoryAuthUserAccountRepository userAccounts, InMemoryRoleUkeyBindingRepository bindings, Clock clock, @@ -318,6 +393,7 @@ class AuthAdminServiceTest { ) { return new AuthAdminServiceImpl( roleAccounts, + fullAccounts, userAccounts, bindings, new FakePasswordHasher(), @@ -327,4 +403,18 @@ class AuthAdminServiceTest { saltGenerator ); } + + private static InMemoryAuthFullAccountRepository fullAccountRepository(RoleUkeyBindingEntity... bindings) { + InMemoryAuthFullAccountRepository repository = new InMemoryAuthFullAccountRepository(); + for (RoleUkeyBindingEntity binding : bindings) { + repository.save(fullAccount( + RoleCode.valueOf(binding.getRoleCode()), + binding.getUid(), + binding.getRoleCode().toLowerCase() + "-full-" + binding.getUid(), + "HASH:12345678:SALT", + "SALT" + )); + } + return repository; + } } 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 353d949..c54c456 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 @@ -12,6 +12,7 @@ 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.AuthFullAccountEntity; import com.cisd.tms.modules.auth.entity.AuthSessionEntity; import com.cisd.tms.modules.auth.entity.AuthUserEntity; import com.cisd.tms.modules.auth.entity.AuthUserAccountEntity; @@ -22,6 +23,7 @@ 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.AuthFullAccountRepository; import com.cisd.tms.modules.auth.repository.AuthUserAccountRepository; import com.cisd.tms.modules.auth.repository.AuthUserRepository; import com.cisd.tms.modules.auth.repository.RoleAccountRepository; @@ -53,12 +55,13 @@ class AuthServiceTest { InMemoryRoleAccountRepository roleAccounts = new InMemoryRoleAccountRepository(); InMemoryAuthUserAccountRepository userAccounts = new InMemoryAuthUserAccountRepository(); InMemoryAuthSessionRepository sessions = new InMemoryAuthSessionRepository(); - roleAccounts.save(activeRole(RoleCode.AUDIT_ADMIN, true)); - userAccounts.save(activeUserAccount("audit-admin-01", RoleCode.AUDIT_ADMIN, "12345678", "SALT-A", 0, null)); + roleAccounts.save(activeRole(RoleCode.AUDIT_ADMIN)); + userAccounts.save(activeUserAccount("audit-admin-01", RoleCode.AUDIT_ADMIN, "12345678", "SALT-A", 0, null, true)); AuthService service = newAuthService( roleAccounts, userAccounts, + new InMemoryAuthFullAccountRepository(), sessions, new InMemoryRoleUkeyBindingRepository(), FIXED_CLOCK, @@ -77,7 +80,7 @@ class AuthServiceTest { Assertions.assertTrue(response.getNeedChangePassword()); AuthSessionEntity session = sessions.findBySessionToken("token-limited-001").orElseThrow(); Assertions.assertEquals(AuthMethod.PASSWORD.name(), session.getAuthMethod()); - Assertions.assertEquals("[\"audit-admin-01\"]", session.getAuthenticatedUsersJson()); + Assertions.assertEquals("[{\"type\":\"LIMITED\",\"uid\":null,\"username\":\"audit-admin-01\"}]", session.getAuthenticatedPrincipalsJson()); } @Test @@ -91,6 +94,7 @@ class AuthServiceTest { AuthService service = newAuthService( roleAccounts, userAccounts, + new InMemoryAuthFullAccountRepository(), new InMemoryAuthSessionRepository(), new InMemoryRoleUkeyBindingRepository(), FIXED_CLOCK, @@ -117,6 +121,7 @@ class AuthServiceTest { AuthService service = newAuthService( roleAccounts, userAccounts, + new InMemoryAuthFullAccountRepository(), new InMemoryAuthSessionRepository(), new InMemoryRoleUkeyBindingRepository(), FIXED_CLOCK, @@ -145,13 +150,18 @@ class AuthServiceTest { @Test void shouldKeepMeEndpointRoleScoped() { InMemoryRoleAccountRepository roleAccounts = new InMemoryRoleAccountRepository(); + InMemoryAuthUserAccountRepository userAccounts = new InMemoryAuthUserAccountRepository(); InMemoryAuthSessionRepository sessions = new InMemoryAuthSessionRepository(); - roleAccounts.save(activeRole(RoleCode.AUDIT_ADMIN, true)); - sessions.save(session("token-me-001", RoleCode.AUDIT_ADMIN.getCode(), AuthLevel.LIMITED.name())); + roleAccounts.save(activeRole(RoleCode.AUDIT_ADMIN)); + userAccounts.save(activeUserAccount("audit-admin-01", RoleCode.AUDIT_ADMIN, "12345678", "SALT-A", 0, null, true)); + AuthSessionEntity session = session("token-me-001", RoleCode.AUDIT_ADMIN.getCode(), AuthLevel.LIMITED.name()); + session.setAuthenticatedPrincipalsJson("[{\"type\":\"LIMITED\",\"uid\":null,\"username\":\"audit-admin-01\"}]"); + sessions.save(session); AuthService service = newAuthService( roleAccounts, - new InMemoryAuthUserAccountRepository(), + userAccounts, + new InMemoryAuthFullAccountRepository(), sessions, new InMemoryRoleUkeyBindingRepository(), FIXED_CLOCK, @@ -168,44 +178,87 @@ class AuthServiceTest { } @Test - void shouldChangeCurrentRolePasswordForActiveSession() { + void shouldChangeCurrentFullAccountPasswordForActiveSession() { InMemoryRoleAccountRepository roleAccounts = new InMemoryRoleAccountRepository(); + InMemoryAuthFullAccountRepository fullAccounts = new InMemoryAuthFullAccountRepository(); InMemoryAuthSessionRepository sessions = new InMemoryAuthSessionRepository(); - 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())); + roleAccounts.save(activeRole(RoleCode.KEY_ADMIN)); + fullAccounts.save(activeFullAccount(RoleCode.KEY_ADMIN, 1, "key-admin-full-01", "12345678", "FULL-SALT-K", 0, null, true)); + AuthSessionEntity session = session("token-change-001", RoleCode.KEY_ADMIN.getCode(), AuthLevel.FULL.name()); + session.setAuthenticatedPrincipalsJson("[{\"type\":\"FULL\",\"uid\":1,\"username\":null}]"); + sessions.save(session); AuthService service = newAuthService( roleAccounts, new InMemoryAuthUserAccountRepository(), + fullAccounts, sessions, new InMemoryRoleUkeyBindingRepository(), FIXED_CLOCK, () -> "unused" ); - service.changePassword("token-change-001", "12345678", "87654321"); + service.changeFullAccountPassword("token-change-001", 1, "12345678", "87654321"); - RoleAccountEntity changed = roleAccounts.findByRoleCode(RoleCode.KEY_ADMIN.getCode()).orElseThrow(); + AuthFullAccountEntity changed = fullAccounts.findByRoleCodeAndUid(RoleCode.KEY_ADMIN.getCode(), 1).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.getLastLoginAt()); Assertions.assertEquals(LocalDateTime.of(2026, 3, 23, 2, 0), changed.getLastActiveAt()); } @Test - void shouldThrowSessionInvalidWhenChangingPasswordWithExpiredSession() { + void shouldChangeCurrentLimitedAccountPasswordForActiveSession() { InMemoryRoleAccountRepository roleAccounts = new InMemoryRoleAccountRepository(); + InMemoryAuthUserAccountRepository userAccounts = new InMemoryAuthUserAccountRepository(); InMemoryAuthSessionRepository sessions = new InMemoryAuthSessionRepository(); - roleAccounts.save(activeRole(RoleCode.KEY_ADMIN, "12345678", "ROLE-SALT-K", 0, null, true)); + roleAccounts.save(activeRole(RoleCode.AUDIT_ADMIN)); + userAccounts.save(activeUserAccount("audit-admin-01", RoleCode.AUDIT_ADMIN, "12345678", "LIMITED-SALT-A", 0, null, true)); + AuthSessionEntity session = session("token-change-limited-001", RoleCode.AUDIT_ADMIN.getCode(), AuthLevel.LIMITED.name()); + session.setAuthenticatedPrincipalsJson("[{\"type\":\"LIMITED\",\"uid\":null,\"username\":\"audit-admin-01\"}]"); + sessions.save(session); + + AuthService service = newAuthService( + roleAccounts, + userAccounts, + new InMemoryAuthFullAccountRepository(), + sessions, + new InMemoryRoleUkeyBindingRepository(), + FIXED_CLOCK, + () -> "unused" + ); + + service.changeLimitedAccountPassword("token-change-limited-001", "audit-admin-01", "12345678", "87654321"); + + AuthUserAccountEntity changed = userAccounts.findByUsername("audit-admin-01").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.getLastLoginAt()); + Assertions.assertEquals(LocalDateTime.of(2026, 3, 23, 2, 0), changed.getLastActiveAt()); + } + + @Test + void shouldThrowSessionInvalidWhenChangingFullAccountPasswordWithExpiredSession() { + InMemoryRoleAccountRepository roleAccounts = new InMemoryRoleAccountRepository(); + InMemoryAuthFullAccountRepository fullAccounts = new InMemoryAuthFullAccountRepository(); + InMemoryAuthSessionRepository sessions = new InMemoryAuthSessionRepository(); + roleAccounts.save(activeRole(RoleCode.KEY_ADMIN)); + fullAccounts.save(activeFullAccount(RoleCode.KEY_ADMIN, 1, "key-admin-full-01", "12345678", "FULL-SALT-K", 0, null)); AuthSessionEntity expiredSession = session("token-expired-001", RoleCode.KEY_ADMIN.getCode(), AuthLevel.FULL.name()); + expiredSession.setAuthenticatedPrincipalsJson("[{\"type\":\"FULL\",\"uid\":1,\"username\":null}]"); expiredSession.setExpiresAt(LocalDateTime.of(2026, 3, 23, 1, 59)); sessions.save(expiredSession); AuthService service = newAuthService( roleAccounts, new InMemoryAuthUserAccountRepository(), + fullAccounts, sessions, new InMemoryRoleUkeyBindingRepository(), FIXED_CLOCK, @@ -213,7 +266,7 @@ class AuthServiceTest { ); BizException exception = Assertions.assertThrows(BizException.class, - () -> service.changePassword("token-expired-001", "12345678", "87654321")); + () -> service.changeFullAccountPassword("token-expired-001", 1, "12345678", "87654321")); Assertions.assertEquals(ErrorCode.SESSION_INVALID.getCode(), exception.getCode()); Assertions.assertEquals("session expired", exception.getMessage()); @@ -233,6 +286,7 @@ class AuthServiceTest { roleAccounts, new InMemoryLegacyAuthUserRepository(), userAccounts, + new InMemoryAuthFullAccountRepository(), new InMemoryAuthSessionRepository(), new InMemoryRoleUkeyBindingRepository(), new FakePasswordHasher(), @@ -262,13 +316,93 @@ class AuthServiceTest { } @Test - void shouldVerifyUkeyProofsBeforeCreatingFullSession() { + void shouldKickPreviousLimitedSessionForSameRoleAndSameAccounts() { InMemoryRoleAccountRepository roleAccounts = new InMemoryRoleAccountRepository(); InMemoryAuthUserAccountRepository userAccounts = new InMemoryAuthUserAccountRepository(); InMemoryAuthSessionRepository sessions = new InMemoryAuthSessionRepository(); + 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)); + + AuthSessionEntity previous = session("token-limited-old", RoleCode.SUPER_ADMIN.getCode(), AuthLevel.LIMITED.name()); + previous.setAuthenticatedPrincipalsJson("[{\"type\":\"LIMITED\",\"uid\":null,\"username\":\"super-admin-01\"},{\"type\":\"LIMITED\",\"uid\":null,\"username\":\"super-admin-02\"}]"); + sessions.save(previous); + + AuthService service = newAuthService( + roleAccounts, + userAccounts, + new InMemoryAuthFullAccountRepository(), + sessions, + new InMemoryRoleUkeyBindingRepository(), + FIXED_CLOCK, + new SequenceSessionTokenGenerator("token-limited-new") + ); + + LoginRequest request = new LoginRequest(); + request.setRoleCode(RoleCode.SUPER_ADMIN.getCode()); + request.setAccounts(List.of( + account("super-admin-02", "22222222"), + account("super-admin-01", "11111111") + )); + + LoginResponse response = service.login(request); + + Assertions.assertEquals("token-limited-new", response.getToken()); + AuthSessionEntity expired = sessions.findBySessionToken("token-limited-old").orElseThrow(); + Assertions.assertEquals(LocalDateTime.of(2026, 3, 23, 2, 0), expired.getLogoutAt()); + Assertions.assertEquals(LocalDateTime.of(2026, 3, 23, 2, 0), expired.getExpiresAt()); + AuthSessionEntity current = sessions.findBySessionToken("token-limited-new").orElseThrow(); + Assertions.assertEquals("[{\"type\":\"LIMITED\",\"uid\":null,\"username\":\"super-admin-01\"},{\"type\":\"LIMITED\",\"uid\":null,\"username\":\"super-admin-02\"}]", current.getAuthenticatedPrincipalsJson()); + } + + @Test + void shouldKickPreviousFullSessionForSameRole() { + InMemoryRoleAccountRepository roleAccounts = new InMemoryRoleAccountRepository(); + InMemoryAuthUserAccountRepository userAccounts = new InMemoryAuthUserAccountRepository(); + InMemoryAuthFullAccountRepository fullAccounts = new InMemoryAuthFullAccountRepository(); + InMemoryAuthSessionRepository sessions = new InMemoryAuthSessionRepository(); InMemoryRoleUkeyBindingRepository ukeyBindings = new InMemoryRoleUkeyBindingRepository(); - roleAccounts.save(activeRole(RoleCode.KEY_ADMIN, "12345678", "ROLE-SALT-K")); - userAccounts.save(activeUserAccount("key-admin-01", RoleCode.KEY_ADMIN, "12345678", "SALT-K", 0, null)); + roleAccounts.save(activeRole(RoleCode.KEY_ADMIN)); + fullAccounts.save(activeFullAccount(RoleCode.KEY_ADMIN, 1, "key-admin-full-01", "12345678", "SALT-K", 0, null)); + ukeyBindings.save(activeBinding(RoleCode.KEY_ADMIN, 1, "UK-1", "PUB-1")); + + AuthSessionEntity previous = session("token-full-old", RoleCode.KEY_ADMIN.getCode(), AuthLevel.FULL.name()); + sessions.save(previous); + + AuthService service = newAuthService( + roleAccounts, + userAccounts, + fullAccounts, + sessions, + ukeyBindings, + FIXED_CLOCK, + new SequenceSessionTokenGenerator("token-full-new") + ); + + LoginRequest request = new LoginRequest(); + request.setRoleCode(RoleCode.KEY_ADMIN.getCode()); + request.setFullAccounts(List.of(fullAccount(1, "12345678"))); + request.setUkeySerials(List.of("UK-1")); + + LoginResponse response = service.login(request); + + Assertions.assertEquals("token-full-new", response.getToken()); + AuthSessionEntity expired = sessions.findBySessionToken("token-full-old").orElseThrow(); + Assertions.assertEquals(LocalDateTime.of(2026, 3, 23, 2, 0), expired.getLogoutAt()); + Assertions.assertEquals(LocalDateTime.of(2026, 3, 23, 2, 0), expired.getExpiresAt()); + AuthSessionEntity current = sessions.findBySessionToken("token-full-new").orElseThrow(); + Assertions.assertEquals(AuthLevel.FULL.name(), current.getAuthLevel()); + } + + @Test + void shouldVerifyUkeyProofsBeforeCreatingFullSession() { + InMemoryRoleAccountRepository roleAccounts = new InMemoryRoleAccountRepository(); + InMemoryAuthUserAccountRepository userAccounts = new InMemoryAuthUserAccountRepository(); + InMemoryAuthFullAccountRepository fullAccounts = new InMemoryAuthFullAccountRepository(); + InMemoryAuthSessionRepository sessions = new InMemoryAuthSessionRepository(); + InMemoryRoleUkeyBindingRepository ukeyBindings = new InMemoryRoleUkeyBindingRepository(); + roleAccounts.save(activeRole(RoleCode.KEY_ADMIN)); + fullAccounts.save(activeFullAccount(RoleCode.KEY_ADMIN, 1, "key-admin-full-01", "12345678", "SALT-K", 0, null)); ukeyBindings.save(activeBinding(RoleCode.KEY_ADMIN, 1, "UK-1", "PUB-1")); LmkService lmkService = org.mockito.Mockito.mock(LmkService.class); @@ -282,6 +416,7 @@ class AuthServiceTest { roleAccounts, new InMemoryLegacyAuthUserRepository(), userAccounts, + fullAccounts, sessions, ukeyBindings, new FakePasswordHasher(), @@ -303,9 +438,8 @@ class AuthServiceTest { UkeyLoginRequest request = new UkeyLoginRequest(); request.setRoleCode(RoleCode.KEY_ADMIN.getCode()); - request.setRolePassword("12345678"); - request.setUkeyProofs(List.of( - proof("PUB-1", 1, "RB-1", "ISSUE-1", "LOGIN-DATA-1", "LOGIN-SIGN-1") + request.setLoginFactors(List.of( + proofWithPassword("PUB-1", 1, "12345678", "RB-1", "ISSUE-1", "LOGIN-DATA-1", "LOGIN-SIGN-1") )); LoginResponse response = service.ukeyLogin(request); @@ -321,12 +455,13 @@ class AuthServiceTest { } @Test - void shouldLockRolePasswordDuringUkeyLoginWhenRolePasswordIsWrong() { + void shouldLockFullAccountDuringUkeyLoginWhenSeatPasswordIsWrong() { InMemoryRoleAccountRepository roleAccounts = new InMemoryRoleAccountRepository(); InMemoryAuthUserAccountRepository userAccounts = new InMemoryAuthUserAccountRepository(); + InMemoryAuthFullAccountRepository fullAccounts = new InMemoryAuthFullAccountRepository(); 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)); + roleAccounts.save(activeRole(RoleCode.KEY_ADMIN)); + fullAccounts.save(activeFullAccount(RoleCode.KEY_ADMIN, 1, "key-admin-full-01", "12345678", "SALT-K", 4, null)); ukeyBindings.save(activeBinding(RoleCode.KEY_ADMIN, 1, "UK-1", "PUB-1")); LmkService lmkService = org.mockito.Mockito.mock(LmkService.class); @@ -339,6 +474,7 @@ class AuthServiceTest { roleAccounts, new InMemoryLegacyAuthUserRepository(), userAccounts, + fullAccounts, new InMemoryAuthSessionRepository(), ukeyBindings, new FakePasswordHasher(), @@ -355,62 +491,150 @@ class AuthServiceTest { 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") + request.setLoginFactors(List.of( + proofWithPassword("PUB-1", 1, "bad-password", "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("full account is locked", exception.getMessage()); + AuthFullAccountEntity updated = fullAccounts.findByRoleCodeAndUid(RoleCode.KEY_ADMIN.getCode(), 1).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 activeRole(RoleCode roleCode) { - return activeRole(roleCode, false); + @Test + void shouldLoginSuperAdminWithPerUidPasswordsDuringUkeyLogin() { + InMemoryRoleAccountRepository roleAccounts = new InMemoryRoleAccountRepository(); + InMemoryAuthUserAccountRepository userAccounts = new InMemoryAuthUserAccountRepository(); + InMemoryAuthFullAccountRepository fullAccounts = new InMemoryAuthFullAccountRepository(); + InMemoryAuthSessionRepository sessions = new InMemoryAuthSessionRepository(); + InMemoryRoleUkeyBindingRepository ukeyBindings = new InMemoryRoleUkeyBindingRepository(); + roleAccounts.save(activeRole(RoleCode.SUPER_ADMIN)); + fullAccounts.save(activeFullAccount(RoleCode.SUPER_ADMIN, 1, "super-admin-full-01", "11111111", "SALT-S1", 0, null)); + fullAccounts.save(activeFullAccount(RoleCode.SUPER_ADMIN, 2, "super-admin-full-02", "22222222", "SALT-S2", 0, null)); + ukeyBindings.save(activeBinding(RoleCode.SUPER_ADMIN, 1, "UK-1", "PUB-1")); + ukeyBindings.save(activeBinding(RoleCode.SUPER_ADMIN, 2, "UK-2", "PUB-2")); + + 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"); + org.mockito.Mockito.when(randomService.issue(RoleCode.SUPER_ADMIN.getCode(), 2)).thenReturn(List.of("RB-1", "RB-2")); + + AuthService service = new AuthServiceImpl( + roleAccounts, + new InMemoryLegacyAuthUserRepository(), + userAccounts, + fullAccounts, + sessions, + ukeyBindings, + new FakePasswordHasher(), + lmkService, + randomService, + verifier, + new InMemoryCaptchaService(), + new ObjectMapper(), + FIXED_CLOCK, + () -> "token-full-super-001", + () -> "salt-full-super-001", + new AuthPolicyServiceImpl() + ); + + UkeyLoginRandomRequest randomRequest = new UkeyLoginRandomRequest(); + randomRequest.setRoleCode(RoleCode.SUPER_ADMIN.getCode()); + UkeyLoginRandomResponse ignored = service.issueUkeyLoginRandoms(randomRequest); + Assertions.assertNotNull(ignored); + + UkeyLoginRequest request = new UkeyLoginRequest(); + request.setRoleCode(RoleCode.SUPER_ADMIN.getCode()); + request.setLoginFactors(List.of( + proofWithPassword("PUB-1", 1, "11111111", "RB-1", "ISSUE-1", "LOGIN-DATA-1", "LOGIN-SIGN-1"), + proofWithPassword("PUB-2", 2, "22222222", "RB-2", "ISSUE-2", "LOGIN-DATA-2", "LOGIN-SIGN-2") + )); + + LoginResponse response = service.ukeyLogin(request); + + Assertions.assertEquals("token-full-super-001", response.getToken()); + Assertions.assertEquals(AuthLevel.FULL.name(), response.getAuthLevel()); + AuthSessionEntity session = sessions.findBySessionToken("token-full-super-001").orElseThrow(); + Assertions.assertEquals("[{\"type\":\"FULL\",\"uid\":1,\"username\":null},{\"type\":\"FULL\",\"uid\":2,\"username\":null}]", session.getAuthenticatedPrincipalsJson()); + org.mockito.Mockito.verify(randomService).assertIssued(RoleCode.SUPER_ADMIN.getCode(), List.of("RB-1", "RB-2")); + org.mockito.Mockito.verify(verifier).verifyIssuedBinding( + "{\"pubKey\":\"PUB-1\",\"authKeyPair\":\"IK-PUB-001\",\"role\":\"SUPER_ADMIN\",\"uid\":1}", + "ISSUE-1" + ); + org.mockito.Mockito.verify(verifier).verifyIssuedBinding( + "{\"pubKey\":\"PUB-2\",\"authKeyPair\":\"IK-PUB-001\",\"role\":\"SUPER_ADMIN\",\"uid\":2}", + "ISSUE-2" + ); } - private static RoleAccountEntity activeRole(RoleCode roleCode, boolean needChangePassword) { + @Test + void shouldLockOnlyMappedFullAccountDuringSuperAdminUkeyLoginWhenPasswordIsWrong() { + InMemoryRoleAccountRepository roleAccounts = new InMemoryRoleAccountRepository(); + InMemoryAuthUserAccountRepository userAccounts = new InMemoryAuthUserAccountRepository(); + InMemoryAuthFullAccountRepository fullAccounts = new InMemoryAuthFullAccountRepository(); + InMemoryRoleUkeyBindingRepository ukeyBindings = new InMemoryRoleUkeyBindingRepository(); + roleAccounts.save(activeRole(RoleCode.SUPER_ADMIN)); + fullAccounts.save(activeFullAccount(RoleCode.SUPER_ADMIN, 1, "super-admin-full-01", "11111111", "SALT-S1", 4, null)); + fullAccounts.save(activeFullAccount(RoleCode.SUPER_ADMIN, 2, "super-admin-full-02", "22222222", "SALT-S2", 0, null)); + ukeyBindings.save(activeBinding(RoleCode.SUPER_ADMIN, 1, "UK-1", "PUB-1")); + ukeyBindings.save(activeBinding(RoleCode.SUPER_ADMIN, 2, "UK-2", "PUB-2")); + + LmkService lmkService = org.mockito.Mockito.mock(LmkService.class); + UkeyLoginRandomService randomService = org.mockito.Mockito.mock(UkeyLoginRandomService.class); + CompatUkeyVerifier verifier = org.mockito.Mockito.mock(CompatUkeyVerifier.class); + org.mockito.Mockito.when(lmkService.getMasterKeyStatus()).thenReturn(MasterKeyStatus.NORMAL.getDetail("MAC-001")); + org.mockito.Mockito.when(lmkService.exportIkPublicKeyHex()).thenReturn("IK-PUB-001"); + + AuthService service = new AuthServiceImpl( + roleAccounts, + new InMemoryLegacyAuthUserRepository(), + userAccounts, + fullAccounts, + new InMemoryAuthSessionRepository(), + ukeyBindings, + new FakePasswordHasher(), + lmkService, + randomService, + verifier, + new InMemoryCaptchaService(), + new ObjectMapper(), + FIXED_CLOCK, + () -> "unused", + () -> "salt-full-super-002", + new AuthPolicyServiceImpl() + ); + + UkeyLoginRequest request = new UkeyLoginRequest(); + request.setRoleCode(RoleCode.SUPER_ADMIN.getCode()); + request.setLoginFactors(List.of( + proofWithPassword("PUB-1", 1, "bad-password", "RB-1", "ISSUE-1", "LOGIN-DATA-1", "LOGIN-SIGN-1"), + proofWithPassword("PUB-2", 2, "22222222", "RB-2", "ISSUE-2", "LOGIN-DATA-2", "LOGIN-SIGN-2") + )); + + BizException exception = Assertions.assertThrows(BizException.class, () -> service.ukeyLogin(request)); + + Assertions.assertEquals("full account is locked", exception.getMessage()); + AuthFullAccountEntity locked = fullAccounts.findByRoleCodeAndUid(RoleCode.SUPER_ADMIN.getCode(), 1).orElseThrow(); + AuthFullAccountEntity untouched = fullAccounts.findByRoleCodeAndUid(RoleCode.SUPER_ADMIN.getCode(), 2).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()); + } + + private static RoleAccountEntity activeRole(RoleCode roleCode) { RoleAccountEntity entity = new RoleAccountEntity(); entity.setId((long) roleCode.ordinal() + 1); entity.setRoleCode(roleCode.getCode()); entity.setDisplayName(roleCode.getDisplayName()); entity.setRequiredUkeyCount(roleCode.getRequiredUkeyCount()); 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; } @@ -421,6 +645,18 @@ class AuthServiceTest { String salt, int failedCount, LocalDateTime lockedUntil + ) { + return activeUserAccount(username, roleCode, password, salt, failedCount, lockedUntil, false); + } + + private static AuthUserAccountEntity activeUserAccount( + String username, + RoleCode roleCode, + String password, + String salt, + int failedCount, + LocalDateTime lockedUntil, + boolean needChangePassword ) { AuthUserAccountEntity entity = new AuthUserAccountEntity(); entity.setId((long) (username.hashCode() & Integer.MAX_VALUE)); @@ -430,6 +666,44 @@ class AuthServiceTest { entity.setPasswordSalt(salt); entity.setPasswordHash("HASH:" + password + ":" + salt); entity.setStatus(RoleAccountStatus.ACTIVE.name()); + entity.setNeedChangePassword(needChangePassword); + entity.setFailedCount(failedCount); + entity.setLockedUntil(lockedUntil); + return entity; + } + + private static AuthFullAccountEntity activeFullAccount( + RoleCode roleCode, + int uid, + String accountName, + String password, + String salt, + int failedCount, + LocalDateTime lockedUntil + ) { + return activeFullAccount(roleCode, uid, accountName, password, salt, failedCount, lockedUntil, false); + } + + private static AuthFullAccountEntity activeFullAccount( + RoleCode roleCode, + int uid, + String accountName, + String password, + String salt, + int failedCount, + LocalDateTime lockedUntil, + boolean needChangePassword + ) { + AuthFullAccountEntity entity = new AuthFullAccountEntity(); + entity.setId((long) (accountName.hashCode() & Integer.MAX_VALUE)); + entity.setRoleCode(roleCode.getCode()); + entity.setUid(uid); + entity.setAccountName(accountName); + entity.setDisplayName(accountName); + entity.setPasswordSalt(salt); + entity.setPasswordHash("HASH:" + password + ":" + salt); + entity.setStatus(RoleAccountStatus.ACTIVE.name()); + entity.setNeedChangePassword(needChangePassword); entity.setFailedCount(failedCount); entity.setLockedUntil(lockedUntil); return entity; @@ -442,6 +716,13 @@ class AuthServiceTest { return request; } + private static com.cisd.tms.modules.auth.dto.FullLoginAccountRequest fullAccount(int uid, String password) { + com.cisd.tms.modules.auth.dto.FullLoginAccountRequest request = new com.cisd.tms.modules.auth.dto.FullLoginAccountRequest(); + request.setUid(uid); + request.setPassword(password); + return request; + } + private static RoleUkeyBindingEntity activeBinding( RoleCode roleCode, int uid, @@ -476,6 +757,20 @@ class AuthServiceTest { return proof; } + private static UkeyLoginProof proofWithPassword( + String pubKey, + int uid, + String password, + String serverRandom, + String issueSignature, + String loginPayload, + String loginSignature + ) { + UkeyLoginProof proof = proof(pubKey, uid, serverRandom, issueSignature, loginPayload, loginSignature); + proof.setPassword(password); + return proof; + } + private static AuthSessionEntity session(String token, String roleCode, String authLevel) { AuthSessionEntity entity = new AuthSessionEntity(); entity.setId((long) (token.hashCode() & Integer.MAX_VALUE)); @@ -491,6 +786,7 @@ class AuthServiceTest { private static AuthService newAuthService( InMemoryRoleAccountRepository roleAccounts, InMemoryAuthUserAccountRepository userAccounts, + InMemoryAuthFullAccountRepository fullAccounts, InMemoryAuthSessionRepository sessions, InMemoryRoleUkeyBindingRepository ukeyBindings, Clock clock, @@ -500,6 +796,7 @@ class AuthServiceTest { roleAccounts, new InMemoryLegacyAuthUserRepository(), userAccounts, + fullAccounts, sessions, ukeyBindings, new FakePasswordHasher(), @@ -528,6 +825,23 @@ class AuthServiceTest { } } + private static class SequenceSessionTokenGenerator implements SessionTokenGenerator { + private final List tokens; + private int index = 0; + + private SequenceSessionTokenGenerator(String... tokens) { + this.tokens = List.of(tokens); + } + + @Override + public String nextToken() { + if (index >= tokens.size()) { + return tokens.get(tokens.size() - 1); + } + return tokens.get(index++); + } + } + private static class InMemoryRoleAccountRepository implements RoleAccountRepository { private final Map store = new ConcurrentHashMap<>(); @@ -574,6 +888,33 @@ class AuthServiceTest { } } + private static class InMemoryAuthFullAccountRepository implements AuthFullAccountRepository { + private final Map store = new ConcurrentHashMap<>(); + + @Override + public Optional findByRoleCodeAndUid(String roleCode, Integer uid) { + return Optional.ofNullable(store.get(roleCode + "#" + uid)); + } + + @Override + public List findByRoleCode(String roleCode) { + return store.values().stream() + .filter(entity -> roleCode.equals(entity.getRoleCode())) + .sorted(Comparator.comparing(AuthFullAccountEntity::getUid)) + .toList(); + } + + @Override + public void save(AuthFullAccountEntity entity) { + store.put(entity.getRoleCode() + "#" + entity.getUid(), entity); + } + + @Override + public void update(AuthFullAccountEntity entity) { + store.put(entity.getRoleCode() + "#" + entity.getUid(), entity); + } + } + private static class InMemoryLegacyAuthUserRepository implements AuthUserRepository { @Override @@ -590,6 +931,32 @@ class AuthServiceTest { return Optional.ofNullable(store.get(sessionToken)); } + @Override + public List findActiveByRoleCodeAndAuthLevel(String roleCode, String authLevel, LocalDateTime now) { + return store.values().stream() + .filter(entity -> roleCode.equals(entity.getRoleCode())) + .filter(entity -> authLevel.equals(entity.getAuthLevel())) + .filter(entity -> entity.getLogoutAt() == null) + .filter(entity -> entity.getExpiresAt() != null && entity.getExpiresAt().isAfter(now)) + .toList(); + } + + @Override + public List findActiveByRoleCodeAndAuthLevelAndAuthenticatedPrincipalsJson( + String roleCode, + String authLevel, + String authenticatedPrincipalsJson, + LocalDateTime now + ) { + return store.values().stream() + .filter(entity -> roleCode.equals(entity.getRoleCode())) + .filter(entity -> authLevel.equals(entity.getAuthLevel())) + .filter(entity -> java.util.Objects.equals(authenticatedPrincipalsJson, entity.getAuthenticatedPrincipalsJson())) + .filter(entity -> entity.getLogoutAt() == null) + .filter(entity -> entity.getExpiresAt() != null && entity.getExpiresAt().isAfter(now)) + .toList(); + } + @Override public void save(AuthSessionEntity entity) { store.put(entity.getSessionToken(), entity); diff --git a/src/test/java/com/cisd/tms/modules/auth/service/impl/PcieCompatUkeyVerifierTest.java b/src/test/java/com/cisd/tms/modules/auth/service/impl/PcieCompatUkeyVerifierTest.java new file mode 100644 index 0000000..a694101 --- /dev/null +++ b/src/test/java/com/cisd/tms/modules/auth/service/impl/PcieCompatUkeyVerifierTest.java @@ -0,0 +1,119 @@ +package com.cisd.tms.modules.auth.service.impl; + +import com.cisd.tms.common.exception.BizException; +import com.cisd.tms.modules.mk.service.LmkService; +import java.io.StringWriter; +import java.nio.charset.StandardCharsets; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.Security; +import java.security.Signature; +import java.security.interfaces.ECPublicKey; +import java.security.spec.ECGenParameterSpec; +import java.util.Base64; +import org.bouncycastle.util.encoders.Hex; +import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.bouncycastle.util.io.pem.PemObject; +import org.bouncycastle.util.io.pem.PemWriter; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +class PcieCompatUkeyVerifierTest { + + private static final String PROVIDER = "BC"; + + static { + if (Security.getProvider(PROVIDER) == null) { + Security.addProvider(new BouncyCastleProvider()); + } + } + + @Test + void shouldVerifyLoginSignatureWithPemPublicKeyAndBase64DerSignature() throws Exception { + KeyPair keyPair = generateSm2KeyPair(); + String payload = "LOGIN-DATA-1"; + String pemPublicKey = toPem("PUBLIC KEY", keyPair.getPublic().getEncoded()); + String signatureBase64 = Base64.getEncoder().encodeToString(signDer(payload, keyPair)); + + verifier().verifyLoginSignature(pemPublicKey, payload, signatureBase64); + } + + @Test + void shouldVerifyLoginSignatureWithRawPointHexAndRawRsHexSignature() throws Exception { + KeyPair keyPair = generateSm2KeyPair(); + String payload = "LOGIN-DATA-2"; + String rawPointHex = Hex.toHexString(uncompressedPoint((ECPublicKey) keyPair.getPublic())); + String rawSignatureHex = Hex.toHexString(derToRawRs(signDer(payload, keyPair))); + + verifier().verifyLoginSignature(rawPointHex, payload, rawSignatureHex); + } + + @Test + void shouldRejectWhenLoginSignatureDoesNotMatch() throws Exception { + KeyPair keyPair = generateSm2KeyPair(); + String payload = "LOGIN-DATA-3"; + String base64PublicKey = Base64.getEncoder().encodeToString(keyPair.getPublic().getEncoded()); + String signatureBase64 = Base64.getEncoder().encodeToString(signDer("OTHER-DATA", keyPair)); + + BizException exception = Assertions.assertThrows( + BizException.class, + () -> verifier().verifyLoginSignature(base64PublicKey, payload, signatureBase64) + ); + + Assertions.assertEquals("login signature verification failed", exception.getMessage()); + } + + private PcieCompatUkeyVerifier verifier() { + return new PcieCompatUkeyVerifier(Mockito.mock(LmkService.class)); + } + + private KeyPair generateSm2KeyPair() throws Exception { + KeyPairGenerator generator = KeyPairGenerator.getInstance("EC", PROVIDER); + generator.initialize(new ECGenParameterSpec("sm2p256v1")); + return generator.generateKeyPair(); + } + + private byte[] signDer(String data, KeyPair keyPair) throws Exception { + Signature signature = Signature.getInstance("SM3withSM2", PROVIDER); + signature.initSign(keyPair.getPrivate()); + signature.update(data.getBytes(StandardCharsets.UTF_8)); + return signature.sign(); + } + + private byte[] uncompressedPoint(ECPublicKey publicKey) { + byte[] x = toFixed32(publicKey.getW().getAffineX().toByteArray()); + byte[] y = toFixed32(publicKey.getW().getAffineY().toByteArray()); + byte[] result = new byte[65]; + result[0] = 0x04; + System.arraycopy(x, 0, result, 1, 32); + System.arraycopy(y, 0, result, 33, 32); + return result; + } + + private byte[] derToRawRs(byte[] derSignature) throws Exception { + org.bouncycastle.asn1.ASN1Sequence sequence = + (org.bouncycastle.asn1.ASN1Sequence) org.bouncycastle.asn1.ASN1Primitive.fromByteArray(derSignature); + byte[] r = toFixed32(((org.bouncycastle.asn1.ASN1Integer) sequence.getObjectAt(0)).getPositiveValue().toByteArray()); + byte[] s = toFixed32(((org.bouncycastle.asn1.ASN1Integer) sequence.getObjectAt(1)).getPositiveValue().toByteArray()); + byte[] result = new byte[64]; + System.arraycopy(r, 0, result, 0, 32); + System.arraycopy(s, 0, result, 32, 32); + return result; + } + + private byte[] toFixed32(byte[] value) { + byte[] result = new byte[32]; + int copyLength = Math.min(value.length, 32); + System.arraycopy(value, value.length - copyLength, result, 32 - copyLength, copyLength); + return result; + } + + private String toPem(String type, byte[] content) throws Exception { + StringWriter writer = new StringWriter(); + try (PemWriter pemWriter = new PemWriter(writer)) { + pemWriter.writeObject(new PemObject(type, content)); + } + return writer.toString(); + } +} diff --git a/src/test/java/com/cisd/tms/security/internal/ReplayProtectedEndpointsTest.java b/src/test/java/com/cisd/tms/security/internal/ReplayProtectedEndpointsTest.java index f7524a2..c2f3847 100644 --- a/src/test/java/com/cisd/tms/security/internal/ReplayProtectedEndpointsTest.java +++ b/src/test/java/com/cisd/tms/security/internal/ReplayProtectedEndpointsTest.java @@ -35,8 +35,10 @@ class ReplayProtectedEndpointsTest { @Test void shouldProtectSensitiveControllersAndMethods() throws Exception { - Assertions.assertNotNull(annotation(AuthController.class, "changePassword", - com.cisd.tms.modules.auth.dto.ChangePasswordRequest.class, jakarta.servlet.http.HttpServletRequest.class)); + Assertions.assertNotNull(annotation(AuthController.class, "changeFullAccountPassword", + Integer.class, com.cisd.tms.modules.auth.dto.ChangePasswordRequest.class, jakarta.servlet.http.HttpServletRequest.class)); + Assertions.assertNotNull(annotation(AuthController.class, "changeLimitedAccountPassword", + String.class, com.cisd.tms.modules.auth.dto.ChangePasswordRequest.class, jakarta.servlet.http.HttpServletRequest.class)); Assertions.assertNotNull(annotation(DeviceController.class, "restart", jakarta.servlet.http.HttpServletRequest.class)); Assertions.assertNotNull(AnnotatedElementUtils.findMergedAnnotation(AuthAdminController.class, ReplayProtected.class));