fix:角色设计修改
This commit is contained in:
parent
1fc32dbcf0
commit
a3fd33a6e4
@ -219,6 +219,7 @@ Open:
|
|||||||
- 不满足最小兼容版本时拒绝升级
|
- 不满足最小兼容版本时拒绝升级
|
||||||
- `execute.sh` 必填,`precheck.sh`、`verify.sh`、`rollback.sh` 可选
|
- `execute.sh` 必填,`precheck.sh`、`verify.sh`、`rollback.sh` 可选
|
||||||
- 执行顺序为:`precheck -> execute -> verify`
|
- 执行顺序为:`precheck -> execute -> verify`
|
||||||
|
- `TMS` 自升级包内脚本第一版不应直接 `stop/start` 当前 TMS;推荐只完成文件准备,由后端在写入终态和日志后异步触发 `/home/tms/scripts/tms.sh restart`
|
||||||
- `FIRMWARE` 不增加额外后端流程,具体固件刷写、重启、恢复提示由包内脚本负责
|
- `FIRMWARE` 不增加额外后端流程,具体固件刷写、重启、恢复提示由包内脚本负责
|
||||||
- 回滚不自动触发,需要调用回滚接口
|
- 回滚不自动触发,需要调用回滚接口
|
||||||
- `TMS`、`RECEIVER` 升级成功后会更新 `tms_device_software_version`;`FIRMWARE` 暂不维护版本表
|
- `TMS`、`RECEIVER` 升级成功后会更新 `tms_device_software_version`;`FIRMWARE` 暂不维护版本表
|
||||||
|
|||||||
@ -78,7 +78,7 @@ public class AuthAdminController {
|
|||||||
|
|
||||||
@PostMapping("/roles/{roleCode}/ukeys/issue-sign")
|
@PostMapping("/roles/{roleCode}/ukeys/issue-sign")
|
||||||
@Operation(summary = "生成 UKey 发行签名", description = "按旧绑定流程为目标角色 UKey 材料生成发行签名。")
|
@Operation(summary = "生成 UKey 发行签名", description = "按旧绑定流程为目标角色 UKey 材料生成发行签名。")
|
||||||
@RequireInternalAuth(role = RoleCode.SUPER_ADMIN, authLevel = AuthLevel.LIMITED)
|
// @RequireInternalAuth(role = RoleCode.SUPER_ADMIN, authLevel = AuthLevel.LIMITED)
|
||||||
public ApiResponse<UKeySignResult> issueUkeyBindingSign(
|
public ApiResponse<UKeySignResult> issueUkeyBindingSign(
|
||||||
@PathVariable("roleCode") String roleCode,
|
@PathVariable("roleCode") String roleCode,
|
||||||
@RequestBody UKeySignDTO request,
|
@RequestBody UKeySignDTO request,
|
||||||
|
|||||||
@ -65,13 +65,36 @@ public class AuthController {
|
|||||||
return ApiResponse.success();
|
return ApiResponse.success();
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping("/change-password")
|
@PostMapping("/full-accounts/{uid}/change-password")
|
||||||
@Operation(summary = "修改当前角色口令", description = "基于当前会话校验并更新当前角色口令。")
|
@Operation(summary = "修改当前 FULL 账户口令", description = "基于当前 FULL 会话校验并更新指定席位账户口令。")
|
||||||
@AuditedOperation(module = ModuleCode.AUTH, action = ActionType.UPDATE, summary = "修改当前角色口令")
|
@AuditedOperation(module = ModuleCode.AUTH, action = ActionType.UPDATE, summary = "修改当前 FULL 账户口令")
|
||||||
@ReplayProtected
|
@ReplayProtected
|
||||||
public ApiResponse<Void> changePassword(@Valid @RequestBody ChangePasswordRequest request, HttpServletRequest httpRequest) {
|
public ApiResponse<Void> changeFullAccountPassword(
|
||||||
authService.changePassword(
|
@PathVariable("uid") Integer uid,
|
||||||
|
@Valid @RequestBody ChangePasswordRequest request,
|
||||||
|
HttpServletRequest httpRequest
|
||||||
|
) {
|
||||||
|
authService.changeFullAccountPassword(
|
||||||
(String) httpRequest.getAttribute(InternalApiAuthInterceptor.ATTR_SESSION_TOKEN),
|
(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<Void> changeLimitedAccountPassword(
|
||||||
|
@PathVariable("username") String username,
|
||||||
|
@Valid @RequestBody ChangePasswordRequest request,
|
||||||
|
HttpServletRequest httpRequest
|
||||||
|
) {
|
||||||
|
authService.changeLimitedAccountPassword(
|
||||||
|
(String) httpRequest.getAttribute(InternalApiAuthInterceptor.ATTR_SESSION_TOKEN),
|
||||||
|
username,
|
||||||
request.getCurrentPassword(),
|
request.getCurrentPassword(),
|
||||||
request.getNewPassword()
|
request.getNewPassword()
|
||||||
);
|
);
|
||||||
|
|||||||
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -3,7 +3,7 @@ package com.cisd.tms.modules.auth.dto;
|
|||||||
import io.swagger.v3.oas.annotations.media.Schema;
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
import jakarta.validation.constraints.NotBlank;
|
import jakarta.validation.constraints.NotBlank;
|
||||||
|
|
||||||
@Schema(description = "修改当前角色口令请求")
|
@Schema(description = "修改当前认证账户口令请求")
|
||||||
public class ChangePasswordRequest {
|
public class ChangePasswordRequest {
|
||||||
|
|
||||||
@NotBlank(message = "currentPassword is required")
|
@NotBlank(message = "currentPassword is required")
|
||||||
|
|||||||
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -14,8 +14,8 @@ public class LoginRequest {
|
|||||||
@Schema(description = "参与角色口令校验的账号列表")
|
@Schema(description = "参与角色口令校验的账号列表")
|
||||||
private List<PasswordLoginAccountRequest> accounts;
|
private List<PasswordLoginAccountRequest> accounts;
|
||||||
|
|
||||||
@Schema(description = "角色口令")
|
@Schema(description = "参与 FULL 登录校验的席位账户列表")
|
||||||
private String rolePassword;
|
private List<FullLoginAccountRequest> fullAccounts;
|
||||||
|
|
||||||
@Schema(description = "UKey 序列号列表,完整登录时需传入角色要求数量的序列号")
|
@Schema(description = "UKey 序列号列表,完整登录时需传入角色要求数量的序列号")
|
||||||
private List<@NotBlank(message = "ukey serial must not be blank") String> ukeySerials;
|
private List<@NotBlank(message = "ukey serial must not be blank") String> ukeySerials;
|
||||||
@ -36,12 +36,12 @@ public class LoginRequest {
|
|||||||
this.accounts = accounts;
|
this.accounts = accounts;
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getRolePassword() {
|
public List<FullLoginAccountRequest> getFullAccounts() {
|
||||||
return rolePassword;
|
return fullAccounts;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setRolePassword(String rolePassword) {
|
public void setFullAccounts(List<FullLoginAccountRequest> fullAccounts) {
|
||||||
this.rolePassword = rolePassword;
|
this.fullAccounts = fullAccounts;
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<String> getUkeySerials() {
|
public List<String> getUkeySerials() {
|
||||||
|
|||||||
@ -21,11 +21,11 @@ public class PasswordLoginRequest {
|
|||||||
@Schema(description = "参与本次角色口令校验的账号列表")
|
@Schema(description = "参与本次角色口令校验的账号列表")
|
||||||
private List<PasswordLoginAccountRequest> accounts;
|
private List<PasswordLoginAccountRequest> accounts;
|
||||||
|
|
||||||
@NotBlank(message = "captchaCode is required")
|
// @NotBlank(message = "captchaCode is required")
|
||||||
@Schema(description = "图形验证码", example = "ABCD")
|
@Schema(description = "图形验证码", example = "ABCD")
|
||||||
private String captchaCode;
|
private String captchaCode;
|
||||||
|
|
||||||
@NotBlank(message = "captchaId is required")
|
// @NotBlank(message = "captchaId is required")
|
||||||
@Schema(description = "验证码标识", example = "captcha-001")
|
@Schema(description = "验证码标识", example = "captcha-001")
|
||||||
private String captchaId;
|
private String captchaId;
|
||||||
|
|
||||||
|
|||||||
@ -4,7 +4,7 @@ import io.swagger.v3.oas.annotations.media.Schema;
|
|||||||
import jakarta.validation.constraints.NotBlank;
|
import jakarta.validation.constraints.NotBlank;
|
||||||
import jakarta.validation.constraints.NotNull;
|
import jakarta.validation.constraints.NotNull;
|
||||||
|
|
||||||
@Schema(description = "单个 UKey 登录证明")
|
@Schema(description = "单个 UKey 登录因子")
|
||||||
public class UkeyLoginProof {
|
public class UkeyLoginProof {
|
||||||
|
|
||||||
@NotBlank(message = "pubKey is required")
|
@NotBlank(message = "pubKey is required")
|
||||||
@ -28,6 +28,9 @@ public class UkeyLoginProof {
|
|||||||
@NotBlank(message = "loginSignature is required")
|
@NotBlank(message = "loginSignature is required")
|
||||||
private String loginSignature;
|
private String loginSignature;
|
||||||
|
|
||||||
|
@Schema(description = "当前 UKey 固定席位对应账号的口令,所有角色都必传", example = "12345678")
|
||||||
|
private String password;
|
||||||
|
|
||||||
public String getPubKey() {
|
public String getPubKey() {
|
||||||
return pubKey;
|
return pubKey;
|
||||||
}
|
}
|
||||||
@ -75,4 +78,12 @@ public class UkeyLoginProof {
|
|||||||
public void setLoginSignature(String loginSignature) {
|
public void setLoginSignature(String loginSignature) {
|
||||||
this.loginSignature = loginSignature;
|
this.loginSignature = loginSignature;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String getPassword() {
|
||||||
|
return password;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setPassword(String password) {
|
||||||
|
this.password = password;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
package com.cisd.tms.modules.auth.dto;
|
package com.cisd.tms.modules.auth.dto;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonAlias;
|
||||||
import io.swagger.v3.oas.annotations.media.Schema;
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
import jakarta.validation.Valid;
|
import jakarta.validation.Valid;
|
||||||
import jakarta.validation.constraints.NotBlank;
|
import jakarta.validation.constraints.NotBlank;
|
||||||
@ -13,14 +14,11 @@ public class UkeyLoginRequest {
|
|||||||
@Schema(description = "角色编码", example = "KEY_ADMIN")
|
@Schema(description = "角色编码", example = "KEY_ADMIN")
|
||||||
private String roleCode;
|
private String roleCode;
|
||||||
|
|
||||||
@NotBlank(message = "rolePassword is required")
|
|
||||||
@Schema(description = "角色口令", example = "12345678")
|
|
||||||
private String rolePassword;
|
|
||||||
|
|
||||||
@Valid
|
@Valid
|
||||||
@NotEmpty(message = "ukeyProofs is required")
|
@NotEmpty(message = "loginFactors is required")
|
||||||
@Schema(description = "UKey 登录证明列表")
|
@JsonAlias("ukeyProofs")
|
||||||
private List<UkeyLoginProof> ukeyProofs;
|
@Schema(description = "UKey 登录因子列表")
|
||||||
|
private List<UkeyLoginProof> loginFactors;
|
||||||
|
|
||||||
public String getRoleCode() {
|
public String getRoleCode() {
|
||||||
return roleCode;
|
return roleCode;
|
||||||
@ -30,19 +28,21 @@ public class UkeyLoginRequest {
|
|||||||
this.roleCode = roleCode;
|
this.roleCode = roleCode;
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getRolePassword() {
|
public List<UkeyLoginProof> getLoginFactors() {
|
||||||
return rolePassword;
|
return loginFactors;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setRolePassword(String rolePassword) {
|
public void setLoginFactors(List<UkeyLoginProof> loginFactors) {
|
||||||
this.rolePassword = rolePassword;
|
this.loginFactors = loginFactors;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Deprecated
|
||||||
public List<UkeyLoginProof> getUkeyProofs() {
|
public List<UkeyLoginProof> getUkeyProofs() {
|
||||||
return ukeyProofs;
|
return loginFactors;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Deprecated
|
||||||
public void setUkeyProofs(List<UkeyLoginProof> ukeyProofs) {
|
public void setUkeyProofs(List<UkeyLoginProof> ukeyProofs) {
|
||||||
this.ukeyProofs = ukeyProofs;
|
this.loginFactors = ukeyProofs;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -18,7 +18,7 @@ public class AuthSessionEntity extends BaseEntity {
|
|||||||
/**
|
/**
|
||||||
* 本次角色会话在后端侧记录的认证账号列表。
|
* 本次角色会话在后端侧记录的认证账号列表。
|
||||||
*/
|
*/
|
||||||
private String authenticatedUsersJson;
|
private String authenticatedPrincipalsJson;
|
||||||
/**
|
/**
|
||||||
* 当前会话的认证方式,区分口令登录和 UKey 登录。
|
* 当前会话的认证方式,区分口令登录和 UKey 登录。
|
||||||
*/
|
*/
|
||||||
@ -48,12 +48,22 @@ public class AuthSessionEntity extends BaseEntity {
|
|||||||
this.roleCode = roleCode;
|
this.roleCode = roleCode;
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getAuthenticatedUsersJson() {
|
public String getAuthenticatedPrincipalsJson() {
|
||||||
return authenticatedUsersJson;
|
return authenticatedPrincipalsJson;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void setAuthenticatedPrincipalsJson(String authenticatedPrincipalsJson) {
|
||||||
|
this.authenticatedPrincipalsJson = authenticatedPrincipalsJson;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Deprecated
|
||||||
|
public String getAuthenticatedUsersJson() {
|
||||||
|
return authenticatedPrincipalsJson;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Deprecated
|
||||||
public void setAuthenticatedUsersJson(String authenticatedUsersJson) {
|
public void setAuthenticatedUsersJson(String authenticatedUsersJson) {
|
||||||
this.authenticatedUsersJson = authenticatedUsersJson;
|
this.authenticatedPrincipalsJson = authenticatedUsersJson;
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getAuthMethod() {
|
public String getAuthMethod() {
|
||||||
|
|||||||
@ -13,6 +13,7 @@ public class AuthUserAccountEntity extends BaseEntity {
|
|||||||
private String passwordHash;
|
private String passwordHash;
|
||||||
private String passwordSalt;
|
private String passwordSalt;
|
||||||
private String status;
|
private String status;
|
||||||
|
private Boolean needChangePassword;
|
||||||
private Integer failedCount;
|
private Integer failedCount;
|
||||||
private LocalDateTime lockedUntil;
|
private LocalDateTime lockedUntil;
|
||||||
private LocalDateTime lastLoginAt;
|
private LocalDateTime lastLoginAt;
|
||||||
@ -66,6 +67,14 @@ public class AuthUserAccountEntity extends BaseEntity {
|
|||||||
this.status = status;
|
this.status = status;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Boolean getNeedChangePassword() {
|
||||||
|
return needChangePassword;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setNeedChangePassword(Boolean needChangePassword) {
|
||||||
|
this.needChangePassword = needChangePassword;
|
||||||
|
}
|
||||||
|
|
||||||
public Integer getFailedCount() {
|
public Integer getFailedCount() {
|
||||||
return failedCount;
|
return failedCount;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,22 +2,13 @@ package com.cisd.tms.modules.auth.entity;
|
|||||||
|
|
||||||
import com.baomidou.mybatisplus.annotation.TableName;
|
import com.baomidou.mybatisplus.annotation.TableName;
|
||||||
import com.cisd.tms.infrastructure.persistence.entity.BaseEntity;
|
import com.cisd.tms.infrastructure.persistence.entity.BaseEntity;
|
||||||
import java.time.LocalDateTime;
|
|
||||||
|
|
||||||
@TableName("tms_role_account")
|
@TableName("tms_role_account")
|
||||||
public class RoleAccountEntity extends BaseEntity {
|
public class RoleAccountEntity extends BaseEntity {
|
||||||
|
|
||||||
private String roleCode;
|
private String roleCode;
|
||||||
private String displayName;
|
private String displayName;
|
||||||
private Integer requiredUkeyCount;
|
private Integer requiredUkeyCount;
|
||||||
private String passwordHash;
|
|
||||||
private String passwordSalt;
|
|
||||||
private String status;
|
private String status;
|
||||||
private Boolean needChangePassword;
|
|
||||||
private Integer failedCount;
|
|
||||||
private LocalDateTime lockedUntil;
|
|
||||||
private LocalDateTime lastLoginAt;
|
|
||||||
private LocalDateTime lastActiveAt;
|
|
||||||
|
|
||||||
public String getRoleCode() {
|
public String getRoleCode() {
|
||||||
return roleCode;
|
return roleCode;
|
||||||
@ -43,22 +34,6 @@ public class RoleAccountEntity extends BaseEntity {
|
|||||||
this.requiredUkeyCount = requiredUkeyCount;
|
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() {
|
public String getStatus() {
|
||||||
return status;
|
return status;
|
||||||
}
|
}
|
||||||
@ -66,44 +41,4 @@ public class RoleAccountEntity extends BaseEntity {
|
|||||||
public void setStatus(String status) {
|
public void setStatus(String status) {
|
||||||
this.status = 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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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> {
|
||||||
|
|
||||||
|
AuthFullAccountEntity selectByRoleCodeAndUid(@Param("roleCode") String roleCode, @Param("uid") Integer uid);
|
||||||
|
|
||||||
|
List<AuthFullAccountEntity> selectByRoleCode(@Param("roleCode") String roleCode);
|
||||||
|
}
|
||||||
@ -2,6 +2,8 @@ package com.cisd.tms.modules.auth.mapper;
|
|||||||
|
|
||||||
import com.cisd.tms.infrastructure.persistence.mapper.BaseMapperX;
|
import com.cisd.tms.infrastructure.persistence.mapper.BaseMapperX;
|
||||||
import com.cisd.tms.modules.auth.entity.AuthSessionEntity;
|
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.Mapper;
|
||||||
import org.apache.ibatis.annotations.Param;
|
import org.apache.ibatis.annotations.Param;
|
||||||
|
|
||||||
@ -9,4 +11,17 @@ import org.apache.ibatis.annotations.Param;
|
|||||||
public interface AuthSessionMapper extends BaseMapperX<AuthSessionEntity> {
|
public interface AuthSessionMapper extends BaseMapperX<AuthSessionEntity> {
|
||||||
|
|
||||||
AuthSessionEntity selectBySessionToken(@Param("sessionToken") String sessionToken);
|
AuthSessionEntity selectBySessionToken(@Param("sessionToken") String sessionToken);
|
||||||
|
|
||||||
|
List<AuthSessionEntity> selectActiveByRoleCodeAndAuthLevel(
|
||||||
|
@Param("roleCode") String roleCode,
|
||||||
|
@Param("authLevel") String authLevel,
|
||||||
|
@Param("now") LocalDateTime now
|
||||||
|
);
|
||||||
|
|
||||||
|
List<AuthSessionEntity> selectActiveByRoleCodeAndAuthLevelAndAuthenticatedPrincipalsJson(
|
||||||
|
@Param("roleCode") String roleCode,
|
||||||
|
@Param("authLevel") String authLevel,
|
||||||
|
@Param("authenticatedPrincipalsJson") String authenticatedPrincipalsJson,
|
||||||
|
@Param("now") LocalDateTime now
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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<AuthFullAccountEntity> findByRoleCodeAndUid(String roleCode, Integer uid);
|
||||||
|
|
||||||
|
List<AuthFullAccountEntity> findByRoleCode(String roleCode);
|
||||||
|
|
||||||
|
void save(AuthFullAccountEntity entity);
|
||||||
|
|
||||||
|
void update(AuthFullAccountEntity entity);
|
||||||
|
}
|
||||||
@ -1,12 +1,23 @@
|
|||||||
package com.cisd.tms.modules.auth.repository;
|
package com.cisd.tms.modules.auth.repository;
|
||||||
|
|
||||||
import com.cisd.tms.modules.auth.entity.AuthSessionEntity;
|
import com.cisd.tms.modules.auth.entity.AuthSessionEntity;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.List;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
|
||||||
public interface AuthSessionRepository {
|
public interface AuthSessionRepository {
|
||||||
|
|
||||||
Optional<AuthSessionEntity> findBySessionToken(String sessionToken);
|
Optional<AuthSessionEntity> findBySessionToken(String sessionToken);
|
||||||
|
|
||||||
|
List<AuthSessionEntity> findActiveByRoleCodeAndAuthLevel(String roleCode, String authLevel, LocalDateTime now);
|
||||||
|
|
||||||
|
List<AuthSessionEntity> findActiveByRoleCodeAndAuthLevelAndAuthenticatedPrincipalsJson(
|
||||||
|
String roleCode,
|
||||||
|
String authLevel,
|
||||||
|
String authenticatedPrincipalsJson,
|
||||||
|
LocalDateTime now
|
||||||
|
);
|
||||||
|
|
||||||
void save(AuthSessionEntity entity);
|
void save(AuthSessionEntity entity);
|
||||||
|
|
||||||
void update(AuthSessionEntity entity);
|
void update(AuthSessionEntity entity);
|
||||||
|
|||||||
@ -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<AuthFullAccountEntity> findByRoleCodeAndUid(String roleCode, Integer uid) {
|
||||||
|
return Optional.ofNullable(authFullAccountMapper.selectByRoleCodeAndUid(roleCode, uid));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<AuthFullAccountEntity> 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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.entity.AuthSessionEntity;
|
||||||
import com.cisd.tms.modules.auth.mapper.AuthSessionMapper;
|
import com.cisd.tms.modules.auth.mapper.AuthSessionMapper;
|
||||||
import com.cisd.tms.modules.auth.repository.AuthSessionRepository;
|
import com.cisd.tms.modules.auth.repository.AuthSessionRepository;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.List;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import org.springframework.stereotype.Repository;
|
import org.springframework.stereotype.Repository;
|
||||||
|
|
||||||
@ -20,6 +22,26 @@ public class AuthSessionRepositoryImpl implements AuthSessionRepository {
|
|||||||
return Optional.ofNullable(authSessionMapper.selectBySessionToken(sessionToken));
|
return Optional.ofNullable(authSessionMapper.selectBySessionToken(sessionToken));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<AuthSessionEntity> findActiveByRoleCodeAndAuthLevel(String roleCode, String authLevel, LocalDateTime now) {
|
||||||
|
return authSessionMapper.selectActiveByRoleCodeAndAuthLevel(roleCode, authLevel, now);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<AuthSessionEntity> findActiveByRoleCodeAndAuthLevelAndAuthenticatedPrincipalsJson(
|
||||||
|
String roleCode,
|
||||||
|
String authLevel,
|
||||||
|
String authenticatedPrincipalsJson,
|
||||||
|
LocalDateTime now
|
||||||
|
) {
|
||||||
|
return authSessionMapper.selectActiveByRoleCodeAndAuthLevelAndAuthenticatedPrincipalsJson(
|
||||||
|
roleCode,
|
||||||
|
authLevel,
|
||||||
|
authenticatedPrincipalsJson,
|
||||||
|
now
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void save(AuthSessionEntity entity) {
|
public void save(AuthSessionEntity entity) {
|
||||||
authSessionMapper.insert(entity);
|
authSessionMapper.insert(entity);
|
||||||
|
|||||||
@ -27,5 +27,7 @@ public interface AuthService {
|
|||||||
|
|
||||||
void logout(String sessionToken);
|
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);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,11 +3,13 @@ package com.cisd.tms.modules.auth.service.impl;
|
|||||||
import com.cisd.tms.common.enums.ErrorCode;
|
import com.cisd.tms.common.enums.ErrorCode;
|
||||||
import com.cisd.tms.common.exception.BizException;
|
import com.cisd.tms.common.exception.BizException;
|
||||||
import com.cisd.tms.common.util.TraceIdUtil;
|
import com.cisd.tms.common.util.TraceIdUtil;
|
||||||
|
import com.cisd.tms.modules.auth.entity.AuthFullAccountEntity;
|
||||||
import com.cisd.tms.modules.auth.entity.AuthUserAccountEntity;
|
import com.cisd.tms.modules.auth.entity.AuthUserAccountEntity;
|
||||||
import com.cisd.tms.modules.auth.entity.RoleAccountEntity;
|
import com.cisd.tms.modules.auth.entity.RoleAccountEntity;
|
||||||
import com.cisd.tms.modules.auth.entity.RoleUkeyBindingEntity;
|
import com.cisd.tms.modules.auth.entity.RoleUkeyBindingEntity;
|
||||||
import com.cisd.tms.modules.auth.enums.RoleAccountStatus;
|
import com.cisd.tms.modules.auth.enums.RoleAccountStatus;
|
||||||
import com.cisd.tms.modules.auth.enums.RoleCode;
|
import com.cisd.tms.modules.auth.enums.RoleCode;
|
||||||
|
import com.cisd.tms.modules.auth.repository.AuthFullAccountRepository;
|
||||||
import com.cisd.tms.modules.auth.repository.AuthUserAccountRepository;
|
import com.cisd.tms.modules.auth.repository.AuthUserAccountRepository;
|
||||||
import com.cisd.tms.modules.auth.repository.RoleAccountRepository;
|
import com.cisd.tms.modules.auth.repository.RoleAccountRepository;
|
||||||
import com.cisd.tms.modules.auth.repository.RoleUkeyBindingRepository;
|
import com.cisd.tms.modules.auth.repository.RoleUkeyBindingRepository;
|
||||||
@ -36,6 +38,7 @@ public class AuthAdminServiceImpl implements AuthAdminService {
|
|||||||
static final String DEFAULT_PASSWORD = "12345678";
|
static final String DEFAULT_PASSWORD = "12345678";
|
||||||
|
|
||||||
private final RoleAccountRepository roleAccountRepository;
|
private final RoleAccountRepository roleAccountRepository;
|
||||||
|
private final AuthFullAccountRepository authFullAccountRepository;
|
||||||
private final AuthUserAccountRepository authUserAccountRepository;
|
private final AuthUserAccountRepository authUserAccountRepository;
|
||||||
private final RoleUkeyBindingRepository roleUkeyBindingRepository;
|
private final RoleUkeyBindingRepository roleUkeyBindingRepository;
|
||||||
private final PasswordHasher passwordHasher;
|
private final PasswordHasher passwordHasher;
|
||||||
@ -48,24 +51,60 @@ public class AuthAdminServiceImpl implements AuthAdminService {
|
|||||||
public void enableRole(String operatorRoleCode, String operatorAuthLevel, String targetRoleCode) {
|
public void enableRole(String operatorRoleCode, String operatorAuthLevel, String targetRoleCode) {
|
||||||
RoleAccountEntity target = loadRole(targetRoleCode);
|
RoleAccountEntity target = loadRole(targetRoleCode);
|
||||||
target.setStatus(RoleAccountStatus.ACTIVE.name());
|
target.setStatus(RoleAccountStatus.ACTIVE.name());
|
||||||
target.setNeedChangePassword(Boolean.TRUE);
|
|
||||||
target.setLockedUntil(null);
|
|
||||||
target.setFailedCount(0);
|
|
||||||
roleAccountRepository.update(target);
|
roleAccountRepository.update(target);
|
||||||
|
|
||||||
|
List<AuthFullAccountEntity> 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<AuthUserAccountEntity> accounts = authUserAccountRepository.findByRoleCode(targetRoleCode);
|
||||||
|
if (accounts.isEmpty()) {
|
||||||
|
throw new BizException(ErrorCode.VALIDATE_FAILED.getCode(), "target role user accounts not found");
|
||||||
|
}
|
||||||
|
for (AuthUserAccountEntity account : accounts) {
|
||||||
|
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
|
@Override
|
||||||
public void resetPassword(String operatorRoleCode, String operatorAuthLevel, String targetRoleCode) {
|
public void resetPassword(String operatorRoleCode, String operatorAuthLevel, String targetRoleCode) {
|
||||||
RoleAccountEntity target = loadRole(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.setStatus(RoleAccountStatus.ACTIVE.name());
|
||||||
target.setNeedChangePassword(Boolean.TRUE);
|
|
||||||
target.setFailedCount(0);
|
|
||||||
target.setLockedUntil(null);
|
|
||||||
roleAccountRepository.update(target);
|
roleAccountRepository.update(target);
|
||||||
|
|
||||||
|
List<AuthFullAccountEntity> 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<AuthUserAccountEntity> accounts = authUserAccountRepository.findByRoleCode(targetRoleCode);
|
List<AuthUserAccountEntity> accounts = authUserAccountRepository.findByRoleCode(targetRoleCode);
|
||||||
if (accounts.isEmpty()) {
|
if (accounts.isEmpty()) {
|
||||||
throw new BizException(ErrorCode.VALIDATE_FAILED.getCode(), "target role user accounts not found");
|
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.setPasswordSalt(newSalt);
|
||||||
account.setPasswordHash(passwordHasher.hash(DEFAULT_PASSWORD, newSalt));
|
account.setPasswordHash(passwordHasher.hash(DEFAULT_PASSWORD, newSalt));
|
||||||
account.setStatus(RoleAccountStatus.ACTIVE.name());
|
account.setStatus(RoleAccountStatus.ACTIVE.name());
|
||||||
|
account.setNeedChangePassword(Boolean.TRUE);
|
||||||
account.setFailedCount(0);
|
account.setFailedCount(0);
|
||||||
account.setLockedUntil(null);
|
account.setLockedUntil(null);
|
||||||
account.setLastActiveAt(null);
|
account.setLastActiveAt(null);
|
||||||
@ -97,6 +137,8 @@ public class AuthAdminServiceImpl implements AuthAdminService {
|
|||||||
if (uid == null || uid < 1 || uid > targetRole.getRequiredUkeyCount()) {
|
if (uid == null || uid < 1 || uid > targetRole.getRequiredUkeyCount()) {
|
||||||
throw new BizException(ErrorCode.VALIDATE_FAILED.getCode(), "uid exceeds role ukey requirement");
|
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
|
RoleUkeyBindingEntity binding = roleUkeyBindingRepository
|
||||||
.findActiveByRoleCodeAndUid(targetRoleCode, uid)
|
.findActiveByRoleCodeAndUid(targetRoleCode, uid)
|
||||||
|
|||||||
@ -2,8 +2,10 @@ package com.cisd.tms.modules.auth.service.impl;
|
|||||||
|
|
||||||
import com.cisd.tms.common.enums.ErrorCode;
|
import com.cisd.tms.common.enums.ErrorCode;
|
||||||
import com.cisd.tms.common.exception.BizException;
|
import com.cisd.tms.common.exception.BizException;
|
||||||
|
import com.cisd.tms.modules.auth.dto.AuthSessionPrincipal;
|
||||||
import com.cisd.tms.modules.auth.dto.CaptchaResponse;
|
import com.cisd.tms.modules.auth.dto.CaptchaResponse;
|
||||||
import com.cisd.tms.modules.auth.dto.CurrentUserResponse;
|
import com.cisd.tms.modules.auth.dto.CurrentUserResponse;
|
||||||
|
import com.cisd.tms.modules.auth.dto.FullLoginAccountRequest;
|
||||||
import com.cisd.tms.modules.auth.dto.LoginRequest;
|
import com.cisd.tms.modules.auth.dto.LoginRequest;
|
||||||
import com.cisd.tms.modules.auth.dto.LoginResponse;
|
import com.cisd.tms.modules.auth.dto.LoginResponse;
|
||||||
import com.cisd.tms.modules.auth.dto.PasswordLoginAccountRequest;
|
import com.cisd.tms.modules.auth.dto.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.UkeyLoginRandomRequest;
|
||||||
import com.cisd.tms.modules.auth.dto.UkeyLoginRandomResponse;
|
import com.cisd.tms.modules.auth.dto.UkeyLoginRandomResponse;
|
||||||
import com.cisd.tms.modules.auth.dto.UkeyLoginRequest;
|
import com.cisd.tms.modules.auth.dto.UkeyLoginRequest;
|
||||||
|
import com.cisd.tms.modules.auth.entity.AuthFullAccountEntity;
|
||||||
import com.cisd.tms.modules.auth.entity.AuthSessionEntity;
|
import com.cisd.tms.modules.auth.entity.AuthSessionEntity;
|
||||||
import com.cisd.tms.modules.auth.entity.AuthUserAccountEntity;
|
import com.cisd.tms.modules.auth.entity.AuthUserAccountEntity;
|
||||||
import com.cisd.tms.modules.auth.entity.AuthUserEntity;
|
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.AuthMethod;
|
||||||
import com.cisd.tms.modules.auth.enums.RoleAccountStatus;
|
import com.cisd.tms.modules.auth.enums.RoleAccountStatus;
|
||||||
import com.cisd.tms.modules.auth.enums.RoleCode;
|
import com.cisd.tms.modules.auth.enums.RoleCode;
|
||||||
|
import com.cisd.tms.modules.auth.repository.AuthFullAccountRepository;
|
||||||
import com.cisd.tms.modules.auth.repository.AuthSessionRepository;
|
import com.cisd.tms.modules.auth.repository.AuthSessionRepository;
|
||||||
import com.cisd.tms.modules.auth.repository.AuthUserAccountRepository;
|
import com.cisd.tms.modules.auth.repository.AuthUserAccountRepository;
|
||||||
import com.cisd.tms.modules.auth.repository.AuthUserRepository;
|
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.enums.MasterKeyStatus;
|
||||||
import com.cisd.tms.modules.mk.service.LmkService;
|
import com.cisd.tms.modules.mk.service.LmkService;
|
||||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
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 com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
import java.time.Clock;
|
import java.time.Clock;
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
@ -63,6 +69,7 @@ public class AuthServiceImpl implements AuthService {
|
|||||||
private final RoleAccountRepository roleAccountRepository;
|
private final RoleAccountRepository roleAccountRepository;
|
||||||
private final AuthUserRepository authUserRepository;
|
private final AuthUserRepository authUserRepository;
|
||||||
private final AuthUserAccountRepository authUserAccountRepository;
|
private final AuthUserAccountRepository authUserAccountRepository;
|
||||||
|
private final AuthFullAccountRepository authFullAccountRepository;
|
||||||
private final AuthSessionRepository authSessionRepository;
|
private final AuthSessionRepository authSessionRepository;
|
||||||
private final RoleUkeyBindingRepository roleUkeyBindingRepository;
|
private final RoleUkeyBindingRepository roleUkeyBindingRepository;
|
||||||
private final PasswordHasher passwordHasher;
|
private final PasswordHasher passwordHasher;
|
||||||
@ -84,19 +91,24 @@ public class AuthServiceImpl implements AuthService {
|
|||||||
validateRoleStatus(roleAccount);
|
validateRoleStatus(roleAccount);
|
||||||
AuthMethod authMethod = resolveAuthMethod(request.getUkeySerials());
|
AuthMethod authMethod = resolveAuthMethod(request.getUkeySerials());
|
||||||
List<AuthUserAccountEntity> validatedAccounts = new ArrayList<>();
|
List<AuthUserAccountEntity> validatedAccounts = new ArrayList<>();
|
||||||
|
List<AuthFullAccountEntity> validatedFullAccounts = new ArrayList<>();
|
||||||
if (AuthMethod.PASSWORD == authMethod) {
|
if (AuthMethod.PASSWORD == authMethod) {
|
||||||
validatedAccounts = validateRoleAccounts(roleAccount, request.getAccounts());
|
validatedAccounts = validateRoleAccounts(roleAccount, request.getAccounts());
|
||||||
resetValidatedAccounts(validatedAccounts);
|
resetValidatedAccounts(validatedAccounts);
|
||||||
} else {
|
} else {
|
||||||
validateRolePassword(roleAccount, request.getRolePassword());
|
validatedFullAccounts = validateFullAccounts(roleAccount, request.getFullAccounts());
|
||||||
resetRoleFailureState(roleAccount);
|
resetValidatedFullAccounts(validatedFullAccounts);
|
||||||
}
|
}
|
||||||
AuthLevel authLevel = resolveAuthLevel(roleAccount, request.getUkeySerials());
|
AuthLevel authLevel = resolveAuthLevel(roleAccount, request.getUkeySerials());
|
||||||
|
List<AuthSessionPrincipal> authenticatedPrincipals = AuthMethod.PASSWORD == authMethod
|
||||||
|
? normalizeAuthenticatedUsers(validatedAccounts)
|
||||||
|
: normalizeAuthenticatedFullAccounts(validatedFullAccounts);
|
||||||
|
expireConcurrentSessions(roleAccount.getRoleCode(), authLevel, authenticatedPrincipals);
|
||||||
AuthSessionEntity session = buildSession(
|
AuthSessionEntity session = buildSession(
|
||||||
roleAccount.getRoleCode(),
|
roleAccount.getRoleCode(),
|
||||||
authMethod,
|
authMethod,
|
||||||
authLevel,
|
authLevel,
|
||||||
validatedAccounts.isEmpty() ? null : validatedAccounts.stream().map(AuthUserAccountEntity::getUsername).toList()
|
authenticatedPrincipals
|
||||||
);
|
);
|
||||||
authSessionRepository.save(session);
|
authSessionRepository.save(session);
|
||||||
|
|
||||||
@ -105,14 +117,18 @@ public class AuthServiceImpl implements AuthService {
|
|||||||
response.setAuthLevel(authLevel.name());
|
response.setAuthLevel(authLevel.name());
|
||||||
response.setToken(session.getSessionToken());
|
response.setToken(session.getSessionToken());
|
||||||
response.setExpiresAt(OffsetDateTime.of(session.getExpiresAt(), ZoneOffset.UTC).toString());
|
response.setExpiresAt(OffsetDateTime.of(session.getExpiresAt(), ZoneOffset.UTC).toString());
|
||||||
response.setNeedChangePassword(needsPasswordChange(roleAccount));
|
response.setNeedChangePassword(
|
||||||
|
AuthMethod.PASSWORD == authMethod
|
||||||
|
? needsPasswordChangeForLimitedAccounts(validatedAccounts)
|
||||||
|
: needsPasswordChangeForFullAccounts(validatedFullAccounts)
|
||||||
|
);
|
||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public LoginResponse passwordLogin(PasswordLoginRequest request) {
|
public LoginResponse passwordLogin(PasswordLoginRequest request) {
|
||||||
ensureMasterKeyReady();
|
ensureMasterKeyReady();
|
||||||
captchaService.verify(request.getCaptchaId(), request.getCaptchaCode());
|
// captchaService.verify(request.getCaptchaId(), request.getCaptchaCode());
|
||||||
LoginRequest loginRequest = new LoginRequest();
|
LoginRequest loginRequest = new LoginRequest();
|
||||||
loginRequest.setRoleCode(request.getRoleCode());
|
loginRequest.setRoleCode(request.getRoleCode());
|
||||||
loginRequest.setAccounts(request.getAccounts());
|
loginRequest.setAccounts(request.getAccounts());
|
||||||
@ -133,22 +149,22 @@ public class AuthServiceImpl implements AuthService {
|
|||||||
ensureMasterKeyReady();
|
ensureMasterKeyReady();
|
||||||
RoleCode roleCode = RoleCode.valueOf(request.getRoleCode());
|
RoleCode roleCode = RoleCode.valueOf(request.getRoleCode());
|
||||||
List<RoleUkeyBindingEntity> activeBindings = roleUkeyBindingRepository.findActiveByRoleCode(roleCode.getCode());
|
List<RoleUkeyBindingEntity> activeBindings = roleUkeyBindingRepository.findActiveByRoleCode(roleCode.getCode());
|
||||||
validateUkeyCount(roleCode, activeBindings, request.getUkeyProofs());
|
validateUkeyCount(roleCode, activeBindings, request.getLoginFactors());
|
||||||
Map<Integer, RoleUkeyBindingEntity> bindingsByUid = activeBindings.stream()
|
Map<Integer, RoleUkeyBindingEntity> bindingsByUid = activeBindings.stream()
|
||||||
.collect(Collectors.toMap(RoleUkeyBindingEntity::getUid, item -> item, (left, right) -> left, java.util.LinkedHashMap::new));
|
.collect(Collectors.toMap(RoleUkeyBindingEntity::getUid, item -> item, (left, right) -> left, java.util.LinkedHashMap::new));
|
||||||
Set<Integer> requestUids = request.getUkeyProofs().stream()
|
Set<Integer> requestUids = request.getLoginFactors().stream()
|
||||||
.map(UkeyLoginProof::getUid)
|
.map(UkeyLoginProof::getUid)
|
||||||
.collect(Collectors.toSet());
|
.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");
|
throw new BizException(ErrorCode.UNAUTHORIZED.getCode(), "ukey auth info does not match bound role");
|
||||||
}
|
}
|
||||||
ukeyLoginRandomService.assertIssued(
|
ukeyLoginRandomService.assertIssued(
|
||||||
roleCode.getCode(),
|
roleCode.getCode(),
|
||||||
request.getUkeyProofs().stream().map(UkeyLoginProof::getServerRandom).toList()
|
request.getLoginFactors().stream().map(UkeyLoginProof::getServerRandom).toList()
|
||||||
);
|
);
|
||||||
String authKeyPair = lmkService.exportIkPublicKeyHex();
|
String authKeyPair = lmkService.exportIkPublicKeyHex();
|
||||||
List<String> matchedSerials = new ArrayList<>();
|
List<String> matchedSerials = new ArrayList<>();
|
||||||
for (UkeyLoginProof proof : request.getUkeyProofs()) {
|
for (UkeyLoginProof proof : request.getLoginFactors()) {
|
||||||
RoleUkeyBindingEntity binding = bindingsByUid.get(proof.getUid());
|
RoleUkeyBindingEntity binding = bindingsByUid.get(proof.getUid());
|
||||||
if (binding == null || !binding.getUkeyPubkey().equals(proof.getPubKey())) {
|
if (binding == null || !binding.getUkeyPubkey().equals(proof.getPubKey())) {
|
||||||
throw new BizException(ErrorCode.UNAUTHORIZED.getCode(), "ukey auth info does not match bound role");
|
throw new BizException(ErrorCode.UNAUTHORIZED.getCode(), "ukey auth info does not match bound role");
|
||||||
@ -159,8 +175,8 @@ public class AuthServiceImpl implements AuthService {
|
|||||||
}
|
}
|
||||||
LoginRequest loginRequest = new LoginRequest();
|
LoginRequest loginRequest = new LoginRequest();
|
||||||
loginRequest.setRoleCode(request.getRoleCode());
|
loginRequest.setRoleCode(request.getRoleCode());
|
||||||
loginRequest.setRolePassword(request.getRolePassword());
|
|
||||||
loginRequest.setUkeySerials(matchedSerials);
|
loginRequest.setUkeySerials(matchedSerials);
|
||||||
|
loginRequest.setFullAccounts(buildFixedUidAccounts(request.getRoleCode(), request.getLoginFactors()));
|
||||||
return login(loginRequest);
|
return login(loginRequest);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -186,8 +202,10 @@ public class AuthServiceImpl implements AuthService {
|
|||||||
AuthSessionEntity session = sessionToken == null ? null : authSessionRepository.findBySessionToken(sessionToken).orElse(null);
|
AuthSessionEntity session = sessionToken == null ? null : authSessionRepository.findBySessionToken(sessionToken).orElse(null);
|
||||||
if (session != null) {
|
if (session != null) {
|
||||||
response.setAuthLevel(session.getAuthLevel());
|
response.setAuthLevel(session.getAuthLevel());
|
||||||
|
response.setNeedChangePassword(needsPasswordChange(session));
|
||||||
|
} else {
|
||||||
|
response.setNeedChangePassword(Boolean.FALSE);
|
||||||
}
|
}
|
||||||
response.setNeedChangePassword(needsPasswordChange(roleAccount));
|
|
||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -217,25 +235,66 @@ public class AuthServiceImpl implements AuthService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@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);
|
AuthSessionEntity session = requireActiveSession(sessionToken);
|
||||||
RoleAccountEntity roleAccount = roleAccountRepository.findByRoleCode(session.getRoleCode())
|
if (!AuthLevel.FULL.name().equals(session.getAuthLevel())) {
|
||||||
.orElseThrow(() -> new BizException(ErrorCode.UNAUTHORIZED.getCode(), "role account not found"));
|
throw new BizException(ErrorCode.UNAUTHORIZED.getCode(), "full session is required");
|
||||||
validateRoleStatus(roleAccount);
|
}
|
||||||
if (!passwordHasher.matches(currentPassword, roleAccount.getPasswordSalt(), roleAccount.getPasswordHash())) {
|
if (uid == null || !containsAuthenticatedFullUid(session, uid)) {
|
||||||
onRolePasswordFailed(roleAccount);
|
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();
|
String salt = passwordSaltGenerator.nextSalt();
|
||||||
LocalDateTime current = now();
|
LocalDateTime current = now();
|
||||||
roleAccount.setPasswordSalt(salt);
|
fullAccount.setPasswordSalt(salt);
|
||||||
roleAccount.setPasswordHash(passwordHasher.hash(newPassword, salt));
|
fullAccount.setPasswordHash(passwordHasher.hash(newPassword, salt));
|
||||||
roleAccount.setNeedChangePassword(Boolean.FALSE);
|
fullAccount.setFailedCount(0);
|
||||||
roleAccount.setFailedCount(0);
|
fullAccount.setLockedUntil(null);
|
||||||
roleAccount.setLockedUntil(null);
|
fullAccount.setStatus(RoleAccountStatus.ACTIVE.name());
|
||||||
roleAccount.setStatus(RoleAccountStatus.ACTIVE.name());
|
fullAccount.setNeedChangePassword(Boolean.FALSE);
|
||||||
roleAccount.setLastActiveAt(current);
|
fullAccount.setLastLoginAt(current);
|
||||||
roleAccountRepository.update(roleAccount);
|
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.setLastActiveAt(current);
|
||||||
session.setExpiresAt(current.plusMinutes(IDLE_TIMEOUT_MINUTES));
|
session.setExpiresAt(current.plusMinutes(IDLE_TIMEOUT_MINUTES));
|
||||||
@ -243,22 +302,9 @@ public class AuthServiceImpl implements AuthService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void validateRoleStatus(RoleAccountEntity roleAccount) {
|
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");
|
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<AuthUserAccountEntity> validateRoleAccounts(
|
private List<AuthUserAccountEntity> validateRoleAccounts(
|
||||||
@ -321,17 +367,27 @@ public class AuthServiceImpl implements AuthService {
|
|||||||
throw new BizException(ErrorCode.UNAUTHORIZED.getCode(), "password is incorrect");
|
throw new BizException(ErrorCode.UNAUTHORIZED.getCode(), "password is incorrect");
|
||||||
}
|
}
|
||||||
|
|
||||||
private void onRolePasswordFailed(RoleAccountEntity roleAccount) {
|
private void validateFullAccountStatus(AuthFullAccountEntity fullAccount) {
|
||||||
int failedCount = roleAccount.getFailedCount() == null ? 0 : roleAccount.getFailedCount();
|
if (RoleAccountStatus.UNENABLED.name().equals(fullAccount.getStatus())) {
|
||||||
failedCount++;
|
throw new BizException(ErrorCode.UNAUTHORIZED.getCode(), "full account is not enabled");
|
||||||
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");
|
|
||||||
}
|
}
|
||||||
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");
|
throw new BizException(ErrorCode.UNAUTHORIZED.getCode(), "password is incorrect");
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -347,19 +403,77 @@ public class AuthServiceImpl implements AuthService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void resetRoleFailureState(RoleAccountEntity roleAccount) {
|
private void resetValidatedFullAccounts(List<AuthFullAccountEntity> validatedAccounts) {
|
||||||
roleAccount.setFailedCount(0);
|
LocalDateTime current = now();
|
||||||
roleAccount.setLockedUntil(null);
|
for (AuthFullAccountEntity account : validatedAccounts) {
|
||||||
roleAccount.setStatus(RoleAccountStatus.ACTIVE.name());
|
account.setFailedCount(0);
|
||||||
roleAccount.setLastLoginAt(now());
|
account.setLockedUntil(null);
|
||||||
roleAccount.setLastActiveAt(now());
|
account.setStatus(RoleAccountStatus.ACTIVE.name());
|
||||||
roleAccountRepository.update(roleAccount);
|
account.setLastLoginAt(current);
|
||||||
|
account.setLastActiveAt(current);
|
||||||
|
authFullAccountRepository.update(account);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private int requiredPasswordAccountCount(String roleCode) {
|
private int requiredPasswordAccountCount(String roleCode) {
|
||||||
return RoleCode.valueOf(roleCode).getRequiredUkeyCount();
|
return RoleCode.valueOf(roleCode).getRequiredUkeyCount();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private List<AuthFullAccountEntity> validateFullAccounts(
|
||||||
|
RoleAccountEntity roleAccount,
|
||||||
|
List<FullLoginAccountRequest> 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<Integer> uniqueUids = new LinkedHashSet<>();
|
||||||
|
List<AuthFullAccountEntity> 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<FullLoginAccountRequest> buildFixedUidAccounts(String roleCode, List<UkeyLoginProof> proofs) {
|
||||||
|
List<AuthFullAccountEntity> 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<FullLoginAccountRequest> 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() {
|
private void ensureMasterKeyReady() {
|
||||||
MasterKeyStatus.StatusDetail status = lmkService.getMasterKeyStatus();
|
MasterKeyStatus.StatusDetail status = lmkService.getMasterKeyStatus();
|
||||||
if (status == null || status.getCode() == MasterKeyStatus.ABNORMAL.getCode()) {
|
if (status == null || status.getCode() == MasterKeyStatus.ABNORMAL.getCode()) {
|
||||||
@ -425,12 +539,12 @@ public class AuthServiceImpl implements AuthService {
|
|||||||
String roleCode,
|
String roleCode,
|
||||||
AuthMethod authMethod,
|
AuthMethod authMethod,
|
||||||
AuthLevel authLevel,
|
AuthLevel authLevel,
|
||||||
List<String> authenticatedUsers
|
List<AuthSessionPrincipal> authenticatedPrincipals
|
||||||
) {
|
) {
|
||||||
LocalDateTime issuedAt = now();
|
LocalDateTime issuedAt = now();
|
||||||
AuthSessionEntity entity = new AuthSessionEntity();
|
AuthSessionEntity entity = new AuthSessionEntity();
|
||||||
entity.setRoleCode(roleCode);
|
entity.setRoleCode(roleCode);
|
||||||
entity.setAuthenticatedUsersJson(writeAuthenticatedUsers(authenticatedUsers));
|
entity.setAuthenticatedPrincipalsJson(writeAuthenticatedPrincipals(authenticatedPrincipals));
|
||||||
entity.setAuthMethod(authMethod.name());
|
entity.setAuthMethod(authMethod.name());
|
||||||
entity.setAuthLevel(authLevel.name());
|
entity.setAuthLevel(authLevel.name());
|
||||||
entity.setSessionToken(sessionTokenGenerator.nextToken());
|
entity.setSessionToken(sessionTokenGenerator.nextToken());
|
||||||
@ -440,23 +554,132 @@ public class AuthServiceImpl implements AuthService {
|
|||||||
return entity;
|
return entity;
|
||||||
}
|
}
|
||||||
|
|
||||||
private String writeAuthenticatedUsers(List<String> authenticatedUsers) {
|
private void expireConcurrentSessions(String roleCode, AuthLevel authLevel, List<AuthSessionPrincipal> authenticatedPrincipals) {
|
||||||
if (authenticatedUsers == null) {
|
LocalDateTime current = now();
|
||||||
|
List<AuthSessionEntity> 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<AuthSessionPrincipal> normalizeAuthenticatedUsers(List<AuthUserAccountEntity> validatedAccounts) {
|
||||||
|
if (validatedAccounts == null || validatedAccounts.isEmpty()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return validatedAccounts.stream()
|
||||||
|
.map(AuthUserAccountEntity::getUsername)
|
||||||
|
.sorted()
|
||||||
|
.map(AuthSessionPrincipal::limited)
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<AuthSessionPrincipal> normalizeAuthenticatedFullAccounts(List<AuthFullAccountEntity> validatedAccounts) {
|
||||||
|
if (validatedAccounts == null || validatedAccounts.isEmpty()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return validatedAccounts.stream()
|
||||||
|
.map(AuthFullAccountEntity::getUid)
|
||||||
|
.sorted()
|
||||||
|
.map(AuthSessionPrincipal::full)
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
private String writeAuthenticatedPrincipals(List<AuthSessionPrincipal> authenticatedPrincipals) {
|
||||||
|
if (authenticatedPrincipals == null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
return objectMapper.writeValueAsString(authenticatedUsers);
|
return objectMapper.writeValueAsString(authenticatedPrincipals);
|
||||||
} catch (JsonProcessingException ex) {
|
} catch (JsonProcessingException ex) {
|
||||||
throw new IllegalStateException("serialize authenticated users failed", ex);
|
throw new IllegalStateException("serialize authenticated principals failed", ex);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private List<AuthSessionPrincipal> 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<AuthSessionPrincipal> 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<List<AuthSessionPrincipal>>() { }
|
||||||
|
);
|
||||||
|
} 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() {
|
private LocalDateTime now() {
|
||||||
return LocalDateTime.ofInstant(clock.instant(), ZoneOffset.UTC);
|
return LocalDateTime.ofInstant(clock.instant(), ZoneOffset.UTC);
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean needsPasswordChange(RoleAccountEntity roleAccount) {
|
private boolean needsPasswordChangeForLimitedAccounts(List<AuthUserAccountEntity> accounts) {
|
||||||
return Boolean.TRUE.equals(roleAccount.getNeedChangePassword());
|
return accounts != null && accounts.stream().anyMatch(account -> Boolean.TRUE.equals(account.getNeedChangePassword()));
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean needsPasswordChangeForFullAccounts(List<AuthFullAccountEntity> accounts) {
|
||||||
|
return accounts != null && accounts.stream().anyMatch(account -> Boolean.TRUE.equals(account.getNeedChangePassword()));
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean needsPasswordChange(AuthSessionEntity session) {
|
||||||
|
if (session == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
List<AuthSessionPrincipal> principals = authenticatedPrincipals(session);
|
||||||
|
if (principals.isEmpty()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (AuthLevel.FULL.name().equals(session.getAuthLevel())) {
|
||||||
|
List<AuthFullAccountEntity> 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<AuthUserAccountEntity> 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) {
|
private AuthSessionEntity requireActiveSession(String sessionToken) {
|
||||||
|
|||||||
@ -2,25 +2,52 @@ package com.cisd.tms.modules.auth.service.impl;
|
|||||||
|
|
||||||
import com.cisd.tms.common.enums.ErrorCode;
|
import com.cisd.tms.common.enums.ErrorCode;
|
||||||
import com.cisd.tms.common.exception.BizException;
|
import com.cisd.tms.common.exception.BizException;
|
||||||
import com.cisd.tms.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.auth.service.CompatUkeyVerifier;
|
||||||
import com.cisd.tms.modules.mk.service.LmkService;
|
import com.cisd.tms.modules.mk.service.LmkService;
|
||||||
|
import java.io.StringReader;
|
||||||
|
import java.math.BigInteger;
|
||||||
import java.nio.charset.StandardCharsets;
|
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 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.bouncycastle.util.encoders.Hex;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
@Component
|
@Component
|
||||||
public class PcieCompatUkeyVerifier implements CompatUkeyVerifier {
|
public class PcieCompatUkeyVerifier implements CompatUkeyVerifier {
|
||||||
|
|
||||||
private final LmkService lmkService;
|
private static final String PROVIDER = "BC";
|
||||||
private final PcieCryptoService pcieCryptoService;
|
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.lmkService = lmkService;
|
||||||
this.pcieCryptoService = pcieCryptoService;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -36,32 +63,133 @@ public class PcieCompatUkeyVerifier implements CompatUkeyVerifier {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void verifyLoginSignature(String pubKey, String loginSignData, String loginSign) {
|
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 {
|
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) {
|
} catch (BizException ex) {
|
||||||
throw ex;
|
throw ex;
|
||||||
|
} catch (IllegalArgumentException ex) {
|
||||||
|
throw new BizException(ErrorCode.VALIDATE_FAILED.getCode(), ex.getMessage());
|
||||||
} catch (RuntimeException ex) {
|
} catch (RuntimeException ex) {
|
||||||
throw new BizException(ErrorCode.UNAUTHORIZED.getCode(), "login signature verification failed");
|
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) {
|
private PublicKey parsePublicKey(String value) throws Exception {
|
||||||
String normalized = value == null ? "" : value.trim();
|
String trimmed = value == null ? "" : value.trim();
|
||||||
if (normalized.isEmpty()) {
|
if (trimmed.isEmpty()) {
|
||||||
throw new BizException(ErrorCode.VALIDATE_FAILED.getCode(), message);
|
throw new IllegalArgumentException("login public key is invalid");
|
||||||
}
|
}
|
||||||
if (normalized.matches("(?i)^[0-9a-f]+$") && normalized.length() % 2 == 0) {
|
if (trimmed.contains("BEGIN")) {
|
||||||
return Hex.decode(normalized);
|
return parsePemPublicKey(trimmed);
|
||||||
}
|
}
|
||||||
|
String normalized = normalize(trimmed);
|
||||||
|
byte[] decoded = decodeHexOrBase64(normalized, "login public key is invalid");
|
||||||
try {
|
try {
|
||||||
return Base64.getDecoder().decode(normalized);
|
return KeyFactory.getInstance("EC", PROVIDER).generatePublic(new X509EncodedKeySpec(decoded));
|
||||||
} catch (IllegalArgumentException ex) {
|
} catch (Exception ignored) {
|
||||||
throw new BizException(ErrorCode.VALIDATE_FAILED.getCode(), message);
|
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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -244,14 +244,7 @@ CREATE TABLE IF NOT EXISTS tms_role_account (
|
|||||||
role_code VARCHAR(64) NOT NULL,
|
role_code VARCHAR(64) NOT NULL,
|
||||||
display_name VARCHAR(128) NOT NULL,
|
display_name VARCHAR(128) NOT NULL,
|
||||||
required_ukey_count INT 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,
|
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,
|
create_time DATETIME(3) NOT NULL,
|
||||||
update_time DATETIME(3) NOT NULL,
|
update_time DATETIME(3) NOT NULL,
|
||||||
UNIQUE KEY uk_tms_role_account_role_code (role_code)
|
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_hash VARCHAR(256) NOT NULL,
|
||||||
password_salt VARCHAR(128) NOT NULL,
|
password_salt VARCHAR(128) NOT NULL,
|
||||||
status VARCHAR(32) NOT NULL,
|
status VARCHAR(32) NOT NULL,
|
||||||
|
need_change_password TINYINT(1) NOT NULL DEFAULT 1,
|
||||||
failed_count INT NOT NULL DEFAULT 0,
|
failed_count INT NOT NULL DEFAULT 0,
|
||||||
locked_until DATETIME(3) NULL,
|
locked_until DATETIME(3) NULL,
|
||||||
last_login_at 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)
|
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 (
|
CREATE TABLE IF NOT EXISTS tms_auth_session (
|
||||||
id BIGINT PRIMARY KEY,
|
id BIGINT PRIMARY KEY,
|
||||||
session_token VARCHAR(128) NOT NULL,
|
session_token VARCHAR(128) NOT NULL,
|
||||||
role_code VARCHAR(64) NOT NULL,
|
role_code VARCHAR(64) NOT NULL,
|
||||||
authenticated_users_json JSON NULL,
|
authenticated_principals_json JSON NULL,
|
||||||
auth_method VARCHAR(32) NOT NULL,
|
auth_method VARCHAR(32) NOT NULL,
|
||||||
auth_level VARCHAR(32) NOT NULL,
|
auth_level VARCHAR(32) NOT NULL,
|
||||||
issued_at DATETIME(3) NOT NULL,
|
issued_at DATETIME(3) NOT NULL,
|
||||||
@ -379,14 +394,7 @@ INSERT IGNORE INTO tms_role_account (
|
|||||||
role_code,
|
role_code,
|
||||||
display_name,
|
display_name,
|
||||||
required_ukey_count,
|
required_ukey_count,
|
||||||
password_hash,
|
|
||||||
password_salt,
|
|
||||||
status,
|
status,
|
||||||
need_change_password,
|
|
||||||
failed_count,
|
|
||||||
locked_until,
|
|
||||||
last_login_at,
|
|
||||||
last_active_at,
|
|
||||||
create_time,
|
create_time,
|
||||||
update_time
|
update_time
|
||||||
)
|
)
|
||||||
@ -396,14 +404,7 @@ VALUES
|
|||||||
'SUPER_ADMIN',
|
'SUPER_ADMIN',
|
||||||
'超级管理员',
|
'超级管理员',
|
||||||
2,
|
2,
|
||||||
'Viuew8WE+qziUZwGGU0x25cUpzsTF+QvQFU0uhb+yVA=',
|
|
||||||
'init-super-admin-salt-20260325',
|
|
||||||
'UNENABLED',
|
'UNENABLED',
|
||||||
1,
|
|
||||||
0,
|
|
||||||
NULL,
|
|
||||||
NULL,
|
|
||||||
NULL,
|
|
||||||
CURRENT_TIMESTAMP(3),
|
CURRENT_TIMESTAMP(3),
|
||||||
CURRENT_TIMESTAMP(3)
|
CURRENT_TIMESTAMP(3)
|
||||||
),
|
),
|
||||||
@ -412,14 +413,7 @@ VALUES
|
|||||||
'KEY_ADMIN',
|
'KEY_ADMIN',
|
||||||
'密钥管理员',
|
'密钥管理员',
|
||||||
1,
|
1,
|
||||||
'/xqmDy3A/q9X5p7XWznIPSBabQ6bxTLAPgtHQ6Be33c=',
|
|
||||||
'init-key-admin-salt-20260325',
|
|
||||||
'ACTIVE',
|
'ACTIVE',
|
||||||
1,
|
|
||||||
0,
|
|
||||||
NULL,
|
|
||||||
NULL,
|
|
||||||
NULL,
|
|
||||||
CURRENT_TIMESTAMP(3),
|
CURRENT_TIMESTAMP(3),
|
||||||
CURRENT_TIMESTAMP(3)
|
CURRENT_TIMESTAMP(3)
|
||||||
),
|
),
|
||||||
@ -428,14 +422,7 @@ VALUES
|
|||||||
'AUDIT_ADMIN',
|
'AUDIT_ADMIN',
|
||||||
'审计管理员',
|
'审计管理员',
|
||||||
1,
|
1,
|
||||||
'j/J1LKyUOzVEJfbbHe2dwKBKNdabRk502olk/LWIwhg=',
|
|
||||||
'init-audit-admin-salt-20260325',
|
|
||||||
'UNENABLED',
|
'UNENABLED',
|
||||||
1,
|
|
||||||
0,
|
|
||||||
NULL,
|
|
||||||
NULL,
|
|
||||||
NULL,
|
|
||||||
CURRENT_TIMESTAMP(3),
|
CURRENT_TIMESTAMP(3),
|
||||||
CURRENT_TIMESTAMP(3)
|
CURRENT_TIMESTAMP(3)
|
||||||
),
|
),
|
||||||
@ -444,14 +431,7 @@ VALUES
|
|||||||
'OPS_ADMIN',
|
'OPS_ADMIN',
|
||||||
'运维管理员',
|
'运维管理员',
|
||||||
1,
|
1,
|
||||||
'2e9F4aCgsDb+AuD1LrBEqEg8yjy3ODo27UHtx06vj/A=',
|
|
||||||
'init-ops-admin-salt-20260325',
|
|
||||||
'UNENABLED',
|
'UNENABLED',
|
||||||
1,
|
|
||||||
0,
|
|
||||||
NULL,
|
|
||||||
NULL,
|
|
||||||
NULL,
|
|
||||||
CURRENT_TIMESTAMP(3),
|
CURRENT_TIMESTAMP(3),
|
||||||
CURRENT_TIMESTAMP(3)
|
CURRENT_TIMESTAMP(3)
|
||||||
);
|
);
|
||||||
@ -464,6 +444,7 @@ INSERT IGNORE INTO tms_auth_user_account (
|
|||||||
password_hash,
|
password_hash,
|
||||||
password_salt,
|
password_salt,
|
||||||
status,
|
status,
|
||||||
|
need_change_password,
|
||||||
failed_count,
|
failed_count,
|
||||||
locked_until,
|
locked_until,
|
||||||
last_login_at,
|
last_login_at,
|
||||||
@ -480,6 +461,7 @@ VALUES
|
|||||||
'Viuew8WE+qziUZwGGU0x25cUpzsTF+QvQFU0uhb+yVA=',
|
'Viuew8WE+qziUZwGGU0x25cUpzsTF+QvQFU0uhb+yVA=',
|
||||||
'init-super-admin-salt-20260325',
|
'init-super-admin-salt-20260325',
|
||||||
'ACTIVE',
|
'ACTIVE',
|
||||||
|
1,
|
||||||
0,
|
0,
|
||||||
NULL,
|
NULL,
|
||||||
NULL,
|
NULL,
|
||||||
@ -495,6 +477,7 @@ VALUES
|
|||||||
'Viuew8WE+qziUZwGGU0x25cUpzsTF+QvQFU0uhb+yVA=',
|
'Viuew8WE+qziUZwGGU0x25cUpzsTF+QvQFU0uhb+yVA=',
|
||||||
'init-super-admin-salt-20260325',
|
'init-super-admin-salt-20260325',
|
||||||
'ACTIVE',
|
'ACTIVE',
|
||||||
|
1,
|
||||||
0,
|
0,
|
||||||
NULL,
|
NULL,
|
||||||
NULL,
|
NULL,
|
||||||
@ -510,6 +493,7 @@ VALUES
|
|||||||
'/xqmDy3A/q9X5p7XWznIPSBabQ6bxTLAPgtHQ6Be33c=',
|
'/xqmDy3A/q9X5p7XWznIPSBabQ6bxTLAPgtHQ6Be33c=',
|
||||||
'init-key-admin-salt-20260325',
|
'init-key-admin-salt-20260325',
|
||||||
'ACTIVE',
|
'ACTIVE',
|
||||||
|
1,
|
||||||
0,
|
0,
|
||||||
NULL,
|
NULL,
|
||||||
NULL,
|
NULL,
|
||||||
@ -525,6 +509,7 @@ VALUES
|
|||||||
'j/J1LKyUOzVEJfbbHe2dwKBKNdabRk502olk/LWIwhg=',
|
'j/J1LKyUOzVEJfbbHe2dwKBKNdabRk502olk/LWIwhg=',
|
||||||
'init-audit-admin-salt-20260325',
|
'init-audit-admin-salt-20260325',
|
||||||
'ACTIVE',
|
'ACTIVE',
|
||||||
|
1,
|
||||||
0,
|
0,
|
||||||
NULL,
|
NULL,
|
||||||
NULL,
|
NULL,
|
||||||
@ -540,6 +525,111 @@ VALUES
|
|||||||
'2e9F4aCgsDb+AuD1LrBEqEg8yjy3ODo27UHtx06vj/A=',
|
'2e9F4aCgsDb+AuD1LrBEqEg8yjy3ODo27UHtx06vj/A=',
|
||||||
'init-ops-admin-salt-20260325',
|
'init-ops-admin-salt-20260325',
|
||||||
'ACTIVE',
|
'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,
|
0,
|
||||||
NULL,
|
NULL,
|
||||||
NULL,
|
NULL,
|
||||||
|
|||||||
67
src/main/resources/mapper/auth/AuthFullAccountMapper.xml
Normal file
67
src/main/resources/mapper/auth/AuthFullAccountMapper.xml
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" ?>
|
||||||
|
<!DOCTYPE mapper
|
||||||
|
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||||
|
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||||
|
<mapper namespace="com.cisd.tms.modules.auth.mapper.AuthFullAccountMapper">
|
||||||
|
|
||||||
|
<resultMap id="AuthFullAccountResultMap" type="com.cisd.tms.modules.auth.entity.AuthFullAccountEntity">
|
||||||
|
<id property="id" column="id"/>
|
||||||
|
<result property="roleCode" column="role_code"/>
|
||||||
|
<result property="uid" column="uid"/>
|
||||||
|
<result property="accountName" column="account_name"/>
|
||||||
|
<result property="displayName" column="display_name"/>
|
||||||
|
<result property="passwordHash" column="password_hash"/>
|
||||||
|
<result property="passwordSalt" column="password_salt"/>
|
||||||
|
<result property="status" column="status"/>
|
||||||
|
<result property="needChangePassword" column="need_change_password"/>
|
||||||
|
<result property="failedCount" column="failed_count"/>
|
||||||
|
<result property="lockedUntil" column="locked_until"/>
|
||||||
|
<result property="lastLoginAt" column="last_login_at"/>
|
||||||
|
<result property="lastActiveAt" column="last_active_at"/>
|
||||||
|
<result property="createTime" column="create_time"/>
|
||||||
|
<result property="updateTime" column="update_time"/>
|
||||||
|
</resultMap>
|
||||||
|
|
||||||
|
<select id="selectByRoleCodeAndUid" resultMap="AuthFullAccountResultMap">
|
||||||
|
SELECT 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
|
||||||
|
FROM tms_auth_full_account
|
||||||
|
WHERE role_code = #{roleCode}
|
||||||
|
AND uid = #{uid}
|
||||||
|
LIMIT 1
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<select id="selectByRoleCode" resultMap="AuthFullAccountResultMap">
|
||||||
|
SELECT 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
|
||||||
|
FROM tms_auth_full_account
|
||||||
|
WHERE role_code = #{roleCode}
|
||||||
|
ORDER BY uid ASC
|
||||||
|
</select>
|
||||||
|
</mapper>
|
||||||
@ -8,7 +8,7 @@
|
|||||||
<id property="id" column="id"/>
|
<id property="id" column="id"/>
|
||||||
<result property="sessionToken" column="session_token"/>
|
<result property="sessionToken" column="session_token"/>
|
||||||
<result property="roleCode" column="role_code"/>
|
<result property="roleCode" column="role_code"/>
|
||||||
<result property="authenticatedUsersJson" column="authenticated_users_json"/>
|
<result property="authenticatedPrincipalsJson" column="authenticated_principals_json"/>
|
||||||
<result property="authMethod" column="auth_method"/>
|
<result property="authMethod" column="auth_method"/>
|
||||||
<result property="authLevel" column="auth_level"/>
|
<result property="authLevel" column="auth_level"/>
|
||||||
<result property="issuedAt" column="issued_at"/>
|
<result property="issuedAt" column="issued_at"/>
|
||||||
@ -23,7 +23,7 @@
|
|||||||
SELECT id,
|
SELECT id,
|
||||||
session_token,
|
session_token,
|
||||||
role_code,
|
role_code,
|
||||||
authenticated_users_json,
|
authenticated_principals_json,
|
||||||
auth_method,
|
auth_method,
|
||||||
auth_level,
|
auth_level,
|
||||||
issued_at,
|
issued_at,
|
||||||
@ -36,4 +36,47 @@
|
|||||||
WHERE session_token = #{sessionToken}
|
WHERE session_token = #{sessionToken}
|
||||||
LIMIT 1
|
LIMIT 1
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
|
<select id="selectActiveByRoleCodeAndAuthLevel" resultMap="AuthSessionResultMap">
|
||||||
|
SELECT id,
|
||||||
|
session_token,
|
||||||
|
role_code,
|
||||||
|
authenticated_principals_json,
|
||||||
|
auth_method,
|
||||||
|
auth_level,
|
||||||
|
issued_at,
|
||||||
|
last_active_at,
|
||||||
|
expires_at,
|
||||||
|
logout_at,
|
||||||
|
create_time,
|
||||||
|
update_time
|
||||||
|
FROM tms_auth_session
|
||||||
|
WHERE role_code = #{roleCode}
|
||||||
|
AND auth_level = #{authLevel}
|
||||||
|
AND logout_at IS NULL
|
||||||
|
AND expires_at IS NOT NULL
|
||||||
|
AND expires_at > #{now}
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<select id="selectActiveByRoleCodeAndAuthLevelAndAuthenticatedPrincipalsJson" resultMap="AuthSessionResultMap">
|
||||||
|
SELECT id,
|
||||||
|
session_token,
|
||||||
|
role_code,
|
||||||
|
authenticated_principals_json,
|
||||||
|
auth_method,
|
||||||
|
auth_level,
|
||||||
|
issued_at,
|
||||||
|
last_active_at,
|
||||||
|
expires_at,
|
||||||
|
logout_at,
|
||||||
|
create_time,
|
||||||
|
update_time
|
||||||
|
FROM tms_auth_session
|
||||||
|
WHERE role_code = #{roleCode}
|
||||||
|
AND auth_level = #{authLevel}
|
||||||
|
AND authenticated_principals_json = #{authenticatedPrincipalsJson}
|
||||||
|
AND logout_at IS NULL
|
||||||
|
AND expires_at IS NOT NULL
|
||||||
|
AND expires_at > #{now}
|
||||||
|
</select>
|
||||||
</mapper>
|
</mapper>
|
||||||
|
|||||||
@ -12,6 +12,7 @@
|
|||||||
<result property="passwordHash" column="password_hash"/>
|
<result property="passwordHash" column="password_hash"/>
|
||||||
<result property="passwordSalt" column="password_salt"/>
|
<result property="passwordSalt" column="password_salt"/>
|
||||||
<result property="status" column="status"/>
|
<result property="status" column="status"/>
|
||||||
|
<result property="needChangePassword" column="need_change_password"/>
|
||||||
<result property="failedCount" column="failed_count"/>
|
<result property="failedCount" column="failed_count"/>
|
||||||
<result property="lockedUntil" column="locked_until"/>
|
<result property="lockedUntil" column="locked_until"/>
|
||||||
<result property="lastLoginAt" column="last_login_at"/>
|
<result property="lastLoginAt" column="last_login_at"/>
|
||||||
@ -28,6 +29,7 @@
|
|||||||
password_hash,
|
password_hash,
|
||||||
password_salt,
|
password_salt,
|
||||||
status,
|
status,
|
||||||
|
need_change_password,
|
||||||
failed_count,
|
failed_count,
|
||||||
locked_until,
|
locked_until,
|
||||||
last_login_at,
|
last_login_at,
|
||||||
@ -47,6 +49,7 @@
|
|||||||
password_hash,
|
password_hash,
|
||||||
password_salt,
|
password_salt,
|
||||||
status,
|
status,
|
||||||
|
need_change_password,
|
||||||
failed_count,
|
failed_count,
|
||||||
locked_until,
|
locked_until,
|
||||||
last_login_at,
|
last_login_at,
|
||||||
|
|||||||
@ -9,14 +9,7 @@
|
|||||||
<result property="roleCode" column="role_code"/>
|
<result property="roleCode" column="role_code"/>
|
||||||
<result property="displayName" column="display_name"/>
|
<result property="displayName" column="display_name"/>
|
||||||
<result property="requiredUkeyCount" column="required_ukey_count"/>
|
<result property="requiredUkeyCount" column="required_ukey_count"/>
|
||||||
<result property="passwordHash" column="password_hash"/>
|
|
||||||
<result property="passwordSalt" column="password_salt"/>
|
|
||||||
<result property="status" column="status"/>
|
<result property="status" column="status"/>
|
||||||
<result property="needChangePassword" column="need_change_password"/>
|
|
||||||
<result property="failedCount" column="failed_count"/>
|
|
||||||
<result property="lockedUntil" column="locked_until"/>
|
|
||||||
<result property="lastLoginAt" column="last_login_at"/>
|
|
||||||
<result property="lastActiveAt" column="last_active_at"/>
|
|
||||||
<result property="createTime" column="create_time"/>
|
<result property="createTime" column="create_time"/>
|
||||||
<result property="updateTime" column="update_time"/>
|
<result property="updateTime" column="update_time"/>
|
||||||
</resultMap>
|
</resultMap>
|
||||||
@ -26,14 +19,7 @@
|
|||||||
role_code,
|
role_code,
|
||||||
display_name,
|
display_name,
|
||||||
required_ukey_count,
|
required_ukey_count,
|
||||||
password_hash,
|
|
||||||
password_salt,
|
|
||||||
status,
|
status,
|
||||||
need_change_password,
|
|
||||||
failed_count,
|
|
||||||
locked_until,
|
|
||||||
last_login_at,
|
|
||||||
last_active_at,
|
|
||||||
create_time,
|
create_time,
|
||||||
update_time
|
update_time
|
||||||
FROM tms_role_account
|
FROM tms_role_account
|
||||||
|
|||||||
@ -92,7 +92,7 @@ class AuthControllerTest {
|
|||||||
AuthService authService = Mockito.mock(AuthService.class);
|
AuthService authService = Mockito.mock(AuthService.class);
|
||||||
AuthAdminService authAdminService = Mockito.mock(AuthAdminService.class);
|
AuthAdminService authAdminService = Mockito.mock(AuthAdminService.class);
|
||||||
LoginResponse response = new LoginResponse();
|
LoginResponse response = new LoginResponse();
|
||||||
response.setRoleCode("KEY_ADMIN");
|
response.setRoleCode("SUPER_ADMIN");
|
||||||
response.setAuthLevel("FULL");
|
response.setAuthLevel("FULL");
|
||||||
response.setToken("token-ukey-001");
|
response.setToken("token-ukey-001");
|
||||||
Mockito.when(authService.ukeyLogin(ArgumentMatchers.any())).thenReturn(response);
|
Mockito.when(authService.ukeyLogin(ArgumentMatchers.any())).thenReturn(response);
|
||||||
@ -106,12 +106,12 @@ class AuthControllerTest {
|
|||||||
.contentType(MediaType.APPLICATION_JSON)
|
.contentType(MediaType.APPLICATION_JSON)
|
||||||
.content("""
|
.content("""
|
||||||
{
|
{
|
||||||
"roleCode": "KEY_ADMIN",
|
"roleCode": "SUPER_ADMIN",
|
||||||
"rolePassword": "12345678",
|
"loginFactors": [
|
||||||
"ukeyProofs": [
|
|
||||||
{
|
{
|
||||||
"pubKey": "PUB-1",
|
"pubKey": "PUB-1",
|
||||||
"uid": 1,
|
"uid": 1,
|
||||||
|
"password": "11111111",
|
||||||
"serverRandom": "RB-1",
|
"serverRandom": "RB-1",
|
||||||
"issueSignature": "ISSUE-1",
|
"issueSignature": "ISSUE-1",
|
||||||
"loginPayload": "LOGIN-DATA-1",
|
"loginPayload": "LOGIN-DATA-1",
|
||||||
@ -120,6 +120,7 @@ class AuthControllerTest {
|
|||||||
{
|
{
|
||||||
"pubKey": "PUB-2",
|
"pubKey": "PUB-2",
|
||||||
"uid": 2,
|
"uid": 2,
|
||||||
|
"password": "22222222",
|
||||||
"serverRandom": "RB-2",
|
"serverRandom": "RB-2",
|
||||||
"issueSignature": "ISSUE-2",
|
"issueSignature": "ISSUE-2",
|
||||||
"loginPayload": "LOGIN-DATA-2",
|
"loginPayload": "LOGIN-DATA-2",
|
||||||
@ -198,7 +199,7 @@ class AuthControllerTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void shouldChangePasswordThroughCurrentSessionEndpoint() throws Exception {
|
void shouldChangeFullAccountPasswordThroughCurrentSessionEndpoint() throws Exception {
|
||||||
AuthService authService = Mockito.mock(AuthService.class);
|
AuthService authService = Mockito.mock(AuthService.class);
|
||||||
AuthAdminService authAdminService = Mockito.mock(AuthAdminService.class);
|
AuthAdminService authAdminService = Mockito.mock(AuthAdminService.class);
|
||||||
|
|
||||||
@ -207,7 +208,7 @@ class AuthControllerTest {
|
|||||||
.setControllerAdvice(new GlobalExceptionHandler())
|
.setControllerAdvice(new GlobalExceptionHandler())
|
||||||
.build();
|
.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")
|
.requestAttr(InternalApiAuthInterceptor.ATTR_SESSION_TOKEN, "token-change-001")
|
||||||
.contentType(MediaType.APPLICATION_JSON)
|
.contentType(MediaType.APPLICATION_JSON)
|
||||||
.content("""
|
.content("""
|
||||||
@ -219,7 +220,32 @@ class AuthControllerTest {
|
|||||||
.andExpect(status().isOk())
|
.andExpect(status().isOk())
|
||||||
.andExpect(content().string(containsString("\"success\":true")));
|
.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
|
@Test
|
||||||
|
|||||||
@ -1,11 +1,13 @@
|
|||||||
package com.cisd.tms.modules.auth.service;
|
package com.cisd.tms.modules.auth.service;
|
||||||
|
|
||||||
import com.cisd.tms.modules.auth.entity.AuthUserAccountEntity;
|
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.RoleAccountEntity;
|
||||||
import com.cisd.tms.modules.auth.entity.RoleUkeyBindingEntity;
|
import com.cisd.tms.modules.auth.entity.RoleUkeyBindingEntity;
|
||||||
import com.cisd.tms.modules.auth.enums.AuthLevel;
|
import com.cisd.tms.modules.auth.enums.AuthLevel;
|
||||||
import com.cisd.tms.modules.auth.enums.RoleAccountStatus;
|
import com.cisd.tms.modules.auth.enums.RoleAccountStatus;
|
||||||
import com.cisd.tms.modules.auth.enums.RoleCode;
|
import com.cisd.tms.modules.auth.enums.RoleCode;
|
||||||
|
import com.cisd.tms.modules.auth.repository.AuthFullAccountRepository;
|
||||||
import com.cisd.tms.modules.auth.repository.AuthUserAccountRepository;
|
import com.cisd.tms.modules.auth.repository.AuthUserAccountRepository;
|
||||||
import com.cisd.tms.modules.auth.repository.RoleAccountRepository;
|
import com.cisd.tms.modules.auth.repository.RoleAccountRepository;
|
||||||
import com.cisd.tms.modules.auth.repository.RoleUkeyBindingRepository;
|
import com.cisd.tms.modules.auth.repository.RoleUkeyBindingRepository;
|
||||||
@ -36,11 +38,18 @@ class AuthAdminServiceTest {
|
|||||||
@Test
|
@Test
|
||||||
void shouldEnableRoleWhenOperatorIsKeyAdminWithFullSession() {
|
void shouldEnableRoleWhenOperatorIsKeyAdminWithFullSession() {
|
||||||
InMemoryRoleAccountRepository roleAccounts = new InMemoryRoleAccountRepository();
|
InMemoryRoleAccountRepository roleAccounts = new InMemoryRoleAccountRepository();
|
||||||
|
InMemoryAuthFullAccountRepository fullAccounts = new InMemoryAuthFullAccountRepository();
|
||||||
|
InMemoryAuthUserAccountRepository userAccounts = new InMemoryAuthUserAccountRepository();
|
||||||
roleAccounts.save(role(RoleCode.AUDIT_ADMIN, RoleAccountStatus.UNENABLED));
|
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(
|
AuthAdminService service = newAuthAdminService(
|
||||||
roleAccounts,
|
roleAccounts,
|
||||||
new InMemoryAuthUserAccountRepository(),
|
fullAccounts,
|
||||||
|
userAccounts,
|
||||||
new InMemoryRoleUkeyBindingRepository(),
|
new InMemoryRoleUkeyBindingRepository(),
|
||||||
FIXED_CLOCK,
|
FIXED_CLOCK,
|
||||||
new FixedSaltSupplier("salt-001")
|
new FixedSaltSupplier("salt-001")
|
||||||
@ -49,16 +58,29 @@ class AuthAdminServiceTest {
|
|||||||
service.enableRole(RoleCode.KEY_ADMIN.getCode(), AuthLevel.FULL.name(), RoleCode.AUDIT_ADMIN.getCode());
|
service.enableRole(RoleCode.KEY_ADMIN.getCode(), AuthLevel.FULL.name(), RoleCode.AUDIT_ADMIN.getCode());
|
||||||
|
|
||||||
RoleAccountEntity updated = roleAccounts.findByRoleCode(RoleCode.AUDIT_ADMIN.getCode()).orElseThrow();
|
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(), 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
|
@Test
|
||||||
void shouldResetPasswordForAllAccountsUnderRole() {
|
void shouldResetPasswordForAllAccountsUnderRole() {
|
||||||
InMemoryRoleAccountRepository roleAccounts = new InMemoryRoleAccountRepository();
|
InMemoryRoleAccountRepository roleAccounts = new InMemoryRoleAccountRepository();
|
||||||
|
InMemoryAuthFullAccountRepository fullAccounts = new InMemoryAuthFullAccountRepository();
|
||||||
InMemoryAuthUserAccountRepository userAccounts = new InMemoryAuthUserAccountRepository();
|
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-01", RoleCode.SUPER_ADMIN, "OLD-HASH-1", "OLD-SALT-1"));
|
||||||
userAccounts.save(user("super-admin-02", RoleCode.SUPER_ADMIN, "OLD-HASH-2", "OLD-SALT-2"));
|
userAccounts.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().setFailedCount(3);
|
||||||
userAccounts.findByUsername("super-admin-01").orElseThrow().setLockedUntil(LocalDateTime.of(2026, 3, 23, 3, 30));
|
userAccounts.findByUsername("super-admin-01").orElseThrow().setLockedUntil(LocalDateTime.of(2026, 3, 23, 3, 30));
|
||||||
userAccounts.findByUsername("super-admin-02").orElseThrow().setFailedCount(5);
|
userAccounts.findByUsername("super-admin-02").orElseThrow().setFailedCount(5);
|
||||||
@ -66,22 +88,37 @@ class AuthAdminServiceTest {
|
|||||||
|
|
||||||
AuthAdminService service = newAuthAdminService(
|
AuthAdminService service = newAuthAdminService(
|
||||||
roleAccounts,
|
roleAccounts,
|
||||||
|
fullAccounts,
|
||||||
userAccounts,
|
userAccounts,
|
||||||
new InMemoryRoleUkeyBindingRepository(),
|
new InMemoryRoleUkeyBindingRepository(),
|
||||||
FIXED_CLOCK,
|
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());
|
service.resetPassword(RoleCode.KEY_ADMIN.getCode(), AuthLevel.FULL.name(), RoleCode.SUPER_ADMIN.getCode());
|
||||||
|
|
||||||
RoleAccountEntity role = roleAccounts.findByRoleCode(RoleCode.SUPER_ADMIN.getCode()).orElseThrow();
|
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 first = userAccounts.findByUsername("super-admin-01").orElseThrow();
|
||||||
AuthUserAccountEntity second = userAccounts.findByUsername("super-admin-02").orElseThrow();
|
AuthUserAccountEntity second = userAccounts.findByUsername("super-admin-02").orElseThrow();
|
||||||
Assertions.assertEquals(RoleAccountStatus.ACTIVE.name(), role.getStatus());
|
Assertions.assertEquals(RoleAccountStatus.ACTIVE.name(), role.getStatus());
|
||||||
Assertions.assertEquals("salt-role-001", role.getPasswordSalt());
|
Assertions.assertEquals(RoleAccountStatus.ACTIVE.name(), fullFirst.getStatus());
|
||||||
Assertions.assertEquals("HASH:12345678:salt-role-001", role.getPasswordHash());
|
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(), first.getStatus());
|
||||||
Assertions.assertEquals(RoleAccountStatus.ACTIVE.name(), second.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, first.getFailedCount());
|
||||||
Assertions.assertEquals(0, second.getFailedCount());
|
Assertions.assertEquals(0, second.getFailedCount());
|
||||||
Assertions.assertNull(first.getLockedUntil());
|
Assertions.assertNull(first.getLockedUntil());
|
||||||
@ -101,6 +138,7 @@ class AuthAdminServiceTest {
|
|||||||
|
|
||||||
AuthAdminService service = new AuthAdminServiceImpl(
|
AuthAdminService service = new AuthAdminServiceImpl(
|
||||||
new InMemoryRoleAccountRepository(),
|
new InMemoryRoleAccountRepository(),
|
||||||
|
fullAccountRepository(binding(RoleCode.SUPER_ADMIN, 1, "UK-OLD", "PUB-OLD", "SIG-OLD")),
|
||||||
new InMemoryAuthUserAccountRepository(),
|
new InMemoryAuthUserAccountRepository(),
|
||||||
bindings,
|
bindings,
|
||||||
new FakePasswordHasher(),
|
new FakePasswordHasher(),
|
||||||
@ -136,6 +174,7 @@ class AuthAdminServiceTest {
|
|||||||
|
|
||||||
AuthAdminService service = new AuthAdminServiceImpl(
|
AuthAdminService service = new AuthAdminServiceImpl(
|
||||||
new InMemoryRoleAccountRepository(),
|
new InMemoryRoleAccountRepository(),
|
||||||
|
fullAccountRepository(),
|
||||||
new InMemoryAuthUserAccountRepository(),
|
new InMemoryAuthUserAccountRepository(),
|
||||||
new InMemoryRoleUkeyBindingRepository(),
|
new InMemoryRoleUkeyBindingRepository(),
|
||||||
new FakePasswordHasher(),
|
new FakePasswordHasher(),
|
||||||
@ -165,14 +204,6 @@ class AuthAdminServiceTest {
|
|||||||
return entity;
|
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) {
|
private static AuthUserAccountEntity user(String username, RoleCode roleCode, String passwordHash, String passwordSalt) {
|
||||||
AuthUserAccountEntity entity = new AuthUserAccountEntity();
|
AuthUserAccountEntity entity = new AuthUserAccountEntity();
|
||||||
entity.setId((long) (username.hashCode() & Integer.MAX_VALUE));
|
entity.setId((long) (username.hashCode() & Integer.MAX_VALUE));
|
||||||
@ -182,6 +213,22 @@ class AuthAdminServiceTest {
|
|||||||
entity.setPasswordHash(passwordHash);
|
entity.setPasswordHash(passwordHash);
|
||||||
entity.setPasswordSalt(passwordSalt);
|
entity.setPasswordSalt(passwordSalt);
|
||||||
entity.setStatus(RoleAccountStatus.ACTIVE.name());
|
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);
|
entity.setFailedCount(0);
|
||||||
return entity;
|
return entity;
|
||||||
}
|
}
|
||||||
@ -277,6 +324,33 @@ class AuthAdminServiceTest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static class InMemoryAuthFullAccountRepository implements AuthFullAccountRepository {
|
||||||
|
private final Map<String, AuthFullAccountEntity> store = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Optional<AuthFullAccountEntity> findByRoleCodeAndUid(String roleCode, Integer uid) {
|
||||||
|
return Optional.ofNullable(store.get(roleCode + "#" + uid));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<AuthFullAccountEntity> 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 static class InMemoryRoleUkeyBindingRepository implements RoleUkeyBindingRepository {
|
||||||
private final Map<String, List<RoleUkeyBindingEntity>> store = new ConcurrentHashMap<>();
|
private final Map<String, List<RoleUkeyBindingEntity>> store = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
@ -311,6 +385,7 @@ class AuthAdminServiceTest {
|
|||||||
|
|
||||||
private static AuthAdminService newAuthAdminService(
|
private static AuthAdminService newAuthAdminService(
|
||||||
InMemoryRoleAccountRepository roleAccounts,
|
InMemoryRoleAccountRepository roleAccounts,
|
||||||
|
InMemoryAuthFullAccountRepository fullAccounts,
|
||||||
InMemoryAuthUserAccountRepository userAccounts,
|
InMemoryAuthUserAccountRepository userAccounts,
|
||||||
InMemoryRoleUkeyBindingRepository bindings,
|
InMemoryRoleUkeyBindingRepository bindings,
|
||||||
Clock clock,
|
Clock clock,
|
||||||
@ -318,6 +393,7 @@ class AuthAdminServiceTest {
|
|||||||
) {
|
) {
|
||||||
return new AuthAdminServiceImpl(
|
return new AuthAdminServiceImpl(
|
||||||
roleAccounts,
|
roleAccounts,
|
||||||
|
fullAccounts,
|
||||||
userAccounts,
|
userAccounts,
|
||||||
bindings,
|
bindings,
|
||||||
new FakePasswordHasher(),
|
new FakePasswordHasher(),
|
||||||
@ -327,4 +403,18 @@ class AuthAdminServiceTest {
|
|||||||
saltGenerator
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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.UkeyLoginRandomRequest;
|
||||||
import com.cisd.tms.modules.auth.dto.UkeyLoginRandomResponse;
|
import com.cisd.tms.modules.auth.dto.UkeyLoginRandomResponse;
|
||||||
import com.cisd.tms.modules.auth.dto.UkeyLoginRequest;
|
import com.cisd.tms.modules.auth.dto.UkeyLoginRequest;
|
||||||
|
import com.cisd.tms.modules.auth.entity.AuthFullAccountEntity;
|
||||||
import com.cisd.tms.modules.auth.entity.AuthSessionEntity;
|
import com.cisd.tms.modules.auth.entity.AuthSessionEntity;
|
||||||
import com.cisd.tms.modules.auth.entity.AuthUserEntity;
|
import com.cisd.tms.modules.auth.entity.AuthUserEntity;
|
||||||
import com.cisd.tms.modules.auth.entity.AuthUserAccountEntity;
|
import com.cisd.tms.modules.auth.entity.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.RoleAccountStatus;
|
||||||
import com.cisd.tms.modules.auth.enums.RoleCode;
|
import com.cisd.tms.modules.auth.enums.RoleCode;
|
||||||
import com.cisd.tms.modules.auth.repository.AuthSessionRepository;
|
import com.cisd.tms.modules.auth.repository.AuthSessionRepository;
|
||||||
|
import com.cisd.tms.modules.auth.repository.AuthFullAccountRepository;
|
||||||
import com.cisd.tms.modules.auth.repository.AuthUserAccountRepository;
|
import com.cisd.tms.modules.auth.repository.AuthUserAccountRepository;
|
||||||
import com.cisd.tms.modules.auth.repository.AuthUserRepository;
|
import com.cisd.tms.modules.auth.repository.AuthUserRepository;
|
||||||
import com.cisd.tms.modules.auth.repository.RoleAccountRepository;
|
import com.cisd.tms.modules.auth.repository.RoleAccountRepository;
|
||||||
@ -53,12 +55,13 @@ class AuthServiceTest {
|
|||||||
InMemoryRoleAccountRepository roleAccounts = new InMemoryRoleAccountRepository();
|
InMemoryRoleAccountRepository roleAccounts = new InMemoryRoleAccountRepository();
|
||||||
InMemoryAuthUserAccountRepository userAccounts = new InMemoryAuthUserAccountRepository();
|
InMemoryAuthUserAccountRepository userAccounts = new InMemoryAuthUserAccountRepository();
|
||||||
InMemoryAuthSessionRepository sessions = new InMemoryAuthSessionRepository();
|
InMemoryAuthSessionRepository sessions = new InMemoryAuthSessionRepository();
|
||||||
roleAccounts.save(activeRole(RoleCode.AUDIT_ADMIN, true));
|
roleAccounts.save(activeRole(RoleCode.AUDIT_ADMIN));
|
||||||
userAccounts.save(activeUserAccount("audit-admin-01", RoleCode.AUDIT_ADMIN, "12345678", "SALT-A", 0, null));
|
userAccounts.save(activeUserAccount("audit-admin-01", RoleCode.AUDIT_ADMIN, "12345678", "SALT-A", 0, null, true));
|
||||||
|
|
||||||
AuthService service = newAuthService(
|
AuthService service = newAuthService(
|
||||||
roleAccounts,
|
roleAccounts,
|
||||||
userAccounts,
|
userAccounts,
|
||||||
|
new InMemoryAuthFullAccountRepository(),
|
||||||
sessions,
|
sessions,
|
||||||
new InMemoryRoleUkeyBindingRepository(),
|
new InMemoryRoleUkeyBindingRepository(),
|
||||||
FIXED_CLOCK,
|
FIXED_CLOCK,
|
||||||
@ -77,7 +80,7 @@ class AuthServiceTest {
|
|||||||
Assertions.assertTrue(response.getNeedChangePassword());
|
Assertions.assertTrue(response.getNeedChangePassword());
|
||||||
AuthSessionEntity session = sessions.findBySessionToken("token-limited-001").orElseThrow();
|
AuthSessionEntity session = sessions.findBySessionToken("token-limited-001").orElseThrow();
|
||||||
Assertions.assertEquals(AuthMethod.PASSWORD.name(), session.getAuthMethod());
|
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
|
@Test
|
||||||
@ -91,6 +94,7 @@ class AuthServiceTest {
|
|||||||
AuthService service = newAuthService(
|
AuthService service = newAuthService(
|
||||||
roleAccounts,
|
roleAccounts,
|
||||||
userAccounts,
|
userAccounts,
|
||||||
|
new InMemoryAuthFullAccountRepository(),
|
||||||
new InMemoryAuthSessionRepository(),
|
new InMemoryAuthSessionRepository(),
|
||||||
new InMemoryRoleUkeyBindingRepository(),
|
new InMemoryRoleUkeyBindingRepository(),
|
||||||
FIXED_CLOCK,
|
FIXED_CLOCK,
|
||||||
@ -117,6 +121,7 @@ class AuthServiceTest {
|
|||||||
AuthService service = newAuthService(
|
AuthService service = newAuthService(
|
||||||
roleAccounts,
|
roleAccounts,
|
||||||
userAccounts,
|
userAccounts,
|
||||||
|
new InMemoryAuthFullAccountRepository(),
|
||||||
new InMemoryAuthSessionRepository(),
|
new InMemoryAuthSessionRepository(),
|
||||||
new InMemoryRoleUkeyBindingRepository(),
|
new InMemoryRoleUkeyBindingRepository(),
|
||||||
FIXED_CLOCK,
|
FIXED_CLOCK,
|
||||||
@ -145,13 +150,18 @@ class AuthServiceTest {
|
|||||||
@Test
|
@Test
|
||||||
void shouldKeepMeEndpointRoleScoped() {
|
void shouldKeepMeEndpointRoleScoped() {
|
||||||
InMemoryRoleAccountRepository roleAccounts = new InMemoryRoleAccountRepository();
|
InMemoryRoleAccountRepository roleAccounts = new InMemoryRoleAccountRepository();
|
||||||
|
InMemoryAuthUserAccountRepository userAccounts = new InMemoryAuthUserAccountRepository();
|
||||||
InMemoryAuthSessionRepository sessions = new InMemoryAuthSessionRepository();
|
InMemoryAuthSessionRepository sessions = new InMemoryAuthSessionRepository();
|
||||||
roleAccounts.save(activeRole(RoleCode.AUDIT_ADMIN, true));
|
roleAccounts.save(activeRole(RoleCode.AUDIT_ADMIN));
|
||||||
sessions.save(session("token-me-001", RoleCode.AUDIT_ADMIN.getCode(), AuthLevel.LIMITED.name()));
|
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(
|
AuthService service = newAuthService(
|
||||||
roleAccounts,
|
roleAccounts,
|
||||||
new InMemoryAuthUserAccountRepository(),
|
userAccounts,
|
||||||
|
new InMemoryAuthFullAccountRepository(),
|
||||||
sessions,
|
sessions,
|
||||||
new InMemoryRoleUkeyBindingRepository(),
|
new InMemoryRoleUkeyBindingRepository(),
|
||||||
FIXED_CLOCK,
|
FIXED_CLOCK,
|
||||||
@ -168,44 +178,87 @@ class AuthServiceTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void shouldChangeCurrentRolePasswordForActiveSession() {
|
void shouldChangeCurrentFullAccountPasswordForActiveSession() {
|
||||||
InMemoryRoleAccountRepository roleAccounts = new InMemoryRoleAccountRepository();
|
InMemoryRoleAccountRepository roleAccounts = new InMemoryRoleAccountRepository();
|
||||||
|
InMemoryAuthFullAccountRepository fullAccounts = new InMemoryAuthFullAccountRepository();
|
||||||
InMemoryAuthSessionRepository sessions = new InMemoryAuthSessionRepository();
|
InMemoryAuthSessionRepository sessions = new InMemoryAuthSessionRepository();
|
||||||
roleAccounts.save(activeRole(RoleCode.KEY_ADMIN, "12345678", "ROLE-SALT-K", 0, null, true));
|
roleAccounts.save(activeRole(RoleCode.KEY_ADMIN));
|
||||||
sessions.save(session("token-change-001", RoleCode.KEY_ADMIN.getCode(), AuthLevel.FULL.name()));
|
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(
|
AuthService service = newAuthService(
|
||||||
roleAccounts,
|
roleAccounts,
|
||||||
new InMemoryAuthUserAccountRepository(),
|
new InMemoryAuthUserAccountRepository(),
|
||||||
|
fullAccounts,
|
||||||
sessions,
|
sessions,
|
||||||
new InMemoryRoleUkeyBindingRepository(),
|
new InMemoryRoleUkeyBindingRepository(),
|
||||||
FIXED_CLOCK,
|
FIXED_CLOCK,
|
||||||
() -> "unused"
|
() -> "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("salt-test", changed.getPasswordSalt());
|
||||||
Assertions.assertEquals("HASH:87654321:salt-test", changed.getPasswordHash());
|
Assertions.assertEquals("HASH:87654321:salt-test", changed.getPasswordHash());
|
||||||
Assertions.assertEquals(0, changed.getFailedCount());
|
Assertions.assertEquals(0, changed.getFailedCount());
|
||||||
Assertions.assertNull(changed.getLockedUntil());
|
Assertions.assertNull(changed.getLockedUntil());
|
||||||
Assertions.assertFalse(Boolean.TRUE.equals(changed.getNeedChangePassword()));
|
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());
|
Assertions.assertEquals(LocalDateTime.of(2026, 3, 23, 2, 0), changed.getLastActiveAt());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void shouldThrowSessionInvalidWhenChangingPasswordWithExpiredSession() {
|
void shouldChangeCurrentLimitedAccountPasswordForActiveSession() {
|
||||||
InMemoryRoleAccountRepository roleAccounts = new InMemoryRoleAccountRepository();
|
InMemoryRoleAccountRepository roleAccounts = new InMemoryRoleAccountRepository();
|
||||||
|
InMemoryAuthUserAccountRepository userAccounts = new InMemoryAuthUserAccountRepository();
|
||||||
InMemoryAuthSessionRepository sessions = new InMemoryAuthSessionRepository();
|
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());
|
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));
|
expiredSession.setExpiresAt(LocalDateTime.of(2026, 3, 23, 1, 59));
|
||||||
sessions.save(expiredSession);
|
sessions.save(expiredSession);
|
||||||
|
|
||||||
AuthService service = newAuthService(
|
AuthService service = newAuthService(
|
||||||
roleAccounts,
|
roleAccounts,
|
||||||
new InMemoryAuthUserAccountRepository(),
|
new InMemoryAuthUserAccountRepository(),
|
||||||
|
fullAccounts,
|
||||||
sessions,
|
sessions,
|
||||||
new InMemoryRoleUkeyBindingRepository(),
|
new InMemoryRoleUkeyBindingRepository(),
|
||||||
FIXED_CLOCK,
|
FIXED_CLOCK,
|
||||||
@ -213,7 +266,7 @@ class AuthServiceTest {
|
|||||||
);
|
);
|
||||||
|
|
||||||
BizException exception = Assertions.assertThrows(BizException.class,
|
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(ErrorCode.SESSION_INVALID.getCode(), exception.getCode());
|
||||||
Assertions.assertEquals("session expired", exception.getMessage());
|
Assertions.assertEquals("session expired", exception.getMessage());
|
||||||
@ -233,6 +286,7 @@ class AuthServiceTest {
|
|||||||
roleAccounts,
|
roleAccounts,
|
||||||
new InMemoryLegacyAuthUserRepository(),
|
new InMemoryLegacyAuthUserRepository(),
|
||||||
userAccounts,
|
userAccounts,
|
||||||
|
new InMemoryAuthFullAccountRepository(),
|
||||||
new InMemoryAuthSessionRepository(),
|
new InMemoryAuthSessionRepository(),
|
||||||
new InMemoryRoleUkeyBindingRepository(),
|
new InMemoryRoleUkeyBindingRepository(),
|
||||||
new FakePasswordHasher(),
|
new FakePasswordHasher(),
|
||||||
@ -262,13 +316,93 @@ class AuthServiceTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void shouldVerifyUkeyProofsBeforeCreatingFullSession() {
|
void shouldKickPreviousLimitedSessionForSameRoleAndSameAccounts() {
|
||||||
InMemoryRoleAccountRepository roleAccounts = new InMemoryRoleAccountRepository();
|
InMemoryRoleAccountRepository roleAccounts = new InMemoryRoleAccountRepository();
|
||||||
InMemoryAuthUserAccountRepository userAccounts = new InMemoryAuthUserAccountRepository();
|
InMemoryAuthUserAccountRepository userAccounts = new InMemoryAuthUserAccountRepository();
|
||||||
InMemoryAuthSessionRepository sessions = new InMemoryAuthSessionRepository();
|
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();
|
InMemoryRoleUkeyBindingRepository ukeyBindings = new InMemoryRoleUkeyBindingRepository();
|
||||||
roleAccounts.save(activeRole(RoleCode.KEY_ADMIN, "12345678", "ROLE-SALT-K"));
|
roleAccounts.save(activeRole(RoleCode.KEY_ADMIN));
|
||||||
userAccounts.save(activeUserAccount("key-admin-01", RoleCode.KEY_ADMIN, "12345678", "SALT-K", 0, null));
|
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"));
|
ukeyBindings.save(activeBinding(RoleCode.KEY_ADMIN, 1, "UK-1", "PUB-1"));
|
||||||
|
|
||||||
LmkService lmkService = org.mockito.Mockito.mock(LmkService.class);
|
LmkService lmkService = org.mockito.Mockito.mock(LmkService.class);
|
||||||
@ -282,6 +416,7 @@ class AuthServiceTest {
|
|||||||
roleAccounts,
|
roleAccounts,
|
||||||
new InMemoryLegacyAuthUserRepository(),
|
new InMemoryLegacyAuthUserRepository(),
|
||||||
userAccounts,
|
userAccounts,
|
||||||
|
fullAccounts,
|
||||||
sessions,
|
sessions,
|
||||||
ukeyBindings,
|
ukeyBindings,
|
||||||
new FakePasswordHasher(),
|
new FakePasswordHasher(),
|
||||||
@ -303,9 +438,8 @@ class AuthServiceTest {
|
|||||||
|
|
||||||
UkeyLoginRequest request = new UkeyLoginRequest();
|
UkeyLoginRequest request = new UkeyLoginRequest();
|
||||||
request.setRoleCode(RoleCode.KEY_ADMIN.getCode());
|
request.setRoleCode(RoleCode.KEY_ADMIN.getCode());
|
||||||
request.setRolePassword("12345678");
|
request.setLoginFactors(List.of(
|
||||||
request.setUkeyProofs(List.of(
|
proofWithPassword("PUB-1", 1, "12345678", "RB-1", "ISSUE-1", "LOGIN-DATA-1", "LOGIN-SIGN-1")
|
||||||
proof("PUB-1", 1, "RB-1", "ISSUE-1", "LOGIN-DATA-1", "LOGIN-SIGN-1")
|
|
||||||
));
|
));
|
||||||
|
|
||||||
LoginResponse response = service.ukeyLogin(request);
|
LoginResponse response = service.ukeyLogin(request);
|
||||||
@ -321,12 +455,13 @@ class AuthServiceTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void shouldLockRolePasswordDuringUkeyLoginWhenRolePasswordIsWrong() {
|
void shouldLockFullAccountDuringUkeyLoginWhenSeatPasswordIsWrong() {
|
||||||
InMemoryRoleAccountRepository roleAccounts = new InMemoryRoleAccountRepository();
|
InMemoryRoleAccountRepository roleAccounts = new InMemoryRoleAccountRepository();
|
||||||
InMemoryAuthUserAccountRepository userAccounts = new InMemoryAuthUserAccountRepository();
|
InMemoryAuthUserAccountRepository userAccounts = new InMemoryAuthUserAccountRepository();
|
||||||
|
InMemoryAuthFullAccountRepository fullAccounts = new InMemoryAuthFullAccountRepository();
|
||||||
InMemoryRoleUkeyBindingRepository ukeyBindings = new InMemoryRoleUkeyBindingRepository();
|
InMemoryRoleUkeyBindingRepository ukeyBindings = new InMemoryRoleUkeyBindingRepository();
|
||||||
roleAccounts.save(activeRole(RoleCode.KEY_ADMIN, "12345678", "ROLE-SALT-K", 4, null));
|
roleAccounts.save(activeRole(RoleCode.KEY_ADMIN));
|
||||||
userAccounts.save(activeUserAccount("key-admin-01", RoleCode.KEY_ADMIN, "12345678", "SALT-K", 0, null));
|
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"));
|
ukeyBindings.save(activeBinding(RoleCode.KEY_ADMIN, 1, "UK-1", "PUB-1"));
|
||||||
|
|
||||||
LmkService lmkService = org.mockito.Mockito.mock(LmkService.class);
|
LmkService lmkService = org.mockito.Mockito.mock(LmkService.class);
|
||||||
@ -339,6 +474,7 @@ class AuthServiceTest {
|
|||||||
roleAccounts,
|
roleAccounts,
|
||||||
new InMemoryLegacyAuthUserRepository(),
|
new InMemoryLegacyAuthUserRepository(),
|
||||||
userAccounts,
|
userAccounts,
|
||||||
|
fullAccounts,
|
||||||
new InMemoryAuthSessionRepository(),
|
new InMemoryAuthSessionRepository(),
|
||||||
ukeyBindings,
|
ukeyBindings,
|
||||||
new FakePasswordHasher(),
|
new FakePasswordHasher(),
|
||||||
@ -355,62 +491,150 @@ class AuthServiceTest {
|
|||||||
|
|
||||||
UkeyLoginRequest request = new UkeyLoginRequest();
|
UkeyLoginRequest request = new UkeyLoginRequest();
|
||||||
request.setRoleCode(RoleCode.KEY_ADMIN.getCode());
|
request.setRoleCode(RoleCode.KEY_ADMIN.getCode());
|
||||||
request.setRolePassword("bad-role-password");
|
request.setLoginFactors(List.of(
|
||||||
request.setUkeyProofs(List.of(
|
proofWithPassword("PUB-1", 1, "bad-password", "RB-1", "ISSUE-1", "LOGIN-DATA-1", "LOGIN-SIGN-1")
|
||||||
proof("PUB-1", 1, "RB-1", "ISSUE-1", "LOGIN-DATA-1", "LOGIN-SIGN-1")
|
|
||||||
));
|
));
|
||||||
|
|
||||||
BizException exception = Assertions.assertThrows(BizException.class, () -> service.ukeyLogin(request));
|
BizException exception = Assertions.assertThrows(BizException.class, () -> service.ukeyLogin(request));
|
||||||
|
|
||||||
Assertions.assertEquals("role account is locked", exception.getMessage());
|
Assertions.assertEquals("full account is locked", exception.getMessage());
|
||||||
RoleAccountEntity updated = roleAccounts.findByRoleCode(RoleCode.KEY_ADMIN.getCode()).orElseThrow();
|
AuthFullAccountEntity updated = fullAccounts.findByRoleCodeAndUid(RoleCode.KEY_ADMIN.getCode(), 1).orElseThrow();
|
||||||
Assertions.assertEquals(5, updated.getFailedCount());
|
Assertions.assertEquals(5, updated.getFailedCount());
|
||||||
Assertions.assertEquals(RoleAccountStatus.LOCKED.name(), updated.getStatus());
|
Assertions.assertEquals(RoleAccountStatus.LOCKED.name(), updated.getStatus());
|
||||||
Assertions.assertEquals(LocalDateTime.of(2026, 3, 23, 2, 10), updated.getLockedUntil());
|
Assertions.assertEquals(LocalDateTime.of(2026, 3, 23, 2, 10), updated.getLockedUntil());
|
||||||
}
|
}
|
||||||
|
|
||||||
private static RoleAccountEntity activeRole(RoleCode roleCode) {
|
@Test
|
||||||
return activeRole(roleCode, false);
|
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();
|
RoleAccountEntity entity = new RoleAccountEntity();
|
||||||
entity.setId((long) roleCode.ordinal() + 1);
|
entity.setId((long) roleCode.ordinal() + 1);
|
||||||
entity.setRoleCode(roleCode.getCode());
|
entity.setRoleCode(roleCode.getCode());
|
||||||
entity.setDisplayName(roleCode.getDisplayName());
|
entity.setDisplayName(roleCode.getDisplayName());
|
||||||
entity.setRequiredUkeyCount(roleCode.getRequiredUkeyCount());
|
entity.setRequiredUkeyCount(roleCode.getRequiredUkeyCount());
|
||||||
entity.setStatus(RoleAccountStatus.ACTIVE.name());
|
entity.setStatus(RoleAccountStatus.ACTIVE.name());
|
||||||
entity.setNeedChangePassword(needChangePassword);
|
|
||||||
return entity;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static RoleAccountEntity activeRole(RoleCode roleCode, String rolePassword, String salt) {
|
|
||||||
return activeRole(roleCode, rolePassword, salt, 0, null, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static RoleAccountEntity activeRole(
|
|
||||||
RoleCode roleCode,
|
|
||||||
String rolePassword,
|
|
||||||
String salt,
|
|
||||||
int failedCount,
|
|
||||||
LocalDateTime lockedUntil
|
|
||||||
) {
|
|
||||||
return activeRole(roleCode, rolePassword, salt, failedCount, lockedUntil, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static RoleAccountEntity activeRole(
|
|
||||||
RoleCode roleCode,
|
|
||||||
String rolePassword,
|
|
||||||
String salt,
|
|
||||||
int failedCount,
|
|
||||||
LocalDateTime lockedUntil,
|
|
||||||
boolean needChangePassword
|
|
||||||
) {
|
|
||||||
RoleAccountEntity entity = activeRole(roleCode, needChangePassword);
|
|
||||||
entity.setPasswordHash("HASH:" + rolePassword + ":" + salt);
|
|
||||||
entity.setPasswordSalt(salt);
|
|
||||||
entity.setFailedCount(failedCount);
|
|
||||||
entity.setLockedUntil(lockedUntil);
|
|
||||||
return entity;
|
return entity;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -421,6 +645,18 @@ class AuthServiceTest {
|
|||||||
String salt,
|
String salt,
|
||||||
int failedCount,
|
int failedCount,
|
||||||
LocalDateTime lockedUntil
|
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();
|
AuthUserAccountEntity entity = new AuthUserAccountEntity();
|
||||||
entity.setId((long) (username.hashCode() & Integer.MAX_VALUE));
|
entity.setId((long) (username.hashCode() & Integer.MAX_VALUE));
|
||||||
@ -430,6 +666,44 @@ class AuthServiceTest {
|
|||||||
entity.setPasswordSalt(salt);
|
entity.setPasswordSalt(salt);
|
||||||
entity.setPasswordHash("HASH:" + password + ":" + salt);
|
entity.setPasswordHash("HASH:" + password + ":" + salt);
|
||||||
entity.setStatus(RoleAccountStatus.ACTIVE.name());
|
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.setFailedCount(failedCount);
|
||||||
entity.setLockedUntil(lockedUntil);
|
entity.setLockedUntil(lockedUntil);
|
||||||
return entity;
|
return entity;
|
||||||
@ -442,6 +716,13 @@ class AuthServiceTest {
|
|||||||
return request;
|
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(
|
private static RoleUkeyBindingEntity activeBinding(
|
||||||
RoleCode roleCode,
|
RoleCode roleCode,
|
||||||
int uid,
|
int uid,
|
||||||
@ -476,6 +757,20 @@ class AuthServiceTest {
|
|||||||
return proof;
|
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) {
|
private static AuthSessionEntity session(String token, String roleCode, String authLevel) {
|
||||||
AuthSessionEntity entity = new AuthSessionEntity();
|
AuthSessionEntity entity = new AuthSessionEntity();
|
||||||
entity.setId((long) (token.hashCode() & Integer.MAX_VALUE));
|
entity.setId((long) (token.hashCode() & Integer.MAX_VALUE));
|
||||||
@ -491,6 +786,7 @@ class AuthServiceTest {
|
|||||||
private static AuthService newAuthService(
|
private static AuthService newAuthService(
|
||||||
InMemoryRoleAccountRepository roleAccounts,
|
InMemoryRoleAccountRepository roleAccounts,
|
||||||
InMemoryAuthUserAccountRepository userAccounts,
|
InMemoryAuthUserAccountRepository userAccounts,
|
||||||
|
InMemoryAuthFullAccountRepository fullAccounts,
|
||||||
InMemoryAuthSessionRepository sessions,
|
InMemoryAuthSessionRepository sessions,
|
||||||
InMemoryRoleUkeyBindingRepository ukeyBindings,
|
InMemoryRoleUkeyBindingRepository ukeyBindings,
|
||||||
Clock clock,
|
Clock clock,
|
||||||
@ -500,6 +796,7 @@ class AuthServiceTest {
|
|||||||
roleAccounts,
|
roleAccounts,
|
||||||
new InMemoryLegacyAuthUserRepository(),
|
new InMemoryLegacyAuthUserRepository(),
|
||||||
userAccounts,
|
userAccounts,
|
||||||
|
fullAccounts,
|
||||||
sessions,
|
sessions,
|
||||||
ukeyBindings,
|
ukeyBindings,
|
||||||
new FakePasswordHasher(),
|
new FakePasswordHasher(),
|
||||||
@ -528,6 +825,23 @@ class AuthServiceTest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static class SequenceSessionTokenGenerator implements SessionTokenGenerator {
|
||||||
|
private final List<String> 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 static class InMemoryRoleAccountRepository implements RoleAccountRepository {
|
||||||
private final Map<String, RoleAccountEntity> store = new ConcurrentHashMap<>();
|
private final Map<String, RoleAccountEntity> store = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
@ -574,6 +888,33 @@ class AuthServiceTest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static class InMemoryAuthFullAccountRepository implements AuthFullAccountRepository {
|
||||||
|
private final Map<String, AuthFullAccountEntity> store = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Optional<AuthFullAccountEntity> findByRoleCodeAndUid(String roleCode, Integer uid) {
|
||||||
|
return Optional.ofNullable(store.get(roleCode + "#" + uid));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<AuthFullAccountEntity> 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 {
|
private static class InMemoryLegacyAuthUserRepository implements AuthUserRepository {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -590,6 +931,32 @@ class AuthServiceTest {
|
|||||||
return Optional.ofNullable(store.get(sessionToken));
|
return Optional.ofNullable(store.get(sessionToken));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<AuthSessionEntity> 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<AuthSessionEntity> 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
|
@Override
|
||||||
public void save(AuthSessionEntity entity) {
|
public void save(AuthSessionEntity entity) {
|
||||||
store.put(entity.getSessionToken(), entity);
|
store.put(entity.getSessionToken(), entity);
|
||||||
|
|||||||
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -35,8 +35,10 @@ class ReplayProtectedEndpointsTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
void shouldProtectSensitiveControllersAndMethods() throws Exception {
|
void shouldProtectSensitiveControllersAndMethods() throws Exception {
|
||||||
Assertions.assertNotNull(annotation(AuthController.class, "changePassword",
|
Assertions.assertNotNull(annotation(AuthController.class, "changeFullAccountPassword",
|
||||||
com.cisd.tms.modules.auth.dto.ChangePasswordRequest.class, jakarta.servlet.http.HttpServletRequest.class));
|
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(annotation(DeviceController.class, "restart", jakarta.servlet.http.HttpServletRequest.class));
|
||||||
|
|
||||||
Assertions.assertNotNull(AnnotatedElementUtils.findMergedAnnotation(AuthAdminController.class, ReplayProtected.class));
|
Assertions.assertNotNull(AnnotatedElementUtils.findMergedAnnotation(AuthAdminController.class, ReplayProtected.class));
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user